Skip to content

Commit 76c2b95

Browse files
committed
added inline and dot support
1 parent 608bc3f commit 76c2b95

9 files changed

Lines changed: 211 additions & 15 deletions

File tree

helper/jsonMap.go

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,15 +66,71 @@ func MapJsonMapToStruct(jsonMapInput map[string]any, structToUpdate any) error {
6666

6767
fieldKey := fieldType.Name
6868
jsonKey := fieldType.Tag.Get("json")
69+
isInline := false
6970
if len(jsonKey) > 0 {
7071
// Split on comma to handle omitempty and other options
71-
jsonKey = strings.Split(jsonKey, ",")[0]
72-
if jsonKey != "-" {
73-
fieldKey = jsonKey
72+
parts := strings.Split(jsonKey, ",")
73+
jsonKeyPrefix := parts[0]
74+
for _, p := range parts[1:] {
75+
if p == "inline" {
76+
isInline = true
77+
}
78+
}
79+
if jsonKeyPrefix != "-" && jsonKeyPrefix != "" {
80+
fieldKey = jsonKeyPrefix
7481
}
7582
}
7683

84+
if isInline {
85+
target := field
86+
if field.Kind() == reflect.Ptr {
87+
if field.IsNil() {
88+
if field.CanSet() {
89+
field.Set(reflect.New(field.Type().Elem()))
90+
}
91+
}
92+
target = field.Elem()
93+
}
94+
if target.CanAddr() {
95+
err := MapJsonMapToStruct(jsonMapInput, target.Addr().Interface())
96+
if err != nil {
97+
return fmt.Errorf("error mapping inline struct %v: %v", fieldType.Name, err.Error())
98+
}
99+
} else if field.Kind() == reflect.Ptr {
100+
err := MapJsonMapToStruct(jsonMapInput, field.Interface())
101+
if err != nil {
102+
return fmt.Errorf("error mapping inline struct %v: %v", fieldType.Name, err.Error())
103+
}
104+
}
105+
continue
106+
}
107+
77108
if jsonValue, ok := jsonMapInput[fieldKey]; ok {
109+
if jsonValueMap, isMap := jsonValue.(map[string]any); isMap && (field.Kind() == reflect.Struct || (field.Kind() == reflect.Ptr && field.Elem().Kind() == reflect.Struct)) {
110+
target := field
111+
if field.Kind() == reflect.Ptr {
112+
if field.IsNil() {
113+
if field.CanSet() {
114+
field.Set(reflect.New(field.Type().Elem()))
115+
}
116+
}
117+
target = field.Elem()
118+
}
119+
if target.CanAddr() {
120+
err := MapJsonMapToStruct(jsonValueMap, target.Addr().Interface())
121+
if err != nil {
122+
return fmt.Errorf("error mapping nested struct %v: %v", fieldType.Name, err.Error())
123+
}
124+
continue
125+
} else if field.Kind() == reflect.Ptr {
126+
err := MapJsonMapToStruct(jsonValueMap, field.Interface())
127+
if err != nil {
128+
return fmt.Errorf("error mapping nested struct %v: %v", fieldType.Name, err.Error())
129+
}
130+
continue
131+
}
132+
}
133+
78134
err := SetStructValueByJson(field, jsonValue)
79135
if err != nil {
80136
return fmt.Errorf("could not set field %v (json key: %v) of %v: %v", fieldType.Name, jsonKey, reflect.TypeOf(structToUpdate), err.Error())

helper/jsonMap_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -613,4 +613,20 @@ func TestMapJsonMapToStruct(t *testing.T) {
613613
assert.Nil(t, result.PointerSlice)
614614
})
615615
})
616+
617+
t.Run("Inline struct fields", func(t *testing.T) {
618+
type InnerStruct struct {
619+
Kind string `json:"kind"`
620+
}
621+
type TestStruct struct {
622+
Inner InnerStruct `json:",inline"`
623+
}
624+
625+
inputMap := map[string]any{"kind": "testKind"}
626+
var result TestStruct
627+
err := MapJsonMapToStruct(inputMap, &result)
628+
assert.NoError(t, err)
629+
630+
assert.Equal(t, "testKind", result.Inner.Kind)
631+
})
616632
}

helper/unpack.go

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"io"
77
"net/http"
88
"net/url"
9+
"strings"
910
)
1011

1112
func UnmarshalRequestToJsonMap(request *http.Request) (map[string]any, error) {
@@ -58,17 +59,48 @@ func UnmapUrlValuesToJsonMap(values url.Values) (map[string]any, error) {
5859
arrayOut = append(arrayOut, v)
5960
}
6061
}
61-
mapOut[k] = arrayOut
62+
if strings.Contains(k, ".") {
63+
setNestedDotValue(mapOut, strings.Split(k, "."), arrayOut)
64+
} else {
65+
mapOut[k] = arrayOut
66+
}
6267
} else {
6368
value := values.Get(k)
6469
var unmarshalled any
70+
var valToSet any
6571
err := json.Unmarshal([]byte(value), &unmarshalled)
6672
if err == nil {
67-
mapOut[k] = unmarshalled
73+
valToSet = unmarshalled
6874
} else {
69-
mapOut[k] = value
75+
valToSet = value
76+
}
77+
78+
if strings.Contains(k, ".") {
79+
setNestedDotValue(mapOut, strings.Split(k, "."), valToSet)
80+
} else {
81+
mapOut[k] = valToSet
7082
}
7183
}
7284
}
7385
return mapOut, nil
7486
}
87+
88+
func setNestedDotValue(m map[string]any, keys []string, value any) {
89+
if len(keys) == 0 {
90+
return
91+
}
92+
if len(keys) == 1 {
93+
m[keys[0]] = value
94+
return
95+
}
96+
key := keys[0]
97+
if existing, ok := m[key]; ok {
98+
if nestedMap, ok := existing.(map[string]any); ok {
99+
setNestedDotValue(nestedMap, keys[1:], value)
100+
return
101+
}
102+
}
103+
nestedMap := make(map[string]any)
104+
m[key] = nestedMap
105+
setNestedDotValue(nestedMap, keys[1:], value)
106+
}

helper/unpack_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,19 @@ func TestUnmapUrlValuesToJsonMap(t *testing.T) {
8888
assert.Equal(t, map[string]any{"food": "banana", "fruit": "apple"}, mapOut["map"], "Expected map to match")
8989
})
9090

91+
t.Run("Valid URL with dot-notation keys", func(t *testing.T) {
92+
values := url.Values{}
93+
values.Set("autoscaling.minReplicas", "3")
94+
values.Set("autoscaling.featureFlag", "true")
95+
96+
mapOut, err := UnmapUrlValuesToJsonMap(values)
97+
assert.NoError(t, err, "Expected no error unmapping URL values to JsonMap")
98+
assert.IsType(t, map[string]any{}, mapOut["autoscaling"], "Expected autoscaling to be a nested map")
99+
autoscalingMap := mapOut["autoscaling"].(map[string]any)
100+
assert.Equal(t, float64(3), autoscalingMap["minReplicas"], "Expected minReplicas to be 3")
101+
assert.Equal(t, true, autoscalingMap["featureFlag"], "Expected featureFlag to be true")
102+
})
103+
91104
t.Run("Empty URL values", func(t *testing.T) {
92105
values := url.Values{}
93106

model/validation.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ type Validation struct {
1010
Requirement string
1111
Groups []*Group
1212
Default string
13+
OmitEmpty bool
1314
// Inner Struct validation
1415
InnerValidation []Validation
1516
// Parsed AST representation of the requirement to avoid redundant parsing

validationExtract_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,23 @@ func TestGetValidationsFromStruct(t *testing.T) {
6363
},
6464
expectedError: false,
6565
},
66+
{
67+
name: "Valid struct with omitempty and inline tags",
68+
args: args{
69+
input: &struct {
70+
Inner struct {
71+
Kind string `json:"kind" vld:"equ1"`
72+
} `json:",inline" vld:"-"`
73+
Field1 string `json:"field1,omitempty" vld:"equ1"`
74+
}{},
75+
tagType: model.VLD,
76+
},
77+
expected: []model.Validation{
78+
{Key: "kind", Type: model.String, Requirement: "equ1"},
79+
{Key: "field1", Type: model.String, Requirement: "equ1", OmitEmpty: true},
80+
},
81+
expectedError: false,
82+
},
6683
{
6784
name: "Valid struct with inner struct",
6885
args: args{

validator.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,10 @@ func (r *Validator) ValidateWithValidation(jsonInput map[string]any, validations
149149
}
150150
}
151151

152+
if validation.OmitEmpty && jsonValue == "" {
153+
continue
154+
}
155+
152156
var err error
153157
switch validation.Type {
154158
case model.Struct:

validatorExtract.go

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,22 @@ func GetValidationsFromStruct(in any, tagType string) ([]model.Validation, error
3232
continue
3333
}
3434

35-
if len(validation.Requirement) > 0 {
36-
validations = append(validations, *validation)
35+
isInline := false
36+
if len(fieldType.Tag.Get("json")) > 0 {
37+
parts := strings.Split(fieldType.Tag.Get("json"), ",")
38+
for _, part := range parts[1:] {
39+
if part == "inline" {
40+
isInline = true
41+
}
42+
}
43+
}
44+
45+
if isInline {
46+
validations = append(validations, validation.InnerValidation...)
47+
} else {
48+
if len(validation.Requirement) > 0 || len(validation.InnerValidation) > 0 {
49+
validations = append(validations, *validation)
50+
}
3751
}
3852
}
3953
return validations, nil
@@ -46,11 +60,16 @@ func GetValidationFromStructField(tagType string, fieldValue reflect.Value, fiel
4660
validation := &model.Validation{}
4761
validation.Key = fieldType.Name
4862
if len(fieldType.Tag.Get("json")) > 0 {
49-
jsonKey := fieldType.Tag.Get("json")
50-
// Split on comma to handle omitempty and other options
51-
jsonKey = strings.Split(jsonKey, ",")[0]
52-
if jsonKey != "-" {
53-
validation.Key = jsonKey
63+
jsonKeyFull := fieldType.Tag.Get("json")
64+
jsonKeyParts := strings.Split(jsonKeyFull, ",")
65+
jsonKeyPrefix := jsonKeyParts[0]
66+
if jsonKeyPrefix != "-" && jsonKeyPrefix != "" {
67+
validation.Key = jsonKeyPrefix
68+
}
69+
for _, part := range jsonKeyParts[1:] {
70+
if part == "omitempty" {
71+
validation.OmitEmpty = true
72+
}
5473
}
5574
}
5675
validation.Type = model.ReflectKindToValidatorType(fieldValue.Type().Kind())
@@ -92,8 +111,12 @@ func GetValidationFromStructField(tagType string, fieldValue reflect.Value, fiel
92111
return nil, fmt.Errorf("error getting inner validation from array: %v", err)
93112
}
94113
validation.InnerValidation = append(validation.InnerValidation, innerValidation...)
95-
} else if helper.IsStruct(fieldValue.Interface()) {
96-
innerStruct := reflect.New(fieldValue.Type()).Interface()
114+
} else if helper.IsStruct(fieldValue.Interface()) || (fieldValue.Type().Kind() == reflect.Ptr && fieldValue.Type().Elem().Kind() == reflect.Struct) {
115+
fieldTypeElem := fieldValue.Type()
116+
if fieldTypeElem.Kind() == reflect.Ptr {
117+
fieldTypeElem = fieldTypeElem.Elem()
118+
}
119+
innerStruct := reflect.New(fieldTypeElem).Interface()
97120
innerValidation, err := GetValidationsFromStruct(innerStruct, string(tagType))
98121
if err != nil {
99122
return nil, fmt.Errorf("error getting inner validation from struct: %v", err)

validator_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,40 @@ func TestValidateWithValidation(t *testing.T) {
312312
expected any
313313
wantErr bool
314314
}{
315+
{
316+
name: "Valid omitempty logic with empty string",
317+
args: args{
318+
jsonMap: map[string]any{
319+
"fruit": "",
320+
},
321+
validations: []model.Validation{
322+
{
323+
Key: "fruit",
324+
Type: model.String,
325+
Requirement: "equapple",
326+
OmitEmpty: true,
327+
},
328+
},
329+
},
330+
expected: map[string]any{},
331+
wantErr: false,
332+
},
333+
{
334+
name: "Valid defFalse logic for missing boolean",
335+
args: args{
336+
jsonMap: map[string]any{},
337+
validations: []model.Validation{
338+
{
339+
Key: "isActive",
340+
Type: model.Bool,
341+
Requirement: "deffalse",
342+
Default: "false",
343+
},
344+
},
345+
},
346+
expected: map[string]any{"isActive": "false"},
347+
wantErr: false,
348+
},
315349
{
316350
name: "Valid validation string",
317351
args: args{

0 commit comments

Comments
 (0)