pelletier / go-toml

Go library for the TOML file format
https://github.com/pelletier/go-toml
Other
1.73k stars 211 forks source link

v2 does not seem to support unmarshaling TOML file into struct with embedded struct fields #772

Closed atc0005 closed 2 years ago

atc0005 commented 2 years ago

Describe the bug

v1 supported unmarshaling a TOML config into a struct with embedded struct fields. v2 does not seem to support this.

To Reproduce

Prototype to emulate issue ```golang package main import ( "fmt" tomlv1 "github.com/pelletier/go-toml" tomlv2 "github.com/pelletier/go-toml/v2" ) type AppMetadata struct { AppName string `toml:"-" arg:"-"` AppDescription string `toml:"-" arg:"-"` AppVersion string `toml:"-" arg:"-"` AppURL string `toml:"-" arg:"-"` } type FileHandling struct { FilePattern *string `toml:"pattern"` FileExtensions []string `toml:"file_extensions"` FileAge *int `toml:"file_age"` NumFilesToKeep *int `toml:"files_to_keep"` KeepOldest *bool `toml:"keep_oldest"` Remove *bool `toml:"remove"` IgnoreErrors *bool `toml:"ignore_errors"` } type Search struct { Paths []string `toml:"paths"` RecursiveSearch *bool `toml:"recursive_search"` } type Logging struct { LogLevel *string `toml:"log_level"` LogFormat *string `toml:"log_format"` LogFilePath *string `toml:"log_file_path"` ConsoleOutput *string `toml:"console_output"` UseSyslog *bool `toml:"use_syslog"` } // Config represents a collection of configuration settings for this // application. Config is created as early as possible upon application // startup. type Config struct { // Embed other structs in an effort to better group related settings AppMetadata FileHandling Logging Search // Path to (optional) configuration file ConfigFile *string `toml:"config_file"` } func main() { // Mock on-disk config file that will be parsed by pelletier/go-toml // library. var defaultConfigFile = []byte(` [filehandling] pattern = "reach-masterdev-" file_extensions = [ ".war", ".tmp", ] file_age = 1 files_to_keep = 2 keep_oldest = false remove = false ignore_errors = true [search] paths = [ "/tmp/elbow/path1", "/tmp/elbow/path2", ] recursive_search = true [logging] log_level = "debug" log_format = "text" # If set, all output to the console will be muted and sent here instead log_file_path = "/tmp/log.json" console_output = "stdout" use_syslog = false`) defaultConfigFilePath := "" tomlv1Config := Config{ ConfigFile: &defaultConfigFilePath, } if err := tomlv1.Unmarshal(defaultConfigFile, &tomlv1Config); err != nil { fmt.Printf("Failed to unmarshal TOML config: %v\n", err) } fmt.Printf("\ntomlv1Config settings: %+v\n", tomlv1Config) tomlv2Config := Config{ ConfigFile: &defaultConfigFilePath, } if err := tomlv2.Unmarshal(defaultConfigFile, &tomlv2Config); err != nil { fmt.Printf("Failed to unmarshal TOML config: %v\n", err) } fmt.Printf("\ntomlv2Config settings: %+v\n", tomlv2Config) } ```

Expected behavior

Replace v1 with v2 and existing use case continues to work.

Versions

Additional context

I've reviewed the list of changes for the v2 release, but honestly I don't understand the details well enough to know whether this particular use case was dropped from v2.

pelletier commented 2 years ago

go-toml v2 did change the default behavior of unmarshalling embedded fields (see Embedded structs behave like stdlib). It follows stdlib's encoding/json behavior. In short, embedded structs' fields are rolled up into the outer struct unless the embedded struct has an explicit name using the toml tag. For example, taking a shortened version of your example:

package main

import (
    "fmt"
    "github.com/pelletier/go-toml/v2"
)

type FileHandling struct {
    FilePattern string `toml:"pattern"`
}

type Config struct {
    FileHandling `toml:"filehandling"` // <--- the tag to add
}

func main() {
    var defaultConfigFile = []byte(`
        [filehandling]
        pattern = "reach-masterdev-"`)

    tomlv2Config := Config{}
    if err := toml.Unmarshal(defaultConfigFile, &tomlv2Config); err != nil {
        fmt.Printf("Failed to unmarshal TOML config: %v\n", err)
    }
    // ...
}

This should work as expected. However, while testing this, I did find a bug in the unmarshal code that deals with anonymous structs. I fixed it with https://github.com/pelletier/go-toml/commit/c5ca2c682b572078ab6242cd90c6328b934dc319. If you use that commit and add the toml tags to the external fields it should work as you expect. Let me know if it doesn't!

atc0005 commented 2 years ago

@pelletier Thank you for your detailed reply and for pointing me at the relevant section of the README.

I've tested the changes you described and they work without issue.

Thank you for your time and for this library!

pelletier commented 2 years ago

Glad it worked!