Skip to content

Commit

Permalink
Merge pull request #159 from cmars/feat/component-generation
Browse files Browse the repository at this point in the history
feat: custom generator functions in JS, test component templating
  • Loading branch information
cmars authored Mar 7, 2022
2 parents 159b526 + 23897b6 commit 6b157d8
Show file tree
Hide file tree
Showing 7 changed files with 423 additions and 82 deletions.
19 changes: 6 additions & 13 deletions config/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ type Generators map[string]*Generator

// Generator describes how files are generated for a resource.
type Generator struct {
Name string `json:"-"`
Scope GeneratorScope `json:"scope"`
Filename string `json:"filename,omitempty"`
Template string `json:"template"`
Files string `json:"files,omitempty"`
Data map[string]*GeneratorData `json:"data,omitempty"`
Name string `json:"-"`
Scope GeneratorScope `json:"scope"`
Filename string `json:"filename,omitempty"`
Template string `json:"template"`
Files string `json:"files,omitempty"`
Functions string `json:"functions,omitempty"`
}

func (g *Generator) validate() error {
Expand Down Expand Up @@ -51,13 +51,6 @@ const (
GeneratorScopeResource = "resource"
)

// GeneratorData describes an item that is added to a generator's template data
// context.
type GeneratorData struct {
FieldName string `json:"-"`
Include string `json:"include"`
}

func (g Generators) init() error {
for name, gen := range g {
gen.Name = name
Expand Down
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.17

require (
github.com/bmatcuk/doublestar/v4 v4.0.2
github.com/dop251/goja v0.0.0-20220214123719-b09a6bfa842f
github.com/frankban/quicktest v1.13.0
github.com/getkin/kin-openapi v0.90.0
github.com/ghodss/yaml v1.0.0
Expand All @@ -23,11 +24,13 @@ require (
github.com/acomagu/bufpipe v1.0.3 // indirect
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 // indirect
github.com/emirpasic/gods v1.12.0 // indirect
github.com/go-git/gcfg v1.5.0 // indirect
github.com/go-git/go-billy/v5 v5.3.1 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/swag v0.19.15 // indirect
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
Expand All @@ -46,6 +49,7 @@ require (
golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce // indirect
golang.org/x/net v0.0.0-20220114011407-0dd24b26b47d // indirect
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect
golang.org/x/text v0.3.6 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
Expand Down
7 changes: 7 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 h1:Izz0+t1Z5nI16/II7vuEo/nHjodOg0p7+OiDpjX5t1E=
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dop251/goja v0.0.0-20220214123719-b09a6bfa842f h1:ztRywKO1rqqS8li0TDcnwi9AGsqAH0ky9NaND69/Ccc=
github.com/dop251/goja v0.0.0-20220214123719-b09a6bfa842f/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk=
github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y=
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
Expand All @@ -53,6 +58,8 @@ github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
Expand Down
126 changes: 126 additions & 0 deletions internal/generator/functions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package generator

import (
"bytes"
"fmt"
"io/ioutil"
"log"
"path/filepath"
"strings"
"text/template"

"github.com/dop251/goja"
"github.com/getkin/kin-openapi/openapi3"
)

var (
builtinFuncs = template.FuncMap{
"map": func(keyValues ...interface{}) (map[string]interface{}, error) {
if len(keyValues)%2 != 0 {
return nil, fmt.Errorf("invalid number of arguments to map")
}
m := make(map[string]interface{}, len(keyValues)/2)
for i := 0; i < len(keyValues); i += 2 {
k, ok := keyValues[i].(string)
if !ok {
return nil, fmt.Errorf("map keys must be strings")
}
m[k] = keyValues[i+1]
}
return m, nil
},
"indent": func(indent int, s string) string {
return strings.ReplaceAll(s, "\n", "\n"+strings.Repeat(" ", indent))
},
"uncapitalize": func(s string) string {
if len(s) > 1 {
return strings.ToLower(s[0:1]) + s[1:]
}
return s
},
"capitalize": func(s string) string {
if len(s) > 1 {
return strings.ToUpper(s[0:1]) + s[1:]
}
return s
},
"replaceall": strings.ReplaceAll,
"pathOperations": MapPathOperations,
"resourceOperations": MapResourceOperations,
"isOneOf": func(s *openapi3.Schema) bool {
return s != nil && len(s.OneOf) > 0
},
"isAnyOf": func(s *openapi3.Schema) bool {
return s != nil && len(s.AnyOf) > 0
},
"isAllOf": func(s *openapi3.Schema) bool {
return s != nil && len(s.AllOf) > 0
},
"isAssociativeArray": func(s *openapi3.Schema) bool {
return s != nil &&
s.Type == "object" &&
len(s.Properties) == 0 &&
s.AdditionalPropertiesAllowed != nil &&
*s.AdditionalPropertiesAllowed
},
"basename": filepath.Base,
}
)

func withIncludeFunc(t *template.Template) *template.Template {
return t.Funcs(template.FuncMap{
"include": func(name string, data interface{}) (string, error) {
buf := bytes.NewBuffer(nil)
if err := t.ExecuteTemplate(buf, name, data); err != nil {
return "", err
}
return buf.String(), nil
},
})
}

var jsConsole = map[string]func(goja.FunctionCall) goja.Value{
"log": func(call goja.FunctionCall) goja.Value {
args := make([]interface{}, len(call.Arguments))
for i := range call.Arguments {
args[i] = call.Arguments[i].Export()
}
log.Println(args...)
return goja.Null()
},
}

func (g *Generator) loadFunctions(filename string) error {
src, err := ioutil.ReadFile(filename)
if err != nil {
return err
}
vm := goja.New()
_, err = vm.RunScript(filename, string(src))
if err != nil {
return err
}
module := vm.GlobalObject()
if err != nil {
return err
}
err = module.Set("console", jsConsole)
if err != nil {
return err
}
for _, key := range module.Keys() {
fn, ok := goja.AssertFunction(module.Get(key))
if !ok {
// not a callable function
continue
}
g.functions[key] = func(args ...interface{}) (interface{}, error) {
jsArgs := make([]goja.Value, len(args))
for i := range args {
jsArgs[i] = vm.ToValue(args[i])
}
return fn(goja.Undefined(), jsArgs...)
}
}
return nil
}
118 changes: 49 additions & 69 deletions internal/generator/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"log"
"os"
"path/filepath"
"strings"
"text/template"

"github.com/ghodss/yaml"
Expand All @@ -18,66 +17,18 @@ import (

// Generator generates files for new resources from data models and templates.
type Generator struct {
name string
filename *template.Template
contents *template.Template
files *template.Template
scope config.GeneratorScope
name string
filename *template.Template
contents *template.Template
files *template.Template
functions template.FuncMap
scope config.GeneratorScope

debug bool
force bool
here string
}

var (
templateFuncs = template.FuncMap{
"map": func(keyValues ...interface{}) (map[string]interface{}, error) {
if len(keyValues)%2 != 0 {
return nil, fmt.Errorf("invalid number of arguments to map")
}
m := make(map[string]interface{}, len(keyValues)/2)
for i := 0; i < len(keyValues); i += 2 {
k, ok := keyValues[i].(string)
if !ok {
return nil, fmt.Errorf("map keys must be strings")
}
m[k] = keyValues[i+1]
}
return m, nil
},
"indent": func(indent int, s string) string {
return strings.ReplaceAll(s, "\n", "\n"+strings.Repeat(" ", indent))
},
"uncapitalize": func(s string) string {
if len(s) > 1 {
return strings.ToLower(s[0:1]) + s[1:]
}
return s
},
"capitalize": func(s string) string {
if len(s) > 1 {
return strings.ToUpper(s[0:1]) + s[1:]
}
return s
},
"replaceall": strings.ReplaceAll,
"pathOperations": MapPathOperations,
"resourceOperations": MapResourceOperations,
}
)

func withIncludeFunc(t *template.Template) *template.Template {
return t.Funcs(template.FuncMap{
"include": func(name string, data interface{}) (string, error) {
buf := bytes.NewBuffer(nil)
if err := t.ExecuteTemplate(buf, name, data); err != nil {
return "", err
}
return buf.String(), nil
},
})
}

// NewMap instanstiates a map of Generators from configuration.
func NewMap(generatorsConf config.Generators, options ...Option) (map[string]*Generator, error) {
result := map[string]*Generator{}
Expand All @@ -94,8 +45,9 @@ func NewMap(generatorsConf config.Generators, options ...Option) (map[string]*Ge
// New returns a new Generator from configuration.
func New(conf *config.Generator, options ...Option) (*Generator, error) {
g := &Generator{
name: conf.Name,
scope: conf.Scope,
name: conf.Name,
scope: conf.Scope,
functions: template.FuncMap{},
}
for i := range options {
options[i](g)
Expand All @@ -113,46 +65,74 @@ func New(conf *config.Generator, options ...Option) (*Generator, error) {
}
}

// Resolve the template filename... with a template. Only .Here is
// Resolve the template 'functions'... with a template. Only .Here is
// supported, not full scope. Just enough to locate files relative to the
// config.
templateTemplate, err := template.New("template").Parse(string(conf.Template))
if err != nil {
return nil, fmt.Errorf("%w: (generators.%s.template)", err, conf.Name)
if conf.Functions != "" {
functionsFilename, err := g.resolveFilename(conf.Functions)
if err != nil {
return nil, fmt.Errorf("%w: (generators.%s.functions)", err, conf.Name)
}
err = g.loadFunctions(functionsFilename)
if err != nil {
return nil, fmt.Errorf("%w: (generators.%s.functions)", err, conf.Name)
}
}
var templateFilenameBuf bytes.Buffer
err = templateTemplate.ExecuteTemplate(&templateFilenameBuf, "template", map[string]string{
"Here": g.here,
})

// Resolve the template filename... with a template. Only .Here is
// supported, not full scope. Just enough to locate files relative to the
// config.
templateFilename, err := g.resolveFilename(conf.Template)
if err != nil {
return nil, fmt.Errorf("%w: (generators.%s.template)", err, conf.Name)
}

// Parse & wire up other templates: contents, filename or files. These do
// support full scope.
contentsTemplate, err := ioutil.ReadFile(templateFilenameBuf.String())
contentsTemplate, err := ioutil.ReadFile(templateFilename)
if err != nil {
return nil, fmt.Errorf("%w: (generators.%s.contents)", err, conf.Name)
}
g.contents, err = template.New("contents").Funcs(templateFuncs).Parse(string(contentsTemplate))
g.contents, err = withIncludeFunc(template.New("contents").
Funcs(g.functions).
Funcs(builtinFuncs)).
Parse(string(contentsTemplate))
if err != nil {
return nil, fmt.Errorf("%w: (generators.%s.contents)", err, conf.Name)
}
if conf.Filename != "" {
g.filename, err = template.New("filename").Funcs(templateFuncs).Parse(conf.Filename)
g.filename, err = template.New("filename").
Funcs(g.functions).
Funcs(builtinFuncs).
Parse(conf.Filename)
if err != nil {
return nil, fmt.Errorf("%w: (generators.%s.filename)", err, conf.Name)
}
}
if conf.Files != "" {
g.files, err = withIncludeFunc(g.contents.New("files")).Parse(conf.Files)
g.files, err = withIncludeFunc(g.contents.New("files").Funcs(g.functions)).Parse(conf.Files)
if err != nil {
return nil, fmt.Errorf("%w: (generators.%s.files)", err, conf.Name)
}
}
return g, nil
}

func (g *Generator) resolveFilename(filenameTemplate string) (string, error) {
t, err := template.New("").Funcs(g.functions).Parse(string(filenameTemplate))
if err != nil {
return "", err
}
var buf bytes.Buffer
err = t.ExecuteTemplate(&buf, "", map[string]string{
"Here": g.here,
})
if err != nil {
return "", err
}
return buf.String(), nil
}

// Option configures a Generator.
type Option func(g *Generator)

Expand Down
Loading

0 comments on commit 6b157d8

Please sign in to comment.