diff --git a/cmd/zog/main.go b/cmd/zog/main.go new file mode 100644 index 0000000..234808e --- /dev/null +++ b/cmd/zog/main.go @@ -0,0 +1,17 @@ +package main + +import ( + "fmt" + "os" + + "github.com/Oudwins/zog/cmd/zog/ssg" +) + +func main() { + switch os.Args[1] { + case "ssg": + ssg.Run(os.Args[2:]) + default: + fmt.Println("Unknown command") + } +} diff --git a/cmd/zog/ssg/generator.go b/cmd/zog/ssg/generator.go new file mode 100644 index 0000000..6ef756f --- /dev/null +++ b/cmd/zog/ssg/generator.go @@ -0,0 +1,216 @@ +// package ssg provides the functionality for the ssg (aka: schema struct generation) command. +package ssg + +import ( + "flag" + "fmt" + "go/ast" + "go/parser" + "go/token" + "log" + "os" + "path/filepath" + "strconv" + "strings" + "text/template" +) + +const ( + defaultGenFileSuffix = "_gen.go" + defaultGenStructSuffix = "Schema" +) + +type GeneratorOutput struct { + PackageName string + StructName string + Fields []Field +} + +type Field struct { + Name string + Type string + Tags *string +} + +func Run(osArgs []string) { + gofile := os.Getenv("GOFILE") + if gofile == "" { + log.Fatal("GOFILE environment variable is not set") + } + + filename := filepath.Base(gofile) + ext := filepath.Ext(gofile) + filenameWithoutExt := strings.TrimSuffix(filename, ext) + + fs := flag.NewFlagSet("ssg", flag.ContinueOnError) + outputFilename := fs.String("output", filenameWithoutExt+defaultGenFileSuffix, "output file") + if err := fs.Parse(osArgs); err != nil { + log.Fatalf("could not parse command: %s", err.Error()) + } + + fset := token.NewFileSet() + node, err := parser.ParseFile(fset, gofile, nil, parser.ParseComments) + if err != nil { + log.Fatal(err) + } + + genLine, err := strconv.Atoi(os.Getenv("GOLINE")) + if err != nil { + log.Fatal("couldn't parse $GOLINE:", err) + } + + var targetType string + ast.Inspect(node, func(n ast.Node) bool { + switch x := n.(type) { + case *ast.GenDecl: + if x.Tok != token.TYPE { + return true + } + if fset.Position(x.Pos()).Line != genLine+1 { + return true + } + for _, spec := range x.Specs { + if ts, ok := spec.(*ast.TypeSpec); ok { + if _, ok := ts.Type.(*ast.StructType); !ok { + log.Fatalf("%s is not a struct type", ts.Name.Name) + } + targetType = ts.Name.Name + return false + } + } + } + return true + }) + + if targetType == "" { + log.Fatal("no struct type found after //go:generate directive") + } + + metadata, err := aggregateMetadata(node, targetType) + if err != nil { + log.Fatal("couldn't aggregate metadata:", err) + } + metadata.StructName += defaultGenStructSuffix + metadata.Fields = convertFieldTypes(metadata.Fields) + + outputDir := filepath.Dir(*outputFilename) + if outputDir != "." { + if err := os.MkdirAll(outputDir, 0o755); err != nil { + log.Fatal("failed to create output directory:", err) + } + } + + of, err := os.Create(*outputFilename) + if err != nil { + log.Fatal("failed to create output file:", err) + } + defer of.Close() + + t, err := template.New("gen").Parse(`// Code generated by zog ssg; DO NOT EDIT. + +package {{.PackageName}} + +import ( + "github.com/Oudwins/zog" +) + +type {{.StructName}} struct { + {{- range $field := .Fields}} + {{$field.Name}} {{$field.Type}}{{if $field.Tags}} {{$field.Tags}}{{end}} + {{- end}} +} +`) + if err != nil { + log.Fatal("failed to parse template:", err) + } + + err = t.Execute(of, metadata) + if err != nil { + log.Fatal("failed to execute template:", err) + } +} + +func aggregateMetadata(node *ast.File, targetType string) (*GeneratorOutput, error) { + var res *GeneratorOutput + + ast.Inspect(node, func(n ast.Node) bool { + switch x := n.(type) { + case *ast.TypeSpec: + if x.Name.Name == targetType { + if st, ok := x.Type.(*ast.StructType); ok { + o := GeneratorOutput{ + PackageName: node.Name.Name, + StructName: targetType, + Fields: []Field{}, + } + + for _, field := range st.Fields.List { + typeExpr := exprToString(field.Type) + + for _, name := range field.Names { + f := Field{ + Name: name.Name, + Type: typeExpr, + } + + if field.Tag != nil { + f.Tags = &field.Tag.Value + } + + o.Fields = append(o.Fields, f) + } + } + res = &o + } + } + } + return true + }) + + if res == nil { + return nil, fmt.Errorf("could not aggregate metadata") + } + + return res, nil +} + +func exprToString(expr ast.Expr) string { + switch t := expr.(type) { + case *ast.Ident: + return t.Name + case *ast.SelectorExpr: + return exprToString(t.X) + "." + t.Sel.Name + case *ast.StarExpr: + return "*" + exprToString(t.X) + case *ast.ArrayType: + if t.Len == nil { + return "[]" + exprToString(t.Elt) + } + return fmt.Sprintf("[%s]%s", exprToString(t.Len), exprToString(t.Elt)) + case *ast.MapType: + return fmt.Sprintf("map[%s]%s", exprToString(t.Key), exprToString(t.Value)) + default: + return fmt.Sprintf("%#v", expr) + } +} + +func convertFieldTypes(fields []Field) []Field { + converted := make([]Field, len(fields)) + for i, field := range fields { + switch field.Type { + // todo: this is where types should be mapped to the most narrowly corresponding schema types + // + // case "string": + // fields[i].Type = "string" + // case "int": + // fields[i].Type = "int" + // case "float64": + // fields[i].Type = "float64" + default: + field.Type = "zog.ZogSchema" + converted[i] = field + } + } + + return converted +} diff --git a/ssgtest/some_test.go b/ssgtest/some_test.go new file mode 100644 index 0000000..7fda38d --- /dev/null +++ b/ssgtest/some_test.go @@ -0,0 +1,17 @@ +package main + +// example with most basic usage +// +//go:generate go run github.com/Oudwins/zog/cmd/zog ssg +type MyType struct { + Field1 string `json:"field1"` + Field2 int +} + +// example with custom output path +// +//go:generate go run github.com/Oudwins/zog/cmd/zog ssg -output=./schema/generated.go +type MyOtherType struct { + Field1 string + Field2 int +}