From 9f2d9b4c1b46a052cfd80ad21083b661cb22ea52 Mon Sep 17 00:00:00 2001 From: Team Humaki Date: Thu, 17 Sep 2026 02:24:27 -0700 Subject: [PATCH] struct: allow custom MapTo tag name MapTo currently only reads the ini struct tag. Callers that already tag fields with json (or another name) have to duplicate tags. Add WithMapToOptionTag for a per-call override and LoadOptions.TagName for load-time default. Empty still means ini. Fixes #376 --- file.go | 3 +++ ini.go | 2 ++ struct.go | 55 ++++++++++++++++++++++++++++++++++++++++++---- struct_test.go | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 115 insertions(+), 4 deletions(-) diff --git a/file.go b/file.go index 58beabc..8cfd356 100644 --- a/file.go +++ b/file.go @@ -44,6 +44,9 @@ type File struct { NameMapper ValueMapper + + // tagNameOverride, when set, replaces LoadOptions.TagName for a MapTo call. + tagNameOverride string } // newFile initializes File object with given data sources. diff --git a/ini.go b/ini.go index 99e7f86..be1a004 100644 --- a/ini.go +++ b/ini.go @@ -125,6 +125,8 @@ type LoadOptions struct { AllowNonUniqueSections bool // AllowDuplicateShadowValues indicates whether values for shadowed keys should be deduplicated. AllowDuplicateShadowValues bool + // TagName is the struct tag used by MapTo and ReflectFrom. Empty means "ini". + TagName string } // DebugFunc is the type of function called to log parse events. diff --git a/struct.go b/struct.go index 819a6e5..35025bd 100644 --- a/struct.go +++ b/struct.go @@ -68,6 +68,49 @@ func (s *Section) parseFieldName(raw, actual string) string { return raw } +func (f *File) structTagName() string { + if f.tagNameOverride != "" { + return f.tagNameOverride + } + if f.options.TagName != "" { + return f.options.TagName + } + return "ini" +} + +func (s *Section) fieldTag(tpField reflect.StructField) string { + return tpField.Tag.Get(s.f.structTagName()) +} + +// MapToOption configures a MapTo or StrictMapTo call. +type MapToOption func(*mapToOptions) + +type mapToOptions struct { + tagName string +} + +// WithMapToOptionTag uses a custom struct tag name instead of "ini". +func WithMapToOptionTag(name string) MapToOption { + return func(o *mapToOptions) { + o.tagName = name + } +} + +func (f *File) applyMapToOptions(opts []MapToOption) func() { + cfg := mapToOptions{} + for _, opt := range opts { + if opt != nil { + opt(&cfg) + } + } + if cfg.tagName == "" { + return func() {} + } + prev := f.tagNameOverride + f.tagNameOverride = cfg.tagName + return func() { f.tagNameOverride = prev } +} + func parseDelim(actual string) string { if len(actual) > 0 { return actual @@ -287,7 +330,7 @@ func (s *Section) mapToField(val reflect.Value, isStrict bool, sectionIndex int, field := val.Field(i) tpField := typ.Field(i) - tag := tpField.Tag.Get("ini") + tag := s.fieldTag(tpField) if tag == "-" { continue } @@ -413,13 +456,17 @@ func (s *Section) StrictMapTo(v interface{}) error { } // MapTo maps file to given struct. -func (f *File) MapTo(v interface{}) error { +func (f *File) MapTo(v interface{}, opts ...MapToOption) error { + restore := f.applyMapToOptions(opts) + defer restore() return f.Section("").MapTo(v) } // StrictMapTo maps file to given struct in strict mode, // which returns all possible error including value parsing error. -func (f *File) StrictMapTo(v interface{}) error { +func (f *File) StrictMapTo(v interface{}, opts ...MapToOption) error { + restore := f.applyMapToOptions(opts) + defer restore() return f.Section("").StrictMapTo(v) } @@ -587,7 +634,7 @@ func (s *Section) reflectFrom(val reflect.Value) error { field := val.Field(i) tpField := typ.Field(i) - tag := tpField.Tag.Get("ini") + tag := s.fieldTag(tpField) if tag == "-" { continue } diff --git a/struct_test.go b/struct_test.go index ad6ad04..005faaf 100644 --- a/struct_test.go +++ b/struct_test.go @@ -910,3 +910,62 @@ Title = Consultant Engineer ) }) } + +func TestMapTo_CustomTagName(t *testing.T) { + const raw = ` +NAME = Unknwon +age = 21 +` + cfg, err := Load([]byte(raw)) + require.NoError(t, err) + + type personJSON struct { + Name string `json:"NAME"` + Age int `json:"age"` + } + p := new(personJSON) + require.NoError(t, cfg.MapTo(p, WithMapToOptionTag("json"))) + assert.Equal(t, "Unknwon", p.Name) + assert.Equal(t, 21, p.Age) + + type personINI struct { + Name string `ini:"NAME"` + Age int `ini:"age"` + } + p2 := new(personINI) + require.NoError(t, cfg.MapTo(p2)) + assert.Equal(t, "Unknwon", p2.Name) + assert.Equal(t, 21, p2.Age) +} + +func TestMapTo_LoadOptionsTagName(t *testing.T) { + const raw = ` +NAME = Unknwon +age = 21 +` + cfg, err := LoadSources(LoadOptions{TagName: "json"}, []byte(raw)) + require.NoError(t, err) + + type personJSON struct { + Name string `json:"NAME"` + Age int `json:"age"` + } + p := new(personJSON) + require.NoError(t, cfg.MapTo(p)) + assert.Equal(t, "Unknwon", p.Name) + assert.Equal(t, 21, p.Age) +} + +func TestReflectFrom_LoadOptionsTagName(t *testing.T) { + type personJSON struct { + Name string `json:"NAME"` + Age int `json:"age"` + } + cfg := Empty(LoadOptions{TagName: "json"}) + require.NoError(t, cfg.ReflectFrom(&personJSON{Name: "Unknwon", Age: 21})) + + var buf bytes.Buffer + _, err := cfg.WriteTo(&buf) + require.NoError(t, err) + assert.Equal(t, "NAME = Unknwon\nage = 21\n", buf.String()) +}