diff --git a/encoding/ini/decode.go b/encoding/ini/decode.go index 2a2fe133d..aa552b8d8 100644 --- a/encoding/ini/decode.go +++ b/encoding/ini/decode.go @@ -19,7 +19,6 @@ // standard for INI files, this package supports a common subset: // // - Sections are declared with [name] headers. -// - Nested sections use dot-separated names like [parent.child]. // - Properties use "key = value" syntax. // - Comments are allowed and start with ; or # and span to the end of the line. // - Multi-word values do not require quoting; leading/trailing @@ -27,6 +26,7 @@ // - Blank lines are ignored. // - Duplicate keys within the same section are an error. // - Key and section name case sensitivity is configured via [Config.CaseSensitivity]. +// - Section name nesting is configured via [Config.SectionNameNesting]. // - Value type parsing is configured via [Config.ValueTypes]. // // The following features found in some INI variants are out of scope: @@ -73,6 +73,15 @@ const ( // quoted values are unquoted and always treated as strings ) +// SectionNameNestingStrategy controls how the decoder interprets dots in section names. +type SectionNameNestingStrategy int + +const ( + sectionNamesUnset SectionNameNestingStrategy = iota // default zero value; equal to [SectionNamesFlat] for now + SectionNamesFlat // dots are treated as regular characters in section names + SectionNamesDotted // dots are treated as nested section separators +) + // Config configures the behavior of the INI decoder. type Config struct { // CaseSensitivity controls how keys and section names are cased. @@ -80,6 +89,12 @@ type Config struct { // Set to [CaseLower] to lowercase all keys and section names. CaseSensitivity CaseSensitivityStrategy + // SectionNameNesting controls how dots in section names are interpreted. + // By default dots are treated as regular characters ([SectionNamesFlat]), + // so all sections are flat. + // Set to [SectionNamesDotted] to treat dots as nested section separators. + SectionNameNesting SectionNameNestingStrategy + // ValueTypes controls how INI values are interpreted. // By default all values are raw CUE strings ([ValuesRawStrings]). // Set to [ValuesCUELiterals] to parse booleans and numbers into @@ -168,8 +183,11 @@ func (d *Decoder) Decode() (ast.Expr, error) { return nil, d.posErrf(lineNum, "duplicate section: %s", sectionName) } - // Build nested structs for dot-separated section names. - parts := strings.Split(sectionName, ".") + parts := []string{sectionName} + if d.cfg.SectionNameNesting == SectionNamesDotted { + // Explicitly opt-in to splitting section names by dots. + parts = strings.Split(sectionName, ".") + } cur = §ion{ struct_: d.buildNestedSection(topLevel, parts, lineNum), keys: make(map[string]bool), diff --git a/encoding/ini/decode_test.go b/encoding/ini/decode_test.go index 043ea6c89..37ddf7f1e 100644 --- a/encoding/ini/decode_test.go +++ b/encoding/ini/decode_test.go @@ -109,7 +109,21 @@ func TestDecoder(t *testing.T) { } `, }, { - name: "NestedSections", + name: "FlatSectionsByDefault/DottedSectionNameIsLiteral", + input: ` + [database.pool] + min = 5 + max = 20 + `, + wantCUE: ` + "database.pool": { + min: "5" + max: "20" + } + `, + }, { + name: "NestedSections", + config: &ini.Config{SectionNameNesting: ini.SectionNamesDotted}, input: ` [database.pool] min = 5 @@ -228,7 +242,8 @@ func TestDecoder(t *testing.T) { } `, }, { - name: "DeeplyNestedSections", + name: "DeeplyNestedSections", + config: &ini.Config{SectionNameNesting: ini.SectionNamesDotted}, input: ` [a.b.c] key = value @@ -237,7 +252,8 @@ func TestDecoder(t *testing.T) { a: b: c: key: "value" `, }, { - name: "SectionWithSiblingAndNestedSection", + name: "SectionWithSiblingAndNestedSection", + config: &ini.Config{SectionNameNesting: ini.SectionNamesDotted}, input: ` [server] host = localhost @@ -256,7 +272,8 @@ func TestDecoder(t *testing.T) { } `, }, { - name: "FullExample", + name: "FullExample", + config: &ini.Config{SectionNameNesting: ini.SectionNamesDotted}, input: ` ; Application configuration app_name = MyWebApp @@ -425,7 +442,7 @@ func TestDecoder(t *testing.T) { `, }, { name: "CaseInsensitive/LowercasesNestedSections", - config: &ini.Config{CaseSensitivity: ini.CaseLower}, + config: &ini.Config{CaseSensitivity: ini.CaseLower, SectionNameNesting: ini.SectionNamesDotted}, input: ` [Server.TLS] Cert = /path/to/cert