用作Golang中Yaml数据目标的默认结构

时间:2018-10-09 11:09:29

标签: go

我尝试改善我维护的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来实现。

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

问题

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

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

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

1 个答案:

答案 0 :(得分:0)

好,所以我没有看到的模式很明显:配置的“最深的叶子”会覆盖给定的数据或空值的go defaults下面的所有内容:

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

[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

...喂了这种形式的薯片。

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