diff --git a/cache.go b/cache.go index b9670f3..9a96add 100644 --- a/cache.go +++ b/cache.go @@ -30,10 +30,31 @@ func getStructCache(rt reflect.Type) *structInfo { // Build struct info info := &structInfo{name: rt.Name()} + + // struct fields traversal for i := 0; i < rt.NumField(); i++ { field := rt.Field(i) + tag := field.Tag.Get("qp") + fieldType := field.Type + if fieldType.Kind() == reflect.Ptr { + fieldType = fieldType.Elem() + } + + isStruct := fieldType.Kind() == reflect.Struct && fieldType != reflect.TypeFor[time.Time]() + + // traverse embedded structs + if field.Anonymous && isStruct { + info.fields = append(info.fields, fieldInfo{ + name: field.Name, + typ: field.Type, + index: field.Index, + isNested: true, + }) + continue + } + if !field.IsExported() { if tag != "" { info.hasUnexportedWithTag = true @@ -41,6 +62,18 @@ func getStructCache(rt reflect.Type) *structInfo { continue } + // tag on struct will be ignored + if isStruct { + info.fields = append(info.fields, fieldInfo{ + name: field.Name, + typ: field.Type, + index: field.Index, + isNested: true, + }) + continue + } + + // leaf field if tag != "" { info.fields = append(info.fields, fieldInfo{ name: field.Name, @@ -49,22 +82,8 @@ func getStructCache(rt reflect.Type) *structInfo { index: field.Index, isNested: false, }) - } else { - // Check if this field is a nested struct (struct or pointer to struct) - fieldType := field.Type - if fieldType.Kind() == reflect.Ptr { - fieldType = fieldType.Elem() - } - if fieldType.Kind() == reflect.Struct && fieldType != reflect.TypeOf(time.Time{}) { - info.fields = append(info.fields, fieldInfo{ - name: field.Name, - tag: "", - typ: field.Type, // Keep the original type (may be pointer) - index: field.Index, - isNested: true, - }) - } } + } // LoadOrStore handles race conditions atomically diff --git a/parser_test.go b/parser_test.go index ad046b9..d0ac657 100644 --- a/parser_test.go +++ b/parser_test.go @@ -738,6 +738,50 @@ func TestNestedStruct(t *testing.T) { err = Parse(values, &s) assert.Error(t, err) }) + + t.Run("Embedded/Anonymous-Nested-Struct", func(t *testing.T) { + type filter1 struct { + Foo []string `qp:"foo"` + } + + type filter2 struct { + filter1 + Bar []string `qp:"bar"` + } + + type sortOrder struct { + By string `qp:"sort_by"` + Dir string `qp:"sort_dir"` + } + + type input struct { + Filter filter2 + Sort sortOrder + } + + var in input + queryParams := "foo=f1&bar=b1,b2&sort_by=foo&sort_dir=desc" + expected := input{ + Filter: filter2{ + filter1: filter1{ + Foo: []string{"f1"}, + }, + Bar: []string{"b1", "b2"}, + }, + Sort: sortOrder{ + By: "foo", + Dir: "desc", + }, + } + + values, err := url.ParseQuery(queryParams) + require.NoError(t, err) + + err = Parse(values, &in) + require.NoError(t, err) + + assert.Equal(t, in, expected) + }) } func ptr[T any](v T) *T {