add custom js functions for templates
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,3 +3,4 @@
|
|||||||
/examples/react-app-generator/generated-blog-app/node_modules/
|
/examples/react-app-generator/generated-blog-app/node_modules/
|
||||||
/examples/react-app-generator/generated-app/node_modules/
|
/examples/react-app-generator/generated-app/node_modules/
|
||||||
/examples/react-app-generator/generated-app/
|
/examples/react-app-generator/generated-app/
|
||||||
|
/examples/custom-js-functions/generated-app/
|
||||||
|
31
examples/custom-js-functions/blog-example.masonry
Normal file
31
examples/custom-js-functions/blog-example.masonry
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
entity User {
|
||||||
|
name: string required
|
||||||
|
email: string required unique
|
||||||
|
password: string required
|
||||||
|
bio: text
|
||||||
|
createdAt: timestamp
|
||||||
|
updatedAt: timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
entity Post {
|
||||||
|
title: string required
|
||||||
|
slug: string required unique
|
||||||
|
content: text required
|
||||||
|
published: boolean
|
||||||
|
authorId: uuid required relates to User as one via "authorId"
|
||||||
|
createdAt: timestamp
|
||||||
|
updatedAt: timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
entity Comment {
|
||||||
|
content: text required
|
||||||
|
authorId: uuid required relates to User as one via "authorId"
|
||||||
|
postId: uuid required relates to Post as one via "postId"
|
||||||
|
createdAt: timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
server BlogAPI {
|
||||||
|
host "localhost"
|
||||||
|
port 3000
|
||||||
|
database_url env "DATABASE_URL" default "postgres://localhost/blog"
|
||||||
|
}
|
68
examples/custom-js-functions/readme.md
Normal file
68
examples/custom-js-functions/readme.md
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
# Custom JavaScript Functions Example
|
||||||
|
|
||||||
|
This example demonstrates how to use custom JavaScript functions in Masonry templates using the Otto JavaScript interpreter. You can define custom template functions directly in your `manifest.yaml` files, allowing for dynamic code generation that can be customized without recompiling the CLI tool.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The custom JavaScript functions feature allows you to:
|
||||||
|
- Define JavaScript functions in your `manifest.yaml` files
|
||||||
|
- Use these functions in your templates just like built-in template functions
|
||||||
|
- Perform complex string manipulations, data transformations, and logic
|
||||||
|
- Extend template functionality dynamically
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
1. **Define Functions**: Add a `functions` section to your `manifest.yaml` file
|
||||||
|
2. **JavaScript Code**: Each function is written in JavaScript and must define a `main()` function
|
||||||
|
3. **Template Usage**: Use the custom functions in your templates like any other template function
|
||||||
|
4. **Otto Execution**: The Otto JavaScript interpreter executes your functions at template generation time
|
||||||
|
|
||||||
|
## Function Structure
|
||||||
|
|
||||||
|
Each custom function must follow this structure:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function main() {
|
||||||
|
// Your custom logic here
|
||||||
|
// Access arguments via the global 'args' array: args[0], args[1], etc.
|
||||||
|
// Or via individual variables: arg0, arg1, etc.
|
||||||
|
|
||||||
|
return "your result";
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example Files
|
||||||
|
|
||||||
|
- `blog-example.masonry` - Sample Masonry file defining a blog application
|
||||||
|
- `templates/manifest.yaml` - Manifest with custom JavaScript functions
|
||||||
|
- `templates/model.tmpl` - Template using the custom functions
|
||||||
|
- `templates/controller.tmpl` - Another template demonstrating function usage
|
||||||
|
|
||||||
|
## Running the Example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From the masonry project root
|
||||||
|
./masonry.exe generate templates examples/custom-js-functions/blog-example.masonry examples/custom-js-functions/templates manifest.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Custom Functions in This Example
|
||||||
|
|
||||||
|
### `generateSlug`
|
||||||
|
Converts a title to a URL-friendly slug:
|
||||||
|
- Input: "My Blog Post Title"
|
||||||
|
- Output: "my-blog-post-title"
|
||||||
|
|
||||||
|
### `pluralize`
|
||||||
|
Converts singular words to plural:
|
||||||
|
- Input: "post"
|
||||||
|
- Output: "posts"
|
||||||
|
|
||||||
|
### `generateValidation`
|
||||||
|
Creates validation rules based on field type:
|
||||||
|
- Input: field type
|
||||||
|
- Output: appropriate validation code
|
||||||
|
|
||||||
|
### `formatComment`
|
||||||
|
Generates formatted code comments:
|
||||||
|
- Input: text
|
||||||
|
- Output: properly formatted comment block
|
97
examples/custom-js-functions/templates/controller.tmpl
Normal file
97
examples/custom-js-functions/templates/controller.tmpl
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"your-app/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
{{formatComment (printf "Controller for %s entity\nGenerated with custom JavaScript template functions" .Entity.Name)}}
|
||||||
|
type {{.Entity.Name | title}}Controller struct {
|
||||||
|
// Add your dependencies here (e.g., database, services)
|
||||||
|
}
|
||||||
|
|
||||||
|
{{formatComment "Create a new controller instance"}}
|
||||||
|
func New{{.Entity.Name | title}}Controller() *{{.Entity.Name | title}}Controller {
|
||||||
|
return &{{.Entity.Name | title}}Controller{}
|
||||||
|
}
|
||||||
|
|
||||||
|
{{formatComment "GET /{{pluralize (.Entity.Name | lower)}}"}}
|
||||||
|
func (c *{{.Entity.Name | title}}Controller) List{{pluralize (.Entity.Name | title)}}(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Implementation for listing {{pluralize (.Entity.Name | lower)}}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
// Example response
|
||||||
|
{{pluralize (.Entity.Name | lower)}} := []models.{{.Entity.Name | title}}{}
|
||||||
|
json.NewEncoder(w).Encode({{pluralize (.Entity.Name | lower)}})
|
||||||
|
}
|
||||||
|
|
||||||
|
{{formatComment "GET /{{pluralize (.Entity.Name | lower)}}/:id"}}
|
||||||
|
func (c *{{.Entity.Name | title}}Controller) Get{{.Entity.Name | title}}(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
id := vars["id"]
|
||||||
|
|
||||||
|
// Convert id and fetch {{.Entity.Name | lower}}
|
||||||
|
_ = id // TODO: implement fetch logic
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
// json.NewEncoder(w).Encode({{.Entity.Name | lower}})
|
||||||
|
}
|
||||||
|
|
||||||
|
{{formatComment "POST /{{pluralize (.Entity.Name | lower)}}"}}
|
||||||
|
func (c *{{.Entity.Name | title}}Controller) Create{{.Entity.Name | title}}(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var {{.Entity.Name | lower}} models.{{.Entity.Name | title}}
|
||||||
|
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&{{.Entity.Name | lower}}); err != nil {
|
||||||
|
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := {{.Entity.Name | lower}}.Validate(); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Save to database
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
json.NewEncoder(w).Encode({{.Entity.Name | lower}})
|
||||||
|
}
|
||||||
|
|
||||||
|
{{formatComment "PUT /{{pluralize (.Entity.Name | lower)}}/:id"}}
|
||||||
|
func (c *{{.Entity.Name | title}}Controller) Update{{.Entity.Name | title}}(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
id := vars["id"]
|
||||||
|
|
||||||
|
var {{.Entity.Name | lower}} models.{{.Entity.Name | title}}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&{{.Entity.Name | lower}}); err != nil {
|
||||||
|
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := {{.Entity.Name | lower}}.Validate(); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Update in database using id
|
||||||
|
_ = id
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode({{.Entity.Name | lower}})
|
||||||
|
}
|
||||||
|
|
||||||
|
{{formatComment "DELETE /{{pluralize (.Entity.Name | lower)}}/:id"}}
|
||||||
|
func (c *{{.Entity.Name | title}}Controller) Delete{{.Entity.Name | title}}(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
id := vars["id"]
|
||||||
|
|
||||||
|
// TODO: Delete from database using id
|
||||||
|
_ = id
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
104
examples/custom-js-functions/templates/manifest.yaml
Normal file
104
examples/custom-js-functions/templates/manifest.yaml
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
name: "Blog Generator with Custom JS Functions"
|
||||||
|
description: "Demonstrates custom JavaScript template functions using Otto interpreter"
|
||||||
|
|
||||||
|
# Custom JavaScript functions that can be used in templates
|
||||||
|
functions:
|
||||||
|
# Convert a string to a URL-friendly slug
|
||||||
|
generateSlug: |
|
||||||
|
function main() {
|
||||||
|
var input = args[0] || "";
|
||||||
|
return input
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, '-')
|
||||||
|
.replace(/^-+|-+$/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
# Convert singular words to plural (simple English rules) - ES5 compatible
|
||||||
|
pluralize: |
|
||||||
|
function main() {
|
||||||
|
var word = args[0] || "";
|
||||||
|
if (word.slice(-1) === 'y') {
|
||||||
|
return word.slice(0, -1) + 'ies';
|
||||||
|
} else if (word.slice(-1) === 's' || word.slice(-2) === 'sh' || word.slice(-2) === 'ch' || word.slice(-1) === 'x' || word.slice(-1) === 'z') {
|
||||||
|
return word + 'es';
|
||||||
|
} else {
|
||||||
|
return word + 's';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Generate validation rules based on field type
|
||||||
|
generateValidation: |
|
||||||
|
function main() {
|
||||||
|
var fieldType = args[0] || "";
|
||||||
|
var fieldName = args[1] || "field";
|
||||||
|
|
||||||
|
switch (fieldType) {
|
||||||
|
case 'string':
|
||||||
|
return 'if len(' + fieldName + ') == 0 { return errors.New("' + fieldName + ' is required") }';
|
||||||
|
case 'uuid':
|
||||||
|
return 'if _, err := uuid.Parse(' + fieldName + '); err != nil { return errors.New("invalid UUID format") }';
|
||||||
|
case 'boolean':
|
||||||
|
return '// Boolean validation not required';
|
||||||
|
case 'timestamp':
|
||||||
|
return 'if ' + fieldName + '.IsZero() { return errors.New("' + fieldName + ' is required") }';
|
||||||
|
default:
|
||||||
|
return '// No validation rules for type: ' + fieldType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Format a comment block
|
||||||
|
formatComment: |
|
||||||
|
function main() {
|
||||||
|
var text = args[0] || "";
|
||||||
|
var lines = text.split('\n');
|
||||||
|
var result = '// ' + lines.join('\n// ');
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Generate database field mapping
|
||||||
|
dbFieldMapping: |
|
||||||
|
function main() {
|
||||||
|
var fieldName = args[0] || "";
|
||||||
|
var fieldType = args[1] || "string";
|
||||||
|
|
||||||
|
var dbType;
|
||||||
|
switch (fieldType) {
|
||||||
|
case 'string':
|
||||||
|
dbType = 'VARCHAR(255)';
|
||||||
|
break;
|
||||||
|
case 'text':
|
||||||
|
dbType = 'TEXT';
|
||||||
|
break;
|
||||||
|
case 'uuid':
|
||||||
|
dbType = 'UUID';
|
||||||
|
break;
|
||||||
|
case 'boolean':
|
||||||
|
dbType = 'BOOLEAN';
|
||||||
|
break;
|
||||||
|
case 'timestamp':
|
||||||
|
dbType = 'TIMESTAMP';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
dbType = 'VARCHAR(255)';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '`db:"' + fieldName.toLowerCase() + '" json:"' + fieldName + '"`';
|
||||||
|
}
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
# Generate Go models for each entity using custom functions
|
||||||
|
- path: "models/{{.Entity.Name | lower}}.go"
|
||||||
|
template: "model"
|
||||||
|
iterator: "entities"
|
||||||
|
item_context: "Entity"
|
||||||
|
|
||||||
|
# Generate controllers for each entity
|
||||||
|
- path: "controllers/{{.Entity.Name | lower}}_controller.go"
|
||||||
|
template: "controller"
|
||||||
|
iterator: "entities"
|
||||||
|
item_context: "Entity"
|
||||||
|
|
||||||
|
# Generate a single router file
|
||||||
|
- path: "router.go"
|
||||||
|
template: "router"
|
||||||
|
condition: "has_entities"
|
44
examples/custom-js-functions/templates/model.tmpl
Normal file
44
examples/custom-js-functions/templates/model.tmpl
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
{{formatComment (printf "Model for %s entity\nGenerated with custom JavaScript functions" .Entity.Name)}}
|
||||||
|
type {{.Entity.Name | title}} struct {
|
||||||
|
{{- range .Entity.Fields}}
|
||||||
|
{{.Name | title}} {{goType .Type}} {{dbFieldMapping .Name .Type}}
|
||||||
|
{{- end}}
|
||||||
|
}
|
||||||
|
|
||||||
|
{{formatComment "Table name for GORM"}}
|
||||||
|
func ({{.Entity.Name | title}}) TableName() string {
|
||||||
|
return "{{pluralize (.Entity.Name | lower)}}"
|
||||||
|
}
|
||||||
|
|
||||||
|
{{formatComment "Validation function using custom JavaScript validation rules"}}
|
||||||
|
func (m *{{.Entity.Name | title}}) Validate() error {
|
||||||
|
{{- range .Entity.Fields}}
|
||||||
|
{{- if .Required}}
|
||||||
|
{{generateValidation .Type .Name}}
|
||||||
|
{{- end}}
|
||||||
|
{{- end}}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
{{formatComment "Create a new instance with validation"}}
|
||||||
|
func New{{.Entity.Name | title}}({{range $i, $field := .Entity.Fields}}{{if $i}}, {{end}}{{$field.Name | lower}} {{goType $field.Type}}{{end}}) (*{{.Entity.Name | title}}, error) {
|
||||||
|
model := &{{.Entity.Name | title}}{
|
||||||
|
{{- range .Entity.Fields}}
|
||||||
|
{{.Name | title}}: {{.Name | lower}},
|
||||||
|
{{- end}}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := model.Validate(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return model, nil
|
||||||
|
}
|
28
examples/custom-js-functions/templates/router.tmpl
Normal file
28
examples/custom-js-functions/templates/router.tmpl
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"your-app/controllers"
|
||||||
|
)
|
||||||
|
|
||||||
|
{{formatComment "Router setup with all entity routes\nGenerated using custom JavaScript template functions"}}
|
||||||
|
func SetupRouter() *mux.Router {
|
||||||
|
router := mux.NewRouter()
|
||||||
|
|
||||||
|
{{range $entity := .AST.Definitions}}
|
||||||
|
{{- if $entity.Entity}}
|
||||||
|
{{formatComment (printf "Routes for %s entity" $entity.Entity.Name)}}
|
||||||
|
{{$entity.Entity.Name | lower}}Controller := controllers.New{{$entity.Entity.Name | title}}Controller()
|
||||||
|
router.HandleFunc("/{{pluralize ($entity.Entity.Name | lower)}}", {{$entity.Entity.Name | lower}}Controller.List{{pluralize ($entity.Entity.Name | title)}}).Methods("GET")
|
||||||
|
router.HandleFunc("/{{pluralize ($entity.Entity.Name | lower)}}/:id", {{$entity.Entity.Name | lower}}Controller.Get{{$entity.Entity.Name | title}}).Methods("GET")
|
||||||
|
router.HandleFunc("/{{pluralize ($entity.Entity.Name | lower)}}", {{$entity.Entity.Name | lower}}Controller.Create{{$entity.Entity.Name | title}}).Methods("POST")
|
||||||
|
router.HandleFunc("/{{pluralize ($entity.Entity.Name | lower)}}/:id", {{$entity.Entity.Name | lower}}Controller.Update{{$entity.Entity.Name | title}}).Methods("PUT")
|
||||||
|
router.HandleFunc("/{{pluralize ($entity.Entity.Name | lower)}}/:id", {{$entity.Entity.Name | lower}}Controller.Delete{{$entity.Entity.Name | title}}).Methods("DELETE")
|
||||||
|
|
||||||
|
{{end}}
|
||||||
|
{{- end}}
|
||||||
|
|
||||||
|
return router
|
||||||
|
}
|
4
go.mod
4
go.mod
@ -4,13 +4,15 @@ go 1.23
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/alecthomas/participle/v2 v2.1.4
|
github.com/alecthomas/participle/v2 v2.1.4
|
||||||
|
github.com/robertkrimen/otto v0.5.1
|
||||||
github.com/urfave/cli/v2 v2.27.5
|
github.com/urfave/cli/v2 v2.27.5
|
||||||
golang.org/x/text v0.22.0
|
golang.org/x/text v0.22.0
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/sourcemap.v1 v1.0.5 // indirect
|
||||||
)
|
)
|
||||||
|
11
go.sum
11
go.sum
@ -6,16 +6,27 @@ github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc
|
|||||||
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
|
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
|
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/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/robertkrimen/otto v0.5.1 h1:avDI4ToRk8k1hppLdYFTuuzND41n37vPGJU7547dGf0=
|
||||||
|
github.com/robertkrimen/otto v0.5.1/go.mod h1:bS433I4Q9p+E5pZLu7r17vP6FkE6/wLxBdmKjoqJXF8=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
|
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
|
||||||
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
|
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
|
||||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
|
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
|
||||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
||||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
|
||||||
|
gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/robertkrimen/otto"
|
||||||
"golang.org/x/text/cases"
|
"golang.org/x/text/cases"
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
@ -18,9 +19,10 @@ import (
|
|||||||
|
|
||||||
// TemplateManifest defines the structure for multi-file template generation
|
// TemplateManifest defines the structure for multi-file template generation
|
||||||
type TemplateManifest struct {
|
type TemplateManifest struct {
|
||||||
Name string `yaml:"name"`
|
Name string `yaml:"name"`
|
||||||
Description string `yaml:"description"`
|
Description string `yaml:"description"`
|
||||||
Outputs []OutputFile `yaml:"outputs"`
|
Functions map[string]string `yaml:"functions,omitempty"`
|
||||||
|
Outputs []OutputFile `yaml:"outputs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// OutputFile defines a single output file configuration
|
// OutputFile defines a single output file configuration
|
||||||
@ -35,12 +37,14 @@ type OutputFile struct {
|
|||||||
// TemplateInterpreter converts Masonry AST using template files
|
// TemplateInterpreter converts Masonry AST using template files
|
||||||
type TemplateInterpreter struct {
|
type TemplateInterpreter struct {
|
||||||
registry *TemplateRegistry
|
registry *TemplateRegistry
|
||||||
|
vm *otto.Otto
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTemplateInterpreter creates a new template interpreter
|
// NewTemplateInterpreter creates a new template interpreter
|
||||||
func NewTemplateInterpreter() *TemplateInterpreter {
|
func NewTemplateInterpreter() *TemplateInterpreter {
|
||||||
return &TemplateInterpreter{
|
return &TemplateInterpreter{
|
||||||
registry: NewTemplateRegistry(),
|
registry: NewTemplateRegistry(),
|
||||||
|
vm: otto.New(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,13 +119,7 @@ func (ti *TemplateInterpreter) Interpret(masonryInput string, tmplText string) (
|
|||||||
|
|
||||||
// InterpretToFiles processes a manifest and returns multiple output files
|
// InterpretToFiles processes a manifest and returns multiple output files
|
||||||
func (ti *TemplateInterpreter) InterpretToFiles(masonryFile, templateDir, manifestFile string) (map[string]string, error) {
|
func (ti *TemplateInterpreter) InterpretToFiles(masonryFile, templateDir, manifestFile string) (map[string]string, error) {
|
||||||
// Load templates from directory
|
// Load manifest first to get custom functions
|
||||||
err := ti.registry.LoadFromDirectory(templateDir)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error loading templates: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load manifest
|
|
||||||
manifestPath := filepath.Join(templateDir, manifestFile)
|
manifestPath := filepath.Join(templateDir, manifestFile)
|
||||||
manifestData, err := os.ReadFile(manifestPath)
|
manifestData, err := os.ReadFile(manifestPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -134,6 +132,18 @@ func (ti *TemplateInterpreter) InterpretToFiles(masonryFile, templateDir, manife
|
|||||||
return nil, fmt.Errorf("error parsing manifest: %w", err)
|
return nil, fmt.Errorf("error parsing manifest: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load custom JavaScript functions from the manifest BEFORE loading templates
|
||||||
|
err = ti.loadCustomFunctions(manifest.Functions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error loading custom functions: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now load templates from directory (they will have access to custom functions)
|
||||||
|
err = ti.registry.LoadFromDirectory(templateDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error loading templates: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Parse Masonry input
|
// Parse Masonry input
|
||||||
masonryInput, err := os.ReadFile(masonryFile)
|
masonryInput, err := os.ReadFile(masonryFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -259,30 +269,31 @@ func (ti *TemplateInterpreter) getAllSections(ast lang.AST) []interface{} {
|
|||||||
|
|
||||||
for _, def := range ast.Definitions {
|
for _, def := range ast.Definitions {
|
||||||
if def.Page != nil {
|
if def.Page != nil {
|
||||||
// Get sections directly in the page
|
// Get sections from page elements
|
||||||
for i := range def.Page.Sections {
|
for i := range def.Page.Elements {
|
||||||
sections = append(sections, &def.Page.Sections[i])
|
element := &def.Page.Elements[i]
|
||||||
|
if element.Section != nil {
|
||||||
|
sections = append(sections, element.Section)
|
||||||
|
// Recursively get nested sections
|
||||||
|
sections = append(sections, ti.getSectionsFromSection(*element.Section)...)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Recursively get nested sections
|
|
||||||
sections = append(sections, ti.getSectionsFromSections(def.Page.Sections)...)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return sections
|
return sections
|
||||||
}
|
}
|
||||||
|
|
||||||
// getSectionsFromSections recursively extracts sections from a list of sections
|
// getSectionsFromSection recursively extracts sections from a single section
|
||||||
func (ti *TemplateInterpreter) getSectionsFromSections(sections []lang.Section) []interface{} {
|
func (ti *TemplateInterpreter) getSectionsFromSection(section lang.Section) []interface{} {
|
||||||
var result []interface{}
|
var result []interface{}
|
||||||
|
|
||||||
for i := range sections {
|
for i := range section.Elements {
|
||||||
for j := range sections[i].Elements {
|
element := §ion.Elements[i]
|
||||||
element := §ions[i].Elements[j]
|
if element.Section != nil {
|
||||||
if element.Section != nil {
|
result = append(result, element.Section)
|
||||||
result = append(result, element.Section)
|
// Recursively get sections from this section
|
||||||
// Recursively get sections from this section
|
result = append(result, ti.getSectionsFromSection(*element.Section)...)
|
||||||
result = append(result, ti.getSectionsFromSections([]lang.Section{*element.Section})...)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -295,50 +306,52 @@ func (ti *TemplateInterpreter) getAllComponents(ast lang.AST) []interface{} {
|
|||||||
|
|
||||||
for _, def := range ast.Definitions {
|
for _, def := range ast.Definitions {
|
||||||
if def.Page != nil {
|
if def.Page != nil {
|
||||||
// Get components directly in the page
|
// Get components from page elements
|
||||||
for i := range def.Page.Components {
|
for i := range def.Page.Elements {
|
||||||
components = append(components, &def.Page.Components[i])
|
element := &def.Page.Elements[i]
|
||||||
|
if element.Component != nil {
|
||||||
|
components = append(components, element.Component)
|
||||||
|
// Get nested components from this component
|
||||||
|
components = append(components, ti.getComponentsFromComponent(*element.Component)...)
|
||||||
|
} else if element.Section != nil {
|
||||||
|
// Get components from sections
|
||||||
|
components = append(components, ti.getComponentsFromSection(*element.Section)...)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Get components from sections
|
|
||||||
components = append(components, ti.getComponentsFromSections(def.Page.Sections)...)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return components
|
return components
|
||||||
}
|
}
|
||||||
|
|
||||||
// getComponentsFromSections recursively extracts components from sections
|
// getComponentsFromSection recursively extracts components from a section
|
||||||
func (ti *TemplateInterpreter) getComponentsFromSections(sections []lang.Section) []interface{} {
|
func (ti *TemplateInterpreter) getComponentsFromSection(section lang.Section) []interface{} {
|
||||||
var components []interface{}
|
var components []interface{}
|
||||||
|
|
||||||
for i := range sections {
|
for i := range section.Elements {
|
||||||
for j := range sections[i].Elements {
|
element := §ion.Elements[i]
|
||||||
element := §ions[i].Elements[j]
|
if element.Component != nil {
|
||||||
if element.Component != nil {
|
components = append(components, element.Component)
|
||||||
components = append(components, element.Component)
|
// Get nested components from this component
|
||||||
// Get nested components from this component
|
components = append(components, ti.getComponentsFromComponent(*element.Component)...)
|
||||||
components = append(components, ti.getComponentsFromComponents([]lang.Component{*element.Component})...)
|
} else if element.Section != nil {
|
||||||
} else if element.Section != nil {
|
// Recursively get components from nested sections
|
||||||
// Recursively get components from nested sections
|
components = append(components, ti.getComponentsFromSection(*element.Section)...)
|
||||||
components = append(components, ti.getComponentsFromSections([]lang.Section{*element.Section})...)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return components
|
return components
|
||||||
}
|
}
|
||||||
|
|
||||||
// getComponentsFromComponents recursively extracts components from nested components
|
// getComponentsFromComponent recursively extracts components from nested components
|
||||||
func (ti *TemplateInterpreter) getComponentsFromComponents(components []lang.Component) []interface{} {
|
func (ti *TemplateInterpreter) getComponentsFromComponent(component lang.Component) []interface{} {
|
||||||
var result []interface{}
|
var result []interface{}
|
||||||
|
|
||||||
for i := range components {
|
for i := range component.Elements {
|
||||||
for j := range components[i].Elements {
|
element := &component.Elements[i]
|
||||||
element := &components[i].Elements[j]
|
if element.Section != nil {
|
||||||
if element.Section != nil {
|
// Get components from nested sections
|
||||||
// Get components from nested sections
|
result = append(result, ti.getComponentsFromSection(*element.Section)...)
|
||||||
result = append(result, ti.getComponentsFromSections([]lang.Section{*element.Section})...)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -648,3 +661,107 @@ func (tr *TemplateRegistry) LoadFromDirectory(dir string) error {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loadCustomFunctions loads JavaScript functions into the template function map
|
||||||
|
func (ti *TemplateInterpreter) loadCustomFunctions(functions map[string]string) error {
|
||||||
|
if functions == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for funcName, jsCode := range functions {
|
||||||
|
// Validate function name
|
||||||
|
if !isValidFunctionName(funcName) {
|
||||||
|
return fmt.Errorf("invalid function name: %s", funcName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a wrapper function that calls the JavaScript code
|
||||||
|
templateFunc := ti.createJavaScriptTemplateFunction(funcName, jsCode)
|
||||||
|
ti.registry.funcMap[funcName] = templateFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createJavaScriptTemplateFunction creates a Go template function that executes JavaScript
|
||||||
|
func (ti *TemplateInterpreter) createJavaScriptTemplateFunction(funcName, jsCode string) interface{} {
|
||||||
|
return func(args ...interface{}) (interface{}, error) {
|
||||||
|
// Create a new VM instance for this function call to avoid conflicts
|
||||||
|
vm := otto.New()
|
||||||
|
|
||||||
|
// Set up global helper functions in the JavaScript environment
|
||||||
|
vm.Set("log", func(call otto.FunctionCall) otto.Value {
|
||||||
|
// For debugging - could be extended to proper logging
|
||||||
|
fmt.Printf("[JS %s]: %v\n", funcName, call.ArgumentList)
|
||||||
|
return otto.UndefinedValue()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Convert Go arguments to JavaScript values
|
||||||
|
for i, arg := range args {
|
||||||
|
vm.Set(fmt.Sprintf("arg%d", i), arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set a convenience 'args' array
|
||||||
|
argsArray, _ := vm.Object("args = []")
|
||||||
|
for i, arg := range args {
|
||||||
|
argsArray.Set(strconv.Itoa(i), arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute the JavaScript function
|
||||||
|
jsWrapper := fmt.Sprintf(`
|
||||||
|
(function() {
|
||||||
|
%s
|
||||||
|
if (typeof main === 'function') {
|
||||||
|
return main.apply(this, args);
|
||||||
|
} else {
|
||||||
|
throw new Error('Custom function must define a main() function');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
`, jsCode)
|
||||||
|
|
||||||
|
result, err := vm.Run(jsWrapper)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error executing JavaScript function %s: %w", funcName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert JavaScript result back to Go value
|
||||||
|
if result.IsUndefined() || result.IsNull() {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
goValue, err := result.Export()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error converting JavaScript result to Go value: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return goValue, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// isValidFunctionName checks if the function name is valid for Go templates
|
||||||
|
func isValidFunctionName(name string) bool {
|
||||||
|
// Basic validation: alphanumeric and underscore, must start with letter
|
||||||
|
if len(name) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !((name[0] >= 'a' && name[0] <= 'z') || (name[0] >= 'A' && name[0] <= 'Z') || name[0] == '_') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, char := range name[1:] {
|
||||||
|
if !((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') ||
|
||||||
|
(char >= '0' && char <= '9') || char == '_') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avoid conflicts with built-in template functions
|
||||||
|
builtinFunctions := map[string]bool{
|
||||||
|
"and": true, "or": true, "not": true, "len": true, "index": true,
|
||||||
|
"print": true, "printf": true, "println": true, "html": true, "js": true,
|
||||||
|
"call": true, "urlquery": true, "eq": true, "ne": true, "lt": true,
|
||||||
|
"le": true, "gt": true, "ge": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
return !builtinFunctions[name]
|
||||||
|
}
|
||||||
|
@ -22,8 +22,8 @@ Masonry is a library that provides and implements all the basics necessary to bu
|
|||||||
|
|
||||||
- [x] Indicate values that are based on Environment variables
|
- [x] Indicate values that are based on Environment variables
|
||||||
- [x] On Entities, we should indicate what CRUD functions should be implemented instead of implementing all, or forcing the user to define each in the Endpoints section
|
- [x] On Entities, we should indicate what CRUD functions should be implemented instead of implementing all, or forcing the user to define each in the Endpoints section
|
||||||
- [ ] Yaml file to include custom template functions (Otto for JS)
|
- [x] Yaml file to include custom template functions (Otto for JS)
|
||||||
- [ ] Support multi-file outputs based on a directory+manifest file.
|
- [x] Support multi-file outputs based on a directory+manifest file.
|
||||||
- [ ] Support multi-step generation. e.g. gen the proto file, use `masonry g` to get the basic server set up, gen `main.go` from a template, then update gorm overrides, etc.
|
- [ ] Support multi-step generation. e.g. gen the proto file, use `masonry g` to get the basic server set up, gen `main.go` from a template, then update gorm overrides, etc.
|
||||||
- [ ] Support field transformations on Entities using a template for `BeforeToORM` in `service.pb.gorm.override.go`
|
- [ ] Support field transformations on Entities using a template for `BeforeToORM` in `service.pb.gorm.override.go`
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user