goccy/go-yaml で yaml の decode/encode

はじめに

goccy/go-yaml というライブラリを使って yaml の decode/encode を行ってみます。

今回は後に openAPI の yaml ファイルを扱うことを想定し anchor/alias を利用したyamlファイルを分割した読み込みと、anchor/aliasを解決した後のyamlファイルの書き出しを行ってみます。

goccy/go-yaml 作者様の情熱溢れる解説記事はQiitaに上がっていました。
私もyamlファイルの分割時に anchor の使い回しをしたくこのライブラリに出会うことができました。

qiita.com

分割したyamlファイルの読み込み

yaml ファイルをまとめて1つのディレクトリに格納しておくと再起的に読み込み、anchor/aliasを解決したのちに decode してくれるようです。

yamlファイルの準備

今回は spec というディレクトリを作り3つのyamlファイルを格納します。
api.yamlからchild.yamlを参照しchild.yamlからchildという2階層目のディレクトリ内のgrandchild.yamlを参照する構成です。

$ tree spec
spec
├── api.yaml
├── child
│   └── grandchild.yaml
└── child.yaml

api.yamlyaml.NewDecoder() として読みこむファイルとして以下のように作成します。
aという構造に&aという alias をつけてbという構造から<<: *aと読み込んでいます。
cは別ファイルchild.yamlに定義します。(VSCode上は unidentified alias と怒られますが 問題なく decodeできます)

a: &a
  aa: 1
  ab: hello
b: 
  <<: *a
  ba: 2
c: *c

child.yamlapi.yamlで定義した*cの実体です。
dは1階層下の別ファイルgrandchild.yamlに定義します。

c: &c
  ca: 3
  d: *d

child/grandchild.yaml*dの実体です。

d: &d
  da: 4

yamlファイルの読み込み

package main

import (
    "bytes"
    "fmt"
    "io/ioutil"

    "github.com/goccy/go-yaml"
    "gopkg.in/go-playground/validator.v9"
)

type Api struct {
    A
    B
    C
}

type A struct {
    AA int
    AB string
}
type B struct {
    *A `yaml:",omitempty,inline"`
    BA int
}

type C struct {
    CA int
    *D
}

type D struct {
    DA int
}

func main() {
    // api.yaml の読み込んで*bytes.Bufferへ変換
    spec, _ := ioutil.ReadFile("./spec/api.yaml")
    buf := bytes.NewBuffer(spec)

    // validation
    validate := validator.New()

    dec := yaml.NewDecoder(
        buf,
        yaml.RecursiveDir(true),  // RecursiveDir(true) とするとReferenceDirs配下のyamlファイルを再起的に読み込んでくれる
        yaml.ReferenceDirs("spec"),
        yaml.Validator(validate),
    )

    // 定義した構造体ApiにDecodeする
    var api Api
    err := dec.Decode(&api)
    if err != nil {
        fmt.Println(err)
    }

    fmt.Printf("%+v\n", api)

}

実行すると以下のようにspecディレクトリ配下のyamlファイルを再起的に読み込み構造体にマッピングできています。
OpenAPIを読み込む時はここの構造体をOpenAPIの仕様とします。

$ go run main.go 
{A:{AA:1 AB:hello} B:{A:0xc0001f6780 BA:2} C:{CA:3 D:0xc0001eac28}}

anchor/aliasを解決したyamlの書き出し

yamlの書き出しは以下の4行を末尾に追加するだけです。
MergeKeyと呼ばれる<<: *aといったシンボルは構造体にyaml:",omitempty,inline" のtagを付与することで解決することができます。

out, err := yaml.Marshal(&api)
if err != nil {
    fmt.Println(err)
}
fmt.Println(string(out), "\n")

実行すると以下のようなanchor/aliasを解決したyamlを書き出すことができます。

$ go run main.go 
a:
  aa: 1
  ab: hello
b:
  aa: 1
  ab: hello
  ba: 2
c:
  ca: 3
  d:
    da: 4

まとめ

goccy/go-yaml というライブラリを使って yaml の decode/encode を行う方法をまとめました。