Refactored codegen tool
This commit is contained in:
15
Makefile
15
Makefile
@@ -68,6 +68,7 @@ GOCRITIC = ${GOPATH}/bin/gocritic
|
||||
MOCKGEN = ${GOPATH}/bin/mockgen
|
||||
STATICCHECK = ${GOPATH}/bin/staticcheck
|
||||
PROTOGEN = ${GOPATH}/bin/protoc-gen-go
|
||||
CODEGEN = build/codegen
|
||||
|
||||
# Using nodemon in development environment for "watch.*" tasks
|
||||
# https://nodemon.io
|
||||
@@ -132,6 +133,10 @@ watch.test.%: $(NODEMON)
|
||||
# changes & reruns tests
|
||||
$(WATCHER) "make test.$* || exit 0"
|
||||
|
||||
# codegen: $(PROTOGEN)
|
||||
codegen: $(CODEGEN)
|
||||
./build/codegen
|
||||
|
||||
########################################################################################################################
|
||||
# Quality Assurance
|
||||
|
||||
@@ -170,13 +175,6 @@ test.pkg:
|
||||
|
||||
test: test.unit
|
||||
|
||||
# Outputs cross-package imports that should not be there.
|
||||
test.cross-dep:
|
||||
@ grep -rE "github.com/cortezaproject/corteza-server/(compose|messaging)/" system || exit 0
|
||||
@ grep -rE "github.com/cortezaproject/corteza-server/(system|messaging)/" compose || exit 0
|
||||
@ grep -rE "github.com/cortezaproject/corteza-server/(system|compose)/" messaging || exit 0
|
||||
@ grep -rE "github.com/cortezaproject/corteza-server/(system|compose|messaging)/" pkg || exit 0
|
||||
|
||||
vet:
|
||||
$(GO) vet ./...
|
||||
|
||||
@@ -218,6 +216,9 @@ $(PROTOGEN):
|
||||
$(NODEMON):
|
||||
npm install -g nodemon
|
||||
|
||||
$(CODEGEN):
|
||||
$(GO) build -o $@ cmd/codegen/main.go
|
||||
|
||||
clean:
|
||||
rm -f $(GOCRITIC)
|
||||
|
||||
|
||||
9
cmd/codegen/main.go
Normal file
9
cmd/codegen/main.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza-server/pkg/codegen"
|
||||
)
|
||||
|
||||
func main() {
|
||||
codegen.Proc()
|
||||
}
|
||||
12
pkg/codegen/README.md
Normal file
12
pkg/codegen/README.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# Code generation package
|
||||
|
||||
It consumes YAMLs from all known locations and provides them to
|
||||
templates
|
||||
|
||||
- determinate all known locations for yaml files
|
||||
- determinate all known output types (go?, html?, adocs?)
|
||||
- can we read this from the filename?
|
||||
- define structure for all locations
|
||||
- define outputs
|
||||
- can be a static file
|
||||
- can have placeholders that are assembled from the structure, and yaml file contents
|
||||
306
pkg/codegen/actions.go
Normal file
306
pkg/codegen/actions.go
Normal file
@@ -0,0 +1,306 @@
|
||||
package codegen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/cortezaproject/corteza-server/pkg/handle"
|
||||
"gopkg.in/yaml.v2"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
type (
|
||||
// definitions are in multiple files and each definition
|
||||
// should produce one output
|
||||
actionsDef struct {
|
||||
App string
|
||||
Source string
|
||||
outputDir string
|
||||
|
||||
// List of imports
|
||||
// Used only by generated file and not pre-generated-user-file
|
||||
Import []string `yaml:"import"`
|
||||
|
||||
Service string `yaml:"service"`
|
||||
Resource string `yaml:"resource"`
|
||||
|
||||
// Default severity for actions
|
||||
DefaultActionSeverity string `yaml:"defaultActionSeverity"`
|
||||
|
||||
// Default severity for errors
|
||||
DefaultErrorSeverity string `yaml:"defaultErrorSeverity"`
|
||||
|
||||
// If at least one of the errors has HTTP status defined,
|
||||
// add support for http errors
|
||||
SupportHttpErrors bool
|
||||
|
||||
Props []*propsDef
|
||||
Actions []*actionDef
|
||||
Errors []*errorDef
|
||||
}
|
||||
|
||||
// List of event/log properties that can/will be captured
|
||||
// and injected into log or message string
|
||||
propsDef struct {
|
||||
Name string
|
||||
Type string
|
||||
Fields []string
|
||||
Builtin bool
|
||||
}
|
||||
|
||||
actionDef struct {
|
||||
// Action name
|
||||
Action string `yaml:"action"`
|
||||
|
||||
// String to log when action is successful
|
||||
Log string `yaml:"log"`
|
||||
|
||||
// String to log when error was yield
|
||||
//ErrorLog string `yaml:"errorLog"`
|
||||
|
||||
// Action severity
|
||||
Severity string `yaml:"severity"`
|
||||
}
|
||||
|
||||
// Event definition
|
||||
errorDef struct {
|
||||
// Error key
|
||||
// message can contain {variables} from meta data
|
||||
Error string `yaml:"error"`
|
||||
|
||||
// Error key
|
||||
// message can contain {variables} from meta data
|
||||
Message string `yaml:"message"`
|
||||
|
||||
// Formatted and readable audit log message
|
||||
// message can contain {variables} from meta data
|
||||
Log string `yaml:"log"`
|
||||
|
||||
// Reference to "safe" error
|
||||
// safe error should hide any information that might cause
|
||||
// personal data leakage or expose system internals
|
||||
Safe string `yaml:"safe"`
|
||||
|
||||
// Error severity
|
||||
Severity string `yaml:"severity"`
|
||||
|
||||
// HTTP Status code for this error
|
||||
HttpStatus string `yaml:"httpStatus"`
|
||||
}
|
||||
)
|
||||
|
||||
// Processes multiple action definitions
|
||||
func procActions() ([]*actionsDef, error) {
|
||||
var (
|
||||
f io.ReadCloser
|
||||
d *actionsDef
|
||||
|
||||
dd = make([]*actionsDef, 0)
|
||||
)
|
||||
|
||||
// <app>/service/<kind>_actions.yaml
|
||||
mm, err := filepath.Glob(filepath.Join("*", "service", "*_actions.yaml"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("glob failed: %w", err)
|
||||
}
|
||||
|
||||
for _, m := range mm {
|
||||
err = func() error {
|
||||
if f, err = os.Open(m); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
d = &actionsDef{}
|
||||
|
||||
if err := yaml.NewDecoder(f).Decode(d); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = actionNormalize(d); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.Source = m
|
||||
d.outputDir = path.Dir(m)
|
||||
|
||||
dd = append(dd, d)
|
||||
return nil
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not process %s: %w", m, err)
|
||||
}
|
||||
}
|
||||
|
||||
return dd, nil
|
||||
}
|
||||
|
||||
func actionNormalize(d *actionsDef) error {
|
||||
// Prepend generic error
|
||||
d.Errors = append([]*errorDef{{
|
||||
Error: "generic",
|
||||
Message: "failed to complete request due to internal error",
|
||||
Log: "{err}",
|
||||
Severity: "error",
|
||||
}}, d.Errors...)
|
||||
|
||||
// index known meta fields and sanitize types (no type => string type)
|
||||
knownProps := map[string]bool{
|
||||
"err": true,
|
||||
}
|
||||
|
||||
for _, m := range d.Props {
|
||||
if m.Type == "" {
|
||||
m.Type = "string"
|
||||
}
|
||||
|
||||
// very optimistic check if referenced type is builtin or not
|
||||
m.Builtin = !strings.Contains(m.Type, ".")
|
||||
|
||||
knownProps[m.Name] = true
|
||||
|
||||
for _, f := range m.Fields {
|
||||
knownProps[fmt.Sprintf("%s.%s", m.Name, f)] = true
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
for _, a := range d.Actions {
|
||||
if a.Severity == "" {
|
||||
a.Severity = d.DefaultActionSeverity
|
||||
}
|
||||
}
|
||||
|
||||
for _, e := range d.Errors {
|
||||
if e.Severity == "" {
|
||||
e.Severity = d.DefaultErrorSeverity
|
||||
}
|
||||
|
||||
if e.HttpStatus != "" {
|
||||
d.SupportHttpErrors = true
|
||||
}
|
||||
}
|
||||
|
||||
checkHandle := func(s string) error {
|
||||
if !handle.IsValid(s) {
|
||||
return fmt.Errorf("handle empty")
|
||||
|
||||
}
|
||||
|
||||
if !handle.IsValid(s) {
|
||||
return fmt.Errorf("invalid handle format: %q", s)
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
placeholderMatcher := regexp.MustCompile(`{(.+?)}`)
|
||||
checkPlaceholders := func(def string, kind, s string) error {
|
||||
for _, match := range placeholderMatcher.FindAllStringSubmatch(s, 1) {
|
||||
placeholder := match[1]
|
||||
if !knownProps[placeholder] {
|
||||
return fmt.Errorf("unknown placeholder %q used in %s for %s", placeholder, def, kind)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, a := range d.Actions {
|
||||
checkHandle(a.Action)
|
||||
if a.Log == "" {
|
||||
// If no log is defined, use action handle
|
||||
a.Log = a.Action
|
||||
}
|
||||
|
||||
if err := checkPlaceholders(a.Action, "log", a.Log); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, e := range d.Errors {
|
||||
if err := checkHandle(e.Error); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if e.Message == "" {
|
||||
// If no error message is defined, use error handle
|
||||
e.Message = e.Error
|
||||
}
|
||||
|
||||
if e.Log == "" {
|
||||
// If no error log is defined, use error message
|
||||
e.Log = e.Message
|
||||
}
|
||||
|
||||
if err := checkPlaceholders(e.Error, "message", e.Message); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkPlaceholders(e.Error, "log", e.Log); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a actionsDef) Package() string {
|
||||
return path.Base(path.Dir(a.Source))
|
||||
}
|
||||
|
||||
func (a actionDef) SeverityConstName() string {
|
||||
return severityConstName(a.Severity)
|
||||
}
|
||||
|
||||
func (e errorDef) SeverityConstName() string {
|
||||
return severityConstName(e.Severity)
|
||||
}
|
||||
|
||||
func severityConstName(s string) string {
|
||||
switch strings.ToLower(s) {
|
||||
case "emergency":
|
||||
return "actionlog.Emergency"
|
||||
case "alert":
|
||||
return "actionlog.Alert"
|
||||
case "crit", "critical":
|
||||
return "actionlog.Critical"
|
||||
case "warn", "warning":
|
||||
return "actionlog.Warning"
|
||||
case "notice":
|
||||
return "actionlog.Notice"
|
||||
case "info", "informational":
|
||||
return "actionlog.Info"
|
||||
case "debug":
|
||||
return "actionlog.Debug"
|
||||
default:
|
||||
return "actionlog.Error"
|
||||
}
|
||||
}
|
||||
|
||||
func genActions(tpl *template.Template, dd []*actionsDef) (err error) {
|
||||
var (
|
||||
// Will only be generated if file does not exist previously
|
||||
tplActionsGen = tpl.Lookup("actions.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")
|
||||
err = goTemplate(dst, tplActionsGen, d)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
480
pkg/codegen/assets/actions.gen.go.tpl
Normal file
480
pkg/codegen/assets/actions.gen.go.tpl
Normal file
@@ -0,0 +1,480 @@
|
||||
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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"errors"
|
||||
"time"
|
||||
{{- if $.SupportHttpErrors }}
|
||||
"net/http"
|
||||
{{- end }}
|
||||
|
||||
"github.com/cortezaproject/corteza-server/pkg/actionlog"
|
||||
{{- range $import := $.Import }}
|
||||
{{ normalizeImport $import }}
|
||||
{{- end }}
|
||||
)
|
||||
|
||||
type (
|
||||
{{ $.Service }}ActionProps struct {
|
||||
{{- range $prop := $.Props }}
|
||||
{{ $prop.Name }} {{ $prop.Type }}
|
||||
{{- end }}
|
||||
}
|
||||
|
||||
{{ if $.Actions }}
|
||||
{{ $.Service }}Action struct {
|
||||
timestamp time.Time
|
||||
resource string
|
||||
action string
|
||||
log string
|
||||
severity actionlog.Severity
|
||||
|
||||
// prefix for error when action fails
|
||||
errorMessage string
|
||||
|
||||
props *{{ $.Service }}ActionProps
|
||||
}
|
||||
{{ end }}
|
||||
|
||||
{{ if $.Errors }}
|
||||
{{ $.Service }}Error struct {
|
||||
timestamp time.Time
|
||||
error string
|
||||
resource string
|
||||
action string
|
||||
message string
|
||||
log string
|
||||
severity actionlog.Severity
|
||||
|
||||
wrap error
|
||||
|
||||
props *{{ $.Service }}ActionProps
|
||||
|
||||
{{ if $.SupportHttpErrors }}
|
||||
httpStatusCode int
|
||||
{{ end }}
|
||||
}
|
||||
{{ end }}
|
||||
)
|
||||
|
||||
var (
|
||||
// just a placeholder to cover template cases w/o fmt package use
|
||||
_ = fmt.Println
|
||||
)
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
// Props methods
|
||||
|
||||
{{- range $prop := $.Props }}
|
||||
// {{ camelCase "set" $prop.Name }} updates {{ $.Service }}ActionProps's {{ $prop.Name }}
|
||||
//
|
||||
// Allows method chaining
|
||||
//
|
||||
// This function is auto-generated.
|
||||
//
|
||||
func (p *{{ $.Service }}ActionProps) {{ camelCase "set" $prop.Name }}({{ $prop.Name }} {{ $prop.Type }}) *{{ $.Service }}ActionProps {
|
||||
p.{{ $prop.Name }} = {{ $prop.Name }}
|
||||
return p
|
||||
}
|
||||
{{ end }}
|
||||
|
||||
|
||||
// serialize converts {{ $.Service }}ActionProps to actionlog.Meta
|
||||
//
|
||||
// This function is auto-generated.
|
||||
//
|
||||
func (p {{ $.Service }}ActionProps) serialize() actionlog.Meta {
|
||||
var (
|
||||
m = make(actionlog.Meta)
|
||||
)
|
||||
|
||||
{{ range $prop := $.Props }}
|
||||
{{- if $prop.Builtin }}
|
||||
m.Set("{{ $prop.Name }}", p.{{ $prop.Name }}, true)
|
||||
{{- else }}
|
||||
if p.{{ $prop.Name }} != nil {
|
||||
{{- range $f := $prop.Fields }}
|
||||
m.Set("{{ $prop.Name }}.{{ $f }}", p.{{ $prop.Name }}.{{ camelCase " " $f }}, true)
|
||||
{{- end }}
|
||||
}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// tr translates string and replaces meta value placeholder with values
|
||||
//
|
||||
// This function is auto-generated.
|
||||
//
|
||||
func (p {{ $.Service }}ActionProps) tr(in string, err error) string {
|
||||
var (
|
||||
pairs = []string{"{err}"}
|
||||
|
||||
{{- if $.Props }}
|
||||
// first non-empty string
|
||||
fns = func(ii ... interface{}) string {
|
||||
for _, i:= range ii {
|
||||
if s :=fmt.Sprintf("%v", i); len(s) > 0 {
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
{{- end }}
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
for {
|
||||
// Unwrap errors
|
||||
ue := errors.Unwrap(err)
|
||||
if ue == nil {
|
||||
break
|
||||
}
|
||||
|
||||
err = ue
|
||||
}
|
||||
|
||||
pairs = append(pairs, err.Error())
|
||||
} else {
|
||||
pairs = append(pairs, "nil")
|
||||
}
|
||||
|
||||
{{- range $prop := $.Props }}
|
||||
{{- if $prop.Builtin }}
|
||||
pairs = append(pairs, "{{"{"}}{{ $prop.Name }}}", fns(p.{{ $prop.Name }}))
|
||||
{{- else }}
|
||||
|
||||
if p.{{ $prop.Name }} != nil {
|
||||
// replacement for "{{"{"}}{{ $prop.Name }}}" (in order how fields are defined)
|
||||
pairs = append(
|
||||
pairs,
|
||||
"{{"{"}}{{ $prop.Name }}}",
|
||||
fns(
|
||||
{{- range $f := $prop.Fields }}
|
||||
p.{{ $prop.Name }}.{{ camelCase " " $f }},
|
||||
{{- end }}
|
||||
),
|
||||
)
|
||||
|
||||
{{- range $f := $prop.Fields }}
|
||||
pairs = append(pairs, "{{"{"}}{{ $prop.Name }}.{{ $f }}}", fns(p.{{ $prop.Name }}.{{ camelCase " " $f }}))
|
||||
{{- end }}
|
||||
}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
return strings.NewReplacer(pairs...).Replace(in)
|
||||
}
|
||||
|
||||
{{ if $.Actions }}
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
// Action methods
|
||||
|
||||
// String returns loggable description as string
|
||||
//
|
||||
// This function is auto-generated.
|
||||
//
|
||||
func (a *{{ $.Service }}Action) String() string {
|
||||
var props = &{{ $.Service }}ActionProps{}
|
||||
|
||||
if a.props != nil {
|
||||
props = a.props
|
||||
}
|
||||
|
||||
return props.tr(a.log, nil)
|
||||
}
|
||||
|
||||
func (e *{{ $.Service }}Action) LoggableAction() *actionlog.Action {
|
||||
return &actionlog.Action{
|
||||
Timestamp: e.timestamp,
|
||||
Resource: e.resource,
|
||||
Action: e.action,
|
||||
Severity: e.severity,
|
||||
Description: e.String(),
|
||||
Meta: e.props.serialize(),
|
||||
}
|
||||
}
|
||||
{{ end }}
|
||||
|
||||
{{ if $.Errors }}
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
// Error methods
|
||||
|
||||
// String returns loggable description as string
|
||||
//
|
||||
// It falls back to message if log is not set
|
||||
//
|
||||
// This function is auto-generated.
|
||||
//
|
||||
func (e *{{ $.Service }}Error) String() string {
|
||||
var props = &{{ $.Service }}ActionProps{}
|
||||
|
||||
if e.props != nil {
|
||||
props = e.props
|
||||
}
|
||||
|
||||
|
||||
if e.wrap != nil && !strings.Contains(e.log, "{err}") {
|
||||
// Suffix error log with {err} to ensure
|
||||
// we log the cause for this error
|
||||
e.log += ": {err}"
|
||||
}
|
||||
|
||||
return props.tr(e.log, e.wrap)
|
||||
}
|
||||
|
||||
// Error satisfies
|
||||
//
|
||||
// This function is auto-generated.
|
||||
//
|
||||
func (e *{{ $.Service }}Error) Error() string {
|
||||
var props = &{{ $.Service }}ActionProps{}
|
||||
|
||||
if e.props != nil {
|
||||
props = e.props
|
||||
}
|
||||
|
||||
return props.tr(e.message, e.wrap)
|
||||
}
|
||||
|
||||
// Is fn for error equality check
|
||||
//
|
||||
// This function is auto-generated.
|
||||
//
|
||||
func (e *{{ $.Service }}Error) Is(Resource error) bool {
|
||||
t, ok := Resource.(*{{ $.Service }}Error)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
return t.resource == e.resource && t.error == e.error
|
||||
}
|
||||
|
||||
// Wrap wraps {{ $.Service }}Error around another error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
//
|
||||
func (e *{{ $.Service }}Error) Wrap(err error) *{{ $.Service }}Error {
|
||||
e.wrap = err
|
||||
return e
|
||||
}
|
||||
|
||||
// Unwrap returns wrapped error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
//
|
||||
func (e *{{ $.Service }}Error) Unwrap() error {
|
||||
return e.wrap
|
||||
}
|
||||
|
||||
func (e *{{ $.Service }}Error) LoggableAction() *actionlog.Action {
|
||||
return &actionlog.Action{
|
||||
Timestamp: e.timestamp,
|
||||
Resource: e.resource,
|
||||
Action: e.action,
|
||||
Severity: e.severity,
|
||||
Description: e.String(),
|
||||
Error: e.Error(),
|
||||
Meta: e.props.serialize(),
|
||||
}
|
||||
}
|
||||
|
||||
{{ if $.SupportHttpErrors }}
|
||||
func (e *{{ $.Service }}Error) HttpResponse(w http.ResponseWriter) {
|
||||
var code = e.httpStatusCode
|
||||
if code == 0 {
|
||||
code = http.StatusInternalServerError
|
||||
}
|
||||
|
||||
http.Error(w, e.message, code)
|
||||
}
|
||||
{{ end }}
|
||||
|
||||
{{ end }}
|
||||
|
||||
{{ if $.Actions }}
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
// Action constructors
|
||||
|
||||
{{ range $a := $.Actions }}
|
||||
// {{ camelCase "" $.Service "Action" $a.Action }} returns "{{ $.Resource }}.{{ $a.Action }}" error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
//
|
||||
func {{ camelCase "" $.Service "Action" $a.Action }}(props ... *{{ $.Service }}ActionProps) *{{ $.Service }}Action {
|
||||
a := &{{ $.Service }}Action{
|
||||
timestamp: time.Now(),
|
||||
resource: "{{ $.Resource }}",
|
||||
action: "{{ $a.Action }}",
|
||||
log: "{{ $a.Log }}",
|
||||
severity: {{ $a.SeverityConstName }},
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ if $.Errors }}
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
// Error constructors
|
||||
|
||||
{{ range $e := $.Errors }}
|
||||
{{- if $e.Safe }}
|
||||
// {{ camelCase "" $.Service "Err" $e.Error }} returns "{{ $.Resource }}.{{ $e.Safe }}" audit event as {{ $e.SeverityConstName }}
|
||||
{{- else }}
|
||||
// {{ camelCase "" $.Service "Err" $e.Error }} returns "{{ $.Resource }}.{{ $e.Error }}" audit event as {{ $e.SeverityConstName }}
|
||||
{{- end }}
|
||||
//
|
||||
{{- if $e.Safe }}
|
||||
// Note: This error will be wrapped with safe ({{ $e.Safe }}) error!
|
||||
{{- end }}
|
||||
//
|
||||
// This function is auto-generated.
|
||||
//
|
||||
func {{ camelCase "" $.Service "Err" $e.Error }}(props ... *{{ $.Service }}ActionProps) *{{ $.Service }}Error {
|
||||
var e = &{{ $.Service }}Error{
|
||||
timestamp: time.Now(),
|
||||
resource: "{{ $.Resource }}",
|
||||
error: "{{ $e.Error }}",
|
||||
action: "error",
|
||||
message: "{{ $e.Message }}",
|
||||
log: "{{ $e.Log }}",
|
||||
severity: {{ $e.SeverityConstName }},
|
||||
props: func() *{{ $.Service }}ActionProps { if len(props) > 0 { return props[0] }; return nil}(),
|
||||
|
||||
{{ if $e.HttpStatus }}
|
||||
httpStatusCode: http.{{ $e.HttpStatus }},
|
||||
{{ end }}
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
e.props = props[0]
|
||||
}
|
||||
|
||||
{{ if $e.Safe }}
|
||||
// Wrap with safe error
|
||||
return {{ camelCase "" $.Service "Err" $e.Safe }}().Wrap(e)
|
||||
{{ else }}
|
||||
return e
|
||||
{{ end }}
|
||||
}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
|
||||
// recordAction is a service helper function wraps function that can return error
|
||||
//
|
||||
// context is used to enrich audit log entry with current user info, request ID, IP address...
|
||||
// props are collected action/error properties
|
||||
// action (optional) fn will be used to construct {{ $.Service }}Action struct from given props (and error)
|
||||
// err is any error that occurred while action was happening
|
||||
//
|
||||
// Action has success and fail (error) state:
|
||||
// - when recorded without an error (4th param), action is recorded as successful.
|
||||
// - when an additional error is given (4th param), action is used to wrap
|
||||
// the additional error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
//
|
||||
func (svc {{ $.Service }}) recordAction(ctx context.Context, props *{{ $.Service }}ActionProps, action func(... *{{ $.Service }}ActionProps) *{{ $.Service }}Action, err error) error {
|
||||
var (
|
||||
ok bool
|
||||
|
||||
// Return error
|
||||
retError *{{ $.Service }}Error
|
||||
|
||||
// Recorder error
|
||||
recError *{{ $.Service }}Error
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
if retError, ok = err.(*{{ $.Service }}Error); !ok {
|
||||
// got non-{{ $.Service }} error, wrap it with {{ camelCase "" $.Service "err" "generic" }}
|
||||
retError = {{ camelCase "" $.Service "err" "generic" }}(props).Wrap(err)
|
||||
|
||||
if action != nil {
|
||||
// copy action to returning and recording error
|
||||
retError.action = action().action
|
||||
}
|
||||
|
||||
// we'll use {{ camelCase "" $.Service "err" "generic" }} for recording too
|
||||
// because it can hold more info
|
||||
recError = retError
|
||||
} else if retError != nil {
|
||||
if action != nil {
|
||||
// copy action to returning and recording error
|
||||
retError.action = action().action
|
||||
}
|
||||
// start with copy of return error for recording
|
||||
// this will be updated with tha root cause as we try and
|
||||
// unwrap the error
|
||||
recError = retError
|
||||
|
||||
// find the original recError for this error
|
||||
// for the purpose of logging
|
||||
var unwrappedError error = retError
|
||||
for {
|
||||
if unwrappedError = errors.Unwrap(unwrappedError); unwrappedError == nil {
|
||||
// nothing wrapped
|
||||
break
|
||||
}
|
||||
|
||||
// update recError ONLY of wrapped error is of type {{ $.Service }}Error
|
||||
if unwrappedSinkError, ok := unwrappedError.(*{{ $.Service }}Error); ok {
|
||||
recError = unwrappedSinkError
|
||||
}
|
||||
}
|
||||
|
||||
if retError.props == nil {
|
||||
// set props on returning error if empty
|
||||
retError.props = props
|
||||
}
|
||||
|
||||
if recError.props == nil {
|
||||
// set props on recording error if empty
|
||||
recError.props = props
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if svc.actionlog != nil {
|
||||
if retError != nil {
|
||||
// failed action, log error
|
||||
svc.actionlog.Record(ctx, recError)
|
||||
} else if action != nil {
|
||||
// successful
|
||||
svc.actionlog.Record(ctx, action(props))
|
||||
}
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
// retError not an interface and that WILL (!!) cause issues
|
||||
// with nil check (== nil) when it is not explicitly returned
|
||||
return nil
|
||||
}
|
||||
|
||||
return retError
|
||||
}
|
||||
29
pkg/codegen/assets/events.gen.adoc.tpl
Normal file
29
pkg/codegen/assets/events.gen.adoc.tpl
Normal file
@@ -0,0 +1,29 @@
|
||||
// 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 }}
|
||||
|
||||
= {{ $.ResourceString }}
|
||||
|
||||
|
||||
.List of events on `{{ $.ResourceString }}`
|
||||
{{- range $event := $.Events.MakeEvents }}
|
||||
- `{{ $event }}`
|
||||
{{- end }}
|
||||
|
||||
.Event arguments for `{{ $.ResourceString }}`
|
||||
[%header,cols=3*]
|
||||
|===
|
||||
|Name
|
||||
|Type
|
||||
|Immutable
|
||||
|
||||
{{- range $p := $.Events.Properties }}
|
||||
|`{{ camelCase $p.Name }}`
|
||||
|`{{ $p.Type }}`
|
||||
|{{ $p.Immutable }}
|
||||
{{- end }}
|
||||
|===
|
||||
183
pkg/codegen/assets/events.gen.go.tpl
Normal file
183
pkg/codegen/assets/events.gen.go.tpl
Normal file
@@ -0,0 +1,183 @@
|
||||
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 (
|
||||
"encoding/json"
|
||||
{{- range $i, $import := .Imports }}
|
||||
{{ normalizeImport $import }}
|
||||
{{- end }}
|
||||
)
|
||||
|
||||
// dummy placing to simplify import generation logic
|
||||
var _ = json.NewEncoder
|
||||
|
||||
type (
|
||||
{{ range $r := $.Resources }}
|
||||
// {{ camelCase $r.ResourceIdent "base" }}
|
||||
//
|
||||
// This type is auto-generated.
|
||||
{{ camelCase $r.ResourceIdent "base" }} struct {
|
||||
immutable bool
|
||||
{{- range $p := $r.Properties }}
|
||||
{{ $p.Name }} {{ $p.Type }}
|
||||
{{- end }}
|
||||
}
|
||||
|
||||
{{ range $event := $r.Events }}
|
||||
// {{ camelCase $r.ResourceIdent $event }}
|
||||
//
|
||||
// This type is auto-generated.
|
||||
{{ camelCase $r.ResourceIdent $event }} struct {
|
||||
*{{ camelCase $r.ResourceIdent "base" }}
|
||||
}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
)
|
||||
|
||||
|
||||
{{ range $r := $.Resources }}
|
||||
|
||||
// ResourceType returns "{{ $r.ResourceString }}"
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func ({{ camelCase .ResourceIdent "base" }}) ResourceType() string {
|
||||
return "{{ .ResourceString }}"
|
||||
}
|
||||
|
||||
{{ range $event := $r.Events }}
|
||||
// EventType on {{ camelCase $r.ResourceIdent $event }} returns "{{ $event }}"
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func ({{ camelCase $r.ResourceIdent $event }}) EventType() string {
|
||||
return "{{ $event }}"
|
||||
}
|
||||
{{ end }}
|
||||
|
||||
{{ range $event := $r.Events }}
|
||||
// {{ camelCase "" $r.ResourceIdent $event }} creates {{ $event }} for {{ $r.ResourceString }} resource
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func {{ camelCase "" $r.ResourceIdent $event }}(
|
||||
{{- range $p := $r.Properties }}
|
||||
{{- if not $p.Internal }}
|
||||
{{ camelCase "arg" $p.Name }} {{ $p.Type }},
|
||||
{{- end -}}
|
||||
{{- end}}
|
||||
) *{{ camelCase $r.ResourceIdent $event }} {
|
||||
return &{{ camelCase $r.ResourceIdent $event }}{
|
||||
{{ camelCase $r.ResourceIdent "base" }}: &{{ camelCase $r.ResourceIdent "base" }}{
|
||||
immutable: false,
|
||||
{{- range $p := $r.Properties }}
|
||||
{{- if not $p.Internal }}
|
||||
{{ $p.Name }}: {{ camelCase "arg" $p.Name }},
|
||||
{{- end -}}
|
||||
{{- end}}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// {{ camelCase "" $r.ResourceIdent $event "Immutable" }} creates {{ $event }} for {{ $r.ResourceString }} resource
|
||||
//
|
||||
// None of the arguments will be mutable!
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func {{ camelCase "" $r.ResourceIdent $event "Immutable" }}(
|
||||
{{- range $p := $r.Properties }}
|
||||
{{- if not $p.Internal }}
|
||||
{{ camelCase "arg" $p.Name }} {{ $p.Type }},
|
||||
{{- end -}}
|
||||
{{- end}}
|
||||
) *{{ camelCase $r.ResourceIdent $event }} {
|
||||
return &{{ camelCase $r.ResourceIdent $event }}{
|
||||
{{ camelCase $r.ResourceIdent "base" }}: &{{ camelCase $r.ResourceIdent "base" }}{
|
||||
immutable: true,
|
||||
{{- range $p := $r.Properties }}
|
||||
{{- if not $p.Internal }}
|
||||
{{ $p.Name }}: {{ camelCase "arg" $p.Name }},
|
||||
{{- end -}}
|
||||
{{- end}}
|
||||
},
|
||||
}
|
||||
}
|
||||
{{ end }}
|
||||
|
||||
|
||||
|
||||
{{ range $p := $r.Properties }}
|
||||
{{ if not $p.Immutable }}
|
||||
// {{ camelCase "Set" $p.Name }} sets new {{ $p.Name }} value
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (res *{{ camelCase $r.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 $r.ResourceIdent "base" }}) {{ camelCase "" $p.Name }}() {{ $p.Type }} {
|
||||
return res.{{ $p.Name }}
|
||||
}
|
||||
{{ end }}
|
||||
|
||||
|
||||
// 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 $r.Properties }}
|
||||
args = make(map[string][]byte)
|
||||
|
||||
{{ range $prop := $r.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 res.immutable {
|
||||
// Respect immutability
|
||||
return
|
||||
}
|
||||
|
||||
{{- if $r.Result }}
|
||||
if res.{{ $r.Result }} != nil {
|
||||
if r, ok := results["result"]; ok && len(results) == 1 {
|
||||
if err = json.Unmarshal(r, res.{{ $r.Result }}); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
{{ end -}}
|
||||
|
||||
{{- range $prop := $r.Properties }}
|
||||
{{- if not $prop.Immutable }}
|
||||
if res.{{ $prop.Name }} != nil {
|
||||
if r, ok := results["{{ $prop.Name }}"]; ok {
|
||||
if err = json.Unmarshal(r, res.{{ $prop.Name }}); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
{{ else }}
|
||||
// Do not decode {{ $prop.Name }}; marked as immutable
|
||||
{{ end -}}
|
||||
{{ end -}}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
{{ end }}
|
||||
19
pkg/codegen/assets/events.go.tpl
Normal file
19
pkg/codegen/assets/events.go.tpl
Normal file
@@ -0,0 +1,19 @@
|
||||
package {{ .Package }}
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza-server/pkg/eventbus"
|
||||
)
|
||||
|
||||
var _=eventbus.ConstraintMaker
|
||||
|
||||
// Match returns false if given conditions do not match event & resource internals
|
||||
func (res {{ camelCase .ResourceIdent "base" }}) Match(c eventbus.ConstraintMatcher) 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
|
||||
}
|
||||
73
pkg/codegen/assets/rest_handler.go.tpl
Normal file
73
pkg/codegen/assets/rest_handler.go.tpl
Normal file
@@ -0,0 +1,73 @@
|
||||
package handlers
|
||||
|
||||
// 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 (
|
||||
"context"
|
||||
"net/http"
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/titpetric/factory/resputil"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/{{ .App }}/rest/request"
|
||||
"github.com/cortezaproject/corteza-server/pkg/logger"
|
||||
)
|
||||
|
||||
type (
|
||||
// Internal API interface
|
||||
{{ pubIdent $.Endpoint.Entrypoint }}API interface {
|
||||
{{- range $a := $.Endpoint.Apis }}
|
||||
{{ pubIdent $a.Name }}(context.Context, *request.{{ pubIdent $.Endpoint.Entrypoint $a.Name }}) (interface{}, error)
|
||||
{{- end }}
|
||||
}
|
||||
|
||||
// HTTP API interface
|
||||
{{ pubIdent .Endpoint.Entrypoint }} struct {
|
||||
{{- range $a := .Endpoint.Apis }}
|
||||
{{ pubIdent $a.Name }} func(http.ResponseWriter, *http.Request)
|
||||
{{- end }}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
func {{ pubIdent "New" $.Endpoint.Entrypoint }}(h {{ pubIdent $.Endpoint.Entrypoint }}API) *{{ pubIdent $.Endpoint.Entrypoint }} {
|
||||
return &{{ pubIdent $.Endpoint.Entrypoint }}{
|
||||
{{- range $a := .Endpoint.Apis }}
|
||||
{{ pubIdent $a.Name }}: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.New{{ pubIdent $.Endpoint.Entrypoint $a.Name }}()
|
||||
if err := params.Fill(r); err != nil {
|
||||
logger.LogParamError("{{ pubIdent $.Endpoint.Entrypoint }}.{{ pubIdent $a.Name }}", r, err)
|
||||
resputil.JSON(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
value, err := h.{{ pubIdent $a.Name }}(r.Context(), params)
|
||||
if err != nil {
|
||||
logger.LogControllerError("{{ pubIdent $.Endpoint.Entrypoint }}.{{ pubIdent $a.Name }}", r, err, params.Auditable())
|
||||
resputil.JSON(w, err)
|
||||
return
|
||||
}
|
||||
logger.LogControllerCall("{{ pubIdent $.Endpoint.Entrypoint }}.{{ pubIdent $a.Name }}", r, params.Auditable())
|
||||
if !serveHTTP(value, w, r) {
|
||||
resputil.JSON(w, value)
|
||||
}
|
||||
},
|
||||
{{- end }}
|
||||
}
|
||||
}
|
||||
|
||||
func (h {{ pubIdent $.Endpoint.Entrypoint }}) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) {
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(middlewares...)
|
||||
|
||||
{{- range $a := .Endpoint.Apis }}
|
||||
r.{{ pubIdent ( toLower $a.Method ) }}("{{ $.Endpoint.Path }}{{ $a.Path }}", h.{{ pubIdent $a.Name }})
|
||||
{{- end }}
|
||||
})
|
||||
}
|
||||
168
pkg/codegen/assets/rest_request.go.tpl
Normal file
168
pkg/codegen/assets/rest_request.go.tpl
Normal file
@@ -0,0 +1,168 @@
|
||||
package request
|
||||
|
||||
// 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 (
|
||||
"encoding/json"
|
||||
"github.com/cortezaproject/corteza-server/pkg/payload"
|
||||
"github.com/go-chi/chi"
|
||||
"io"
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"strings"
|
||||
{{- range $i, $import := $.Imports }}
|
||||
{{ normalizeImport $import }}
|
||||
{{- end }}
|
||||
)
|
||||
|
||||
// dummy vars to prevent
|
||||
// unused imports complain
|
||||
var (
|
||||
_ = chi.URLParam
|
||||
_ = multipart.ErrMessageTooLarge
|
||||
_ = payload.ParseUint64s
|
||||
)
|
||||
|
||||
type (
|
||||
// Internal API interface
|
||||
{{- range $a := $.Endpoint.Apis }}
|
||||
{{ pubIdent $.Endpoint.Entrypoint $a.Name }} struct {
|
||||
{{- range $p := $a.Params.All }}
|
||||
// {{ pubIdent $p.Name }} {{ $p.Origin }} parameter
|
||||
//
|
||||
// {{ $p.Title }}
|
||||
{{ pubIdent $p.Name }} {{ $p.Type }} {{ $p.FieldTag }}
|
||||
{{ end }}
|
||||
}
|
||||
{{ end }}
|
||||
)
|
||||
|
||||
{{- range $a := $.Endpoint.Apis }}
|
||||
// {{ pubIdent "New" $.Endpoint.Entrypoint $a.Name }} request
|
||||
func {{ pubIdent "New" $.Endpoint.Entrypoint $a.Name }}() *{{ pubIdent $.Endpoint.Entrypoint $a.Name }} {
|
||||
return &{{ pubIdent $.Endpoint.Entrypoint $a.Name }}{}
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r {{ pubIdent $.Endpoint.Entrypoint $a.Name }}) Auditable() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
{{- range $p := $a.Params.All }}
|
||||
"{{ $p.Name }}": r.{{ pubIdent $p.Name }},
|
||||
{{- end }}
|
||||
}
|
||||
}
|
||||
|
||||
{{- range $p := $a.Params.All }}
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r {{ pubIdent $.Endpoint.Entrypoint $a.Name }}) Get{{ pubIdent $p.Name }}() {{ $p.Type }} {
|
||||
return r.{{ pubIdent $p.Name }}
|
||||
}
|
||||
{{- end }}
|
||||
|
||||
|
||||
|
||||
// Fill processes request and fills internal variables
|
||||
func (r *{{ pubIdent $.Endpoint.Entrypoint $a.Name }}) Fill(req *http.Request) (err error) {
|
||||
if strings.ToLower(req.Header.Get("content-type")) == "application/json" {
|
||||
err = json.NewDecoder(req.Body).Decode(r)
|
||||
|
||||
switch {
|
||||
case err == io.EOF:
|
||||
err = nil
|
||||
case err != nil:
|
||||
return fmt.Errorf("error parsing http request body: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
{{ if $a.Params.Get }}
|
||||
{
|
||||
// GET params
|
||||
tmp := req.URL.Query()
|
||||
{{ range $p := $a.Params.Get }}
|
||||
{{- if not $p.IsSlice }}
|
||||
if val, ok := tmp["{{ $p.Name }}"]; ok && len(val) > 0 {
|
||||
r.{{ pubIdent $p.Name }}, err = {{ $p.Parser "val[0]" }}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
{{- end }}
|
||||
{{- if $p.IsSlice }}
|
||||
if val, ok := tmp["{{ $p.Name }}[]"]; ok {
|
||||
r.{{ pubIdent $p.Name }}, err = {{ $p.Parser "val" }}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if val, ok := tmp["{{ $p.Name }}"]; ok {
|
||||
r.{{ pubIdent $p.Name }}, err = {{ $p.Parser "val" }}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
}
|
||||
{{- end }}
|
||||
|
||||
{{ if $a.Params.Post }}
|
||||
{
|
||||
if err = req.ParseForm(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// POST params
|
||||
{{ range $p := $a.Params.Post }}
|
||||
{{ if $p.IsUpload }}
|
||||
if _, r.{{ pubIdent $p.Name }}, err = req.FormFile("{{ $p.Name }}"); err != nil {
|
||||
return fmt.Errorf("error processing uploaded file: %w", err)
|
||||
}
|
||||
{{ else }}
|
||||
{{- if not $p.IsSlice }}
|
||||
if val, ok := req.Form["{{ $p.Name }}"]; ok && len(val) > 0 {
|
||||
r.{{ pubIdent $p.Name }}, err = {{ $p.Parser "val[0]" }}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
{{- end }}
|
||||
{{- if $p.IsSlice }}
|
||||
//if val, ok := req.Form["{{ $p.Name }}[]"]; ok && len(val) > 0 {
|
||||
// r.{{ pubIdent $p.Name }}, err = {{ $p.Parser "val" }}
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
//}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- end }}
|
||||
}
|
||||
{{ end }}
|
||||
|
||||
{{ if $a.Params.Path }}
|
||||
{
|
||||
var val string
|
||||
// path params
|
||||
{{ range $p := $a.Params.Path }}
|
||||
val = chi.URLParam(req, "{{ $p.Name }}")
|
||||
r.{{ pubIdent $p.Name }}, err = {{ $p.Parser "val" }}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
{{ end }}
|
||||
|
||||
}
|
||||
{{ end }}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
{{- end }}
|
||||
74
pkg/codegen/assets/store_bulk.gen.go.tpl
Normal file
74
pkg/codegen/assets/store_bulk.gen.go.tpl
Normal file
@@ -0,0 +1,74 @@
|
||||
package bulk
|
||||
|
||||
// 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 (
|
||||
"context"
|
||||
{{- range $import := $.Import }}
|
||||
{{ normalizeImport $import }}
|
||||
{{- end }}
|
||||
)
|
||||
|
||||
type (
|
||||
{{ unpubIdent $.Types.Singular }}Create struct {
|
||||
Done chan struct{}
|
||||
res *{{ $.Types.GoType }}
|
||||
err error
|
||||
}
|
||||
|
||||
{{ unpubIdent $.Types.Singular }}Update struct {
|
||||
Done chan struct{}
|
||||
res *{{ $.Types.GoType }}
|
||||
err error
|
||||
}
|
||||
|
||||
{{ unpubIdent $.Types.Singular }}Remove struct {
|
||||
Done chan struct{}
|
||||
res *{{ $.Types.GoType }}
|
||||
err error
|
||||
}
|
||||
)
|
||||
|
||||
// Create{{ pubIdent $.Types.Singular }} creates a new {{ pubIdent $.Types.Singular }}
|
||||
// create job that can be pushed to store's transaction handler
|
||||
func Create{{ pubIdent $.Types.Singular }}(res *{{ $.Types.GoType }}) *{{ unpubIdent $.Types.Singular }}Create {
|
||||
return &{{ unpubIdent $.Types.Singular }}Create{res: res}
|
||||
}
|
||||
|
||||
// Do Executes {{ unpubIdent $.Types.Singular }}Create job
|
||||
func (j *{{ unpubIdent $.Types.Singular }}Create) Do(ctx context.Context, s storeInterface) error {
|
||||
j.err = s.Create{{ pubIdent $.Types.Singular }}(ctx, j.res)
|
||||
j.Done <- struct{}{}
|
||||
return j.err
|
||||
}
|
||||
|
||||
// Update{{ pubIdent $.Types.Singular }} creates a new {{ pubIdent $.Types.Singular }}
|
||||
// update job that can be pushed to store's transaction handler
|
||||
func Update{{ pubIdent $.Types.Singular }}(res *{{ $.Types.GoType }}) *{{ unpubIdent $.Types.Singular }}Update {
|
||||
return &{{ unpubIdent $.Types.Singular }}Update{res: res}
|
||||
}
|
||||
|
||||
// Do Executes {{ unpubIdent $.Types.Singular }}Update job
|
||||
func (j *{{ unpubIdent $.Types.Singular }}Update) Do(ctx context.Context, s storeInterface) error {
|
||||
j.err = s.Update{{ pubIdent $.Types.Singular }}(ctx, j.res)
|
||||
j.Done <- struct{}{}
|
||||
return j.err
|
||||
}
|
||||
|
||||
// Remove{{ pubIdent $.Types.Singular }} creates a new {{ pubIdent $.Types.Singular }}
|
||||
// remove job that can be pushed to store's transaction handler
|
||||
func Remove{{ pubIdent $.Types.Singular }}(res *{{ $.Types.GoType }}) *{{ unpubIdent $.Types.Singular }}Remove {
|
||||
return &{{ unpubIdent $.Types.Singular }}Remove{res: res}
|
||||
}
|
||||
|
||||
// Do Executes {{ unpubIdent $.Types.Singular }}Remove job
|
||||
func (j *{{ unpubIdent $.Types.Singular }}Remove) Do(ctx context.Context, s storeInterface) error {
|
||||
j.err = s.Remove{{ pubIdent $.Types.Singular }}(ctx, j.res)
|
||||
j.Done <- struct{}{}
|
||||
return j.err
|
||||
}
|
||||
37
pkg/codegen/assets/store_interfaces.gen.go.tpl
Normal file
37
pkg/codegen/assets/store_interfaces.gen.go.tpl
Normal file
@@ -0,0 +1,37 @@
|
||||
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 (
|
||||
"context"
|
||||
{{- range .Import }}
|
||||
{{ normalizeImport . }}
|
||||
{{- end }}
|
||||
)
|
||||
|
||||
type (
|
||||
{{- $Types := .Types }}
|
||||
{{- $Fields := .Fields }}
|
||||
|
||||
{{ unpubIdent .Types.Plural }}Store interface {
|
||||
{{- if not .Search.Disable }}
|
||||
Search{{ pubIdent $Types.Plural }}(ctx context.Context, f {{ $Types.GoFilterType }}) ({{ $Types.GoSetType }}, {{ $Types.GoFilterType }}, error)
|
||||
{{- end }}
|
||||
{{- range .Lookups }}
|
||||
Lookup{{ pubIdent $Types.Singular }}By{{ pubIdent .Suffix }}(ctx context.Context{{- range $field := .Fields }}, {{ cc2underscore $field }} {{ ($field | $Fields.Find).Type }}{{- end }}) (*{{ $Types.GoType }}, error)
|
||||
{{- end }}
|
||||
Create{{ pubIdent $Types.Singular }}(ctx context.Context, rr ... *{{ $Types.GoType }}) error
|
||||
Update{{ pubIdent $Types.Singular }}(ctx context.Context, rr ... *{{ $Types.GoType }}) error
|
||||
PartialUpdate{{ pubIdent $Types.Singular }}(ctx context.Context, onlyColumns []string, rr ... *{{ $Types.GoType }}) error
|
||||
Remove{{ pubIdent $Types.Singular }}(ctx context.Context, rr ... *{{ $Types.GoType }}) error
|
||||
Remove{{ pubIdent $Types.Singular }}By{{ template "primaryKeySuffix" $Fields }}(ctx context.Context {{ template "primaryKeyArgs" $Fields }}) error
|
||||
|
||||
Truncate{{ pubIdent $Types.Plural }}(ctx context.Context) error
|
||||
}
|
||||
)
|
||||
20
pkg/codegen/assets/store_interfaces_joined.gen.go.tpl
Normal file
20
pkg/codegen/assets/store_interfaces_joined.gen.go.tpl
Normal file
@@ -0,0 +1,20 @@
|
||||
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:
|
||||
{{- range .Definitions }}
|
||||
// - {{ .Source }}
|
||||
{{- end }}
|
||||
|
||||
type (
|
||||
// Interface combines interfaces of all supported store interfaces
|
||||
storeInterface interface {
|
||||
{{ range .Definitions -}}
|
||||
{{ unpubIdent .Types.Plural }}Store
|
||||
{{ end }}
|
||||
}
|
||||
)
|
||||
17
pkg/codegen/assets/store_partials.go.tpl
Normal file
17
pkg/codegen/assets/store_partials.go.tpl
Normal file
@@ -0,0 +1,17 @@
|
||||
{{- define "primaryKeyArgs" -}}
|
||||
{{- range $field := . -}}
|
||||
{{- if $field.IsPrimaryKey -}}
|
||||
, {{ $field.Arg }} {{ camelCase $field.Type }}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "primaryKeySuffix" -}}
|
||||
{{- range $field := . }}{{ if $field.IsPrimaryKey }}{{ $field.Field }}{{ end }}{{ end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "partialUpdateArgs" -}}
|
||||
{{- range .Args -}}
|
||||
, {{ .Arg }} {{ .Type }}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
291
pkg/codegen/assets/store_rdbms.gen.go.tpl
Normal file
291
pkg/codegen/assets/store_rdbms.gen.go.tpl
Normal file
@@ -0,0 +1,291 @@
|
||||
package rdbms
|
||||
|
||||
// This file is an auto-generated file
|
||||
//
|
||||
// Template: pkg/codegen/assets/store_rdbms.gen.go.tpl
|
||||
// Definitions: {{ .Source }}
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/Masterminds/squirrel"
|
||||
"github.com/cortezaproject/corteza-server/store"
|
||||
|
||||
{{- range $import := $.Import }}
|
||||
{{ normalizeImport $import }}
|
||||
{{- end }}
|
||||
)
|
||||
|
||||
{{ if not $.Search.Disable }}
|
||||
// Search{{ pubIdent $.Types.Plural }} returns all matching rows
|
||||
//
|
||||
// This function calls convert{{ pubIdent $.Types.Singular }}Filter with the given
|
||||
// {{ $.Types.GoFilterType }} and expects to receive a working squirrel.SelectBuilder
|
||||
func (s Store) Search{{ pubIdent $.Types.Plural }}(ctx context.Context, f {{ $.Types.GoFilterType }}) ({{ $.Types.GoSetType }}, {{ $.Types.GoFilterType }}, error) {
|
||||
{{- if .RDBMS.CustomFilterConverter }}
|
||||
q, err := s.convert{{ pubIdent $.Types.Singular }}Filter(f)
|
||||
if err != nil {
|
||||
return nil, f, err
|
||||
}
|
||||
{{- else }}
|
||||
q := s.Query{{ pubIdent $.Types.Plural }}()
|
||||
{{- end }}
|
||||
|
||||
{{ if $.Search.DisablePaging }}
|
||||
scap := DefaultSliceCapacity
|
||||
{{ else }}
|
||||
q = ApplyPaging(q, f.PageFilter)
|
||||
|
||||
scap := f.PerPage
|
||||
if scap == 0 {
|
||||
scap = DefaultSliceCapacity
|
||||
}
|
||||
{{ end }}
|
||||
|
||||
var (
|
||||
set = make([]*{{ $.Types.GoType }}, 0, scap)
|
||||
res *{{ $.Types.GoType }}
|
||||
)
|
||||
|
||||
return set, f, func() error {
|
||||
{{- if not $.Search.DisablePaging }}
|
||||
if f.Count, err = Count(ctx, s.db, q); err != nil || f.Count == 0 {
|
||||
return err
|
||||
}
|
||||
{{- end }}
|
||||
rows, err := s.Query(ctx, q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
if res, err = s.internal{{ pubIdent $.Types.Singular }}RowScanner(rows, rows.Err()); err != nil {
|
||||
if cerr := rows.Close(); cerr != nil {
|
||||
return fmt.Errorf("could not close rows (%v) after scan error: %w", cerr, err)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
set = append(set, res)
|
||||
}
|
||||
|
||||
return rows.Close()
|
||||
}()
|
||||
}
|
||||
{{ end }}
|
||||
|
||||
{{- range $lookup := $.Lookups }}
|
||||
// Lookup{{ pubIdent $.Types.Singular }}By{{ pubIdent $lookup.Suffix }} {{ comment $lookup.Description true -}}
|
||||
func (s Store) Lookup{{ pubIdent $.Types.Singular }}By{{ pubIdent $lookup.Suffix }}(ctx context.Context{{- range $field := $lookup.Fields }}, {{ cc2underscore $field }} {{ ($field | $.Fields.Find).Type }}{{- end }}) (*{{ $.Types.GoType }}, error) {
|
||||
return s.{{ $.Types.Singular }}Lookup(ctx, squirrel.Eq{
|
||||
{{- range $field := $lookup.Fields }}
|
||||
"{{ ($field | $.Fields.Find).AliasedColumn }}": {{ cc2underscore $field }},
|
||||
{{- end }}
|
||||
{{- range $field, $value := $lookup.Filter }}
|
||||
"{{ ($field | $.Fields.Find).AliasedColumn }}": {{ $value }},
|
||||
{{- end }}
|
||||
})
|
||||
}
|
||||
{{ end }}
|
||||
|
||||
// Create{{ pubIdent $.Types.Singular }} creates one or more rows in {{ $.RDBMS.Table }} table
|
||||
func (s Store) Create{{ pubIdent $.Types.Singular }}(ctx context.Context, rr ... *{{ $.Types.GoType }}) error {
|
||||
if len(rr) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return Tx(ctx, s.db, s.config, nil, func(db *sqlx.Tx) (err error) {
|
||||
for _, res := range rr {
|
||||
err = ExecuteSqlizer(ctx, s.DB(), s.Insert(s.{{ $.Types.Singular }}Table()).SetMap(s.internal{{ pubIdent $.Types.Singular }}Encoder(res)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Update{{ pubIdent $.Types.Singular }} updates one or more existing rows in {{ $.RDBMS.Table }}
|
||||
func (s Store) Update{{ pubIdent $.Types.Singular }}(ctx context.Context, rr ... *{{ $.Types.GoType }}) error {
|
||||
return s.PartialUpdate{{ pubIdent $.Types.Singular }}(ctx, nil, rr...)
|
||||
}
|
||||
|
||||
// PartialUpdate{{ pubIdent $.Types.Singular }} updates one or more existing rows in {{ $.RDBMS.Table }}
|
||||
//
|
||||
// It wraps the update into transaction and can perform partial update by providing list of updatable columns
|
||||
func (s Store) PartialUpdate{{ pubIdent $.Types.Singular }}(ctx context.Context, onlyColumns []string, rr ... *{{ $.Types.GoType }}) error {
|
||||
if len(rr) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return Tx(ctx, s.db, s.config, nil, func(db *sqlx.Tx) (err error) {
|
||||
for _, res := range rr {
|
||||
err = s.ExecUpdate{{ pubIdent $.Types.Plural }}(
|
||||
ctx,
|
||||
{{ template "filterByPrimaryKeys" $.Fields }},
|
||||
s.internal{{ pubIdent $.Types.Singular }}Encoder(res).Skip(
|
||||
{{- range $field := $.Fields -}}
|
||||
{{- if $field.IsPrimaryKey -}}
|
||||
{{ printf "%q" $field.Column }},
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
).Only(onlyColumns...))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Remove{{ pubIdent $.Types.Singular }} removes one or more rows from {{ $.RDBMS.Table }} table
|
||||
func (s Store) Remove{{ pubIdent $.Types.Singular }}(ctx context.Context, rr ... *{{ $.Types.GoType }}) error {
|
||||
if len(rr) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return Tx(ctx, s.db, s.config, nil, func(db *sqlx.Tx) (err error) {
|
||||
for _, res := range rr {
|
||||
err = ExecuteSqlizer(ctx, s.DB(), s.Delete(s.{{ $.Types.Singular }}Table({{ printf "%q" .RDBMS.Alias }})).Where({{ template "filterByPrimaryKeys" $.Fields }},))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// Remove{{ pubIdent $.Types.Singular }}By{{ template "primaryKeySuffix" $.Fields }} removes row from the {{ $.RDBMS.Table }} table
|
||||
func (s Store) Remove{{ pubIdent $.Types.Singular }}By{{ template "primaryKeySuffix" $.Fields }}(ctx context.Context {{ template "primaryKeyArgs" $.Fields }}) error {
|
||||
return ExecuteSqlizer(ctx, s.DB(), s.Delete(s.{{ $.Types.Singular }}Table()).Where({{ template "filterByPrimaryKeysWithArgs" $.Fields }},))
|
||||
}
|
||||
|
||||
|
||||
// Truncate{{ pubIdent $.Types.Plural }} removes all rows from the {{ $.RDBMS.Table }} table
|
||||
func (s Store) Truncate{{ pubIdent $.Types.Plural }}(ctx context.Context) error {
|
||||
return Truncate(ctx, s.DB(), s.{{ $.Types.Singular }}Table())
|
||||
}
|
||||
|
||||
|
||||
// ExecUpdate{{ pubIdent $.Types.Plural }} updates all matched (by cnd) rows in {{ $.RDBMS.Table }} with given data
|
||||
func (s Store) ExecUpdate{{ pubIdent $.Types.Plural }}(ctx context.Context, cnd squirrel.Sqlizer, set store.Payload) error {
|
||||
return ExecuteSqlizer(ctx, s.DB(), s.Update(s.{{ $.Types.Singular }}Table({{ printf "%q" .RDBMS.Alias }})).Where(cnd).SetMap(set))
|
||||
}
|
||||
|
||||
// {{ $.Types.Singular }}Lookup prepares {{ $.Types.Singular }} query and executes it,
|
||||
// returning {{ $.Types.GoType }} (or error)
|
||||
func (s Store) {{ $.Types.Singular }}Lookup(ctx context.Context, cnd squirrel.Sqlizer) (*{{ $.Types.GoType }}, error) {
|
||||
return s.internal{{ $.Types.Singular }}RowScanner(s.QueryRow(ctx, s.Query{{ pubIdent $.Types.Plural }}().Where(cnd)))
|
||||
}
|
||||
|
||||
func (s Store) internal{{ $.Types.Singular }}RowScanner(row rowScanner, err error) (*{{ $.Types.GoType }}, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var res = &{{ $.Types.GoType }}{}
|
||||
if _, has := s.config.RowScanners[{{ printf "%q" (unpubIdent $.Types.Singular) }}]; has {
|
||||
scanner := s.config.RowScanners[{{ printf "%q" (unpubIdent $.Types.Singular) }}].(func(rowScanner, *{{ $.Types.GoType }}) error)
|
||||
err = scanner(row, res)
|
||||
} else {
|
||||
{{- if .RDBMS.CustomRowScanner }}
|
||||
err = s.scan{{ $.Types.Singular }}Row(row, res)
|
||||
{{- else }}
|
||||
err = row.Scan(
|
||||
{{- range $.Fields }}
|
||||
&res.{{ .Field }},
|
||||
{{- end }}
|
||||
)
|
||||
{{- end }}
|
||||
}
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, store.ErrNotFound
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not scan db row for {{ $.Types.Singular }}: %w", err)
|
||||
} else {
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Query{{ pubIdent $.Types.Plural }} returns squirrel.SelectBuilder with set table and all columns
|
||||
func (s Store) Query{{ pubIdent $.Types.Plural }}() squirrel.SelectBuilder {
|
||||
return s.Select(s.{{ $.Types.Singular }}Table({{ printf "%q" .RDBMS.Alias }}), s.{{ $.Types.Singular }}Columns({{ printf "%q" $.RDBMS.Alias }})...)
|
||||
}
|
||||
|
||||
// {{ $.Types.Singular }}Table name of the db table
|
||||
func (Store) {{ $.Types.Singular }}Table(aa ... string) string {
|
||||
var alias string
|
||||
if len(aa) > 0 {
|
||||
alias = " AS " + aa[0]
|
||||
}
|
||||
|
||||
return "{{ $.RDBMS.Table }}" + alias
|
||||
}
|
||||
|
||||
// {{ $.Types.Singular }}Columns returns all defined table columns
|
||||
//
|
||||
// With optional string arg, all columns are returned aliased
|
||||
func (Store) {{ $.Types.Singular }}Columns(aa ... string) []string {
|
||||
var alias string
|
||||
if len(aa) > 0 {
|
||||
alias = aa[0] + "."
|
||||
}
|
||||
|
||||
return []string{
|
||||
{{- range $.Fields }}
|
||||
alias + "{{ .Column }}",
|
||||
{{- end }}
|
||||
}
|
||||
}
|
||||
|
||||
// internal{{ pubIdent $.Types.Singular }}Encoder encodes fields from {{ $.Types.GoType }} to store.Payload (map)
|
||||
//
|
||||
// Encoding is done by using generic approach or by calling encode{{ pubIdent $.Types.Singular }}
|
||||
// func when rdbms.customEncoder=true
|
||||
func (s Store) internal{{ pubIdent $.Types.Singular }}Encoder(res *{{ $.Types.GoType }}) store.Payload {
|
||||
{{- if .RDBMS.CustomEncoder }}
|
||||
return s.encode{{ pubIdent $.Types.Singular }}(res)
|
||||
{{- else }}
|
||||
return store.Payload{
|
||||
{{- range $.Fields }}
|
||||
"{{ .Column }}": res.{{ .Field }},
|
||||
{{- end }}
|
||||
}
|
||||
{{- end }}
|
||||
}
|
||||
|
||||
{{/* ************************************************************ */}}
|
||||
|
||||
{{- define "filterByPrimaryKeys" -}}
|
||||
squirrel.Eq{
|
||||
{{- range $field := . -}}
|
||||
{{- if $field.IsPrimaryKey -}}
|
||||
s.preprocessColumn({{ printf "%q" $field.AliasedColumn }}, {{ printf "%q" $field.LookupFilterPreprocess }}): s.preprocessValue(res.{{ $field.Field }}, {{ printf "%q" $field.LookupFilterPreprocess }}),
|
||||
{{ end }}
|
||||
{{- end -}}
|
||||
}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "filterByPrimaryKeysWithArgs" -}}
|
||||
squirrel.Eq{
|
||||
{{- range $field := . }}
|
||||
{{- if $field.IsPrimaryKey -}}
|
||||
s.preprocessColumn({{ printf "%q" $field.AliasedColumn }}, {{ printf "%q" $field.LookupFilterPreprocess }}): s.preprocessValue({{ $field.Arg }}, {{ printf "%q" $field.LookupFilterPreprocess }}),
|
||||
{{ end }}
|
||||
{{ end -}}
|
||||
}
|
||||
{{- end -}}
|
||||
|
||||
24
pkg/codegen/assets/store_test_all.gen.go.tpl
Normal file
24
pkg/codegen/assets/store_test_all.gen.go.tpl
Normal file
@@ -0,0 +1,24 @@
|
||||
package tests
|
||||
|
||||
// This file is auto-generated.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
//
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testAllGenerated(t *testing.T, all interface{}) {
|
||||
{{ range . }}
|
||||
// Run generated tests for {{ .Types.Base }}
|
||||
t.Run({{ printf "%q" .Types.Base }}, func(t *testing.T) {
|
||||
var s = all.({{ unpubIdent .Types.Plural }}Store)
|
||||
require.New(t).NoError(s.Truncate{{ pubIdent .Types.Plural }}(context.Background()))
|
||||
test{{ pubIdent .Types.Base }}(t, s)
|
||||
})
|
||||
{{ end }}
|
||||
}
|
||||
89
pkg/codegen/assets/type_set.gen.go.tpl
Normal file
89
pkg/codegen/assets/type_set.gen.go.tpl
Normal file
@@ -0,0 +1,89 @@
|
||||
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 $i, $import := .Imports }}
|
||||
"{{ $import }}"
|
||||
{{ end }}
|
||||
)
|
||||
{{ end }}
|
||||
|
||||
type (
|
||||
{{ range $name, $set := .Types }}
|
||||
// {{ $name }}Set slice of {{ $name }}
|
||||
//
|
||||
// This type is auto-generated.
|
||||
{{ $name }}Set []*{{ $name }}
|
||||
{{ end }}
|
||||
)
|
||||
|
||||
{{ range $name, $set := .Types }}
|
||||
// Walk iterates through every slice item and calls w({{ $name }}) err
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (set {{ $name }}Set) Walk(w func(*{{ $name }}) error) (err error) {
|
||||
for i := range set {
|
||||
if err = w(set[i]); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Filter iterates through every slice item, calls f({{ $name }}) (bool, err) and return filtered slice
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (set {{ $name }}Set) Filter(f func(*{{ $name }}) (bool, error)) (out {{ $name }}Set, err error) {
|
||||
var ok bool
|
||||
out = {{ $name }}Set{}
|
||||
for i := range set {
|
||||
if ok, err = f(set[i]); err != nil {
|
||||
return
|
||||
} else if ok {
|
||||
out = append(out, set[i])
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
{{ if not $set.NoIdField }}
|
||||
// FindByID finds items from slice by its ID property
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (set {{ $name }}Set) FindByID(ID uint64) *{{ $name }} {
|
||||
for i := range set {
|
||||
if set[i].ID == ID {
|
||||
return set[i]
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IDs returns a slice of uint64s from all items in the set
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (set {{ $name }}Set) IDs() (IDs []uint64) {
|
||||
IDs = make([]uint64, len(set))
|
||||
|
||||
for i := range set {
|
||||
IDs[i] = set[i].ID
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
{{ end }}
|
||||
|
||||
|
||||
{{ end }}
|
||||
116
pkg/codegen/assets/type_set.gen_test.go.tpl
Normal file
116
pkg/codegen/assets/type_set.gen_test.go.tpl
Normal file
@@ -0,0 +1,116 @@
|
||||
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 (
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
|
||||
{{ range $i, $import := .Imports }}
|
||||
"{{ $import }}"
|
||||
{{ end }}
|
||||
)
|
||||
|
||||
|
||||
{{ range $name, $set := .Types }}
|
||||
|
||||
func Test{{ $name }}SetWalk(t *testing.T) {
|
||||
var (
|
||||
value = make({{ $name }}Set, 3)
|
||||
req = require.New(t)
|
||||
)
|
||||
|
||||
// check walk with no errors
|
||||
{
|
||||
err := value.Walk(func(*{{ $name }}) error {
|
||||
return nil
|
||||
})
|
||||
req.NoError(err)
|
||||
}
|
||||
|
||||
// check walk with error
|
||||
req.Error(value.Walk(func(*{{ $name }}) error { return fmt.Errorf("walk error") }))
|
||||
}
|
||||
|
||||
func Test{{ $name }}SetFilter(t *testing.T) {
|
||||
var (
|
||||
value = make({{ $name }}Set, 3)
|
||||
req = require.New(t)
|
||||
)
|
||||
|
||||
// filter nothing
|
||||
{
|
||||
set, err := value.Filter(func(*{{ $name }}) (bool, error) {
|
||||
return true, nil
|
||||
})
|
||||
req.NoError(err)
|
||||
req.Equal(len(set), len(value))
|
||||
}
|
||||
|
||||
// filter one item
|
||||
{
|
||||
found := false
|
||||
set, err := value.Filter(func(*{{ $name }}) (bool, error) {
|
||||
if !found {
|
||||
found = true
|
||||
return found, nil
|
||||
}
|
||||
return false, nil
|
||||
})
|
||||
req.NoError(err)
|
||||
req.Len(set, 1)
|
||||
}
|
||||
|
||||
// filter error
|
||||
{
|
||||
_, err := value.Filter(func(*{{ $name }}) (bool, error) {
|
||||
return false, fmt.Errorf("filter error")
|
||||
})
|
||||
req.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
{{ if not $set.NoIdField }}
|
||||
func Test{{ $name }}SetIDs(t *testing.T) {
|
||||
var (
|
||||
value = make({{ $name }}Set, 3)
|
||||
req = require.New(t)
|
||||
)
|
||||
|
||||
// construct objects
|
||||
value[0] = new({{ $name }})
|
||||
value[1] = new({{ $name }})
|
||||
value[2] = new({{ $name }})
|
||||
// set ids
|
||||
value[0].ID = 1
|
||||
value[1].ID = 2
|
||||
value[2].ID = 3
|
||||
|
||||
// Find existing
|
||||
{
|
||||
val := value.FindByID(2)
|
||||
req.Equal(uint64(2), val.ID)
|
||||
}
|
||||
|
||||
// Find non-existing
|
||||
{
|
||||
val := value.FindByID(4)
|
||||
req.Nil(val)
|
||||
}
|
||||
|
||||
// List IDs from set
|
||||
{
|
||||
val := value.IDs()
|
||||
req.Equal(len(val), len(value))
|
||||
}
|
||||
}
|
||||
{{ end }}
|
||||
|
||||
{{ end }}
|
||||
83
pkg/codegen/codegen.go
Normal file
83
pkg/codegen/codegen.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package codegen
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza-server/pkg/cli"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
type (
|
||||
definitions struct {
|
||||
App string
|
||||
|
||||
Rest []*restDef
|
||||
Actions []*actionsDef
|
||||
Events []*eventsDef
|
||||
Types []*typesDef
|
||||
Store []*storeDef
|
||||
}
|
||||
)
|
||||
|
||||
func Proc() {
|
||||
var (
|
||||
err error
|
||||
|
||||
def = &definitions{}
|
||||
|
||||
tpls = template.New("").Funcs(map[string]interface{}{
|
||||
"camelCase": camelCase,
|
||||
"pubIdent": pubIdent,
|
||||
"unpubIdent": unpubIdent,
|
||||
"toLower": strings.ToLower,
|
||||
"cc2underscore": cc2underscore,
|
||||
"normalizeImport": normalizeImport,
|
||||
"comment": func(text string, skip1st bool) string {
|
||||
ll := strings.Split(text, "\n")
|
||||
s := 0
|
||||
out := ""
|
||||
if skip1st {
|
||||
s = 1
|
||||
out = ll[0] + "\n"
|
||||
}
|
||||
|
||||
for ; s < len(ll); s++ {
|
||||
out += "// " + ll[s] + "\n"
|
||||
}
|
||||
|
||||
return out
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
tpls = template.Must(tpls.ParseGlob("pkg/codegen/assets/*.tpl"))
|
||||
|
||||
if def.Actions, err = procActions(); err != nil {
|
||||
cli.HandleError(err)
|
||||
} else {
|
||||
cli.HandleError(genActions(tpls, def.Actions))
|
||||
}
|
||||
|
||||
if def.Events, err = procEvents(); err != nil {
|
||||
cli.HandleError(err)
|
||||
} else {
|
||||
cli.HandleError(genEvents(tpls, def.Events))
|
||||
}
|
||||
|
||||
if def.Types, err = procTypes(); err != nil {
|
||||
cli.HandleError(err)
|
||||
} else {
|
||||
cli.HandleError(genTypes(tpls, def.Types))
|
||||
}
|
||||
|
||||
if def.Rest, err = procRest(); err != nil {
|
||||
cli.HandleError(err)
|
||||
} else {
|
||||
cli.HandleError(genRest(tpls, def.Rest))
|
||||
}
|
||||
|
||||
if def.Store, err = procStore(); err != nil {
|
||||
cli.HandleError(err)
|
||||
} else {
|
||||
cli.HandleError(genStore(tpls, def.Store))
|
||||
}
|
||||
}
|
||||
213
pkg/codegen/events.go
Normal file
213
pkg/codegen/events.go
Normal file
@@ -0,0 +1,213 @@
|
||||
package codegen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/cortezaproject/corteza-server/pkg/slice"
|
||||
"gopkg.in/yaml.v2"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
type (
|
||||
// definitions are in one file
|
||||
eventsDef struct {
|
||||
Package string
|
||||
App string
|
||||
Source string
|
||||
outputDir string
|
||||
|
||||
// List of imports
|
||||
// Used only by generated file and not pre-generated-user-file
|
||||
Imports []string
|
||||
|
||||
Resources evResourceDefMap
|
||||
}
|
||||
|
||||
evResourceDefMap map[string]evResourceDef
|
||||
|
||||
evResourceDef struct {
|
||||
// used as string
|
||||
ResourceString string
|
||||
|
||||
// used as (go) ident
|
||||
ResourceIdent string
|
||||
|
||||
// used for filename
|
||||
ResourceFile string
|
||||
|
||||
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
|
||||
}
|
||||
)
|
||||
|
||||
func procEvents() ([]*eventsDef, error) {
|
||||
// <app>/service/event/events.yaml
|
||||
const (
|
||||
importTypePathTpl = "github.com/cortezaproject/corteza-server/%s/types"
|
||||
importAuthPath = "github.com/cortezaproject/corteza-server/pkg/auth"
|
||||
)
|
||||
|
||||
var (
|
||||
dd = make([]*eventsDef, 0)
|
||||
)
|
||||
|
||||
mm, err := filepath.Glob(filepath.Join("*", "service", "event", "events.yaml"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("glob failed: %w", err)
|
||||
}
|
||||
|
||||
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 (
|
||||
e = evResourceDefMap{}
|
||||
d = &eventsDef{
|
||||
Package: "event",
|
||||
Source: m,
|
||||
App: m[:strings.Index(m, "/")],
|
||||
outputDir: path.Dir(m),
|
||||
Resources: map[string]evResourceDef{},
|
||||
}
|
||||
)
|
||||
|
||||
if err := yaml.NewDecoder(f).Decode(e); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for resName, evDef := range e {
|
||||
|
||||
d.Imports = []string{fmt.Sprintf(importTypePathTpl, d.App)}
|
||||
evDef.ResourceString = resName
|
||||
|
||||
if l := strings.Index(resName, ":"); l > 0 {
|
||||
evDef.ResourceIdent = resName[l+1:]
|
||||
} else {
|
||||
evDef.ResourceIdent = resName
|
||||
}
|
||||
|
||||
// make filename
|
||||
evDef.ResourceFile = strings.ReplaceAll(evDef.ResourceIdent, ":", "_")
|
||||
evDef.ResourceFile = strings.ReplaceAll(evDef.ResourceFile, "-", "_")
|
||||
|
||||
// make identifier (string that will be used for struct name)
|
||||
evDef.ResourceIdent = camelCase(strings.Split(evDef.ResourceFile, "_")...)
|
||||
|
||||
// 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,
|
||||
})
|
||||
|
||||
// Ensure all imports are checked
|
||||
for _, p := range evDef.Properties {
|
||||
if p.Import == "" {
|
||||
if strings.HasPrefix(p.Type, "*types.") || strings.HasPrefix(p.Type, "types.") {
|
||||
p.Import = fmt.Sprintf(importTypePathTpl, d.App)
|
||||
}
|
||||
}
|
||||
|
||||
if p.Import != "" && !slice.HasString(d.Imports, p.Import) {
|
||||
d.Imports = append(d.Imports, p.Import)
|
||||
}
|
||||
|
||||
p.Import = ""
|
||||
}
|
||||
|
||||
d.Resources[resName] = evDef
|
||||
}
|
||||
|
||||
dd = append(dd, d)
|
||||
}
|
||||
|
||||
return dd, nil
|
||||
}
|
||||
|
||||
func genEvents(tpl *template.Template, dd []*eventsDef) (err error) {
|
||||
var (
|
||||
// Will only be generated if file does not exist previously
|
||||
tplEvents = tpl.Lookup("events.go.tpl")
|
||||
|
||||
// Always regenerated
|
||||
tplEventsGen = tpl.Lookup("events.gen.go.tpl")
|
||||
|
||||
dst string
|
||||
)
|
||||
|
||||
for _, d := range dd {
|
||||
// Generic code, all events go into one file (per app)
|
||||
err = goTemplate(path.Join(d.outputDir, "events.gen.go"), tplEventsGen, d)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, r := range d.Resources {
|
||||
dst = path.Join(d.outputDir, r.ResourceFile+".go")
|
||||
_, err = os.Stat(dst)
|
||||
if os.IsNotExist(err) {
|
||||
err = goTemplate(dst, tplEvents, map[string]interface{}{
|
||||
"Package": d.Package,
|
||||
"ResourceIdent": r.ResourceIdent,
|
||||
})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Merge on/before/after events
|
||||
func (def evResourceDef) Events() []string {
|
||||
return append(
|
||||
makeEventGroup("on", def.On),
|
||||
append(
|
||||
makeEventGroup("before", def.BeforeAfter),
|
||||
makeEventGroup("after", def.BeforeAfter)...,
|
||||
)...,
|
||||
)
|
||||
}
|
||||
|
||||
func makeEventGroup(pfix string, ee []string) (out []string) {
|
||||
for _, e := range ee {
|
||||
out = append(out, pfix+strings.ToUpper(e[:1])+e[1:])
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
222
pkg/codegen/rest.go
Normal file
222
pkg/codegen/rest.go
Normal file
@@ -0,0 +1,222 @@
|
||||
package codegen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gopkg.in/yaml.v2"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
type (
|
||||
// The following structure represents
|
||||
// legacy API definition (spec.json)
|
||||
//
|
||||
//
|
||||
|
||||
// definitions are in one file
|
||||
restDef struct {
|
||||
App string
|
||||
Source string
|
||||
outputDir string
|
||||
|
||||
Endpoints []*restEndpointDef
|
||||
}
|
||||
|
||||
restEndpointDef struct {
|
||||
Title string `yaml:"title"`
|
||||
Path string `yaml:"path"`
|
||||
Entrypoint string `yaml:"entrypoint"`
|
||||
Authentication []interface{} `yaml:"authentication,omitempty"`
|
||||
Apis []*restEndpointApi `yaml:"apis"`
|
||||
Imports []string `yaml:"imports"`
|
||||
Description string `yaml:"description,omitempty"`
|
||||
Params restEndpointParamsDef `yaml:"parameters,omitempty"`
|
||||
}
|
||||
|
||||
restEndpointApi struct {
|
||||
Name string `yaml:"name"`
|
||||
Method string `yaml:"method"`
|
||||
Title string `yaml:"title"`
|
||||
Path string `yaml:"path"`
|
||||
Params restEndpointParamsDef `yaml:"parameters,omitempty"`
|
||||
}
|
||||
|
||||
restEndpointParamsDef struct {
|
||||
Post []*restEndpointParamDef `yaml:"post"`
|
||||
Path []*restEndpointParamDef `yaml:"path"`
|
||||
Get []*restEndpointParamDef `yaml:"get"`
|
||||
}
|
||||
|
||||
restEndpointParamDef struct {
|
||||
Name string `yaml:"name"`
|
||||
Type string `yaml:"type"`
|
||||
Required bool `yaml:"required"`
|
||||
Title string `yaml:"title"`
|
||||
Origin string
|
||||
}
|
||||
)
|
||||
|
||||
func procRest() ([]*restDef, error) {
|
||||
// <app>/rest.yaml
|
||||
|
||||
var (
|
||||
dd = make([]*restDef, 0)
|
||||
)
|
||||
|
||||
mm, err := filepath.Glob(filepath.Join("*", "rest.yaml"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("glob failed: %w", err)
|
||||
}
|
||||
|
||||
for _, m := range mm {
|
||||
err = func() error {
|
||||
f, err := os.Open(m)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s read failed: %w", m, err)
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
var d = &restDef{}
|
||||
|
||||
if err := yaml.NewDecoder(f).Decode(d); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.outputDir = path.Dir(m)
|
||||
|
||||
// Append params from endpoit to all apis
|
||||
for _, e := range d.Endpoints {
|
||||
for _, a := range e.Apis {
|
||||
a.Params.Path = append(e.Params.Path, a.Params.Path...)
|
||||
a.Params.Post = append(e.Params.Post, a.Params.Post...)
|
||||
a.Params.Get = append(e.Params.Get, a.Params.Get...)
|
||||
}
|
||||
}
|
||||
|
||||
dd = append(dd, d)
|
||||
return nil
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to process %s: %w", m, err)
|
||||
}
|
||||
}
|
||||
|
||||
return dd, nil
|
||||
}
|
||||
|
||||
func genRest(tpl *template.Template, dd []*restDef) (err error) {
|
||||
var (
|
||||
// Will only be generated if file does not exist previously
|
||||
tplHandler = tpl.Lookup("rest_handler.go.tpl")
|
||||
tplRequest = tpl.Lookup("rest_request.go.tpl")
|
||||
|
||||
dst string
|
||||
)
|
||||
|
||||
for _, d := range dd {
|
||||
for _, e := range d.Endpoints {
|
||||
|
||||
// Generic code, every event goes into one file (per app)
|
||||
dst = path.Join(d.outputDir, "rest", "handlers", e.Entrypoint+".go")
|
||||
err = goTemplate(dst, tplHandler, map[string]interface{}{
|
||||
"Source": d.Source,
|
||||
"Endpoint": e,
|
||||
"App": path.Base(d.outputDir),
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Generic code, every event goes into one file (per app)
|
||||
dst = path.Join(d.outputDir, "rest", "request", e.Entrypoint+".go")
|
||||
err = goTemplate(dst, tplRequest, map[string]interface{}{
|
||||
"Source": d.Source,
|
||||
"Endpoint": e,
|
||||
"Imports": e.Imports,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *restEndpointParamsDef) All() []*restEndpointParamDef {
|
||||
var pp = make([]*restEndpointParamDef, 0)
|
||||
|
||||
for _, p := range d.Path {
|
||||
p.Origin = "PATH"
|
||||
pp = append(pp, p)
|
||||
}
|
||||
|
||||
for _, p := range d.Get {
|
||||
p.Origin = "GET"
|
||||
pp = append(pp, p)
|
||||
}
|
||||
|
||||
for _, p := range d.Post {
|
||||
p.Origin = "POST"
|
||||
pp = append(pp, p)
|
||||
}
|
||||
|
||||
return pp
|
||||
}
|
||||
|
||||
func (d *restEndpointParamDef) IsUpload() bool {
|
||||
switch d.Type {
|
||||
case "*multipart.FileHeader":
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (d *restEndpointParamDef) IsSlice() bool {
|
||||
return strings.HasPrefix(d.Type, "[]") || strings.HasSuffix(d.Type, "Set")
|
||||
}
|
||||
|
||||
func (d *restEndpointParamDef) IsString() bool {
|
||||
switch d.Type {
|
||||
case "string", "[]string", "[]*string":
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (d *restEndpointParamDef) FieldTag() string {
|
||||
switch d.Type {
|
||||
case "uint64":
|
||||
return "`json:\",string\"`"
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (d *restEndpointParamDef) Parser(arg string) string {
|
||||
switch d.Type {
|
||||
case "[]uint64":
|
||||
return fmt.Sprintf("payload.ParseUint64s(%s), nil", arg)
|
||||
case "time.Time":
|
||||
return fmt.Sprintf("payload.ParseISODateWithErr(%s)", arg)
|
||||
case "*time.Time":
|
||||
return fmt.Sprintf("payload.ParseISODatePtrWithErr(%s)", arg)
|
||||
case "sqlxTypes.JSONText":
|
||||
return fmt.Sprintf("payload.ParseJSONTextWithErr(%s)", arg)
|
||||
case "int", "uint", "uint64", "int64", "float", "float64", "bool":
|
||||
return fmt.Sprintf("payload.Parse%s(%s), nil", pubIdent(d.Type), arg)
|
||||
case "string", "[]string":
|
||||
return fmt.Sprintf("%s, nil", arg)
|
||||
default:
|
||||
return fmt.Sprintf("%s(%s), nil", d.Type, arg)
|
||||
}
|
||||
|
||||
}
|
||||
452
pkg/codegen/store.go
Normal file
452
pkg/codegen/store.go
Normal file
@@ -0,0 +1,452 @@
|
||||
package codegen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/cortezaproject/corteza-server/pkg/slice"
|
||||
"gopkg.in/yaml.v2"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
type (
|
||||
// definitions are in one file
|
||||
storeDef struct {
|
||||
Package string
|
||||
App string
|
||||
Source string
|
||||
Filename string
|
||||
|
||||
Import []string `yaml:"import"`
|
||||
|
||||
// List of all locations where we should export the store interface to
|
||||
Interface []string `yaml:"interface"`
|
||||
|
||||
// Tries to autogenerate type by changing it to singular and prefixing it with *types.
|
||||
Types storeTypeDef `yaml:"types"`
|
||||
|
||||
// All known fields that we need to store on a particular type
|
||||
//
|
||||
// For now, this set does not variate between different implementation
|
||||
// To support that, a (sub)set will need to be defined under each implementation (rdbms, mysql, mongo...)
|
||||
//
|
||||
Fields storeTypeFieldSetDef `yaml:"fields"`
|
||||
Lookups []*storeTypeLookups `yaml:"lookups"`
|
||||
PartialUpdates []*storeTypePartialUpdate `yaml:"partialUpdates"`
|
||||
RDBMS *storeTypeRdbmsDef `yaml:"rdbms"`
|
||||
|
||||
Search storeTypeSearchDef `yaml:"search"`
|
||||
}
|
||||
|
||||
storeTypeDef struct {
|
||||
// Name of the package where type can be found
|
||||
// (defaults to types)
|
||||
Package string `yaml:"package"`
|
||||
|
||||
// Name of the base type
|
||||
// (defaults to base name of the yaml file)
|
||||
Base string `yaml:"base"`
|
||||
|
||||
// Singular variation of name
|
||||
// (defaults to <Base> (s trimmed))
|
||||
Singular string `yaml:"singular"`
|
||||
|
||||
// Plural variantion of name
|
||||
// (defaults to <Singular> (s appended))
|
||||
Plural string `yaml:"plural"`
|
||||
|
||||
// Name of the set go type
|
||||
// (defaults to <Package>.<Singular>)
|
||||
GoType string `yaml:"type"`
|
||||
|
||||
// Name of the set go type
|
||||
// (defaults to <GoType>Set)
|
||||
GoSetType string `yaml:"setType"`
|
||||
|
||||
// Name of the filter go type
|
||||
// (defaults to <GoType>Filter)
|
||||
GoFilterType string `yaml:"filterType"`
|
||||
}
|
||||
|
||||
storeTypeRdbmsDef struct {
|
||||
// Alias used in SQL queries
|
||||
Alias string `yaml:"alias,omitempty"`
|
||||
Table string `yaml:"table,omitempty"`
|
||||
|
||||
CustomRowScanner bool `yaml:"customRowScanner"`
|
||||
CustomFilterConverter bool `yaml:"customFilterConverter"`
|
||||
CustomEncoder bool `yaml:"customEncoder"`
|
||||
}
|
||||
|
||||
storeTypeFieldSetDef []*storeTypeFieldDef
|
||||
|
||||
storeTypeFieldDef struct {
|
||||
Field string `yaml:"field"`
|
||||
|
||||
// Autodiscovery logic (when not explicitly set)
|
||||
// uint64: when field has "ID" suffix
|
||||
// time.Time: when field equals with "created_at"
|
||||
// *time.Time: when field ends with "_at"
|
||||
// string: default
|
||||
Type string `yaml:"type"`
|
||||
|
||||
// When not explicitly set, defaults to snake-cased value from field
|
||||
//
|
||||
// Exceptions:
|
||||
// If field name ends with ID (<base>ID), it converts that to rel_<snake-cased-base>
|
||||
Column string `yaml:"column"`
|
||||
|
||||
// If field is flagged as PK it is used in update & remove conditions
|
||||
// Note: if no other field is set as primary and field with ID name
|
||||
// exists, that field is auto-set as primary.
|
||||
IsPrimaryKey bool `yaml:"isPrimaryKey"`
|
||||
|
||||
// FilterPreprocess sets preprocessing function used on
|
||||
// conditions for lookup functions
|
||||
//
|
||||
// See specific implementation for details
|
||||
LookupFilterPreprocess string `yaml:"lookupFilterPreprocessor"`
|
||||
|
||||
alias string
|
||||
}
|
||||
|
||||
storeTypeLookups struct {
|
||||
// LookupBy<suffix>
|
||||
// When not explicitly defined, it names of all fields
|
||||
Suffix string `yaml:"suffix"`
|
||||
Description string `yaml:"description"`
|
||||
Fields []string `yaml:"fields"`
|
||||
Filter map[string]string `yaml:"filter"`
|
||||
fields storeTypeFieldSetDef
|
||||
}
|
||||
|
||||
storeTypePartialUpdate struct {
|
||||
Name string `yaml:"name"`
|
||||
Description string `yaml:"description"`
|
||||
Set map[string]string `yaml:"set"`
|
||||
XX_Args []string `yaml:"args"`
|
||||
fields storeTypeFieldSetDef
|
||||
}
|
||||
|
||||
storeTypeSearchDef struct {
|
||||
Disable bool `yaml:"disable"`
|
||||
DisablePaging bool `yaml:"disablePaging"`
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
outputDir string = "store"
|
||||
)
|
||||
|
||||
func procStore() ([]*storeDef, error) {
|
||||
procDef := func(m string) (*storeDef, error) {
|
||||
def := &storeDef{Source: m}
|
||||
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(&def); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
def.Filename = path.Base(m)
|
||||
def.Filename = def.Filename[:len(def.Filename)-5]
|
||||
|
||||
// Always generate interface in store/tests and store/bulk
|
||||
def.Interface = append(def.Interface, "store/tests", "store/bulk")
|
||||
|
||||
if def.Types.Base == "" {
|
||||
def.Types.Base = pubIdent(strings.Split(def.Filename, "_")...)
|
||||
}
|
||||
|
||||
if def.Types.Singular == "" {
|
||||
def.Types.Singular = strings.TrimRight(def.Types.Base, "s")
|
||||
}
|
||||
if def.Types.Plural == "" {
|
||||
def.Types.Plural = def.Types.Singular + "s"
|
||||
}
|
||||
|
||||
if def.Types.Package == "" {
|
||||
def.Types.Package = "types"
|
||||
}
|
||||
|
||||
if def.Types.GoType == "" {
|
||||
def.Types.GoType = def.Types.Package + "." + pubIdent(def.Types.Singular)
|
||||
}
|
||||
|
||||
if def.Types.GoSetType == "" {
|
||||
def.Types.GoSetType = def.Types.GoType + "Set"
|
||||
}
|
||||
|
||||
if def.Types.GoFilterType == "" {
|
||||
def.Types.GoFilterType = def.Types.GoType + "Filter"
|
||||
}
|
||||
|
||||
if def.RDBMS.Alias == "" {
|
||||
def.RDBMS.Alias = def.Types.Base[0:1]
|
||||
}
|
||||
|
||||
var hasPrimaryKey = false
|
||||
for _, f := range def.Fields {
|
||||
if f.IsPrimaryKey {
|
||||
hasPrimaryKey = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for _, f := range def.Fields {
|
||||
if !hasPrimaryKey && f.Field == "ID" {
|
||||
f.IsPrimaryKey = true
|
||||
}
|
||||
|
||||
// copy alias from global spec so we can
|
||||
// generate aliased columsn
|
||||
f.alias = def.RDBMS.Alias
|
||||
|
||||
if f.Column == "" {
|
||||
switch {
|
||||
case f.Field != "ID" && strings.HasSuffix(f.Field, "ID"):
|
||||
f.Column = "rel_" + cc2underscore(f.Field[:len(f.Field)-2])
|
||||
default:
|
||||
f.Column = cc2underscore(f.Field)
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case f.Type != "":
|
||||
// type set
|
||||
case strings.HasSuffix(f.Field, "ID") || strings.HasSuffix(f.Field, "By"):
|
||||
f.Type = "uint64"
|
||||
case f.Field == "CreatedAt":
|
||||
f.Type = "time.Time"
|
||||
case strings.HasSuffix(f.Field, "At"):
|
||||
f.Type = "uint64"
|
||||
default:
|
||||
f.Type = "string"
|
||||
}
|
||||
}
|
||||
|
||||
if len(def.PartialUpdates) > 0 && def.Fields.Find("ID") == nil {
|
||||
return nil, fmt.Errorf("partial updates without ID field are not supported")
|
||||
}
|
||||
|
||||
// Checking if filters exist in the fields
|
||||
for i, p := range def.PartialUpdates {
|
||||
// Check and normalize set
|
||||
for f, v := range p.Set {
|
||||
if def.Fields.Find(f) == nil {
|
||||
return nil, fmt.Errorf("undefined field %q used in partialUpdate #%d set", f, i)
|
||||
}
|
||||
|
||||
if v == "" {
|
||||
// Set empty strings to nil
|
||||
p.Set[f] = "nil"
|
||||
}
|
||||
}
|
||||
|
||||
for _, a := range p.XX_Args {
|
||||
if def.Fields.Find(a) == nil {
|
||||
return nil, fmt.Errorf("undefined field %q used in partialUpdate #%d arguments", a, i)
|
||||
}
|
||||
}
|
||||
|
||||
p.fields = def.Fields
|
||||
|
||||
}
|
||||
|
||||
for i, l := range def.Lookups {
|
||||
if len(l.Fields) == 0 {
|
||||
return nil, fmt.Errorf("define at least one lookup field in lookup #%d", i)
|
||||
}
|
||||
|
||||
// Checking if fields exist in the fields
|
||||
for _, f := range l.Fields {
|
||||
if def.Fields.Find(f) == nil {
|
||||
return nil, fmt.Errorf("undefined lookup field %q used", f)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Checking if filters exist in the fields
|
||||
for f, v := range l.Filter {
|
||||
if def.Fields.Find(f) == nil {
|
||||
return nil, fmt.Errorf("undefined lookup filter %q used", f)
|
||||
}
|
||||
|
||||
if v == "" {
|
||||
// Set empty strings to nil
|
||||
l.Filter[f] = "nil"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if l.Suffix == "" {
|
||||
l.Suffix = strings.Join(l.Fields, "")
|
||||
}
|
||||
|
||||
l.fields = def.Fields
|
||||
}
|
||||
|
||||
return def, nil
|
||||
}
|
||||
|
||||
mm, err := filepath.Glob(filepath.Join(outputDir, "*.yaml"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to glob: %w", err)
|
||||
}
|
||||
|
||||
dd := []*storeDef{}
|
||||
for _, m := range mm {
|
||||
def, err := procDef(m)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to process %s: %w", m, err)
|
||||
}
|
||||
|
||||
dd = append(dd, def)
|
||||
}
|
||||
|
||||
return dd, nil
|
||||
}
|
||||
|
||||
// genStore generates all store related code, functions, interfaces...
|
||||
//
|
||||
// Templates can be found under assets/store*.tpl
|
||||
func genStore(tpl *template.Template, dd []*storeDef) (err error) {
|
||||
var (
|
||||
// general interfaces
|
||||
tplInterfacesJoined = tpl.Lookup("store_interfaces_joined.gen.go.tpl")
|
||||
tplInterfaces = tpl.Lookup("store_interfaces.gen.go.tpl")
|
||||
|
||||
// general tests
|
||||
tplTestAll = tpl.Lookup("store_test_all.gen.go.tpl")
|
||||
|
||||
// @todo in-memory
|
||||
|
||||
// rdbms specific
|
||||
tplRdbms = tpl.Lookup("store_rdbms.gen.go.tpl")
|
||||
|
||||
// @todo redis
|
||||
// @todo mongodb
|
||||
// @todo elasticsearch
|
||||
|
||||
// bulk specific
|
||||
tplBulk = tpl.Lookup("store_bulk.gen.go.tpl")
|
||||
|
||||
dst string
|
||||
joinedInterface = make(map[string][]*storeDef)
|
||||
)
|
||||
|
||||
// Output all test setup into a single file
|
||||
dst = path.Join(outputDir, "tests", "gen_test.go")
|
||||
if err = goTemplate(dst, tplTestAll, dd); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Multi-file output
|
||||
for _, d := range dd {
|
||||
dst = path.Join(outputDir, "rdbms", d.Filename+".gen.go")
|
||||
if err = goTemplate(dst, tplRdbms, d); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
dst = path.Join(outputDir, "bulk", d.Filename+".gen.go")
|
||||
if err = goTemplate(dst, tplBulk, d); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Collect and map all interface output locations
|
||||
// and their corresponding definitions
|
||||
for _, dst = range d.Interface {
|
||||
if err = genStoreInterfaces(tplInterfaces, path.Join(dst, "store_interface_"+d.Filename+".gen.go"), path.Base(dst), d); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if joinedInterface[dst] == nil {
|
||||
joinedInterface[dst] = make([]*storeDef, 0, len(dd))
|
||||
}
|
||||
|
||||
joinedInterface[dst] = append(joinedInterface[dst], d)
|
||||
}
|
||||
}
|
||||
|
||||
// Add joined interfaces for each interface destination
|
||||
for dst, dd := range joinedInterface {
|
||||
if err = genStoreInterfacesJoined(tplInterfacesJoined, path.Join(dst, "store_interface.gen.go"), path.Base(dst), dd); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//for _, d := range dd {
|
||||
// d.Package = "tests"
|
||||
// dst = path.Join("store/tests", "store_interface_"+d.Filename+".gen.go")
|
||||
// if err = goTemplate(dst, tplInterfaces, d); err != nil {
|
||||
// return
|
||||
// }
|
||||
//}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func genStoreInterfaces(tpl *template.Template, dst, pkg string, d *storeDef) error {
|
||||
d.Package = pkg
|
||||
return goTemplate(dst, tpl, d)
|
||||
}
|
||||
|
||||
func genStoreInterfacesJoined(tpl *template.Template, dst, pkg string, dd []*storeDef) error {
|
||||
payload := map[string]interface{}{
|
||||
"Package": pkg,
|
||||
"Definitions": dd,
|
||||
"Import": collectStoreDefImports("", dd...),
|
||||
}
|
||||
|
||||
return goTemplate(dst, tpl, payload)
|
||||
}
|
||||
|
||||
func collectStoreDefImports(basePkg string, dd ...*storeDef) []string {
|
||||
ii := make([]string, 0, len(dd))
|
||||
for _, d := range dd {
|
||||
for _, i := range d.Import {
|
||||
if !slice.HasString(ii, i) && (basePkg == "" || !strings.HasSuffix(i, basePkg)) {
|
||||
ii = append(ii, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ii
|
||||
}
|
||||
|
||||
func (s storeTypeFieldSetDef) Find(name string) *storeTypeFieldDef {
|
||||
for _, f := range s {
|
||||
if f.Field == name {
|
||||
return f
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f storeTypeFieldDef) Arg() string {
|
||||
if f.Field == "ID" {
|
||||
return f.Field
|
||||
}
|
||||
|
||||
return strings.ToLower(f.Field[:1]) + f.Field[1:]
|
||||
}
|
||||
|
||||
func (f storeTypeFieldDef) AliasedColumn() string {
|
||||
return fmt.Sprintf("%s.%s", f.alias, f.Column)
|
||||
}
|
||||
|
||||
func (p storeTypePartialUpdate) Args() []*storeTypeFieldDef {
|
||||
ff := make([]*storeTypeFieldDef, len(p.XX_Args))
|
||||
for a := range p.XX_Args {
|
||||
ff[a] = p.fields.Find(p.XX_Args[a])
|
||||
}
|
||||
|
||||
return ff
|
||||
}
|
||||
129
pkg/codegen/templating.go
Normal file
129
pkg/codegen/templating.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package codegen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/cortezaproject/corteza-server/pkg/cli"
|
||||
"go/format"
|
||||
"io"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
func goTemplate(dst string, tpl *template.Template, payload interface{}) (err error) {
|
||||
var output io.WriteCloser
|
||||
buf := bytes.Buffer{}
|
||||
|
||||
if err := tpl.Execute(&buf, payload); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmtsrc, err := format.Source(buf.Bytes())
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintf(os.Stderr, "%s fmt warn: %v\n", dst, err)
|
||||
|
||||
err = nil
|
||||
fmtsrc = buf.Bytes()
|
||||
}
|
||||
|
||||
if dst == "" || dst == "-" {
|
||||
output = os.Stdout
|
||||
} else {
|
||||
if output, err = os.Create(dst); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer output.Close()
|
||||
}
|
||||
|
||||
if _, err := output.Write(fmtsrc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func WritePlainTo(tpl *template.Template, payload interface{}, tplName, dst string) {
|
||||
var output io.WriteCloser
|
||||
buf := bytes.Buffer{}
|
||||
|
||||
if err := tpl.ExecuteTemplate(&buf, tplName, payload); err != nil {
|
||||
cli.HandleError(err)
|
||||
} else {
|
||||
if dst == "" || dst == "-" {
|
||||
output = os.Stdout
|
||||
} else {
|
||||
if output, err = os.Create(dst); err != nil {
|
||||
cli.HandleError(err)
|
||||
}
|
||||
|
||||
defer output.Close()
|
||||
}
|
||||
|
||||
if _, err := output.Write(buf.Bytes()); err != nil {
|
||||
cli.HandleError(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// PubIdent returns published identifier by uppercasing
|
||||
// input, cammelcasing it and removing ident unfriendly characters
|
||||
var nonIdentChars = regexp.MustCompile(`[\s\\/]+`)
|
||||
|
||||
func pubIdent(pp ...string) (out string) {
|
||||
for _, p := range pp {
|
||||
if len(p) > 1 {
|
||||
p = strings.ToUpper(p[:1]) + p[1:]
|
||||
}
|
||||
|
||||
if ss := nonIdentChars.Split(p, -1); len(ss) > 1 {
|
||||
p = pubIdent(ss...)
|
||||
}
|
||||
|
||||
out = out + p
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func unpubIdent(pp ...string) (out string) {
|
||||
out = pubIdent(pp...)
|
||||
return strings.ToLower(out[:1]) + out[1:]
|
||||
}
|
||||
|
||||
// convets to underscore
|
||||
func cc2underscore(cc string) string {
|
||||
var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)")
|
||||
var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])")
|
||||
|
||||
u := matchFirstCap.ReplaceAllString(cc, "${1}_${2}")
|
||||
u = matchAllCap.ReplaceAllString(u, "${1}_${2}")
|
||||
return strings.ToLower(u)
|
||||
}
|
||||
|
||||
// Handle list of imports, adds quotes around each import
|
||||
//
|
||||
// If import string contains a space, assume import alias and
|
||||
// quotes only the 2nd part
|
||||
func normalizeImport(i string) string {
|
||||
if strings.Contains(i, " ") {
|
||||
p := strings.SplitN(i, " ", 2)
|
||||
return fmt.Sprintf(`%s "%s"`, p[0], strings.Trim(p[1], `"`))
|
||||
} else {
|
||||
return fmt.Sprintf(`"%s"`, strings.Trim(i, `"`))
|
||||
}
|
||||
}
|
||||
93
pkg/codegen/types.go
Normal file
93
pkg/codegen/types.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package codegen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gopkg.in/yaml.v2"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
type (
|
||||
typesDef struct {
|
||||
// source file path
|
||||
Source string
|
||||
|
||||
// outputDir
|
||||
// dir where the source file is
|
||||
outputDir string
|
||||
|
||||
Imports []string
|
||||
Package string `yaml:"package"`
|
||||
Types map[string]typeDef `yaml:"types"`
|
||||
}
|
||||
|
||||
typeDef struct {
|
||||
NoIdField bool `yaml:"noIdField"`
|
||||
}
|
||||
)
|
||||
|
||||
func procTypes() ([]*typesDef, error) {
|
||||
var (
|
||||
dd = make([]*typesDef, 0)
|
||||
)
|
||||
|
||||
mm, err := filepath.Glob(filepath.Join("*", "*", "types.yaml"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("glob failed: %w", err)
|
||||
}
|
||||
|
||||
for _, m := range mm {
|
||||
var (
|
||||
d = &typesDef{
|
||||
Source: m,
|
||||
outputDir: path.Dir(m),
|
||||
|
||||
Package: "types",
|
||||
Types: map[string]typeDef{},
|
||||
}
|
||||
)
|
||||
|
||||
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 genTypes(tpl *template.Template, dd []*typesDef) (err error) {
|
||||
var (
|
||||
typeGen = tpl.Lookup("type_set.gen.go.tpl")
|
||||
typeGenTest = tpl.Lookup("type_set.gen_test.go.tpl")
|
||||
)
|
||||
|
||||
for _, d := range dd {
|
||||
err = goTemplate(path.Join(d.outputDir, "type_set.gen.go"), typeGen, d)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = goTemplate(path.Join(d.outputDir, "type_set.gen_test.go"), typeGenTest, d)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user