From d56f2fa82f9ac9d8c4233f94f4aaf25a52123448 Mon Sep 17 00:00:00 2001 From: Denis Arh Date: Wed, 20 Jan 2021 19:58:08 +0100 Subject: [PATCH] Codegen for automation types & functions --- pkg/codegen/afunc.go | 155 ++++++++++++++++++++ pkg/codegen/assets/afunc.gen.go.tpl | 171 +++++++++++++++++++++++ pkg/codegen/assets/expr_types.gen.go.tpl | 47 +++++++ pkg/codegen/codegen.go | 38 +++++ pkg/codegen/expr_types.go | 74 ++++++++++ 5 files changed, 485 insertions(+) create mode 100644 pkg/codegen/afunc.go create mode 100644 pkg/codegen/assets/afunc.gen.go.tpl create mode 100644 pkg/codegen/assets/expr_types.gen.go.tpl create mode 100644 pkg/codegen/expr_types.go diff --git a/pkg/codegen/afunc.go b/pkg/codegen/afunc.go new file mode 100644 index 000000000..563391106 --- /dev/null +++ b/pkg/codegen/afunc.go @@ -0,0 +1,155 @@ +package codegen + +import ( + "encoding/json" + "fmt" + "github.com/cortezaproject/corteza-server/automation/types" + . "github.com/cortezaproject/corteza-server/pkg/y7s" + "gopkg.in/yaml.v3" + "os" + "path" + "strings" + "text/template" +) + +type ( + // definitions are in one file + aFuncDefs struct { + Package string + Name string + Source string + Prefix string + outputDir string + + // List of imports + // Used only by generated file and not pre-generated-user-file + Imports []string + + Functions aFunctionSet + } + + aFunctionSet []aFuncDef + + aFuncDef struct { + Name string + Meta *types.FunctionMeta + Params aFuncParamSet + Results aFuncResultSet + } + + aFuncParamSet []*aFuncParamDef + aFuncResultSet []*aFuncResultDef + + aFuncParamDef struct { + Name string + Required bool + SetOf bool + Types []*aFuncParamTypeVarDef + Meta *types.ParamMeta + } + + aFuncParamTypeVarDef struct { + WorkflowType string `yaml:"wf"` + GoType string `yaml:"go"` + Suffix string + } + + aFuncResultDef struct { + Name string + Required bool + SetOf bool + WorkflowType string `yaml:"wf"` + GoType string `yaml:"go"` + Meta *types.ParamMeta + } +) + +func procAutomationFunctions(mm ...string) (dd []*aFuncDefs, err error) { + for _, m := range mm { + f, err := os.Open(m) + if err != nil { + return nil, fmt.Errorf("%s read failed: %w", m, err) + } + + defer f.Close() + + var ( + d = &aFuncDefs{ + Package: "automation", + Source: m, + Name: path.Base(m), + outputDir: path.Dir(m), + } + ) + + d.Name = d.Name[:len(d.Name)-13] + + if err := yaml.NewDecoder(f).Decode(d); err != nil { + return nil, err + } + + dd = append(dd, d) + } + + return +} + +func (set *aFunctionSet) UnmarshalYAML(n *yaml.Node) error { + return Each(n, func(k *yaml.Node, v *yaml.Node) (err error) { + def := aFuncDef{Name: k.Value} + + if err = v.Decode(&def); err != nil { + return err + } + + *set = append(*set, def) + + return nil + }) +} + +func (set *aFuncParamSet) UnmarshalYAML(n *yaml.Node) error { + return Each(n, func(k *yaml.Node, v *yaml.Node) (err error) { + def := aFuncParamDef{} + if k != nil { + def.Name = k.Value + } + + *set = append(*set, &def) + return v.Decode(&def) + }) +} + +func (set *aFuncResultSet) UnmarshalYAML(n *yaml.Node) error { + return Each(n, func(k *yaml.Node, v *yaml.Node) (err error) { + def := aFuncResultDef{} + if k != nil { + def.Name = k.Value + } + + *set = append(*set, &def) + return v.Decode(&def) + }) +} + +func genAutomationFunctions(tpl *template.Template, dd ...*aFuncDefs) (err error) { + var ( + // Will only be generated if file does not exist previously + tplAFuncGen = tpl.Lookup("afunc.gen.go.tpl") + + dst string + ) + + for _, d := range dd { + // Generic code, actions for every resource goes to a separated file + dst = path.Join(d.outputDir, path.Base(d.Source)[:strings.LastIndex(path.Base(d.Source), ".")]+".gen.go") + json.NewEncoder(os.Stdout).SetIndent("", " ") + err = goTemplate(dst, tplAFuncGen, d) + if err != nil { + return + } + + } + + return nil +} diff --git a/pkg/codegen/assets/afunc.gen.go.tpl b/pkg/codegen/assets/afunc.gen.go.tpl new file mode 100644 index 000000000..dc70ea2ae --- /dev/null +++ b/pkg/codegen/assets/afunc.gen.go.tpl @@ -0,0 +1,171 @@ +package {{ .Package }} + +// This file is auto-generated. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// Definitions file that controls how this file is generated: +// {{ .Source }} + +import ( + atypes "github.com/cortezaproject/corteza-server/automation/types" + "context" + "github.com/cortezaproject/corteza-server/pkg/expr" +{{- range .Imports }} + {{ normalizeImport . }} +{{- end }} +) + +var ( + {{ $.Name }} = &{{ $.Name }}Handler{} +) + +func (h {{ $.Name }}Handler) register(reg func(*atypes.Function)) { +{{- range .Functions }} + reg(h.{{ export .Name }}()) +{{- end }} +} + +{{ range .Functions }} + {{ $REF := unexport $.Prefix $.Name .Name }} + {{ $ARGS := unexport $.Name .Name "Args" }} + {{ $RESULTS := unexport $.Name .Name "Results" }} + + +type ( + {{ $ARGS }} struct { + {{ range .Params }} + {{ $NAME := .Name }} + has{{ export .Name }} bool + {{ if gt (len .Types) 1 }} + {{ export .Name }} interface{} + {{- range .Types }} + {{ $NAME }}{{ export .Suffix }} {{ .GoType }} + {{- end }} + {{- else -}} + {{ range .Types }} + {{ export $NAME }}{{ export .Suffix }} {{ .GoType }} + {{- end }} + {{- end -}} + {{- end }} + } + + {{ if .Results }} + {{ unexport $.Name .Name }}Results struct { + {{ range .Results }} + {{ export .Name }} {{ .GoType }} + {{- end }} + } + {{- end }} +) + +// +// +// expects implementation of {{ .Name }} function: +// func (h {{ $.Name }}) {{ .Name }}(ctx context.Context, args *{{ $ARGS }}) (results *{{ $RESULTS }}, err error) { +// return +// } +func (h {{ $.Name }}Handler) {{ export .Name }}() *atypes.Function { + return &atypes.Function{ + Ref: {{ printf "%q" ( $REF ) }}, + {{- if .Meta }} + Meta: &atypes.FunctionMeta{ + {{- if .Meta.Short }} + Short: {{ printf "%q" .Meta.Short }}, + {{- end }} + {{- if .Meta.Description }} + Description: {{ printf "%q" .Meta.Description }}, + {{- end }} + {{- if .Meta.Visual }} + Visual: {{ printf "%#v" .Meta.Visual }}, + {{- end }} + }, + {{- end }} + + Parameters: []*atypes.Param{ + {{- range .Params }} + { + Name: {{ printf "%q" .Name }}, + Types: []string{ {{ range .Types }}({{ .WorkflowType }}{}).Type(),{{ end }} }, + {{- if .Required }}Required: true,{{ end }} + {{- if .SetOf }}SetOf: true,{{ end }} + {{- if .Meta }} + Meta: &atypes.ParamMeta{ + {{- if .Meta.Label }} + Label: {{ printf "%#v" .Meta.Label }}, + {{- end }} + {{- if .Meta.Description }} + Description: {{ printf "%#v" .Meta.Description }}, + {{- end }} + {{- if .Meta.Visual }} + Visual: {{ printf "%#v" .Meta.Visual }}, + {{- end }} + }, + {{ end }} + }, + {{- end }} + }, + + {{ if .Results }} + Results: []*atypes.Param{ + {{ range .Results }} + atypes.NewParam({{ printf "%q" .Name }}, + atypes.Types(&{{ .WorkflowType }}{}), + ), + {{ end }} + }, + {{ end }} + + Handler: func(ctx context.Context, in expr.Vars) (out expr.Vars, err error) { + var ( + args = &{{ $ARGS }}{ + {{- range .Params }} + has{{ export .Name }}: in.Has({{ printf "%q" .Name }}), + {{- end }} + } + + {{ if .Results }} + results *{{ $RESULTS }} + {{ end }} + ) + + if err = in.Decode(args); err != nil { + return + } + + {{ range .Params }} + {{ $NAME := .Name }} + {{ if gt (len .Types) 1 }} + // Converting {{ export .Name }} to go type + switch casted := args.{{ export .Name }}.(type) { + {{- range .Types }} + case {{ .GoType }}: + args.{{ $NAME }}{{ export .Suffix }} = casted + {{- end -}} + } + {{- end }} + {{ end }} + + + {{ if .Results }} + if results, err = h.{{ .Name }}(ctx, args); err != nil { + return + } + + out = expr.Vars{} + + {{- range .Results }} + if out[{{ printf "%q" .Name }}], err = ({{ .WorkflowType }}{}).Cast(results.{{ export .Name }}); err != nil { + return nil, err + } + {{- end }} + + return + {{- else }} + return out, h.{{ .Name }}(ctx, args) + {{- end }} + }, + } +} +{{ end }} diff --git a/pkg/codegen/assets/expr_types.gen.go.tpl b/pkg/codegen/assets/expr_types.gen.go.tpl new file mode 100644 index 000000000..bd5214d6d --- /dev/null +++ b/pkg/codegen/assets/expr_types.gen.go.tpl @@ -0,0 +1,47 @@ +package {{ .Package }} + +// This file is auto-generated. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// Definitions file that controls how this file is generated: +// {{ .Source }} + +{{ if .Imports }} +import ( +{{- range .Imports }} + {{ normalizeImport . }} +{{- end }} +{{- if ne .Package "expr" }} + "github.com/cortezaproject/corteza-server/pkg/expr" +{{- end }} +) +{{ end }} + + +{{ $TypedValue := "TypedValue" }} +{{ if ne .Package "expr" }} + {{ $TypedValue = "expr.TypedValue" }} +{{ end }} + +{{ range $exprType, $nativeType := .Types }} +// {{ $exprType }} is an expression type, wrapper for {{ $nativeType }} type +type {{ $exprType }} struct{ value {{ $nativeType }} } + +// New{{ $exprType }} creates new instance of {{ $exprType }} expression type +func New{{ $exprType }}(new interface{}) ({{ $TypedValue }}, error) { + t := &{{ $exprType }}{} + return t, t.Set(new) +} + +// Returns underlying value on {{ $exprType }} +func (t {{ $exprType }}) Get() interface{} { return t.value } + +// Returns type name +func ({{ $exprType }}) Type() string { return "{{ $exprType }}" } + +// Casts value to {{ $nativeType }} +func ({{ $exprType }}) Cast(value interface{}) ({{ $TypedValue }}, error) { return New{{ $exprType }}(value) } + +{{ end }} diff --git a/pkg/codegen/codegen.go b/pkg/codegen/codegen.go index c17dbda5e..6b4ddd454 100644 --- a/pkg/codegen/codegen.go +++ b/pkg/codegen/codegen.go @@ -47,6 +47,12 @@ func Proc() { typeSrc []string typeDefs []*typesDef + // workaround because + // filepath.Join merges "*","*" into "**" instead of "*/*" + exprTypeSrcPath = filepath.Join("*"+string(filepath.Separator)+"*", "expr_types.yaml") + exprTypeSrc []string + exprTypeDefs []*exprTypesDef + restSrcPath = filepath.Join("*", "rest.yaml") restSrc []string restDefs []*restDef @@ -59,6 +65,10 @@ func Proc() { optionSrc []string optionDefs []*optionsDef + aFuncsSrcPath = filepath.Join("*", "automation", "*_handler.yaml") + aFuncsSrc []string + aFuncsDefs []*aFuncDefs + tpls *template.Template tplBase = template.New(""). Funcs(map[string]interface{}{ @@ -142,6 +152,9 @@ func Proc() { typeSrc = glob(typeSrcPath) output("loaded %d type definitions from %s\n", len(typeSrc), typeSrcPath) + exprTypeSrc = glob(exprTypeSrcPath) + output("loaded %d exprType definitions from %s\n", len(exprTypeSrc), exprTypeSrcPath) + restSrc = glob(restSrcPath) output("loaded %d rest definitions from %s\n", len(restSrc), restSrcPath) @@ -151,6 +164,9 @@ func Proc() { optionSrc = glob(optionSrcPath) output("loaded %d option definitions from %s\n", len(optionSrc), optionSrcPath) + aFuncsSrc = glob(aFuncsSrcPath) + output("loaded %d function definitions from %s\n", len(aFuncsSrc), aFuncsSrcPath) + if watchChanges { if watcher != nil { watcher.Close() @@ -163,9 +179,11 @@ func Proc() { fileList = append(fileList, actionSrc...) fileList = append(fileList, eventSrc...) fileList = append(fileList, typeSrc...) + fileList = append(fileList, exprTypeSrc...) fileList = append(fileList, restSrc...) fileList = append(fileList, storeSrc...) fileList = append(fileList, optionSrc...) + fileList = append(fileList, aFuncsSrc...) for _, d := range fileList { handleError(watcher.Add(d)) @@ -211,6 +229,16 @@ func Proc() { return } + if exprTypeDefs, err = procExprTypes(exprTypeSrc...); err == nil { + if genCode { + err = genExprTypes(tpls, exprTypeDefs...) + } + } + + if outputErr(err, "failed to process expr types:\n") { + return + } + if restDefs, err = procRest(restSrc...); err == nil { if genCode { err = genRest(tpls, restDefs...) @@ -245,6 +273,16 @@ func Proc() { return } + if aFuncsDefs, err = procAutomationFunctions(aFuncsSrc...); err == nil { + if genCode { + err = genAutomationFunctions(tpls, aFuncsDefs...) + } + } + + if outputErr(err, "failed to process store:\n") { + return + } + }() if !watchChanges { diff --git a/pkg/codegen/expr_types.go b/pkg/codegen/expr_types.go new file mode 100644 index 000000000..e03cde7ff --- /dev/null +++ b/pkg/codegen/expr_types.go @@ -0,0 +1,74 @@ +package codegen + +import ( + "fmt" + "gopkg.in/yaml.v3" + "os" + "path" + "text/template" +) + +type ( + exprTypesDef struct { + // source file path + Source string + + // outputDir + // dir where the source file is + outputDir string + + Imports []string + Package string `yaml:"package"` + Types map[string]string `yaml:"types"` + } +) + +func procExprTypes(mm ...string) (dd []*exprTypesDef, err error) { + dd = make([]*exprTypesDef, 0) + + for _, m := range mm { + var ( + d = &exprTypesDef{ + Source: m, + outputDir: path.Dir(m), + + Package: "types", + Types: make(map[string]string), + } + ) + + f, err := os.Open(m) + if err != nil { + return nil, fmt.Errorf("%s read failed: %w", m, err) + } + + defer f.Close() + + if err := yaml.NewDecoder(f).Decode(d); err != nil { + return nil, fmt.Errorf("%s decode failed: %w", m, err) + } + + dd = append(dd, d) + } + + return dd, nil +} + +// Generates all type set files & accompanying tests +// +// generates 2 files per type definition +func genExprTypes(tpl *template.Template, dd ...*exprTypesDef) (err error) { + var ( + typeGen = tpl.Lookup("expr_types.gen.go.tpl") + ) + + for _, d := range dd { + err = goTemplate(path.Join(d.outputDir, "expr_types.gen.go"), typeGen, d) + + if err != nil { + return + } + } + + return nil +}