默认结构用作 golang 中 yaml 数据的目标



我尝试改善我维护的 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(,并使用YAMLAchors来执行此操作。

ConfigSection的各个部分(InputOutput(将在使用该配置的软件包中定义。此部分中的每一个都提供了一个Defaults()和一个自定义UnmarshalYAML()功能。此外,ConfigSection本身也提供了一个UnmarshalYAML()功能。这个想法是从 https://github.com/go-yaml/yaml/issues/165#issuecomment-255223956 那里偷来的。

问题

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

  • 在配置节(empty示例中未定义任何内容(中,则不会应用默认值。
  • 如果定义了没有数据字段的部件(ConfigSection(,则该部件将没有默认值。"未定义"部分具有默认值(请参阅inputoutput(。
  • 如果定义了两个部分(如第both节(,但没有数据字段,则设置任何默认值。

我根本没有看到任何模式,并且不知道为什么这样工作以及如何获得预期的结果(例如,让测试通过(。

好的,所以我没有看到的模式非常明显:配置的"最深叶"覆盖下面的所有内容,无论是给定的数据还是空值的 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