add custom js functions for templates

This commit is contained in:
2025-09-09 22:56:39 -06:00
parent b82e22c38d
commit a899fd6c4d
11 changed files with 558 additions and 55 deletions

View 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"
}

View 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

View 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)
}

View 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"

View 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
}

View 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
}