LoginSignup
5
3

More than 5 years have passed since last update.

リストまたは値のいずれかを取るYAMLの読み込み

Last updated at Posted at 2017-01-31

概要

通常はリストを取るが,要素が一つしかない場合はリストにせず値を直接書いて良い,
というフォーマットの YAML 文書を読み込む方法.

コメントにてMarshaler/Unmarshalerについて教えていただいたので修正 (2017年2月1日)

次の文書のように author はリストを受け取るのだが,

book.yml
title: some book
author:
  - Alice
  - Bob

著者が一人しかいない場合は,

book.yml
title: some book
author:
  - Alice

book.yml
title: some book
author: Alice

のどちらでも OK というような場合を考える.
この時,次のような構造体を用意して,

type Book struct {
    Title string
    Author []string
}

yaml.Unmarshal で次のように読み込もうとすると,author がリストになっていない場合にエラーになる.

t := &Book{}

// data は bool.yml のバイト列
err := yaml.Unmarshal([]byte(data), t)
if err != nil {
  log.Fatalf("error: %v", err)
}

解決策

Author の型が string と []string の二種類の場合があることが問題なので,
まずは一般的な interface{} で受けておいて,後で変換することにする.

補助の構造体を使って読み込むには,Unmarshalerを実装すれば良いらしい.(参考:Decoding YAML in Go

まず, Unmarshaler を適用させたい項目用の型を定義する.

type ListOrString []string

type Book struct {
    Title string
    Author ListOrString
}

次に,この ListOrString 型に Unmarshaler を実装させる.

func (e *ListOrString) UnmarshalYAML(unmarshal func(interface{}) error) (err error) {

    var aux interface{}
    if err = unmarshal(&aux); err != nil {
        return
    }

    switch raw := aux.(type) {
    case string:
        *e = []string{raw}

    case []interface{}:
        list := make([]string, len(raw))
        for i, r := range raw {
            v, ok := r.(string)
            if !ok {
                return fmt.Errorf("An item in evn cannot be converted to a string: %v", aux)
            }
            list[i] = v
        }
        *e = list

    }
    return
}

UnmarshalYAML の中では,unmarshal関数を使って中間的な構造体(aux)に値を読み込ませる.
その後,必要な変換を行なって,目的の変数(*e)に値を保存する.

解決策(旧)

最初に投稿したバージョンも一応残しておきます

Author の型が string と []string の二種類の場合があることが問題なので,
まずは一般的な interface{} で受けておいて,後で変換することにする.

type Book struct {
    Title string
    RawAuthor interface{} `yaml:"author"`
    Author []string `yaml:"_author,omitempty"`
}

YAML 文書中の author 属性を一旦 interface{} 型の RawAuthor に入れるためにタグを設定する.
タグを設定していても Author 属性があると,そちらが優先されてしまうようなので,
Author 属性の方には,ダミーの _author を与えておく.

次に, refrect を使って RawAuthor から Author へ値をコピーする.
次のメソッドはより一般的に, 属性名を与えると Raw 付きの属性値から与えられた属性に値をコピーする.

func (t *Book) parseRawField(name string) (err error) {

    r := reflect.Indirect(reflect.ValueOf(t))
    src := r.FieldByName(fmt.Sprintf("Raw%s", name))
    dest := r.FieldByName(name)

    switch raw := src.Interface().(type) {
    // 単一文字列の場合
    case string:
        dest.Set(reflect.ValueOf([]string{raw}))

    // リストの場合
    case []interface{}:
        list := make([]string, len(raw))
        for i, r := range raw {
            v, ok := r.(string)
            if !ok {
                return fmt.Errorf("Cannot convert to a string: %v", r)
            }
            list[i] = v
        }
        dest.Set(reflect.ValueOf(list))
    }
    return

}

これを用いて,

t := &Book{}

// data は bool.yml のバイト列
err := yaml.Unmarshal([]byte(data), t)
if err != nil {
  log.Fatalf("error: %v", err)
}
t.parseRawField("Author")

なお,この後 Marshal することがなければ, RawAuthor は nil 設定しても良い.
逆に Marshal する場合は, Author の方を nil にしてからでないと _author が出力されてしまう.

5
3
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3