🧞‍♂️

【Golang】テンプレートからEnumコード生成してみた(text/template)

2022/08/17に公開

少し探した感じでは見つけられなかったので,GolangでEnumコードを生成するコードジェネレーターを自分で書いた時の記録になります.
text/templateの具体例として参考にしていただければ幸いです.

何を作ったか

複数種の(単純な)Enumを大量生成するにあたって必要な情報が書かれたjsonファイルを入力とし,goのEnumコードを出力生成するコードジェネレーターを作成しました.

入力(jsonファイルenums.json

[
    {
        "type": "CITY",
        "package": "types",
        "data": [
            {
                "id": "TYO",
                "name": "Tokyo"
            },
            {
                "id": "NYC",
                "name": "New York"
            },
            {
                "id": "LDN",
                "name": "London"
            }
        ]
    },
    {
        "type": "COUNTRY",
        "package": "enums",
        "data": [
            {
                "id": "JPN",
                "name": "Japan"
            },
            {
                "id": "US",
                "name": "United States"
            },
            {
                "id": "UK",
                "name": "United Kingdom"
            }
        ]
    }
]

生成されたEnum(types/city.go)

// Code generated by go generate; DO NOT EDIT.
package types

type CITY int

const (
	TYO CITY = iota
	NYC CITY = iota
	LDN CITY = iota
)

func (e CITY) String() string {
    switch e {
	case TYO:
		return "Tokyo"
	case NYC:
		return "New York"
	case LDN:
		return "London"
    default:
        return "Unknown CITY"
    }
}

生成されたEnum(enums/country.go)

// Code generated by go generate; DO NOT EDIT.
package enums

type COUNTRY int

const (
	JPN COUNTRY = iota
	US COUNTRY = iota
	UK COUNTRY = iota
)

func (e COUNTRY) String() string {
    switch e {
	case JPN:
		return "Japan"
	case US:
		return "United States"
	case UK:
		return "United Kingdom"
    default:
        return "Unknown COUNTRY"
    }
}

TL;DR

package main

//go:generate go run main.go

import (
	_ "embed"
	"encoding/json"
	"fmt"
	"os"
	"strings"
	"text/template"
)

const (
	ENUM_FILE_NAME = "enum.json"
)

type Enum struct {
	Id   string `json:"id"`
	Name string `json:"name"`
}

type Enums struct {
	Type    string `json:"type"`
	Package string `json:"package"`
	Data    []Enum `json:"data"`
}

func main() {
	var enums []Enums
	enumBytes, _ := os.ReadFile(ENUM_FILE_NAME)
	if err := json.Unmarshal(enumBytes, &enums); err != nil {
		panic(err)
	}
	fmt.Println(enums)

	for _, e := range enums {
		os.Mkdir(e.Package, 0755)
		f, err := os.Create("./" + e.Package + "/" + strings.ToLower(e.Type) + ".go")
		if err != nil {
			panic(err)
		}
		defer f.Close()

		err = enumTemplate.Execute(f, e)
		if err != nil {
			panic(err)
		}
	}
}

var enumTemplate = template.Must(template.New("").Parse(`// Code generated by go generate; DO NOT EDIT.
package {{.Package}}
{{ $type := .Type }}
type {{$type}} int
{{ $data := .Data }}
const (
{{- range $d := $data }}
	{{ $d.Id }} {{ $type }} = iota
{{- end }}
)

func (e {{ $type }}) String() string {
    switch e {
{{- range $i, $d := .Data }}
	case {{ $d.Id }}:
		return {{ printf "%q" $d.Name }}
{{- end }}
    default:
        return "Unknown {{ $type }}"
    }
}
`))

使い方

$ go generate
[{CITY types [{TYO Tokyo} {NYC New York} {LDN London}]} {COUNTRY enums [{JPN Japan} {US United States} {UK United Kingdom}]}]

実行後,それぞれのpackage配下にファイルが生成されます.

個人的なハマりポイントとしては,テンプレートにおいて {{range}}{{end}} 内では {{.Type}} を呼び出すとエラーになるので,一回 {{ $type := .Type }} で変数置換する必要があるという点でした.

なお,Go 1.16 から採用されたgo:embedディレクティブを使うと,1バイナリになったコードジェネレーターが作れるので,実行時にjsonファイルがなくてもコード生成できるようになります.

まとめ

今回のこの単純な具体例を通して,text/templateでコード自動生成へ興味を持っていただけたなら幸いです.
また,間違いがありましたら指摘くださると助かります.

利用したコードは下記に格納してあります
https://github.com/tk42/go-enum-gen

参考

Discussion