go generateでORMモデル→domainモデルのmapperを自動生成

goを学習していく過程で　go generate を使って

既存のORMモデルを元に、ORMモデルをdomainモデルに変換するマッパーを自動生成するようにしました。

## フォルダ構成

クリーンアーキテクチャで構成しているため、必要もファイルのみ抜粋しています。


```
├── domains
│   ├── entity
│   │   ├── recipe.go                 　　　　　 # Domainモデル
│   │   ├── recipe_material.go                 # Domainモデル
└── repositories
    ├── model
    │   ├── recipe_model.go                    # ORMモデル
    │   └── recipe_material_model.go           # ORMモデル
    ├── gen
    │   ├── gen.go                    # generator
    │   └── mappter.tmpl              # 参照するテンプレート

```

## 元となるORMモデル

bunを使用して作成しています。

``` go
# repositories/rerecipe_model.go
package model

import (
	"time"

	"github.com/uptrace/bun"
)

// ORM モデル
type Recipe struct {
	bun.BaseModel `bun:"table:recipes"`

	Id        int64     `bun:"id,pk,type:uuid,default:gen_random_uuid()"`
	Title     string    `bun:"title,notnull"`
	Content   string    `bun:"content,notnull"`
	Image     string    `bun:"image"`
	CreatedAt time.Time `bun:"created_at,nullzero,notnull,default:current_timestamp"`
	UpdatedAt time.Time `bun:"updated_at,nullzero,notnull,default:current_timestamp"`

	RecipeMaterials []RecipeMaterial `bun:"rel:has-many,join:id=recipe_id"`
}




# repositories/rerecipe_material_model.go
package model

import (
	"time"

	"github.com/uptrace/bun"
)

// ORM モデル
type RecipeMaterial struct {
	bun.BaseModel `bun:"table:recipe_materials"`

	Id        int64     `bun:"id,pk,type:uuid,default:gen_random_uuid()"`
	RecipeId  int64     `bun:"recipe_id,notnull"`
	Name      string    `bun:"name,notnull"`
	CreatedAt time.Time `bun:"created_at,nullzero,notnull,default:current_timestamp"`
	UpdatedAt time.Time `bun:"updated_at,nullzero,notnull,default:current_timestamp"`
}

```

## generate のコード

今回は、recipe_model.go、recipe_material_model.go を元にmodelを生成するようにしています。

``` go
/repositories/gen/gen.go


//go:generate go run .
//go:generate gofmt -w ../

package main

import (
	"bytes"
	"fmt"
	"go/ast"
	"go/parser"
	"go/printer"
	"go/token"
	"log"
	"os"
	"strings"
	"text/template"
)

// 構造体定義
type StructDef struct {
	Name   string
	Fields []FieldDef
}

// 構造体フィールド定義
type FieldDef struct {
	Name      string
	Type      string
	ArrayType string
}

// 配列のフィールド保存用
type FieldArray struct {
	Name string
	Type string
}

// ファイルをパースしてASTから必要な構造体情報を返却
func parseFirstStruct(fpath string) ([]StructDef, []FieldArray, error) {

	// ファイルの中身をast構造にする
	fset := token.NewFileSet()
	f, err := parser.ParseFile(fset, fpath, nil, parser.Mode(0))
	if err != nil {
		return nil, nil, err
	}

	list := []StructDef{}
	farray := []FieldArray{}

	ast.Inspect(f, func(n ast.Node) bool {

		switch x := n.(type) {
		case *ast.TypeSpec:

			sdef := StructDef{}

			// 対象が構造体
			st, ok := x.Type.(*ast.StructType)
			if !ok {
				return true
			}
			sdef.Name = x.Name.Name

			for _, fld := range st.Fields.List {
				// bun.BaseModel `bun:"table:recipes"` などはNamesがない
				if fld.Names == nil {
					continue
				}

				arrayType := ""
				switch fldType := fld.Type.(type) {
				case *ast.ArrayType:

					var fldTypeBuf bytes.Buffer
					err := printer.Fprint(&fldTypeBuf, fset, fldType.Elt)
					if err != nil {
						log.Fatalf("failed printing %s", err)
					}

					arrayType = fldTypeBuf.String()
					farray = append(farray, FieldArray{Name: fld.Names[0].Name, Type: arrayType})

				}

				var typeNameBuf bytes.Buffer
				err := printer.Fprint(&typeNameBuf, fset, fld.Type)
				if err != nil {
					log.Fatalf("failed printing %s", err)
				}

				sdef.Fields = append(
					sdef.Fields,
					FieldDef{Name: fld.Names[0].Name,
						Type:      typeNameBuf.String(),
						ArrayType: arrayType,
					})
			}
			list = append(list, sdef)
		}
		return true

	})

	return list, farray, nil
}

// マッパーファイルを生成
func createMapperFile(outputFilePath string, def StructDef, fa []FieldArray) error {
	file, err := os.Create(outputFilePath)
	if err != nil {
		return err
	}
	defer file.Close()

	t := template.Must(template.ParseFiles("./mappter.tmpl"))
	data := map[string]interface{}{
		"ModelName":   def.Name, // "Comment"
		"Fields":      def.Fields,
		"ArrayFields": fa,
	}

	if err := t.Execute(file, data); err != nil {
		return err
	}
	fmt.Println(outputFilePath + " is generated.")
	return nil
}

// エントリーポイント
func main() {

	inputFilePaths := []string{"../recipe_model.go", "../recipe_material_model.go"}

	for _, filepath := range inputFilePaths {
		list, farray, err := parseFirstStruct(filepath)
		if err != nil || len(list) == 0 {
			fmt.Fprintf(os.Stderr, "model parse faild.\n: %s", err)
			os.Exit(1)
		}

		outputFilePath := "../" + strings.Split(strings.Split(filepath, "/")[1], ".")[0] + "_mapper.gen.go"
		err = createMapperFile(outputFilePath, list[0], farray)
		if err != nil {
			fmt.Fprintf(os.Stderr, "code generate failed.\n: %s", err)
			os.Exit(1)
		}
	}
}

```

## generate実行
```
/repositories/model/gen # go generate .
../recipe_model_mapper.gen.go is generated.
../recipe_material_model_mapper.gen.go is generated.
```

``` go
/repositories/gen/mapper.tmpl
// Code generated by go generate DO NOT EDIT.

package model

import "github.com/react/next-sample/backend/domain/entity"

// ORM モデルを Entity に変換
func (db{{ .ModelName }} *{{ .ModelName }}) ToEntity() *entity.{{ .ModelName }} {

    {{range .ArrayFields -}}
    entity{{ .Type }} := make([]entity.{{ .Type }}, len(db{{ $.ModelName }}.{{ .Name }}))
    for i, tmpData := range db{{ $.ModelName }}.{{ .Name }} {
      entity{{ .Type }}[i] = *tmpData.ToEntity()
    }
    {{end -}}


    return &entity.{{ .ModelName }}{
    {{range .Fields -}}
      {{ .Name }}:  
    {{if ne .ArrayType "" -}}
      entity{{ .ArrayType -}} 
    {{ else -}}
      db{{ $.ModelName }}.{{ .Name -}}
    {{ end -}},
    {{end -}}
    }
}

func To{{ .ModelName }}Mapper(entity *entity.{{ .ModelName }}) *{{ .ModelName }} {
    return &{{ .ModelName }}{
    {{range .Fields -}}
    {{if ne .ArrayType "" -}}
    {{ else -}}
    {{ .Name }}:  entity.{{ .Name }},
    {{end -}}
    {{end -}}
    }
}

```

## 自動生成されたモデル

``` go
/repositories/model/recipe_model_mapper.gen.go


// Code generated by go generate DO NOT EDIT.

package model

import "github.com/react/next-sample/backend/domain/entity"

// ORM モデルを Entity に変換
func (dbRecipe *Recipe) ToEntity() *entity.Recipe {

	entityRecipeMaterial := make([]entity.RecipeMaterial, len(dbRecipe.RecipeMaterials))
	for i, tmpData := range dbRecipe.RecipeMaterials {
		entityRecipeMaterial[i] = *tmpData.ToEntity()
	}
	return &entity.Recipe{
		Id:              dbRecipe.Id,
		Title:           dbRecipe.Title,
		Content:         dbRecipe.Content,
		Image:           dbRecipe.Image,
		CreatedAt:       dbRecipe.CreatedAt,
		UpdatedAt:       dbRecipe.UpdatedAt,
		RecipeMaterials: entityRecipeMaterial,
	}
}

func ToRecipeMapper(entity *entity.Recipe) *Recipe {
	return &Recipe{
		Id:        entity.Id,
		Title:     entity.Title,
		Content:   entity.Content,
		Image:     entity.Image,
		CreatedAt: entity.CreatedAt,
		UpdatedAt: entity.UpdatedAt,
	}
}





/repositories/model/recipe_material_model_mapper.gen.go

// Code generated by go generate DO NOT EDIT.

package model

import "github.com/react/next-sample/backend/domain/entity"

// ORM モデルを Entity に変換
func (dbRecipeMaterial *RecipeMaterial) ToEntity() *entity.RecipeMaterial {

	return &entity.RecipeMaterial{
		Id:        dbRecipeMaterial.Id,
		RecipeId:  dbRecipeMaterial.RecipeId,
		Name:      dbRecipeMaterial.Name,
		CreatedAt: dbRecipeMaterial.CreatedAt,
		UpdatedAt: dbRecipeMaterial.UpdatedAt,
	}
}

func ToRecipeMaterialMapper(entity *entity.RecipeMaterial) *RecipeMaterial {
	return &RecipeMaterial{
		Id:        entity.Id,
		RecipeId:  entity.RecipeId,
		Name:      entity.Name,
		CreatedAt: entity.CreatedAt,
		UpdatedAt: entity.UpdatedAt,
	}
}


```

## 参考サイト
https://rinoguchi.net/2022/09/go-generate.html

https://monpoke1.hatenablog.com/entry/2018/12/16/110943