From 3bdd36b4aa9f213c53d4d0db53102e517e8813e4 Mon Sep 17 00:00:00 2001 From: brent saner Date: Wed, 20 Nov 2024 13:21:53 -0500 Subject: [PATCH 1/3] Add user-defined tag naming These changes allow the caller to define a custom tag name, either globally or locally to a function call. --- README.md | 10 ++++++++++ defaults.go | 42 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e8096b6..b44bfaa 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ Initialize structs with default values - Recursively initializes fields in a struct - Dynamically sets default values by [`defaults.Setter`](./setter.go) interface - Preserves non-initial values from being reset with a default value +- User-Definable tag name Usage @@ -42,6 +43,9 @@ import ( type Gender string +// Optionally use a different tag name globally. +//defaults.TagName = "my_custom_tag" + type Sample struct { Name string `default:"John Smith"` Age int `default:"27"` @@ -83,6 +87,12 @@ func main() { if err := defaults.Set(obj); err != nil { panic(err) } + // Or with a specific tag name (without setting defaults.TagName globally): + /* + if err := defaults.SetWithTag(obj, "some_tag_name"); err != nil { + panic(err) + } + */ out, err := json.MarshalIndent(obj, "", " ") if err != nil { diff --git a/defaults.go b/defaults.go index f453928..58d03c3 100644 --- a/defaults.go +++ b/defaults.go @@ -6,10 +6,12 @@ import ( "errors" "reflect" "strconv" + `strings` "time" ) var ( + errInvalidTag = errors.New("invalid tag name") errInvalidType = errors.New("not a struct pointer") ) @@ -17,6 +19,10 @@ const ( fieldName = "default" ) +var ( + TagName string = fieldName +) + // Set initializes members in a struct referenced by a pointer. // Maps and slices are initialized by `make` and other primitive types are set with default values. // `ptr` should be a struct pointer @@ -25,6 +31,40 @@ func Set(ptr interface{}) error { return errInvalidType } + if strings.TrimSpace(TagName) == "" { + return errInvalidTag + } + + v := reflect.ValueOf(ptr).Elem() + t := v.Type() + + if t.Kind() != reflect.Struct { + return errInvalidType + } + + for i := 0; i < t.NumField(); i++ { + if defaultVal := t.Field(i).Tag.Get(TagName); defaultVal != "-" { + if err := setField(v.Field(i), defaultVal); err != nil { + return err + } + } + } + callSetter(ptr) + return nil +} + +// SetWithTag initializes members in a struct referenced by a pointer using an explicit tag name. +// Maps and slices are initialized by `make` and other primitive types are set with default values. +// `ptr` should be a struct pointer +func SetWithTag(ptr interface{}, runtimeTag string) error { + if reflect.TypeOf(ptr).Kind() != reflect.Ptr { + return errInvalidType + } + + if strings.TrimSpace(runtimeTag) == "" { + return errInvalidTag + } + v := reflect.ValueOf(ptr).Elem() t := v.Type() @@ -33,7 +73,7 @@ func Set(ptr interface{}) error { } for i := 0; i < t.NumField(); i++ { - if defaultVal := t.Field(i).Tag.Get(fieldName); defaultVal != "-" { + if defaultVal := t.Field(i).Tag.Get(runtimeTag); defaultVal != "-" { if err := setField(v.Field(i), defaultVal); err != nil { return err } From 36bbebb0f72dda6e689c095fcbd4883c0b8e60c0 Mon Sep 17 00:00:00 2001 From: brent saner Date: Wed, 20 Nov 2024 13:32:02 -0500 Subject: [PATCH 2/3] linting upstream fails on backtick quoting, apparently? --- defaults.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/defaults.go b/defaults.go index 58d03c3..153291f 100644 --- a/defaults.go +++ b/defaults.go @@ -6,7 +6,7 @@ import ( "errors" "reflect" "strconv" - `strings` + "strings" "time" ) From 9cc0c2550632af7ee4bdd9646c8551746d77706b Mon Sep 17 00:00:00 2001 From: brent saner Date: Thu, 5 Dec 2024 01:56:56 -0500 Subject: [PATCH 3/3] adding suggested/requested changes and performing for Set calls in setField --- README.md | 244 +++++++++++++++++++++++++++++++----------------- defaults.go | 57 ++++------- example/main.go | 237 ++++++++++++++++++++++++++++++---------------- 3 files changed, 334 insertions(+), 204 deletions(-) diff --git a/README.md b/README.md index b44bfaa..8561aeb 100644 --- a/README.md +++ b/README.md @@ -43,34 +43,31 @@ import ( type Gender string -// Optionally use a different tag name globally. -//defaults.TagName = "my_custom_tag" - type Sample struct { - Name string `default:"John Smith"` - Age int `default:"27"` - Gender Gender `default:"m"` + Name string `default:"John Smith" default_alt:"Jane Doe"` + Age int `default:"27" default_alt:"28"` + Gender Gender `default:"m" default_alt:"f"` Working bool `default:"true"` - SliceInt []int `default:"[1, 2, 3]"` - SlicePtr []*int `default:"[1, 2, 3]"` - SliceString []string `default:"[\"a\", \"b\"]"` + SliceInt []int `default:"[1, 2, 3]" default_alt:"[4, 5, 6]"` + SlicePtr []*int `default:"[1, 2, 3]" default_alt:"[4, 5, 6]"` + SliceString []string `default:"[\"a\", \"b\"]" default_alt:"[\"c\", \"d\"]"` - MapNull map[string]int `default:"{}"` - Map map[string]int `default:"{\"key1\": 123}"` - MapOfStruct map[string]OtherStruct `default:"{\"Key2\": {\"Foo\":123}}"` - MapOfPtrStruct map[string]*OtherStruct `default:"{\"Key3\": {\"Foo\":123}}"` - MapOfStructWithTag map[string]OtherStruct `default:"{\"Key4\": {\"Foo\":123}}"` + MapNull map[string]int `default:"{}"` // Using default_alt would leave as a nil map + Map map[string]int `default:"{\"key1\": 123}" default_alt:"{\"key1\": 456}"` + MapOfStruct map[string]OtherStruct `default:"{\"Key2\": {\"Foo\":123}}" default_alt:"{\"Key2\": {\"Foo\":456}}"` + MapOfPtrStruct map[string]*OtherStruct `default:"{\"Key3\": {\"Foo\":123}}" default_alt:"{\"Key3\": {\"Foo\":456}}"` + MapOfStructWithTag map[string]OtherStruct `default:"{\"Key4\": {\"Foo\":123}}" default_alt:"{\"Key4\": {\"Foo\":456}}"` - Struct OtherStruct `default:"{\"Foo\": 123}"` - StructPtr *OtherStruct `default:"{\"Foo\": 123}"` + Struct OtherStruct `default:"{\"Foo\": 123}" default:"{\"Foo\": 456}"` + StructPtr *OtherStruct `default:"{\"Foo\": 123}" default:"{\"Foo\": 456}"` NoTag OtherStruct // Recurses into a nested struct by default NoOption OtherStruct `default:"-"` // no option } type OtherStruct struct { - Hello string `default:"world"` // Tags in a nested struct also work + Hello string `default:"world" default_alt:"person"` // Tags in a nested struct also work Foo int `default:"-"` Random int `default:"-"` } @@ -87,84 +84,157 @@ func main() { if err := defaults.Set(obj); err != nil { panic(err) } - // Or with a specific tag name (without setting defaults.TagName globally): - /* - if err := defaults.SetWithTag(obj, "some_tag_name"); err != nil { - panic(err) + + out, err := json.MarshalIndent(obj, "", " ") + if err != nil { + panic(err) } + fmt.Println(string(out)) + + // Output: + /* + { + "Name": "John Smith", + "Age": 27, + "Gender": "m", + "Working": true, + "SliceInt": [ + 1, + 2, + 3 + ], + "SlicePtr": [ + 1, + 2, + 3 + ], + "SliceString": [ + "a", + "b" + ], + "MapNull": {}, + "Map": { + "key1": 123 + }, + "MapOfStruct": { + "Key2": { + "Hello": "world", + "Foo": 123, + "Random": 7924310560672650386 + } + }, + "MapOfPtrStruct": { + "Key3": { + "Hello": "world", + "Foo": 123, + "Random": 5864827752208870454 + } + }, + "MapOfStructWithTag": { + "Key4": { + "Hello": "world", + "Foo": 123, + "Random": 9020930397672710767 + } + }, + "Struct": { + "Hello": "world", + "Foo": 123, + "Random": 2100809313953071154 + }, + "StructPtr": { + "Hello": "world", + "Foo": 123, + "Random": 4589638854976076769 + }, + "NoTag": { + "Hello": "world", + "Foo": 0, + "Random": 93481895446446669 + }, + "NoOption": { + "Hello": "", + "Foo": 0, + "Random": 0 + } + } */ - out, err := json.MarshalIndent(obj, "", " ") + // Or with a specific tag name: + otherObj := &Sample{} + if err = defaults.SetWithTag(otherObj, "default_alt"); err != nil { + panic(err) + } + + out, err = json.MarshalIndent(otherObj, "", " ") if err != nil { panic(err) } fmt.Println(string(out)) // Output: - // { - // "Name": "John Smith", - // "Age": 27, - // "Gender": "m", - // "Working": true, - // "SliceInt": [ - // 1, - // 2, - // 3 - // ], - // "SlicePtr": [ - // 1, - // 2, - // 3 - // ], - // "SliceString": [ - // "a", - // "b" - // ], - // "MapNull": {}, - // "Map": { - // "key1": 123 - // }, - // "MapOfStruct": { - // "Key2": { - // "Hello": "world", - // "Foo": 123, - // "Random": 5577006791947779410 - // } - // }, - // "MapOfPtrStruct": { - // "Key3": { - // "Hello": "world", - // "Foo": 123, - // "Random": 8674665223082153551 - // } - // }, - // "MapOfStructWithTag": { - // "Key4": { - // "Hello": "world", - // "Foo": 123, - // "Random": 6129484611666145821 - // } - // }, - // "Struct": { - // "Hello": "world", - // "Foo": 123, - // "Random": 4037200794235010051 - // }, - // "StructPtr": { - // "Hello": "world", - // "Foo": 123, - // "Random": 3916589616287113937 - // }, - // "NoTag": { - // "Hello": "world", - // "Foo": 0, - // "Random": 6334824724549167320 - // }, - // "NoOption": { - // "Hello": "", - // "Foo": 0, - // "Random": 0 - // } - // } + /* + { + "Name": "Jane Doe", + "Age": 28, + "Gender": "f", + "Working": false, + "SliceInt": [ + 4, + 5, + 6 + ], + "SlicePtr": [ + 4, + 5, + 6 + ], + "SliceString": [ + "c", + "d" + ], + "MapNull": null, + "Map": { + "key1": 456 + }, + "MapOfStruct": { + "Key2": { + "Hello": "person", + "Foo": 456, + "Random": 3527321973631593020 + } + }, + "MapOfPtrStruct": { + "Key3": { + "Hello": "person", + "Foo": 456, + "Random": 6391170103701304261 + } + }, + "MapOfStructWithTag": { + "Key4": { + "Hello": "person", + "Foo": 456, + "Random": 5779747168002044569 + } + }, + "Struct": { + "Hello": "person", + "Foo": 0, + "Random": 4391597250420769051 + }, + "StructPtr": null, + "NoTag": { + "Hello": "person", + "Foo": 0, + "Random": 5000048969203402981 + }, + "NoOption": { + "Hello": "person", + "Foo": 0, + "Random": 4800395242796521920 + } + } + */ } ``` diff --git a/defaults.go b/defaults.go index 153291f..00b8e38 100644 --- a/defaults.go +++ b/defaults.go @@ -15,48 +15,21 @@ var ( errInvalidType = errors.New("not a struct pointer") ) -const ( - fieldName = "default" -) - -var ( - TagName string = fieldName -) - // Set initializes members in a struct referenced by a pointer. // Maps and slices are initialized by `make` and other primitive types are set with default values. // `ptr` should be a struct pointer -func Set(ptr interface{}) error { - if reflect.TypeOf(ptr).Kind() != reflect.Ptr { - return errInvalidType - } +func Set(ptr interface{}) (err error) { - if strings.TrimSpace(TagName) == "" { - return errInvalidTag - } - - v := reflect.ValueOf(ptr).Elem() - t := v.Type() - - if t.Kind() != reflect.Struct { - return errInvalidType - } + err = SetWithTag(ptr, "default") - for i := 0; i < t.NumField(); i++ { - if defaultVal := t.Field(i).Tag.Get(TagName); defaultVal != "-" { - if err := setField(v.Field(i), defaultVal); err != nil { - return err - } - } - } - callSetter(ptr) - return nil + return } // SetWithTag initializes members in a struct referenced by a pointer using an explicit tag name. // Maps and slices are initialized by `make` and other primitive types are set with default values. // `ptr` should be a struct pointer func SetWithTag(ptr interface{}, runtimeTag string) error { + if reflect.TypeOf(ptr).Kind() != reflect.Ptr { return errInvalidType } @@ -74,7 +47,7 @@ func SetWithTag(ptr interface{}, runtimeTag string) error { for i := 0; i < t.NumField(); i++ { if defaultVal := t.Field(i).Tag.Get(runtimeTag); defaultVal != "-" { - if err := setField(v.Field(i), defaultVal); err != nil { + if err := setField(v.Field(i), defaultVal, runtimeTag); err != nil { return err } } @@ -91,7 +64,15 @@ func MustSet(ptr interface{}) { } } -func setField(field reflect.Value, defaultVal string) error { +// MustSetWithTag function is a wrapper of SetWithTag function +// It will call Set and panic if err not equals nil. +func MustSetWithTag(ptr interface{}, runtimeTag string) { + if err := SetWithTag(ptr, runtimeTag); err != nil { + panic(err) + } +} + +func setField(field reflect.Value, defaultVal string, tagName string) error { if !field.CanSet() { return nil } @@ -200,16 +181,16 @@ func setField(field reflect.Value, defaultVal string) error { switch field.Kind() { case reflect.Ptr: if isInitial || field.Elem().Kind() == reflect.Struct { - setField(field.Elem(), defaultVal) + setField(field.Elem(), defaultVal, tagName) callSetter(field.Interface()) } case reflect.Struct: - if err := Set(field.Addr().Interface()); err != nil { + if err := SetWithTag(field.Addr().Interface(), tagName); err != nil { return err } case reflect.Slice: for j := 0; j < field.Len(); j++ { - if err := setField(field.Index(j), ""); err != nil { + if err := setField(field.Index(j), "", tagName); err != nil { return err } } @@ -221,14 +202,14 @@ func setField(field reflect.Value, defaultVal string) error { case reflect.Ptr: switch v.Elem().Kind() { case reflect.Struct, reflect.Slice, reflect.Map: - if err := setField(v.Elem(), ""); err != nil { + if err := setField(v.Elem(), "", tagName); err != nil { return err } } case reflect.Struct, reflect.Slice, reflect.Map: ref := reflect.New(v.Type()) ref.Elem().Set(v) - if err := setField(ref.Elem(), ""); err != nil { + if err := setField(ref.Elem(), "", tagName); err != nil { return err } field.SetMapIndex(e, ref.Elem().Convert(v.Type())) diff --git a/example/main.go b/example/main.go index 8388350..f46b545 100644 --- a/example/main.go +++ b/example/main.go @@ -11,30 +11,30 @@ import ( type Gender string type Sample struct { - Name string `default:"John Smith"` - Age int `default:"27"` - Gender Gender `default:"m"` + Name string `default:"John Smith" default_alt:"Jane Doe"` + Age int `default:"27" default_alt:"28"` + Gender Gender `default:"m" default_alt:"f"` Working bool `default:"true"` - SliceInt []int `default:"[1, 2, 3]"` - SlicePtr []*int `default:"[1, 2, 3]"` - SliceString []string `default:"[\"a\", \"b\"]"` + SliceInt []int `default:"[1, 2, 3]" default_alt:"[4, 5, 6]"` + SlicePtr []*int `default:"[1, 2, 3]" default_alt:"[4, 5, 6]"` + SliceString []string `default:"[\"a\", \"b\"]" default_alt:"[\"c\", \"d\"]"` - MapNull map[string]int `default:"{}"` - Map map[string]int `default:"{\"key1\": 123}"` - MapOfStruct map[string]OtherStruct `default:"{\"Key2\": {\"Foo\":123}}"` - MapOfPtrStruct map[string]*OtherStruct `default:"{\"Key3\": {\"Foo\":123}}"` - MapOfStructWithTag map[string]OtherStruct `default:"{\"Key4\": {\"Foo\":123}}"` + MapNull map[string]int `default:"{}"` // Using default_alt would leave as a nil map + Map map[string]int `default:"{\"key1\": 123}" default_alt:"{\"key1\": 456}"` + MapOfStruct map[string]OtherStruct `default:"{\"Key2\": {\"Foo\":123}}" default_alt:"{\"Key2\": {\"Foo\":456}}"` + MapOfPtrStruct map[string]*OtherStruct `default:"{\"Key3\": {\"Foo\":123}}" default_alt:"{\"Key3\": {\"Foo\":456}}"` + MapOfStructWithTag map[string]OtherStruct `default:"{\"Key4\": {\"Foo\":123}}" default_alt:"{\"Key4\": {\"Foo\":456}}"` - Struct OtherStruct `default:"{\"Foo\": 123}"` - StructPtr *OtherStruct `default:"{\"Foo\": 123}"` + Struct OtherStruct `default:"{\"Foo\": 123}" default:"{\"Foo\": 456}"` + StructPtr *OtherStruct `default:"{\"Foo\": 123}" default:"{\"Foo\": 456}"` NoTag OtherStruct // Recurses into a nested struct by default NoOption OtherStruct `default:"-"` // no option } type OtherStruct struct { - Hello string `default:"world"` // Tags in a nested struct also work + Hello string `default:"world" default_alt:"person"` // Tags in a nested struct also work Foo int `default:"-"` Random int `default:"-"` } @@ -59,69 +59,148 @@ func main() { fmt.Println(string(out)) // Output: - // { - // "Name": "John Smith", - // "Age": 27, - // "Gender": "m", - // "Working": true, - // "SliceInt": [ - // 1, - // 2, - // 3 - // ], - // "SlicePtr": [ - // 1, - // 2, - // 3 - // ], - // "SliceString": [ - // "a", - // "b" - // ], - // "MapNull": {}, - // "Map": { - // "key1": 123 - // }, - // "MapOfStruct": { - // "Key2": { - // "Hello": "world", - // "Foo": 123, - // "Random": 5577006791947779410 - // } - // }, - // "MapOfPtrStruct": { - // "Key3": { - // "Hello": "world", - // "Foo": 123, - // "Random": 8674665223082153551 - // } - // }, - // "MapOfStructWithTag": { - // "Key4": { - // "Hello": "world", - // "Foo": 123, - // "Random": 6129484611666145821 - // } - // }, - // "Struct": { - // "Hello": "world", - // "Foo": 123, - // "Random": 4037200794235010051 - // }, - // "StructPtr": { - // "Hello": "world", - // "Foo": 123, - // "Random": 3916589616287113937 - // }, - // "NoTag": { - // "Hello": "world", - // "Foo": 0, - // "Random": 6334824724549167320 - // }, - // "NoOption": { - // "Hello": "", - // "Foo": 0, - // "Random": 0 - // } - // } + /* + { + "Name": "John Smith", + "Age": 27, + "Gender": "m", + "Working": true, + "SliceInt": [ + 1, + 2, + 3 + ], + "SlicePtr": [ + 1, + 2, + 3 + ], + "SliceString": [ + "a", + "b" + ], + "MapNull": {}, + "Map": { + "key1": 123 + }, + "MapOfStruct": { + "Key2": { + "Hello": "world", + "Foo": 123, + "Random": 7924310560672650386 + } + }, + "MapOfPtrStruct": { + "Key3": { + "Hello": "world", + "Foo": 123, + "Random": 5864827752208870454 + } + }, + "MapOfStructWithTag": { + "Key4": { + "Hello": "world", + "Foo": 123, + "Random": 9020930397672710767 + } + }, + "Struct": { + "Hello": "world", + "Foo": 123, + "Random": 2100809313953071154 + }, + "StructPtr": { + "Hello": "world", + "Foo": 123, + "Random": 4589638854976076769 + }, + "NoTag": { + "Hello": "world", + "Foo": 0, + "Random": 93481895446446669 + }, + "NoOption": { + "Hello": "", + "Foo": 0, + "Random": 0 + } + } + */ + + // Or with a specific tag name: + otherObj := &Sample{} + if err = defaults.SetWithTag(otherObj, "default_alt"); err != nil { + panic(err) + } + + out, err = json.MarshalIndent(otherObj, "", " ") + if err != nil { + panic(err) + } + fmt.Println(string(out)) + + // Output: + /* + { + "Name": "Jane Doe", + "Age": 28, + "Gender": "f", + "Working": false, + "SliceInt": [ + 4, + 5, + 6 + ], + "SlicePtr": [ + 4, + 5, + 6 + ], + "SliceString": [ + "c", + "d" + ], + "MapNull": null, + "Map": { + "key1": 456 + }, + "MapOfStruct": { + "Key2": { + "Hello": "person", + "Foo": 456, + "Random": 3527321973631593020 + } + }, + "MapOfPtrStruct": { + "Key3": { + "Hello": "person", + "Foo": 456, + "Random": 6391170103701304261 + } + }, + "MapOfStructWithTag": { + "Key4": { + "Hello": "person", + "Foo": 456, + "Random": 5779747168002044569 + } + }, + "Struct": { + "Hello": "person", + "Foo": 0, + "Random": 4391597250420769051 + }, + "StructPtr": null, + "NoTag": { + "Hello": "person", + "Foo": 0, + "Random": 5000048969203402981 + }, + "NoOption": { + "Hello": "person", + "Foo": 0, + "Random": 4800395242796521920 + } + } + */ }