From 7a40650336d825c05e1c1c303cad3f16e2f6bc5d Mon Sep 17 00:00:00 2001 From: Gabriel <45315755+gm0stache@users.noreply.github.com> Date: Sat, 8 Feb 2025 22:36:59 +0100 Subject: [PATCH 1/8] feat: draft generator logic --- cmd/ssg/main.go | 102 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 cmd/ssg/main.go diff --git a/cmd/ssg/main.go b/cmd/ssg/main.go new file mode 100644 index 0000000..e24cc39 --- /dev/null +++ b/cmd/ssg/main.go @@ -0,0 +1,102 @@ +// package main provides the functionality for the ssg (aka: schema struct generation) command. +package main + +import ( + "fmt" + "go/ast" + "go/parser" + "go/token" + "log" + "os" + "strconv" +) + +func main() { + fset := token.NewFileSet() + node, err := parser.ParseFile(fset, os.Getenv("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") + } + + generateWrappedStruct(node, targetType) +} + +func generateWrappedStruct(node *ast.File, targetType string) { + fmt.Printf("// Code generated by go generate; DO NOT EDIT.\n\n") + fmt.Printf("package %s\n\n", node.Name.Name) + + 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 { + fmt.Printf("type %sSchema struct {\n", targetType) + for _, field := range st.Fields.List { + typeExpr := exprToString(field.Type) + for _, name := range field.Names { + fmt.Printf("\t%s Wrapper[%s]", name.Name, typeExpr) + if field.Tag != nil { + fmt.Printf(" %s", field.Tag.Value) + } + fmt.Printf("\n") + } + } + fmt.Printf("}\n") + } + } + } + return true + }) +} + +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) + } +} From 11a48935ef7602105d2b9445269b1b005692d735 Mon Sep 17 00:00:00 2001 From: Gabriel <45315755+gm0stache@users.noreply.github.com> Date: Sat, 8 Feb 2025 23:48:05 +0100 Subject: [PATCH 2/8] refactor: use template for output --- cmd/ssg/main.go | 67 ++++++++++++++++++++++++++++++++++++++------ cmd/ssg/some_test.go | 12 ++++++++ cmd/ssg/todos.md | 3 ++ 3 files changed, 73 insertions(+), 9 deletions(-) create mode 100644 cmd/ssg/some_test.go create mode 100644 cmd/ssg/todos.md diff --git a/cmd/ssg/main.go b/cmd/ssg/main.go index e24cc39..6a32cba 100644 --- a/cmd/ssg/main.go +++ b/cmd/ssg/main.go @@ -9,9 +9,35 @@ import ( "log" "os" "strconv" + "text/template" ) +type GeneratorOutput struct { + PackageName string + StructName string + Fields []Field +} + +type Field struct { + Name string + Type string + Tags *string +} + +var tmpl *template.Template = template.Must(template.New("gen").Parse(` +// Code generated by zog ssg; DO NOT EDIT. + +package {{.Package}} + +type {{.TypeName}} struct { + {{-range $field := .Fields}} + {{$field.Name}} {{$field.Type}}{{if $field.Tags}} {{$field.Tags}}{{end}} + {{-end}} +} +`)) + func main() { + fset := token.NewFileSet() node, err := parser.ParseFile(fset, os.Getenv("GOFILE"), nil, parser.ParseComments) if err != nil { @@ -50,35 +76,58 @@ func main() { log.Fatal("no struct type found after //go:generate directive") } - generateWrappedStruct(node, targetType) + output, err := aggregateMetadata(node, targetType) + if err != nil { + log.Fatal("couldn't aggregate metadata:", err) + } + + // todo: create output file + // todo: pass output to template + // todo: write output to file } -func generateWrappedStruct(node *ast.File, targetType string) { - fmt.Printf("// Code generated by go generate; DO NOT EDIT.\n\n") - fmt.Printf("package %s\n\n", node.Name.Name) +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 { - fmt.Printf("type %sSchema struct {\n", targetType) + 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 { - fmt.Printf("\t%s Wrapper[%s]", name.Name, typeExpr) + f := Field{ + Name: name.Name, + Type: typeExpr, + } + if field.Tag != nil { - fmt.Printf(" %s", field.Tag.Value) + f.Tags = &field.Tag.Value } - fmt.Printf("\n") + + o.Fields = append(o.Fields, f) } } - fmt.Printf("}\n") + res = &o } } } return true }) + + if res == nil { + return nil, fmt.Errorf("could not aggregate metadata") + } + + return res, nil } func exprToString(expr ast.Expr) string { diff --git a/cmd/ssg/some_test.go b/cmd/ssg/some_test.go new file mode 100644 index 0000000..47486e7 --- /dev/null +++ b/cmd/ssg/some_test.go @@ -0,0 +1,12 @@ +package main_test + +//go:generate go run main.go +type MyType struct { + Field1 string + Field2 int +} + +// type MyOtherType struct { +// Field1 string +// Field2 int +// } diff --git a/cmd/ssg/todos.md b/cmd/ssg/todos.md new file mode 100644 index 0000000..ec16685 --- /dev/null +++ b/cmd/ssg/todos.md @@ -0,0 +1,3 @@ +1. enable basic zog.Schema type def +2. add stuff to enable code gen statements like `//go:generate zog ssg --output=output.go` +3. add tests From 2067efaa4907f725c6c9821d3b5b4bf8c38549e4 Mon Sep 17 00:00:00 2001 From: Gabriel <45315755+gm0stache@users.noreply.github.com> Date: Sun, 9 Feb 2025 12:34:20 +0100 Subject: [PATCH 3/8] feat: dump generated file --- cmd/ssg/main.go | 67 +++++++++++++++++++++++++++++++++----------- cmd/ssg/some_test.go | 4 +-- 2 files changed, 53 insertions(+), 18 deletions(-) diff --git a/cmd/ssg/main.go b/cmd/ssg/main.go index 6a32cba..6924209 100644 --- a/cmd/ssg/main.go +++ b/cmd/ssg/main.go @@ -2,16 +2,24 @@ package main 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 @@ -24,22 +32,21 @@ type Field struct { Tags *string } -var tmpl *template.Template = template.Must(template.New("gen").Parse(` -// Code generated by zog ssg; DO NOT EDIT. - -package {{.Package}} +func main() { + gofile := os.Getenv("GOFILE") + if gofile == "" { + log.Fatal("GOFILE environment variable is not set") + } -type {{.TypeName}} struct { - {{-range $field := .Fields}} - {{$field.Name}} {{$field.Type}}{{if $field.Tags}} {{$field.Tags}}{{end}} - {{-end}} -} -`)) + filename := filepath.Base(gofile) + ext := filepath.Ext(gofile) + filenameWithoutExt := strings.TrimSuffix(filename, ext) -func main() { + outputFilename := flag.String("output", filenameWithoutExt+defaultGenFileSuffix, "output file") + flag.Parse() fset := token.NewFileSet() - node, err := parser.ParseFile(fset, os.Getenv("GOFILE"), nil, parser.ParseComments) + node, err := parser.ParseFile(fset, gofile, nil, parser.ParseComments) if err != nil { log.Fatal(err) } @@ -76,14 +83,42 @@ func main() { log.Fatal("no struct type found after //go:generate directive") } - output, err := aggregateMetadata(node, targetType) + metadata, err := aggregateMetadata(node, targetType) if err != nil { log.Fatal("couldn't aggregate metadata:", err) } + metadata.StructName += defaultGenStructSuffix + + outputDir := filepath.Dir(*outputFilename) + 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}} + +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) + } - // todo: create output file - // todo: pass output to template - // todo: write output to file + err = t.Execute(of, metadata) + if err != nil { + log.Fatal("failed to execute template:", err) + } } func aggregateMetadata(node *ast.File, targetType string) (*GeneratorOutput, error) { diff --git a/cmd/ssg/some_test.go b/cmd/ssg/some_test.go index 47486e7..f89a5dd 100644 --- a/cmd/ssg/some_test.go +++ b/cmd/ssg/some_test.go @@ -1,8 +1,8 @@ -package main_test +package main //go:generate go run main.go type MyType struct { - Field1 string + Field1 string `json:"field1"` Field2 int } From af9e75c9c03c8f2c116acd38fdc887d57efad1e5 Mon Sep 17 00:00:00 2001 From: Gabriel <45315755+gm0stache@users.noreply.github.com> Date: Sun, 9 Feb 2025 13:50:08 +0100 Subject: [PATCH 4/8] feat: enable custom path dumping --- cmd/ssg/main.go | 17 +++++++++++++---- cmd/ssg/some_test.go | 11 ++++++----- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/cmd/ssg/main.go b/cmd/ssg/main.go index 6924209..63f60aa 100644 --- a/cmd/ssg/main.go +++ b/cmd/ssg/main.go @@ -42,8 +42,11 @@ func main() { ext := filepath.Ext(gofile) filenameWithoutExt := strings.TrimSuffix(filename, ext) - outputFilename := flag.String("output", filenameWithoutExt+defaultGenFileSuffix, "output file") - flag.Parse() + fs := flag.NewFlagSet("ssg", flag.ContinueOnError) + outputFilename := fs.String("output", filenameWithoutExt+defaultGenFileSuffix, "output file") + if err := fs.Parse(os.Args[2:]); err != nil { + log.Fatalf("could not parse command: %s", err.Error()) + } fset := token.NewFileSet() node, err := parser.ParseFile(fset, gofile, nil, parser.ParseComments) @@ -90,8 +93,10 @@ func main() { metadata.StructName += defaultGenStructSuffix outputDir := filepath.Dir(*outputFilename) - if err := os.MkdirAll(outputDir, 0o755); err != nil { - log.Fatal("failed to create output directory:", err) + if outputDir != "." { + if err := os.MkdirAll(outputDir, 0o755); err != nil { + log.Fatal("failed to create output directory:", err) + } } of, err := os.Create(*outputFilename) @@ -105,6 +110,10 @@ func main() { package {{.PackageName}} +import ( + "github.com/Oudwins/zog" +) + type {{.StructName}} struct { {{- range $field := .Fields}} {{$field.Name}} {{$field.Type}}{{if $field.Tags}} {{$field.Tags}}{{end}} diff --git a/cmd/ssg/some_test.go b/cmd/ssg/some_test.go index f89a5dd..a220958 100644 --- a/cmd/ssg/some_test.go +++ b/cmd/ssg/some_test.go @@ -1,12 +1,13 @@ package main -//go:generate go run main.go +//go:generate go run main.go ssg -output=./schema/generated.go type MyType struct { Field1 string `json:"field1"` Field2 int } -// type MyOtherType struct { -// Field1 string -// Field2 int -// } +//go:generate go run main.go ssg -output=./schema/generated.go +type MyOtherType struct { + Field1 string + Field2 int +} From 9b42c99381f6b5059f87df4676266361e4295cc7 Mon Sep 17 00:00:00 2001 From: Gabriel <45315755+gm0stache@users.noreply.github.com> Date: Sun, 9 Feb 2025 13:59:33 +0100 Subject: [PATCH 5/8] feat: convert field types to 'zog.ZogSchema' --- cmd/ssg/main.go | 23 +++++++++++++++++++++-- cmd/ssg/some_test.go | 6 +++++- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/cmd/ssg/main.go b/cmd/ssg/main.go index 63f60aa..18d618b 100644 --- a/cmd/ssg/main.go +++ b/cmd/ssg/main.go @@ -91,6 +91,7 @@ func main() { log.Fatal("couldn't aggregate metadata:", err) } metadata.StructName += defaultGenStructSuffix + metadata.Fields = convertFieldTypes(metadata.Fields) outputDir := filepath.Dir(*outputFilename) if outputDir != "." { @@ -105,8 +106,7 @@ func main() { } defer of.Close() - t, err := template.New("gen").Parse(` -// Code generated by zog ssg; DO NOT EDIT. + t, err := template.New("gen").Parse(`// Code generated by zog ssg; DO NOT EDIT. package {{.PackageName}} @@ -193,3 +193,22 @@ func exprToString(expr ast.Expr) string { return fmt.Sprintf("%#v", expr) } } + +func convertFieldTypes(fields []Field) []Field { + converted := make([]Field, len(fields)) + for i, field := range fields { + switch field.Type { + // 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/cmd/ssg/some_test.go b/cmd/ssg/some_test.go index a220958..965c4df 100644 --- a/cmd/ssg/some_test.go +++ b/cmd/ssg/some_test.go @@ -1,11 +1,15 @@ package main -//go:generate go run main.go ssg -output=./schema/generated.go +// example with most basic usage +// +//go:generate go run main.go ssg type MyType struct { Field1 string `json:"field1"` Field2 int } +// example with custom output path +// //go:generate go run main.go ssg -output=./schema/generated.go type MyOtherType struct { Field1 string From 8666729163c473275c865117f5b3c2a21e34a3c3 Mon Sep 17 00:00:00 2001 From: Gabriel <45315755+gm0stache@users.noreply.github.com> Date: Sun, 9 Feb 2025 14:20:24 +0100 Subject: [PATCH 6/8] feat: use 'zog' cmd --- cmd/zog/main.go | 17 +++++++++++++++++ cmd/{ => zog}/ssg/main.go | 8 ++++---- cmd/{ => zog}/ssg/todos.md | 0 {cmd/ssg => ssgtest}/some_test.go | 4 ++-- 4 files changed, 23 insertions(+), 6 deletions(-) create mode 100644 cmd/zog/main.go rename cmd/{ => zog}/ssg/main.go (96%) rename cmd/{ => zog}/ssg/todos.md (100%) rename {cmd/ssg => ssgtest}/some_test.go (59%) 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/ssg/main.go b/cmd/zog/ssg/main.go similarity index 96% rename from cmd/ssg/main.go rename to cmd/zog/ssg/main.go index 18d618b..9963f5b 100644 --- a/cmd/ssg/main.go +++ b/cmd/zog/ssg/main.go @@ -1,5 +1,5 @@ -// package main provides the functionality for the ssg (aka: schema struct generation) command. -package main +// package ssg provides the functionality for the ssg (aka: schema struct generation) command. +package ssg import ( "flag" @@ -32,7 +32,7 @@ type Field struct { Tags *string } -func main() { +func Run(osArgs []string) { gofile := os.Getenv("GOFILE") if gofile == "" { log.Fatal("GOFILE environment variable is not set") @@ -44,7 +44,7 @@ func main() { fs := flag.NewFlagSet("ssg", flag.ContinueOnError) outputFilename := fs.String("output", filenameWithoutExt+defaultGenFileSuffix, "output file") - if err := fs.Parse(os.Args[2:]); err != nil { + if err := fs.Parse(osArgs); err != nil { log.Fatalf("could not parse command: %s", err.Error()) } diff --git a/cmd/ssg/todos.md b/cmd/zog/ssg/todos.md similarity index 100% rename from cmd/ssg/todos.md rename to cmd/zog/ssg/todos.md diff --git a/cmd/ssg/some_test.go b/ssgtest/some_test.go similarity index 59% rename from cmd/ssg/some_test.go rename to ssgtest/some_test.go index 965c4df..7fda38d 100644 --- a/cmd/ssg/some_test.go +++ b/ssgtest/some_test.go @@ -2,7 +2,7 @@ package main // example with most basic usage // -//go:generate go run main.go ssg +//go:generate go run github.com/Oudwins/zog/cmd/zog ssg type MyType struct { Field1 string `json:"field1"` Field2 int @@ -10,7 +10,7 @@ type MyType struct { // example with custom output path // -//go:generate go run main.go ssg -output=./schema/generated.go +//go:generate go run github.com/Oudwins/zog/cmd/zog ssg -output=./schema/generated.go type MyOtherType struct { Field1 string Field2 int From e33286cfedc8d98ab79af024afc5ac0e60234f49 Mon Sep 17 00:00:00 2001 From: Gabriel <45315755+gm0stache@users.noreply.github.com> Date: Sun, 9 Feb 2025 14:32:08 +0100 Subject: [PATCH 7/8] chore: cleanup --- cmd/zog/ssg/{main.go => generator.go} | 2 ++ cmd/zog/ssg/todos.md | 3 --- 2 files changed, 2 insertions(+), 3 deletions(-) rename cmd/zog/ssg/{main.go => generator.go} (97%) delete mode 100644 cmd/zog/ssg/todos.md diff --git a/cmd/zog/ssg/main.go b/cmd/zog/ssg/generator.go similarity index 97% rename from cmd/zog/ssg/main.go rename to cmd/zog/ssg/generator.go index 9963f5b..e2bb62b 100644 --- a/cmd/zog/ssg/main.go +++ b/cmd/zog/ssg/generator.go @@ -198,6 +198,8 @@ func convertFieldTypes(fields []Field) []Field { converted := make([]Field, len(fields)) for i, field := range fields { switch field.Type { + // todo: this is where type should be mapped to the most narrowly corresponding schema types + // // case "string": // fields[i].Type = "string" // case "int": diff --git a/cmd/zog/ssg/todos.md b/cmd/zog/ssg/todos.md deleted file mode 100644 index ec16685..0000000 --- a/cmd/zog/ssg/todos.md +++ /dev/null @@ -1,3 +0,0 @@ -1. enable basic zog.Schema type def -2. add stuff to enable code gen statements like `//go:generate zog ssg --output=output.go` -3. add tests From 8146af7c80a623ac785a8cd23e60ed551beac382 Mon Sep 17 00:00:00 2001 From: Gabriel <45315755+gm0stache@users.noreply.github.com> Date: Sun, 9 Feb 2025 14:43:21 +0100 Subject: [PATCH 8/8] chore: fix typo --- cmd/zog/ssg/generator.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/zog/ssg/generator.go b/cmd/zog/ssg/generator.go index e2bb62b..6ef756f 100644 --- a/cmd/zog/ssg/generator.go +++ b/cmd/zog/ssg/generator.go @@ -198,7 +198,7 @@ func convertFieldTypes(fields []Field) []Field { converted := make([]Field, len(fields)) for i, field := range fields { switch field.Type { - // todo: this is where type should be mapped to the most narrowly corresponding schema types + // todo: this is where types should be mapped to the most narrowly corresponding schema types // // case "string": // fields[i].Type = "string"