diff --git a/codegen.sh b/codegen.sh index 65a805e51..fbb462f84 100755 --- a/codegen.sh +++ b/codegen.sh @@ -125,6 +125,19 @@ function provision { } +function events { + if [ ! -f "build/event-gen" ]; then + CGO_ENABLED=0 go build -o ./build/event-gen codegen/v2/events.go + fi + + for SERVICE in system compose messaging; do + yellow "> event files for ${SERVICE}" + ./build/event-gen --service ${SERVICE} + done + green "OK" +} + + function specs { yellow "> specs" if [ ! -f "build/gen-spec" ]; then @@ -187,12 +200,16 @@ case ${1:-"all"} in proto) proto ;; + events) + events + ;; all) types database provision specs proto + events esac # Always finish with fmt diff --git a/codegen/v2/events.gen.go.tpl b/codegen/v2/events.gen.go.tpl new file mode 100644 index 000000000..89235dd52 --- /dev/null +++ b/codegen/v2/events.gen.go.tpl @@ -0,0 +1,98 @@ +package {{ .Package }} + +// This file is auto-generated. +// +// YAML event definitions: +// {{ .YAML }} +// +// Regenerate with: +// {{ .Command }} +// + +{{ if .Imports }} +import ( +{{ range $i, $import := .Imports }} + "{{ $import }}" +{{ end }} +) +{{ end }} + + +type ( +// {{ camelCase $.ResourceIdent "base" }} +// +// This type is auto-generated. +{{ camelCase $.ResourceIdent "base" }} struct { +{{- range $p := $.Events.Properties }} + {{ $p.Name }} {{ $p.Type }} +{{- end }} +} + + +{{ range $event := makeEvents $.Events }} +// {{ camelCase $.ResourceIdent $event }} +// +// This type is auto-generated. +{{ camelCase $.ResourceIdent $event }} struct { + *{{ camelCase $.ResourceIdent "base" }} +} +{{ end }} +) + +// ResourceType returns "{{ .ResourceString }}" +// +// This function is auto-generated. +func ({{ camelCase .ResourceIdent "base" }}) ResourceType() string { + return "{{ .ResourceString }}" +} + +{{ range $event := makeEvents $.Events }} +// EventType on {{ camelCase $.ResourceIdent $event }} returns "{{ $event }}" +// +// This function is auto-generated. +func ({{ camelCase $.ResourceIdent $event }}) EventType() string { + return "{{ $event }}" +} +{{ end }} + + +{{ range $event := makeEvents $.Events }} +// {{ camelCase "" $.ResourceIdent $event }} creates {{ $event }} for {{ $.ResourceString }} resource +// +// This function is auto-generated. +func {{ camelCase "" $.ResourceIdent $event }}( +{{- range $p := $.Events.Properties }} + {{- if not $p.Internal }} + {{ camelCase "arg" $p.Name }} {{ $p.Type }}, + {{- end -}} +{{- end}} +) *{{ camelCase $.ResourceIdent $event }} { + return &{{ camelCase $.ResourceIdent $event }}{ + {{ camelCase $.ResourceIdent "base" }}: &{{ camelCase $.ResourceIdent "base" }}{ + {{- range $p := $.Events.Properties }} + {{- if not $p.Internal }} + {{ $p.Name }}: {{ camelCase "arg" $p.Name }}, + {{- end -}} + {{- end}} + }, + } +} +{{ end }} + +{{ range $p := $.Events.Properties }} +{{ if not $p.Immutable }} +// {{ camelCase "Set" $p.Name }} sets new {{ $p.Name }} value +// +// This function is auto-generated. +func (res *{{ camelCase $.ResourceIdent "base" }}) {{ camelCase "Set" $p.Name }}({{ camelCase "arg" $p.Name }} {{ $p.Type }}) { + res.{{ $p.Name }} = {{ camelCase "arg" $p.Name }} +} +{{ end }} + +// {{ camelCase "" $p.Name }} returns {{ $p.Name }} +// +// This function is auto-generated. +func (res {{ camelCase $.ResourceIdent "base" }}) {{ camelCase "" $p.Name }}() {{ $p.Type }} { + return res.{{ $p.Name }} +} +{{ end }} diff --git a/codegen/v2/events.go b/codegen/v2/events.go new file mode 100644 index 000000000..43c8bb7cd --- /dev/null +++ b/codegen/v2/events.go @@ -0,0 +1,266 @@ +package main + +// *Set type generator for functions like Walk(), Filter(), FindByID() & IDs() + +import ( + "bytes" + "flag" + "fmt" + "go/format" + "io" + "os" + "strings" + "text/template" + + "gopkg.in/yaml.v2" + + "github.com/cortezaproject/corteza-server/pkg/cli" +) + +const ( + eventsTemplateFile = "codegen/v2/*.go.tpl" +) + +type ( + eventDefMap map[string]eventDef + + eventDef struct { + On []string `yaml:"on"` + BeforeAfter []string `yaml:"ba"` + Properties []eventProps `yaml:"props"` + Result string `yaml:"result"` + } + + eventProps struct { + Name string + Type string + + // Import path for prop type, use package's type by default (see importTypePathTpl) + Import string + + // Set property internally only, not via constructor + Internal bool + + // Do not allow change of the variable through + Immutable bool + } + + tplPayload struct { + Command string + YAML string + + Package string + + // List of imports + // Used only by generated file and not pre-generated-user-file + Imports []string + + // will be used as string + ResourceString string + + // will be used as (go) ident + ResourceIdent string + Events eventDef + } +) + +func main() { + + tpl := template.New("").Funcs(map[string]interface{}{ + "camelCase": camelCase, + "makeEvents": makeEvents, + }) + + tpl = template.Must(tpl.ParseGlob(eventsTemplateFile)) + + var ( + definitionsPathStr string + serviceStr string + overwrite bool + // outputFile string + + decoder *yaml.Decoder + + tplData = &tplPayload{} + ) + + const ( + defResArgName = "result" + + yamlDefFileName = "events.yaml" + definitionsPathTpl = "%s/service/event/" + importTypePathTpl = "github.com/cortezaproject/corteza-server/%s/types" + importAuthPath = "github.com/cortezaproject/corteza-server/pkg/auth" + ) + + flag.StringVar(&definitionsPathStr, "definitions", "", "Location of event definitions file (generated from service if omitted) and output dir") + flag.StringVar(&serviceStr, "service", "", "Comma separated list of imports") + flag.BoolVar(&overwrite, "overwrite", false, "Overwrite all files") + + flag.Parse() + + if serviceStr == "" { + cli.HandleError(fmt.Errorf("can not generate event code without service")) + } + + if definitionsPathStr == "" { + definitionsPathStr = fmt.Sprintf(definitionsPathTpl, serviceStr) + } + + if f, err := os.Open(definitionsPathStr + yamlDefFileName); err != nil { + cli.HandleError(err) + } else { + decoder = yaml.NewDecoder(f) + } + + tplData.Command = "go run codegen/v2/events.go --service " + serviceStr + tplData.YAML = definitionsPathStr + yamlDefFileName + tplData.Package = "event" + + defs := eventDefMap{} + cli.HandleError(decoder.Decode(&defs)) + + for resName, evDef := range defs { + var ( + l = len(serviceStr) + fname = resName + ) + + tplData.Imports = make([]string, 0) + tplData.ResourceString = resName + + if resName == serviceStr { + // if resource name = service name, leave it as-is + tplData.ResourceIdent = resName + } else if len(resName) <= l || resName[:l+2] == serviceStr+":" { + // check if resource name is shorter and has invalid prefix + cli.HandleError(fmt.Errorf("invalid resource prefix: %q", resName)) + } else { + tplData.ResourceIdent = camelCase(strings.Split(resName[l+1:], ":")...) + + fname = resName[l+1:] + fname = strings.ReplaceAll(fname, ":", "_") + fname = strings.ReplaceAll(fname, "-", "_") + } + + // Prepare the data + + // no default ("result") result set, use first one from properties + if evDef.Result == "" && len(evDef.Properties) > 0 { + evDef.Result = evDef.Properties[0].Name + } + + // Invoker - user that invoked (triggered) the event + evDef.Properties = append(evDef.Properties, eventProps{ + Name: "invoker", + Type: "auth.Identifiable", + Import: importAuthPath, + Immutable: false, + Internal: true, + }) + + for _, p := range evDef.Properties { + if p.Import == "" { + if strings.HasPrefix(p.Type, "*types.") || strings.HasPrefix(p.Type, "types.") { + p.Import = fmt.Sprintf(importTypePathTpl, serviceStr) + } + } + + if p.Import == "" { + // Do not import empty paths + continue + } + + if inSlice(p.Import, tplData.Imports) { + continue + } + + tplData.Imports = append(tplData.Imports, p.Import) + } + + tplData.Events = evDef + + var ( + usrOutput = fmt.Sprintf("%s%s.go", definitionsPathStr, fname) + genOutput = fmt.Sprintf("%s/%s.gen.go", definitionsPathStr, fname) + ) + + _, err := os.Stat(usrOutput) + if overwrite || os.IsNotExist(err) { + writeTo(tpl, tplData, "events.go.tpl", usrOutput) + } + + writeTo(tpl, tplData, "events.gen.go.tpl", genOutput) + } +} + +func inSlice(s string, ss []string) bool { + for i := range ss { + if ss[i] == s { + return true + } + } + + return false +} + +// Merge on/before/after events +func makeEvents(def eventDef) []string { + return append( + makeEventGroup("on", def.On), + append( + makeEventGroup("before", def.BeforeAfter), + makeEventGroup("after", def.BeforeAfter)..., + )..., + ) +} + +func camelCase(pp ...string) (out string) { + for i, p := range pp { + if i > 0 && len(p) > 1 { + p = strings.ToUpper(p[:1]) + p[1:] + } + + out = out + p + } + + return out +} + +func makeEventGroup(pfix string, ee []string) (out []string) { + for _, e := range ee { + out = append(out, pfix+strings.ToUpper(e[:1])+e[1:]) + } + + return +} + +func writeTo(tpl *template.Template, payload interface{}, name, dst string) { + var output io.WriteCloser + buf := bytes.Buffer{} + + if err := tpl.ExecuteTemplate(&buf, name, payload); err != nil { + cli.HandleError(err) + } else { + fmtsrc, err := format.Source(buf.Bytes()) + if err != nil { + fmt.Fprintf(os.Stderr, "fmt warn: %v", err) + fmtsrc = buf.Bytes() + } + + if dst == "" || dst == "-" { + output = os.Stdout + } else { + // cli.HandleError(os.Remove(dst)) + if output, err = os.Create(dst); err != nil { + cli.HandleError(err) + } + + defer output.Close() + } + + if _, err := output.Write(fmtsrc); err != nil { + cli.HandleError(err) + } + } +} diff --git a/codegen/v2/events.go.tpl b/codegen/v2/events.go.tpl new file mode 100644 index 000000000..b0bcf64ef --- /dev/null +++ b/codegen/v2/events.go.tpl @@ -0,0 +1,58 @@ +package {{ .Package }} + +{{ if $.Events.Properties }} +import ( + "encoding/json" +) +{{ end }} + +// Match returns false if given conditions do not match event & resource internals +func (res {{ camelCase .ResourceIdent "base" }}) Match(name string, op string, values ...string) bool { + // By default we match no mather what kind of constraints we receive + // + // Function will be called multiple times - once for every trigger constraint + // All should match (return true): + // constraint#1 AND constraint#2 AND constraint#3 ... + // + // When there are multiple values, Match() can decide how to treat them (OR, AND...) + return true +} + +// Encode internal data to be passed as event params & arguments to triggered Corredor script +func (res {{ camelCase .ResourceIdent "base" }}) Encode() (args map[string][]byte, err error) { + {{- if $.Events.Properties }} + args = make(map[string][]byte) + + {{ range $prop := $.Events.Properties }} + if args["{{ $prop.Name }}"], err = json.Marshal(res.{{ $prop.Name }}); err != nil { + return nil, err + } + {{ end }} + {{ else }} + // Handle argument encoding + {{ end -}} + return +} + +// Decode return values from Corredor script into struct props +func (res *{{ camelCase .ResourceIdent "base" }}) Decode(results map[string][]byte)( err error) { + {{- if $.Events.Result }} + if r, ok := results["result"]; ok && len(results) == 1 { + if err = json.Unmarshal(r, res.{{ $.Events.Result }}); err != nil { + return + } + } + {{ end -}} + + {{- range $prop := $.Events.Properties }} + {{- if not $prop.Immutable }} + if r, ok := results["{{ $prop.Name }}"]; ok && len(results) == 1 { + if err = json.Unmarshal(r, res.{{ $prop.Name }}); err != nil { + return + } + } + {{ end -}} + {{ end -}} + + return +}