From 621a5b6ffc3a0654df6680c74626c7f7b71bb972 Mon Sep 17 00:00:00 2001 From: Jai Radhakrishnan <55522316+jairad26@users.noreply.github.com> Date: Tue, 8 Oct 2024 10:00:44 -0700 Subject: [PATCH] auto generate scaffolded functions from types --- sdk/go/examples/generate-func/build.cmd | 10 + sdk/go/examples/generate-func/build.sh | 12 + sdk/go/examples/generate-func/go.mod | 7 + sdk/go/examples/generate-func/hypermode.json | 15 ++ sdk/go/examples/generate-func/main.go | 41 +++ .../modus-go-build/extractor/extractor.go | 2 + .../modus-go-build/extractor/functions.go | 12 + .../modus-go-build/extractor/gen_templates.go | 222 ++++++++++++++++ .../modus-go-build/extractor/generator.go | 237 ++++++++++++++++++ 9 files changed, 558 insertions(+) create mode 100644 sdk/go/examples/generate-func/build.cmd create mode 100755 sdk/go/examples/generate-func/build.sh create mode 100644 sdk/go/examples/generate-func/go.mod create mode 100644 sdk/go/examples/generate-func/hypermode.json create mode 100644 sdk/go/examples/generate-func/main.go create mode 100644 sdk/go/tools/modus-go-build/extractor/gen_templates.go create mode 100644 sdk/go/tools/modus-go-build/extractor/generator.go diff --git a/sdk/go/examples/generate-func/build.cmd b/sdk/go/examples/generate-func/build.cmd new file mode 100644 index 00000000..ed6c59a5 --- /dev/null +++ b/sdk/go/examples/generate-func/build.cmd @@ -0,0 +1,10 @@ +@echo off + +:: This build script works best for examples that are in this repository. +:: If you are using this as a template for your own project, you may need to modify this script, +:: to invoke the hypbuild tool with the correct path to your project. + +SET "PROJECTDIR=%~dp0" +pushd ..\..\tools\hypbuild > nul +go run . "%PROJECTDIR%" +popd > nul diff --git a/sdk/go/examples/generate-func/build.sh b/sdk/go/examples/generate-func/build.sh new file mode 100755 index 00000000..02743fec --- /dev/null +++ b/sdk/go/examples/generate-func/build.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# This build script works best for examples that are in this repository. +# If you are using this as a template for your own project, you may need to modify this script, +# to invoke the modus-go-build tool with the correct path to your project. + +PROJECTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +pushd ../../tools/modus-go-build > /dev/null +go run . "$PROJECTDIR" +exit_code=$? +popd > /dev/null +exit $exit_code diff --git a/sdk/go/examples/generate-func/go.mod b/sdk/go/examples/generate-func/go.mod new file mode 100644 index 00000000..f6e7e033 --- /dev/null +++ b/sdk/go/examples/generate-func/go.mod @@ -0,0 +1,7 @@ +module generate-hyp-example + +go 1.23.0 + +require github.com/hypermodeinc/modus/sdk/go v0.0.0 + +replace github.com/hypermodeinc/modus/sdk/go => ../../ diff --git a/sdk/go/examples/generate-func/hypermode.json b/sdk/go/examples/generate-func/hypermode.json new file mode 100644 index 00000000..9fef6758 --- /dev/null +++ b/sdk/go/examples/generate-func/hypermode.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://manifest.hypermode.com/hypermode.json", + "models": { + // No models are used by this example, but if you add any, they would go here. + }, + "hosts": { + // This defines the dgraph host that is used by the example functions. + // The {{API_KEY}} will be replaced by the secret provided in the Hypermode Console. + "dgraph": { + "type": "dgraph", + "grpcTarget": "frozen-mango.grpc.eu-central-1.aws.cloud.dgraph.io:443", + "key": "{{API_KEY}}" + } + } +} diff --git a/sdk/go/examples/generate-func/main.go b/sdk/go/examples/generate-func/main.go new file mode 100644 index 00000000..86ca10ad --- /dev/null +++ b/sdk/go/examples/generate-func/main.go @@ -0,0 +1,41 @@ +package main + +import ( + "github.com/hypermodeinc/modus/sdk/go/pkg/dgraph" +) + +func AlterSchema() error { + const schema = ` + name: string @index(term) . + description: string @index(term) . + price: float . + items: [uid] @reverse . + + type Product { + name + description + price + } + + type ShoppingCart { + items + } + ` + return dgraph.AlterSchema("dgraph", schema) +} + +//hyp:generate +type Product struct { + Uid string `json:"uid,omitempty"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + Price float64 `json:"price,omitempty"` + DType []string `json:"dgraph.type,omitempty"` +} + +//hyp:generate +type ShoppingCart struct { + Uid string `json:"uid,omitempty"` + Items []Product `json:"items,omitempty"` + DType []string `json:"dgraph.type,omitempty"` +} diff --git a/sdk/go/tools/modus-go-build/extractor/extractor.go b/sdk/go/tools/modus-go-build/extractor/extractor.go index 6d5c44ad..227b79cd 100644 --- a/sdk/go/tools/modus-go-build/extractor/extractor.go +++ b/sdk/go/tools/modus-go-build/extractor/extractor.go @@ -27,6 +27,8 @@ func CollectProgramInfo(config *config.Config, meta *metadata.Metadata, wasmFunc requiredTypes := make(map[string]types.Type) + generateFunctions(config.SourceDir, pkgs) + for name, f := range getExportedFunctions(pkgs) { if _, ok := wasmFunctions.Exports[name]; ok { meta.FnExports[name] = transformFunc(name, f) diff --git a/sdk/go/tools/modus-go-build/extractor/functions.go b/sdk/go/tools/modus-go-build/extractor/functions.go index c875f1f6..46381a2a 100644 --- a/sdk/go/tools/modus-go-build/extractor/functions.go +++ b/sdk/go/tools/modus-go-build/extractor/functions.go @@ -80,6 +80,18 @@ func getImportedFunctions(pkgs map[string]*packages.Package) map[string]*types.F return results } +func getHypGenTypeName(genDecl *ast.GenDecl) (string, bool) { + if genDecl.Doc == nil { + return "", false + } + for _, c := range genDecl.Doc.List { + if strings.HasPrefix(c.Text, "//hyp:generate") { + return strings.TrimSpace(strings.TrimPrefix(c.Text, "//hyp:generate")), true + } + } + return "", false +} + func getExportedFuncName(fn *ast.FuncDecl) string { /* Exported functions must have a body, and are decorated in one of the following forms: diff --git a/sdk/go/tools/modus-go-build/extractor/gen_templates.go b/sdk/go/tools/modus-go-build/extractor/gen_templates.go new file mode 100644 index 00000000..28c79db2 --- /dev/null +++ b/sdk/go/tools/modus-go-build/extractor/gen_templates.go @@ -0,0 +1,222 @@ +package extractor + +const upsertTmplStruct = ` + +// Upsert{{.StructName}}Input struct +type Upsert{{.StructName}}Input struct { +{{- range .Fields}} + {{- if and (not (eq (lower .Name) "uid")) (not (eq (lower .Name) "dtype"))}} + {{- if .IsComplexType}} + {{- if .IsSliceType}} + {{.Name}} []string + {{- else}} + {{.Name}} string + {{- end}} + {{- else}} + {{.Name}} {{.Type}} + {{- end}} + {{- end}} +{{- end}} +} + +// Upsert{{$.StructName}} method +func Upsert{{$.StructName}}(input Upsert{{$.StructName}}Input) (map[string]string, error) { + // Convert input to the original struct type +{{- if $.HasComplexFields}} + type Temp{{$.StructName}} struct { + {{- range .Fields}} + {{- if .IsComplexType}} + {{- if .IsSliceType}} + {{.Name}} []string ` + "`{{.Tag}}`" + ` + {{- else}} + {{.Name}} string ` + "`{{.Tag}}`" + ` + {{- end}} + {{- else}} + {{.Name}} {{.Type}} ` + "`{{.Tag}}`" + ` + {{- end}} + {{- end}} + } + + p := Temp{{$.StructName}}{ + {{- range .Fields}} + {{- if not (eq (lower .Name) "uid")}} + {{- if eq (.Name) "DType"}} + {{.Name}}: []string{"{{$.StructName}}"}, + {{- else}} + {{.Name}}: input.{{.Name}}, + {{- end}} + {{- end}} + {{- if eq (lower .Name) "uid"}} + {{.Name}}: "_:{{lower $.StructName}}", + {{- end}} + {{- end}} + } +{{- else}} + p := {{$.StructName}}{ + {{- range .Fields}} + {{- if not (eq (lower .Name) "uid")}} + {{- if eq (.Name) "DType"}} + {{.Name}}: []string{"{{$.StructName}}"}, + {{- else}} + {{.Name}}: input.{{.Name}}, + {{- end}} + {{- end}} + {{- if eq (lower .Name) "uid"}} + {{.Name}}: "_:{{lower $.StructName}}", + {{- end}} + {{- end}} + } +{{- end}} + + // Convert the struct to JSON + jsonBytes, err := json.Marshal(p) + if err != nil { + return nil, err + } + + response, err := dgraph.Execute(hostName, &dgraph.Request{ + Mutations: []*dgraph.Mutation{ + { + SetJson: string(jsonBytes), + }, + }, + }) + if err != nil { + return nil, err + } + return response.Uids, nil +} + + +` + +const deleteTmplStruct = ` +// Delete{{.StructName}}Input struct +type Delete{{.StructName}}Input struct { + Uid string +} + +// Delete{{$.StructName}} method +func Delete{{$.StructName}}(input Delete{{$.StructName}}Input) (string, error) { + statement := fmt.Sprintf("<%s> * * .", input.Uid) + + _, err := dgraph.Execute(hostName, &dgraph.Request{ + Mutations: []*dgraph.Mutation{ + { + DelNquads: statement, + }, + }, + }) + if err != nil { + return "", err + } + + return "success", nil +} + + +` + +const getTmplStruct = ` +// Get{{.StructName}} method +func Get{{.StructName}}(uid string) (*{{.StructName}}, error) { + statement := ` + "`" + + ` + query query{{.StructName}}($uid: string) { + {{lower .StructName}}(func: uid($uid)) { + uid + dgraph.type +{{- range .Fields}} + {{- if and (not (eq (lower .Name) "uid")) (not (eq (lower .Name) "dtype")) }} + {{- if .IsComplexType}} + {{lower .Name}} { + uid + dgraph.type + expand(_all_) + } + {{- else}} + {{lower .Name}} + {{- end}} + {{- end}} +{{- end}} + } + } +` + "`" + ` + variables := map[string]string{"$uid": uid} + response, err := dgraph.Execute(hostName, &dgraph.Request{ + Query: &dgraph.Query{ + Query: statement, + Variables: variables, + }, + }) + if err != nil { + return nil, err + } + type {{.StructName}}Data struct { + {{.StructName}}s []*{{.StructName}} ` + "`json:\"{{lower .StructName}}\"`" + ` + } + var {{lower .StructName}}Data {{.StructName}}Data + if err := json.Unmarshal([]byte(response.Json), &{{lower .StructName}}Data); err != nil { + return nil, err + } + + if len({{lower .StructName}}Data.{{.StructName}}s) == 0 { + return nil, fmt.Errorf("{{.StructName}} not found") + } + + return {{lower .StructName}}Data.{{.StructName}}s[0], nil +} + + +` + +const queryTmplStruct = ` +// Query{{.StructName}} method +func Query{{.StructName}}(limit, offset int) ([]*{{.StructName}}, error) { + statement := ` + "`" + + ` + query query{{.StructName}}($limit: int, $offset: int) { + {{lower .StructName}}(func: type({{.StructName}}), first: $limit, offset: $offset) { + uid + dgraph.type +{{- range .Fields}} + {{- if and (not (eq (lower .Name) "uid")) (not (eq (lower .Name) "dtype")) }} + {{- if .IsComplexType}} + {{lower .Name}} { + uid + dgraph.type + expand(_all_) + } + {{- else}} + {{lower .Name}} + {{- end}} + {{- end}} +{{- end}} + } + } +` + "`" + ` + variables := map[string]string{"$limit": fmt.Sprintf("%d", limit), "$offset": fmt.Sprintf("%d", offset)} + response, err := dgraph.Execute(hostName, &dgraph.Request{ + Query: &dgraph.Query{ + Query: statement, + Variables: variables, + }, + }) + if err != nil { + return nil, err + } + type {{.StructName}}Data struct { + {{.StructName}}s []*{{.StructName}} ` + "`json:\"{{lower .StructName}}\"`" + ` + } + var {{lower .StructName}}Data {{.StructName}}Data + if err := json.Unmarshal([]byte(response.Json), &{{lower .StructName}}Data); err != nil { + return nil, err + } + + if len({{lower .StructName}}Data.{{.StructName}}s) == 0 { + return nil, fmt.Errorf("{{.StructName}} not found") + } + + return {{lower .StructName}}Data.{{.StructName}}s, nil +} +` diff --git a/sdk/go/tools/modus-go-build/extractor/generator.go b/sdk/go/tools/modus-go-build/extractor/generator.go new file mode 100644 index 00000000..129ba5a2 --- /dev/null +++ b/sdk/go/tools/modus-go-build/extractor/generator.go @@ -0,0 +1,237 @@ +package extractor + +import ( + "bytes" + "fmt" + "go/ast" + "go/format" + "go/types" + "os" + "path/filepath" + "slices" + "strings" + "text/template" + + "golang.org/x/tools/go/packages" +) + +type Field struct { + Name string + Type string + Tag string + IsComplexType bool + IsSliceType bool +} + +type TemplateData struct { + StructName string + HasComplexFields bool + Fields []Field + OmittedFuncs []string +} + +func isSlice(t types.Type) bool { + _, ok := t.(*types.Slice) + return ok +} + +func hasNamedObject(typ types.Type) bool { + switch t := typ.(type) { + case *types.Slice: + return hasNamedObject(t.Elem()) + case *types.Array: + return hasNamedObject(t.Elem()) + case *types.Pointer: + return hasNamedObject(t.Elem()) + case *types.Map: + return hasNamedObject(t.Key()) || hasNamedObject(t.Elem()) + case *types.Named: + // If we find a named type, return true + return true + case *types.Basic: + return false + default: + // Handle other cases, e.g., interfaces, maps, etc. + // If these types can contain other types, you should add appropriate checks + return false + } +} + +func formatType(typ types.Type) string { + switch t := typ.(type) { + case *types.Slice: + return "[]" + formatType(t.Elem()) + case *types.Array: + return fmt.Sprintf("[%d]%s", t.Len(), formatType(t.Elem())) + case *types.Pointer: + return "*" + formatType(t.Elem()) + case *types.Map: + return "map[" + formatType(t.Key()) + "]" + formatType(t.Elem()) + case *types.Named: + // Extract just the type name, omit the package name + return t.Obj().Name() + case *types.Basic: + return t.Name() + default: + // Handle other cases, e.g., interfaces, maps, etc. + return t.String() + } +} + +func getExportedTypes(pkgs map[string]*packages.Package) []TemplateData { + var structs []TemplateData + for _, pkg := range pkgs { + for _, file := range pkg.Syntax { + for _, decl := range file.Decls { + if genDecl, ok := decl.(*ast.GenDecl); ok { + if attrs, toGen := getHypGenTypeName(genDecl); toGen { + for _, spec := range genDecl.Specs { + typeSpec, ok := spec.(*ast.TypeSpec) + if !ok { + continue + } + // Lookup the struct type + obj := pkg.Types.Scope().Lookup(typeSpec.Name.Name) + if obj == nil { + continue + } + st, ok := obj.Type().Underlying().(*types.Struct) + if !ok { + continue + } + var fields []Field + var hasComplexFields bool + // Iterate through the fields of the struct + for i := 0; i < st.NumFields(); i++ { + field := st.Field(i) + fieldName := field.Name() + fieldType := formatType(field.Type()) + fieldIsSlice := isSlice(field.Type()) + fieldIsComplex := hasNamedObject(field.Type()) + if fieldIsComplex { + hasComplexFields = true + } + fields = append(fields, Field{Name: fieldName, Type: fieldType, Tag: strings.TrimSuffix(strings.TrimPrefix(st.Tag(i), "`"), "`"), IsComplexType: fieldIsComplex, IsSliceType: fieldIsSlice}) + } + + splittedAttrs := strings.Split(attrs, " ") + var omitFuncs []string + for _, attr := range splittedAttrs { + if strings.HasPrefix(attr, "omit:") { + omitFuncs = strings.Split(strings.TrimPrefix(attr, "omit:"), ",") + } + } + structs = append(structs, TemplateData{ + StructName: typeSpec.Name.Name, + HasComplexFields: hasComplexFields, + Fields: fields, + OmittedFuncs: omitFuncs, + }) + } + } + } + } + } + } + + return structs +} + +func cleanup(dir, outputFileName string) error { + err := os.Remove(filepath.Join(dir, outputFileName)) + if err != nil && !os.IsNotExist(err) { + return err + } + return nil +} + +func generateFunctions(sourceDir string, pkgs map[string]*packages.Package) { + structs := getExportedTypes(pkgs) + + if len(structs) == 0 { + return // No structs to generate functions for + } + + var buf bytes.Buffer + headerStr := ` + package main + import ( + "fmt" + "encoding/json" + "github.com/hypermodeinc/modus/sdk/go/pkg/dgraph" + ) + + const hostName = "dgraph" + + + ` + + buf.WriteString(headerStr) + + funcMap := template.FuncMap{ + "lower": strings.ToLower, + } + + t1 := template.Must(template.New("upsert").Funcs(funcMap).Parse(upsertTmplStruct)) + t2 := template.Must(template.New("delete").Funcs(funcMap).Parse(deleteTmplStruct)) + t3 := template.Must(template.New("get").Funcs(funcMap).Parse(getTmplStruct)) + t4 := template.Must(template.New("query").Funcs(funcMap).Parse(queryTmplStruct)) + + for _, data := range structs { + var err error + if !slices.Contains(data.OmittedFuncs, "upsert") { + err = t1.Execute(&buf, data) + if err != nil { + fmt.Println("Error executing upsert template:", err) + os.Exit(1) + } + } + + if !slices.Contains(data.OmittedFuncs, "delete") { + err = t2.Execute(&buf, data) + if err != nil { + fmt.Println("Error executing delete template:", err) + os.Exit(1) + } + } + + if !slices.Contains(data.OmittedFuncs, "get") { + err = t3.Execute(&buf, data) + if err != nil { + fmt.Println("Error executing get template:", err) + os.Exit(1) + } + } + + if !slices.Contains(data.OmittedFuncs, "query") { + err = t4.Execute(&buf, data) + if err != nil { + fmt.Println("Error executing query template:", err) + os.Exit(1) + } + } + } + + formatted, err := format.Source(buf.Bytes()) + + if err != nil { + fmt.Println("Error formatting source:", err) + fmt.Println("Unformatted source:\n", buf.String()) + os.Exit(1) + } + + outputFileName := "hyp_functions_generated.go" + err = cleanup(sourceDir, outputFileName) + if err != nil { + fmt.Println("Error cleaning up:", err) + os.Exit(1) + } + + err = os.WriteFile(filepath.Join(sourceDir, outputFileName), formatted, 0644) + if err != nil { + fmt.Println("Error writing file:", err) + os.Exit(1) + } + + fmt.Println("Methods generated in", outputFileName) +}