@@ -11,20 +11,78 @@ import (
11
11
// tagCategories is a list of tag categories to check for.
12
12
var tagCategories = []string {"json" , "yaml" }
13
13
14
+ // WithTags allows you to specify custom tag categories to check for.
15
+ // It can be used to override the default "json" and "yaml" tags.
16
+ // The tags are checked in the order they are provided.
17
+ func WithTags (tags ... string ) ToMapOption {
18
+ return func (h * handler ) {
19
+ h .tags = tags
20
+ }
21
+ }
22
+
23
+ // WithAllowNoTags allows you to specify whether to allow fields without tags.
24
+ // If used, fields without tags will be included in the output map.
25
+ func WithAllowNoTags () ToMapOption {
26
+ return func (h * handler ) {
27
+ h .allowNoTags = true
28
+ }
29
+ }
30
+
31
+ // ToMapOption is a function that modifies the handler.
32
+ type ToMapOption func (* handler )
33
+
34
+ // handler is a struct that contains the options for the ToMap function.
35
+ // It contains a list of tags to check for and a flag to allow fields
36
+ // without tags.
37
+ type handler struct {
38
+ tags []string
39
+ allowNoTags bool
40
+ }
41
+
42
+ // tagWrapper is a struct that contains the name and options of a tag.
43
+ // It is used to store the tag information for a field.
44
+ // The name is the key name to use in the output map.
45
+ // The options are the options specified in the tag.
46
+ type tagWrapper struct {
47
+ Name string
48
+ Options []string
49
+ }
50
+
51
+ // newHandler creates a new handler with the default options.
52
+ // It initializes the tags to the default "json" and "yaml" tags.
53
+ // It also initializes the allowNoTags flag to false.
54
+ // It can be modified using the ToMapOptions functions.
55
+ // It returns a pointer to the handler.
56
+ func newHandler (opts ... ToMapOption ) * handler {
57
+ h := & handler {
58
+ tags : tagCategories ,
59
+ allowNoTags : false ,
60
+ }
61
+
62
+ for _ , opt := range opts {
63
+ opt (h )
64
+ }
65
+
66
+ return h
67
+ }
68
+
14
69
// ToMap converts a struct or map to a map[string]any.
15
70
// It handles nested structs, maps, and slices.
16
- // It uses the "json" and "yaml" tags to determine the key names.
71
+ // By default, it uses the "json" and "yaml" tags
72
+ // to determine the key names in that order.
17
73
// It respects the `omitempty` tag for fields.
18
74
// It respects the `inline` tag for nested structs.
19
75
// It respects the `-` tag to omit fields.
20
76
//
21
77
// If the input is nil, it returns nil.
22
78
// If the input is not a struct or map, it returns an error.
23
- func ToMap (obj any ) (map [string ]any , error ) {
79
+ func ToMap (obj any , opts ... ToMapOption ) (map [string ]any , error ) {
80
+ handler := newHandler (opts ... )
81
+
24
82
if obj == nil {
25
83
return nil , nil
26
84
}
27
- res := handle (obj )
85
+ res := handler . handle (obj )
28
86
if v , ok := res .(map [string ]any ); ok {
29
87
return v , nil
30
88
}
@@ -33,7 +91,7 @@ func ToMap(obj any) (map[string]any, error) {
33
91
34
92
// handle is a helper function that recursively handles
35
93
// the conversion of structs, maps, and slices to a map[string]any.
36
- func handle (obj any ) any {
94
+ func ( h * handler ) handle (obj any ) any {
37
95
if obj == nil {
38
96
return nil
39
97
}
@@ -45,19 +103,19 @@ func handle(obj any) any {
45
103
46
104
switch val .Kind () {
47
105
case reflect .Map :
48
- return handleMap (obj )
106
+ return h . handleMap (obj )
49
107
case reflect .Struct :
50
- return handleStruct (obj )
108
+ return h . handleStruct (obj )
51
109
case reflect .Slice :
52
- return handleSlice (obj )
110
+ return h . handleSlice (obj )
53
111
default :
54
112
return obj
55
113
}
56
114
}
57
115
58
116
// handleStruct handles the conversion of a struct to a map[string]any.
59
- // It uses the "json" and "yaml" tags to determine the key names.
60
- func handleStruct (obj any ) any {
117
+ // It uses the tags from the handler to determine the key names.
118
+ func ( h * handler ) handleStruct (obj any ) any {
61
119
res := map [string ]any {}
62
120
val := reflect .ValueOf (obj )
63
121
if val .Kind () == reflect .Ptr {
@@ -74,19 +132,30 @@ func handleStruct(obj any) any {
74
132
75
133
name := field .Name
76
134
value := val .Field (i )
77
- tagName , tagOpts := getTag (field )
78
- if tagName != "" {
79
- name = tagName
135
+ tagInfo , err := h .getTag (field )
136
+ if err != nil && ! h .allowNoTags {
137
+ continue
138
+ }
139
+
140
+ if h .allowNoTags && tagInfo == nil {
141
+ tagInfo = & tagWrapper {
142
+ Name : "" ,
143
+ Options : []string {},
144
+ }
145
+ }
146
+
147
+ if tagInfo .Name != "" {
148
+ name = tagInfo .Name
80
149
}
81
150
82
151
// Omit struct tag "-"
83
- if _ , ok := xslices .FindFunc (tagOpts , func (s string ) bool {
152
+ if _ , ok := xslices .FindFunc (tagInfo . Options , func (s string ) bool {
84
153
return s == "-"
85
- }); ok || (name == "-" && len (tagOpts ) == 0 ) {
154
+ }); ok || (name == "-" && len (tagInfo . Options ) == 0 ) {
86
155
continue
87
156
}
88
157
89
- if _ , ok := xslices .FindFunc (tagOpts , func (s string ) bool {
158
+ if _ , ok := xslices .FindFunc (tagInfo . Options , func (s string ) bool {
90
159
return s == "omitempty"
91
160
}); ok {
92
161
if reflect .DeepEqual (value .Interface (), reflect .Zero (val .Field (i ).Type ()).Interface ()) {
@@ -102,10 +171,10 @@ func handleStruct(obj any) any {
102
171
value = value .Elem ()
103
172
}
104
173
if value .Kind () == reflect .Struct || value .Kind () == reflect .Map {
105
- if _ , ok := xslices .FindFunc (tagOpts , func (s string ) bool {
174
+ if _ , ok := xslices .FindFunc (tagInfo . Options , func (s string ) bool {
106
175
return s == "inline"
107
176
}); ok {
108
- if nestedValues , ok := handle (value .Interface ()).(map [string ]any ); ok {
177
+ if nestedValues , ok := h . handle (value .Interface ()).(map [string ]any ); ok {
109
178
for k , v := range nestedValues {
110
179
if _ , ok := res [k ]; ! ok {
111
180
res [k ] = v
@@ -116,15 +185,15 @@ func handleStruct(obj any) any {
116
185
}
117
186
}
118
187
119
- res [name ] = handle (value .Interface ())
188
+ res [name ] = h . handle (value .Interface ())
120
189
}
121
190
122
191
return res
123
192
}
124
193
125
194
// handleMap handles the conversion of a map to a map[string]any,
126
195
// recursively converting nested maps, slices and structs.
127
- func handleMap (obj any ) any {
196
+ func ( h * handler ) handleMap (obj any ) any {
128
197
m := map [string ]any {}
129
198
val := reflect .ValueOf (obj )
130
199
for _ , key := range val .MapKeys () {
@@ -136,32 +205,39 @@ func handleMap(obj any) any {
136
205
if v == nil {
137
206
continue
138
207
}
139
- m [fmt .Sprintf ("%v" , k )] = handle (v )
208
+ m [fmt .Sprintf ("%v" , k )] = h . handle (v )
140
209
}
141
210
return m
142
211
}
143
212
144
213
// handleSlice handles the conversion of a slice to a slice of any,
145
214
// recursively converting nested maps, slices and structs.
146
- func handleSlice (obj any ) any {
215
+ func ( h * handler ) handleSlice (obj any ) any {
147
216
s := []any {}
148
217
val := reflect .ValueOf (obj )
149
218
for i := range val .Len () {
150
- s = append (s , handle (val .Index (i ).Interface ()))
219
+ s = append (s , h . handle (val .Index (i ).Interface ()))
151
220
}
152
221
return s
153
222
}
154
223
155
224
// getTag retrieves the tag name and options from a struct field.
156
- // It checks for the "json" and "yaml" tags in that order .
225
+ // It checks the tags provided by the handler one by one .
157
226
// If one tag is empty, it will return the other tag.
158
- // If both tags are empty, it returns an empty string and an empty slice .
159
- func getTag (field reflect.StructField ) (string , [] string ) {
160
- for _ , category := range tagCategories {
227
+ // If all tags are empty, it returns an error .
228
+ func ( h * handler ) getTag (field reflect.StructField ) (* tagWrapper , error ) {
229
+ for _ , category := range h . tags {
161
230
if tag := field .Tag .Get (category ); tag != "" {
162
231
splitTag := strings .Split (tag , "," )
163
- return splitTag [0 ], splitTag [1 :]
232
+ // Test if tag is solitary comma, i.e. `json:","`
233
+ if splitTag [0 ] == "" && len (splitTag [1 ]) == 0 {
234
+ continue
235
+ }
236
+ return & tagWrapper {
237
+ Name : splitTag [0 ],
238
+ Options : splitTag [1 :],
239
+ }, nil
164
240
}
165
241
}
166
- return " " , [] string {}
242
+ return nil , fmt . Errorf ( "no tag of %s found for field %s " , strings . Join ( h . tags , ", " ), field . Name )
167
243
}
0 commit comments