douzhouqin6223 2018-10-09 11:09
浏览 181
已采纳

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

I try to improve the user experience of a cli I maintain. A main goal is to provide reasonable defaults. It uses yaml extensively for configuration.

A basic demo implementation of the configuration can be found here: https://github.com/unprofession-al/configuration/tree/bf5a89b3eee7338899b28c047f3795546ce3d2e6

General

The main configuration looks like this:

type Config map[string]ConfigSection

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

Config holds a bunch of ConfigSections. This allow the user to define variations of the configuration (lets say prod, dev and testing for example) and use YAML Achors to do so.

The parts of the ConfigSection (Input and Output) would be defined in the package that consumes the configuration. Each of this part provides a Defaults() and a custom UnmarshalYAML() func. Also ConfigSection itself provides an UnmarshalYAML() func. This idea is stolen from https://github.com/go-yaml/yaml/issues/165#issuecomment-255223956.

Question

In data.go in the repo some test input as well as the expected output is defined. Running the tests (go test -v) shows:

  • Having nothing defined in the ConfigSection (empty example) no defaults are applied.
  • If a part (of a ConfigSection) with no data field is defined, this part will have no defaults. The "undefined" part has defaults (see input, output).
  • If both parts are defined (as is section both) but have no data fields, any defaults are set.

I see no pattern at all and ran out of ideas why this works like this and how to get the expected results (eg. get the test to pass).

  • 写回答

1条回答 默认 最新

  • dongren1353 2018-10-10 07:02
    关注

    Ok, so the pattern I did not see was quite obviouse: The "deepest leaf" of a configuration overwrites everything below, either with the given data or with the go defaults for empty values:

    That means that a struct like this...

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

    ... defaulted with the data...

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

    ... feeded with a yaml of this form..

    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
    

    ... turn out as...

    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
    

    For my use case this is a too complicated behavior, I therefore try a different approach: I if demanded I inject a default config into the yaml and put a reference to its anchor in each section. I feel like this is more transparent and reproducible to the end user. Heres an ugly draft of the func:

    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("
    "))
        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("
    "))
    
        // compose injected yaml
        out := []byte("---
    ")
        out = append(out, defaultSection...)
        out = append(out, updatedData...)
        return out, nil
    }
    

    Full example at: https://github.com/unprofession-al/configuration/tree/7c2eb7da58b51f52b50f2a0fbac193c799c9eb08

    本回答被题主选为最佳回答 , 对您是否有帮助呢?
    评论

报告相同问题?

悬赏问题

  • ¥50 高维数据处理方法求指导
  • ¥100 数字取证课程 关于FAT文件系统的操作
  • ¥15 如何使用js实现打印时每页设置统一的标题
  • ¥15 安装TIA PortalV15.1报错
  • ¥15 能把水桶搬到饮水机的机械设计
  • ¥15 Android Studio中如何把H5逻辑放在Assets 文件夹中以实现将h5代码打包为apk
  • ¥15 使用小程序wx.createWebAudioContext()开发节拍器
  • ¥15 关于#爬虫#的问题:请问HMDB代谢物爬虫的那个工具可以提供一下吗
  • ¥15 vue3+electron打包获取本地视频属性,文件夹里面有ffprobe.exe 文件还会报错这是什么原因呢?
  • ¥20 用51单片机控制急停。