概要
通常はリストを取るが,要素が一つしかない場合はリストにせず値を直接書いて良い,
というフォーマットの YAML 文書を読み込む方法.
コメントにてMarshaler/Unmarshalerについて教えていただいたので修正 (2017年2月1日)
例
次の文書のように author はリストを受け取るのだが,
title: some book
author:
- Alice
- Bob
著者が一人しかいない場合は,
title: some book
author:
- Alice
と
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 が出力されてしまう.