【问题标题】:Defaulting structs used as destination for yaml data in golang在golang中用作yaml数据目标的默认结构
【发布时间】:2019-03-14 03:33:08
【问题描述】:

我尝试改善我维护的 cli 的用户体验。一个主要目标是提供合理的默认值。它广泛使用yaml 进行配置。

配置的基本演示实现可以在这里找到:https://github.com/unprofession-al/configuration/tree/bf5a89b3eee7338899b28c047f3795546ce3d2e6

常规

主要配置如下:

type Config map[string]ConfigSection

type ConfigSection struct {
    Input  InputConfig  `yaml:"input"`
    Output OutputConfig `yaml:"output"`
}

Config 拥有一堆ConfigSections。这允许用户定义配置的变体(例如proddevtesting)并使用 YAML Achors 来执行此操作。

ConfigSection 的各个部分(InputOutput)将在使用配置的包中定义。这部分中的每一个都提供了一个Defaults() 和一个自定义的UnmarshalYAML() 函数。 ConfigSection 本身也提供了一个 UnmarshalYAML() 函数。这个想法是从https://github.com/go-yaml/yaml/issues/165#issuecomment-255223956盗来的。

问题

data.go 的 repo 中定义了一些测试输入以及预期的输出。运行测试 (go test -v) 显示:

  • 在 ConfigSection 中没有定义任何内容(empty 示例)没有应用默认值。
  • 如果定义了没有数据字段的(ConfigSection)部分,则该部分将没有默认值。 “未定义”部分具有默认值(请参阅inputoutput)。
  • 如果两个部分都已定义(如both 部分一样)但没有数据字段,则设置任何默认值。

我根本看不到任何模式,并且想不出为什么会这样以及如何获得预期的结果(例如,让测试通过)。

【问题讨论】:

    标签: go


    【解决方案1】:

    好的,所以我没有看到的模式非常明显:配置的“最深叶”覆盖了下面的所有内容,无论是使用给定的数据还是使用空值的 go 默认值:

    这意味着像这样的结构......

    [key_string]:
      input:
        listener: [string]
        static: [string]
      output:
        listener: [string]
        details:
          filter: [string]
          retention: [string]
    

    ...默认使用数据...

    defaults:
      input:
        listener: 127.0.0.1:8910
        static: default
      output:
        listener: 127.0.0.1:8989
        details:
          filter: '*foo*'
          retention: 3h
    

    ...用这种形式的 yaml 喂食..

    empty:
    
    both:
      input:
      output:
    
    input: &input
      input:
    
    input-modified-with-anchor:
      <<: *input
      input:
        static: NOTDEFAULT
    
    input-modified-without-anchor:
      input:
        static: NOTDEFAULT
    
    output: &output
      output:
    
    output-modified-with-anchor:
      <<: *output
      output:
        details:
          filter: NOTDEFAULT
    
    output-modified-without-anchor:
      output:
        details:
          filter: NOTDEFAULT
    

    ...原来是...

    both:
      input:
        listener: ""
        static: ""
      output:
        listener: ""
        details:
          filter: ""
          retention: ""
    empty:
      input:
        listener: ""
        static: ""
      output:
        listener: ""
        details:
          filter: ""
          retention: ""
    input:
      input:
        listener: ""
        static: ""
      output:
        listener: 127.0.0.1:8989
        details:
          filter: '*foo*'
          retention: 3h
    input-modified-with-anchor:
      input:
        listener: 127.0.0.1:8910
        static: NOTDEFAULT
      output:
        listener: 127.0.0.1:8989
        details:
          filter: '*foo*'
          retention: 3h
    input-modified-without-anchor:
      input:
        listener: 127.0.0.1:8910
        static: NOTDEFAULT
      output:
        listener: 127.0.0.1:8989
        details:
          filter: '*foo*'
          retention: 3h
    

    对于我的用例,这是一个过于复杂的行为,因此我尝试了一种不同的方法:如果需要,我将默认配置注入 yaml 并在每个部分中引用其锚点。我觉得这对最终用户来说更加透明和可重复。这是一个丑陋的函数草稿:

    func injectYAML(data []byte) ([]byte, error) {
        // render a default section an add an anchor
        key := "injected_defaults"
        defaultData := Config{key: Defaults()}
        var defaultSection []byte
        defaultSection, _ = yaml.Marshal(defaultData)
        defaultSection = bytes.Replace(defaultSection, []byte(key+":"), []byte(key+": &"+key), 1)
    
        // get list of sections in input data
        c := Config{}
        err := yaml.Unmarshal(data, &c)
        if err != nil {
            return data, fmt.Errorf("Error while reading sections from yaml: %s", err.Error())
        }
    
        // remove "---" at beginning when present
        data = bytes.TrimLeft(data, "---")
    
        // add reference to default section to each section
        lines := bytes.Split(data, []byte("\n"))
        var updatedLines [][]byte
        for _, line := range lines {
            updatedLines = append(updatedLines, line)
            for section := range c {
                if bytes.HasPrefix(line, []byte(section+":")) {
                    updatedLines = append(updatedLines, []byte("  <<: *"+key))
                }
            }
        }
        updatedData := bytes.Join(updatedLines, []byte("\n"))
    
        // compose injected yaml
        out := []byte("---\n")
        out = append(out, defaultSection...)
        out = append(out, updatedData...)
        return out, nil
    }
    

    完整示例位于:https://github.com/unprofession-al/configuration/tree/7c2eb7da58b51f52b50f2a0fbac193c799c9eb08

    【讨论】:

      猜你喜欢
      • 2019-06-20
      • 2017-01-02
      • 1970-01-01
      • 1970-01-01
      • 2010-12-23
      • 1970-01-01
      • 1970-01-01
      • 2019-10-19
      • 1970-01-01
      相关资源
      最近更新 更多