Compare commits

...

8 Commits

Author SHA1 Message Date
d36e1bfd86 add html and server interpreters
these are basic and lack most features.
the server seems to work the best.
the html on the other hand is really rough and doesn't seem to work yet.
but it does build the pages and they have all the shapes and sections we
wanted. More work to come. :)
2025-08-25 00:50:55 -06:00
cf3ad736b7 add an html interpreter 2025-08-25 00:10:18 -06:00
e71b1c3a23 add bracket syntax replace tests 2025-08-24 23:25:43 -06:00
4ac93ee924 update the syntax highlight for example.masonry 2025-08-22 01:17:16 -06:00
1ee8de23da split tests into separate files 2025-08-22 00:59:14 -06:00
da43647b54 improve the page, sections, components 2025-08-22 00:51:55 -06:00
e28b6c89ef move files into an examples dir 2025-08-20 00:17:40 -06:00
e9a422ef07 define a DSL 2025-08-15 01:51:43 -06:00
24 changed files with 5138 additions and 92 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/masonry.exe

42
.idea/copilotDiffState.xml generated Normal file

File diff suppressed because one or more lines are too long

View File

@ -14,6 +14,7 @@ func main() {
tailwindCmd(),
setupCmd(),
vueGenCmd(),
serveCmd(), // New command for server interpreter
}
app := &cli.App{

View File

@ -11,9 +11,15 @@ import (
vue_gen "masonry/vue-gen"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"text/template"
"github.com/alecthomas/participle/v2"
"masonry/interpreter"
"masonry/lang"
)
//go:embed templates/proto/application.proto.tmpl
@ -156,104 +162,143 @@ func generateCmd() *cli.Command {
return &cli.Command{
Name: "generate",
Aliases: []string{"g"},
Usage: "Generate code from proto files",
Usage: "Generate code from proto files or Masonry files",
Category: "generator",
Description: "This command will generate code from the proto files in the proto directory and place them in a language folder in the gen folder.",
Description: "This command will generate code from proto files or convert Masonry files to various formats.",
Subcommands: []*cli.Command{
{
Name: "proto",
Usage: "Generate code from proto files",
Action: func(c *cli.Context) error {
return generateProtoCode()
},
},
{
Name: "html",
Usage: "Generate HTML from Masonry files",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "input",
Usage: "Input Masonry file path",
Required: true,
Aliases: []string{"i"},
},
&cli.StringFlag{
Name: "output",
Usage: "Output directory for generated HTML files",
Value: "./output",
Aliases: []string{"o"},
},
},
Action: func(c *cli.Context) error {
inputFile := c.String("input")
outputDir := c.String("output")
return generateHTML(inputFile, outputDir)
},
},
},
Action: func(c *cli.Context) error {
fmt.Println("Generating code...")
protocArgs := []string{
"-I",
".",
"--go_out",
"gen/go",
"--go-grpc_out",
"gen/go",
"--go-grpc_opt=require_unimplemented_servers=false",
"--gorm_out",
"gen/go",
"--grpc-gateway_out",
"gen/go",
"--grpc-gateway_opt",
"logtostderr=true",
"--openapiv2_out",
"gen/openapi",
"--openapiv2_opt",
"logtostderr=true",
"--proto_path=./proto_include",
"proto/*.proto",
}
// generate go code
cmd := exec.Command(
"protoc",
protocArgs...,
)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
var buff bytes.Buffer
cmd.Stderr = &buff
fmt.Println(buff.String())
return fmt.Errorf("error generating go code | %w", err)
}
// Generate ts code
// if webapp folder is present, generate typescript code
if _, err := os.Stat("webapp"); err == nil {
err = os.Chdir("webapp")
if err != nil {
return fmt.Errorf("error changing directory to webapp | %w", err)
}
cmd = exec.Command("npx",
"openapi-typescript-codegen",
"--input",
"../gen/openapi/proto/service.swagger.json",
"--output",
"src/generated",
"--client",
"fetch",
)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err = cmd.Run()
if err != nil {
return fmt.Errorf("error generating typescript code | %w", err)
}
// make sure src/generated-sample-components exists
err = os.Mkdir("src/generated-sample-components", 0755)
if err != nil {
return fmt.Errorf("error creating src/generated-components directory | %w", err)
}
// generate vue components
err = vue_gen.GenVueFromSwagger("../gen/openapi/proto/service.swagger.json", "src/generated-sample-components")
if err != nil {
return fmt.Errorf("error generating vue components | %w", err)
}
cmd := exec.Command("npm", "install", "@masonitestudios/dynamic-vue")
err = cmd.Run()
if err != nil {
return fmt.Errorf("error installing @masonitestudios/dynamic-vue | %w", err)
}
err = os.Chdir("..")
if err != nil {
return fmt.Errorf("error changing directory back to root | %w", err)
}
}
return nil
// Default action - generate proto code for backward compatibility
return generateProtoCode()
},
}
}
// generateProtoCode handles the original proto code generation logic
func generateProtoCode() error {
fmt.Println("Generating code...")
protocArgs := []string{
"-I",
".",
"--go_out",
"gen/go",
"--go-grpc_out",
"gen/go",
"--go-grpc_opt=require_unimplemented_servers=false",
"--gorm_out",
"gen/go",
"--grpc-gateway_out",
"gen/go",
"--grpc-gateway_opt",
"logtostderr=true",
"--openapiv2_out",
"gen/openapi",
"--openapiv2_opt",
"logtostderr=true",
"--proto_path=./proto_include",
"proto/*.proto",
}
// generate go code
cmd := exec.Command(
"protoc",
protocArgs...,
)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
var buff bytes.Buffer
cmd.Stderr = &buff
fmt.Println(buff.String())
return fmt.Errorf("error generating go code | %w", err)
}
// Generate ts code
// if webapp folder is present, generate typescript code
if _, err := os.Stat("webapp"); err == nil {
err = os.Chdir("webapp")
if err != nil {
return fmt.Errorf("error changing directory to webapp | %w", err)
}
cmd = exec.Command("npx",
"openapi-typescript-codegen",
"--input",
"../gen/openapi/proto/service.swagger.json",
"--output",
"src/generated",
"--client",
"fetch",
)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err = cmd.Run()
if err != nil {
return fmt.Errorf("error generating typescript code | %w", err)
}
// make sure src/generated-sample-components exists
err = os.Mkdir("src/generated-sample-components", 0755)
if err != nil {
return fmt.Errorf("error creating src/generated-components directory | %w", err)
}
// generate vue components
err = vue_gen.GenVueFromSwagger("../gen/openapi/proto/service.swagger.json", "src/generated-sample-components")
if err != nil {
return fmt.Errorf("error generating vue components | %w", err)
}
cmd := exec.Command("npm", "install", "@masonitestudios/dynamic-vue")
err = cmd.Run()
if err != nil {
return fmt.Errorf("error installing @masonitestudios/dynamic-vue | %w", err)
}
err = os.Chdir("..")
if err != nil {
return fmt.Errorf("error changing directory back to root | %w", err)
}
}
return nil
}
func webappCmd() *cli.Command {
return &cli.Command{
Name: "webapp",
@ -368,3 +413,162 @@ func vueGenCmd() *cli.Command {
},
}
}
// generateHTML parses a Masonry file and generates HTML output
func generateHTML(inputFile, outputDir string) error {
fmt.Printf("Generating HTML from %s to %s\n", inputFile, outputDir)
// Read the input file
content, err := os.ReadFile(inputFile)
if err != nil {
return fmt.Errorf("failed to read input file: %w", err)
}
// Create parser
parser, err := participle.Build[lang.AST]()
if err != nil {
return fmt.Errorf("failed to create parser: %w", err)
}
// Parse the Masonry file
ast, err := parser.ParseString(inputFile, string(content))
if err != nil {
return fmt.Errorf("failed to parse Masonry file: %w", err)
}
// Create HTML interpreter
htmlInterpreter := interpreter.NewHTMLInterpreter()
// Generate HTML
htmlFiles, err := htmlInterpreter.GenerateHTML(ast)
if err != nil {
return fmt.Errorf("failed to generate HTML: %w", err)
}
// Create output directory
err = os.MkdirAll(outputDir, 0755)
if err != nil {
return fmt.Errorf("failed to create output directory: %w", err)
}
// Write HTML files
for filename, content := range htmlFiles {
outputPath := filepath.Join(outputDir, filename)
err = os.WriteFile(outputPath, []byte(content), 0644)
if err != nil {
return fmt.Errorf("failed to write file %s: %w", outputPath, err)
}
fmt.Printf("Generated: %s\n", outputPath)
}
fmt.Printf("Successfully generated %d HTML file(s)\n", len(htmlFiles))
return nil
}
func serveCmd() *cli.Command {
return &cli.Command{
Name: "serve",
Usage: "Generate and run a simple HTTP server from a Masonry file",
Description: "This command parses a Masonry file and generates a simple Go HTTP server with in-memory database.",
Category: "development",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "file",
Usage: "Path to the Masonry file to interpret",
Required: true,
Aliases: []string{"f"},
},
&cli.StringFlag{
Name: "output",
Usage: "Output file for the generated server code",
Value: "server.go",
Aliases: []string{"o"},
},
&cli.BoolFlag{
Name: "run",
Usage: "Run the server after generating it",
Value: false,
Aliases: []string{"r"},
},
},
Action: func(c *cli.Context) error {
masonryFile := c.String("file")
outputFile := c.String("output")
shouldRun := c.Bool("run")
fmt.Printf("Parsing Masonry file: %s\n", masonryFile)
// Read the Masonry file
content, err := os.ReadFile(masonryFile)
if err != nil {
return fmt.Errorf("error reading Masonry file: %w", err)
}
// Parse the Masonry file
parser, err := participle.Build[lang.AST](
participle.Unquote("String"),
)
if err != nil {
return fmt.Errorf("error building parser: %w", err)
}
ast, err := parser.ParseString("", string(content))
if err != nil {
return fmt.Errorf("error parsing Masonry file: %w", err)
}
// Generate server code using the server interpreter
serverInterpreter := interpreter.NewServerInterpreter()
serverCode, err := serverInterpreter.Interpret(*ast)
if err != nil {
return fmt.Errorf("error interpreting Masonry file: %w", err)
}
// Write the generated server code to the output file
err = os.WriteFile(outputFile, []byte(serverCode), 0644)
if err != nil {
return fmt.Errorf("error writing server code to file: %w", err)
}
fmt.Printf("Server code generated successfully: %s\n", outputFile)
if shouldRun {
fmt.Println("Installing dependencies...")
// Initialize go module if it doesn't exist
if _, err := os.Stat("go.mod"); os.IsNotExist(err) {
cmd := exec.Command("go", "mod", "init", "masonry-server")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("error initializing go module: %w", err)
}
}
// Install required dependencies
dependencies := []string{
"github.com/google/uuid",
"github.com/gorilla/mux",
}
for _, dep := range dependencies {
fmt.Printf("Installing %s...\n", dep)
cmd := exec.Command("go", "get", dep)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("error installing dependency %s: %w", dep, err)
}
}
fmt.Printf("Running server from %s...\n", outputFile)
cmd := exec.Command("go", "run", outputFile)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
return nil
},
}
}

148
examples/lang/debug.go Normal file
View File

@ -0,0 +1,148 @@
package main
import (
"fmt"
"masonry/lang"
"os"
)
func main() {
// Read the example.masonry file from the correct path
content, err := os.ReadFile("examples/lang/example.masonry")
if err != nil {
fmt.Printf("Error reading example.masonry: %v\n", err)
return
}
input := string(content)
// Try to parse the DSL
ast, err := lang.ParseInput(input)
if err != nil {
fmt.Printf("❌ Parse Error: %v\n", err)
return
}
// If we get here, parsing was successful!
fmt.Printf("🎉 Successfully parsed DSL with block delimiters!\n\n")
// Count what we parsed
var servers, entities, endpoints, pages int
for _, def := range ast.Definitions {
if def.Server != nil {
servers++
}
if def.Entity != nil {
entities++
}
if def.Endpoint != nil {
endpoints++
}
if def.Page != nil {
pages++
}
}
fmt.Printf("📊 Parsing Summary:\n")
fmt.Printf(" Servers: %d\n", servers)
fmt.Printf(" Entities: %d\n", entities)
fmt.Printf(" Endpoints: %d\n", endpoints)
fmt.Printf(" Pages: %d\n", pages)
fmt.Printf(" Total Definitions: %d\n", len(ast.Definitions))
// Verify key structures parsed correctly
fmt.Printf("\n✅ Validation Results:\n")
// Check server has settings in block
for _, def := range ast.Definitions {
if def.Server != nil {
if len(def.Server.Settings) > 0 {
fmt.Printf(" ✓ Server '%s' has %d settings (block syntax working)\n", def.Server.Name, len(def.Server.Settings))
}
break
}
}
// Check entities have fields in blocks
entityCount := 0
for _, def := range ast.Definitions {
if def.Entity != nil {
entityCount++
if len(def.Entity.Fields) > 0 {
fmt.Printf(" ✓ Entity '%s' has %d fields (block syntax working)\n", def.Entity.Name, len(def.Entity.Fields))
}
if entityCount >= 2 { // Just show first couple
break
}
}
}
// Check endpoints have params in blocks
endpointCount := 0
for _, def := range ast.Definitions {
if def.Endpoint != nil {
endpointCount++
if len(def.Endpoint.Params) > 0 {
fmt.Printf(" ✓ Endpoint '%s %s' has %d params (block syntax working)\n", def.Endpoint.Method, def.Endpoint.Path, len(def.Endpoint.Params))
}
if endpointCount >= 2 { // Just show first couple
break
}
}
}
// Check pages have content in blocks
pageCount := 0
for _, def := range ast.Definitions {
if def.Page != nil {
pageCount++
totalContent := len(def.Page.Meta) + len(def.Page.Sections) + len(def.Page.Components)
if totalContent > 0 {
fmt.Printf(" ✓ Page '%s' has %d content items (block syntax working)\n", def.Page.Name, totalContent)
}
if pageCount >= 2 { // Just show first couple
break
}
}
}
// Check for nested sections (complex structures)
var totalSections, nestedSections int
for _, def := range ast.Definitions {
if def.Page != nil {
totalSections += len(def.Page.Sections)
for _, section := range def.Page.Sections {
nestedSections += countNestedSections(section)
}
}
}
if totalSections > 0 {
fmt.Printf(" ✓ Found %d sections with %d nested levels (recursive parsing working)\n", totalSections, nestedSections)
}
fmt.Printf("\n🎯 Block delimiter syntax is working correctly!\n")
fmt.Printf(" All constructs (server, entity, endpoint, page, section, component) now use { } blocks\n")
fmt.Printf(" No more ambiguous whitespace-dependent parsing\n")
fmt.Printf(" Language is now unambiguous and consistent\n")
}
// Helper function to count nested sections recursively
func countNestedSections(section lang.Section) int {
count := 0
for _, element := range section.Elements {
if element.Section != nil {
count++
count += countNestedSections(*element.Section)
}
if element.Component != nil {
for _, compElement := range element.Component.Elements {
if compElement.Section != nil {
count++
count += countNestedSections(*compElement.Section)
}
}
}
}
return count
}

View File

@ -0,0 +1,295 @@
// Enhanced Masonry DSL example demonstrating simplified unified structure
// This shows how containers, tabs, panels, modals, and master-detail are now unified as sections
// Server configuration
server MyApp {
host "localhost"
port 8080
}
// Entity definitions with various field types and relationships
entity User desc "User account management" {
id: uuid required unique
email: string required validate email validate min_length "5"
name: string default "Anonymous"
created_at: timestamp default "now()"
profile_id: uuid relates to Profile as one via "user_id"
}
entity Profile desc "User profile information" {
id: uuid required unique
user_id: uuid required relates to User as one
bio: text validate max_length "500"
avatar_url: string validate url
updated_at: timestamp
posts: uuid relates to Post as many
}
entity Post desc "Blog posts" {
id: uuid required unique
title: string required validate min_length "1" validate max_length "200"
content: text required
author_id: uuid required relates to User as one
published: boolean default "false"
created_at: timestamp default "now()"
tags: uuid relates to Tag as many through "post_tags"
}
entity Tag desc "Content tags" {
id: uuid required unique
name: string required unique validate min_length "1" validate max_length "50"
slug: string required unique indexed
created_at: timestamp default "now()"
}
// API Endpoints with different HTTP methods and parameter sources
endpoint GET "/users" for User desc "List users" auth {
param page: int from query
param limit: int required from query
returns list as "json" fields [id, email, name]
}
endpoint POST "/users" for User desc "Create user" {
param user_data: object required from body
returns object as "json" fields [id, email, name]
}
endpoint PUT "/users/{id}" for User desc "Update user" {
param id: uuid required from path
param user_data: object required from body
returns object
custom "update_user_logic"
}
endpoint DELETE "/users/{id}" for User desc "Delete user" auth {
param id: uuid required from path
returns object
}
endpoint GET "/posts" for Post desc "List posts" {
param author_id: uuid from query
param published: boolean from query
param page: int from query
returns list as "json" fields [id, title, author_id, published]
}
endpoint POST "/posts" for Post desc "Create post" auth {
param post_data: object required from body
returns object fields [id, title, content, author_id]
}
// Enhanced User Management page with unified section layout
page UserManagement at "/admin/users" layout AdminLayout title "User Management" auth {
meta description "Manage system users"
meta keywords "users, admin, management"
section main type container class "grid grid-cols-3 gap-4" {
section sidebar class "col-span-1" {
component UserStats for User {
data from "/users/stats"
}
}
section content class "col-span-2" {
component UserTable for User {
fields [email, name, role, created_at]
actions [edit, delete, view]
data from "/users"
}
section editPanel type panel trigger "edit" position "slide-right" for User {
component UserForm for User {
field email type text label "Email" required
field name type text label "Name" required
field role type select options ["admin", "user"]
button save label "Save User" style "primary" via "/users/{id}"
button cancel label "Cancel" style "secondary"
}
}
}
}
}
// Enhanced Form component with detailed field configurations
page UserFormPage at "/admin/users/new" layout AdminLayout title "Create User" auth {
component Form for User {
field email type text label "Email Address" placeholder "Enter your email" required validate email
field name type text label "Full Name" placeholder "Enter your full name" required
field role type select label "User Role" options ["admin", "user", "moderator"] default "user"
field avatar type file label "Profile Picture" accept "image/*"
field bio type textarea label "Biography" placeholder "Tell us about yourself" rows 4
when role equals "admin" {
component AdminPermissions {
field permissions type multiselect label "Permissions" {
options ["users.manage", "posts.manage", "system.config"]
}
}
}
section actions {
component ActionButtons {
button save label "Save User" style "primary" loading "Saving..." via "/users"
button cancel label "Cancel" style "secondary"
}
}
}
}
// Dashboard with tabbed interface using unified sections
page Dashboard at "/dashboard" layout MainLayout title "Dashboard" {
section tabs type container {
section overview type tab label "Overview" active {
component StatsCards
component RecentActivity
}
section users type tab label "Users" {
component UserTable for User {
data from "/users"
}
}
section posts type tab label "Posts" {
component PostTable for Post {
data from "/posts"
}
}
}
section createUserModal type modal trigger "create-user" {
component UserForm for User {
field email type text label "Email" required
field name type text label "Name" required
button save label "Create" via "/users"
button cancel label "Cancel"
}
}
}
// Post Management with master-detail layout using unified sections
page PostManagement at "/admin/posts" layout AdminLayout title "Post Management" auth {
section master type master {
component PostTable for Post {
field title type text label "Title" sortable
field author type relation label "Author" display "name" relates to User
field status type badge label "Status"
}
}
section detail type detail trigger "edit" {
component PostForm for Post {
section basic class "mb-4" {
component BasicFields {
field title type text label "Post Title" required
field content type richtext label "Content" required
}
}
section metadata class "grid grid-cols-2 gap-4" {
component MetadataFields {
field author_id type autocomplete label "Author" {
source "/users" display "name" value "id"
}
field published type toggle label "Published" default "false"
field tags type multiselect label "Tags" {
source "/tags" display "name" value "id"
}
}
}
}
}
}
// Simple table component with smart defaults
page SimpleUserList at "/users" layout MainLayout title "Users" {
component SimpleTable for User {
fields [email, name, created_at]
actions [edit, delete]
data from "/users"
}
}
// Detailed table with simplified component attributes
page DetailedUserList at "/admin/users/detailed" layout AdminLayout title "Detailed User Management" auth {
component DetailedTable for User {
data from "/users"
pagination size 20
field email type text label "Email Address"
field name type text label "Full Name"
}
}
// Complex nested sections example
page ComplexLayout at "/complex" layout MainLayout title "Complex Layout" {
section mainContainer type container class "flex h-screen" {
section sidebar type container class "w-64 bg-gray-100" {
section navigation {
component NavMenu
}
section userInfo type panel trigger "profile" position "bottom" {
component UserProfile
}
}
section content type container class "flex-1" {
section header class "h-16 border-b" {
component PageHeader
}
section body class "flex-1 p-4" {
section tabs type container {
section overview type tab label "Overview" active {
section metrics class "grid grid-cols-3 gap-4" {
component MetricCard
component MetricCard
component MetricCard
}
}
section details type tab label "Details" {
component DetailView
}
}
}
}
}
}
// Conditional rendering with sections and components
page ConditionalForm at "/conditional" layout MainLayout title "Conditional Form" {
component UserForm for User {
field email type text label "Email" required
field role type select options ["admin", "user", "moderator"]
when role equals "admin" {
section adminSection class "border-l-4 border-red-500 pl-4" {
component AdminPermissions {
field permissions type multiselect label "Admin Permissions" {
options ["users.manage", "posts.manage", "system.config"]
}
}
component AdminSettings {
field max_users type number label "Max Users"
}
}
}
when role equals "moderator" {
component ModeratorSettings {
field moderation_level type select label "Moderation Level" {
options ["basic", "advanced", "full"]
}
}
}
section actions {
component ActionButtons {
button save label "Save User" style "primary" loading "Saving..."
button cancel label "Cancel" style "secondary"
}
}
}
}

View File

@ -0,0 +1,61 @@
// Sample Masonry application demonstrating the language features
entity User {
name: string required
email: string required unique
age: number
role: string default "user"
password: string required
}
page UserDashboard at "/dashboard" layout main title "User Dashboard" {
meta description "User management dashboard"
meta keywords "user, dashboard, management"
section header type container class "header" {
component nav {
field logo type text value "MyApp"
button profile label "Profile"
button logout label "Logout"
}
}
section content type tab {
section users label "Users" active {
component table for User {
field name type text label "Full Name" sortable searchable
field email type email label "Email Address" sortable
field role type select options ["admin", "user", "moderator"]
button edit label "Edit"
button delete label "Delete"
}
}
section settings label "Settings" {
component form for User {
field name type text label "Full Name" required placeholder "Enter your name"
field email type email label "Email" required placeholder "Enter your email"
field age type number label "Age" placeholder "Enter your age"
field role type select label "Role" options ["admin", "user", "moderator"] default "user"
field password type password label "Password" required
when role equals "admin" {
field permissions type select label "Admin Permissions" options ["read", "write", "delete"]
}
button save label "Save Changes"
button cancel label "Cancel"
}
}
section profile label "Profile" {
component form {
field avatar type file label "Profile Picture" accept ".jpg,.png"
field bio type textarea label "Biography" rows 4 placeholder "Tell us about yourself"
field theme type select label "Theme" options ["light", "dark"] default "light"
field notifications type checkbox label "Enable Notifications" default "true"
button update label "Update Profile"
}
}
}
}

1
go.mod
View File

@ -3,6 +3,7 @@ module masonry
go 1.23
require (
github.com/alecthomas/participle/v2 v2.1.4
github.com/urfave/cli/v2 v2.27.5
golang.org/x/text v0.22.0
)

8
go.sum
View File

@ -1,5 +1,13 @@
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/participle/v2 v2.1.4 h1:W/H79S8Sat/krZ3el6sQMvMaahJ+XcM9WSI2naI7w2U=
github.com/alecthomas/participle/v2 v2.1.4/go.mod h1:8tqVbpTX20Ru4NfYQgZf4mP18eXPTBViyMWiArNEgGI=
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/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
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/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/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=

View File

@ -0,0 +1,707 @@
package interpreter
import (
"fmt"
"masonry/lang"
"strings"
)
// HTMLInterpreter converts Masonry AST to HTML/JavaScript
type HTMLInterpreter struct {
entities map[string]*lang.Entity
pages map[string]*lang.Page
server *lang.Server
}
// NewHTMLInterpreter creates a new HTML interpreter
func NewHTMLInterpreter() *HTMLInterpreter {
return &HTMLInterpreter{
entities: make(map[string]*lang.Entity),
pages: make(map[string]*lang.Page),
}
}
// cleanString removes surrounding quotes from string literals
func (hi *HTMLInterpreter) cleanString(s string) string {
if len(s) >= 2 && ((s[0] == '"' && s[len(s)-1] == '"') || (s[0] == '\'' && s[len(s)-1] == '\'')) {
return s[1 : len(s)-1]
}
return s
}
// escapeHTML escapes HTML special characters
func (hi *HTMLInterpreter) escapeHTML(s string) string {
s = strings.ReplaceAll(s, "&", "&")
s = strings.ReplaceAll(s, "<", "&lt;")
s = strings.ReplaceAll(s, ">", "&gt;")
s = strings.ReplaceAll(s, "\"", "&quot;")
s = strings.ReplaceAll(s, "'", "&#39;")
return s
}
// GenerateHTML converts a Masonry AST to HTML output
func (hi *HTMLInterpreter) GenerateHTML(ast *lang.AST) (map[string]string, error) {
// First pass: collect entities, pages, and server config
for _, def := range ast.Definitions {
if def.Entity != nil {
hi.entities[def.Entity.Name] = def.Entity
}
if def.Page != nil {
hi.pages[def.Page.Name] = def.Page
}
if def.Server != nil {
hi.server = def.Server
}
}
// Second pass: generate HTML for each page
htmlFiles := make(map[string]string)
for pageName, page := range hi.pages {
html, err := hi.generatePageHTML(page)
if err != nil {
return nil, fmt.Errorf("error generating HTML for page %s: %w", pageName, err)
}
htmlFiles[fmt.Sprintf("%s.html", strings.ToLower(pageName))] = html
}
return htmlFiles, nil
}
// generatePageHTML creates HTML for a single page
func (hi *HTMLInterpreter) generatePageHTML(page *lang.Page) (string, error) {
var html strings.Builder
// HTML document structure
html.WriteString("<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n")
html.WriteString(" <meta charset=\"UTF-8\">\n")
html.WriteString(" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n")
// Page title
title := page.Name
if page.Title != nil {
title = hi.cleanString(*page.Title)
}
html.WriteString(fmt.Sprintf(" <title>%s</title>\n", hi.escapeHTML(title)))
// Meta tags
for _, meta := range page.Meta {
cleanName := hi.cleanString(meta.Name)
cleanContent := hi.cleanString(meta.Content)
html.WriteString(fmt.Sprintf(" <meta name=\"%s\" content=\"%s\">\n",
hi.escapeHTML(cleanName), hi.escapeHTML(cleanContent)))
}
// Basic CSS for styling
html.WriteString(" <style>\n")
html.WriteString(" body { font-family: Arial, sans-serif; margin: 0; padding: 20px; }\n")
html.WriteString(" .container { max-width: 1200px; margin: 0 auto; }\n")
html.WriteString(" .section { margin: 20px 0; padding: 20px; border: 1px solid #ddd; border-radius: 8px; }\n")
html.WriteString(" .tabs { border-bottom: 2px solid #ddd; margin-bottom: 20px; }\n")
html.WriteString(" .tab-button { padding: 10px 20px; border: none; background: none; cursor: pointer; }\n")
html.WriteString(" .tab-button.active { background: #007bff; color: white; }\n")
html.WriteString(" .tab-content { display: none; }\n")
html.WriteString(" .tab-content.active { display: block; }\n")
html.WriteString(" .form-group { margin: 15px 0; }\n")
html.WriteString(" .form-group label { display: block; margin-bottom: 5px; font-weight: bold; }\n")
html.WriteString(" .form-group input, .form-group textarea, .form-group select { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; }\n")
html.WriteString(" .button { padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; margin: 5px; }\n")
html.WriteString(" .button-primary { background: #007bff; color: white; }\n")
html.WriteString(" .button-secondary { background: #6c757d; color: white; }\n")
html.WriteString(" .modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); }\n")
html.WriteString(" .modal-content { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px; border-radius: 8px; min-width: 400px; }\n")
html.WriteString(" </style>\n")
html.WriteString("</head>\n<body>\n")
// Page content
html.WriteString(" <div class=\"container\">\n")
html.WriteString(fmt.Sprintf(" <h1>%s</h1>\n", hi.escapeHTML(title)))
// Generate sections
for _, section := range page.Sections {
sectionHTML, err := hi.generateSectionHTML(&section, 2)
if err != nil {
return "", err
}
html.WriteString(sectionHTML)
}
// Generate direct components
for _, component := range page.Components {
componentHTML, err := hi.generateComponentHTML(&component, 2)
if err != nil {
return "", err
}
html.WriteString(componentHTML)
}
html.WriteString(" </div>\n")
// JavaScript for interactivity
html.WriteString(" <script>\n")
// API Base URL configuration
apiBaseURL := "http://localhost:8080"
if hi.server != nil {
host := "localhost"
port := 8080
for _, setting := range hi.server.Settings {
if setting.Host != nil {
host = *setting.Host
}
if setting.Port != nil {
port = *setting.Port
}
}
apiBaseURL = fmt.Sprintf("http://%s:%d", host, port)
}
html.WriteString(fmt.Sprintf(" const API_BASE_URL = '%s';\n", apiBaseURL))
html.WriteString(" \n")
html.WriteString(" // API helper functions\n")
html.WriteString(" async function apiRequest(method, endpoint, data = null) {\n")
html.WriteString(" const config = {\n")
html.WriteString(" method: method,\n")
html.WriteString(" headers: {\n")
html.WriteString(" 'Content-Type': 'application/json',\n")
html.WriteString(" },\n")
html.WriteString(" };\n")
html.WriteString(" \n")
html.WriteString(" if (data) {\n")
html.WriteString(" config.body = JSON.stringify(data);\n")
html.WriteString(" }\n")
html.WriteString(" \n")
html.WriteString(" try {\n")
html.WriteString(" const response = await fetch(API_BASE_URL + endpoint, config);\n")
html.WriteString(" \n")
html.WriteString(" if (!response.ok) {\n")
html.WriteString(" throw new Error(`HTTP error! status: ${response.status}`);\n")
html.WriteString(" }\n")
html.WriteString(" \n")
html.WriteString(" if (response.status === 204) {\n")
html.WriteString(" return null; // No content\n")
html.WriteString(" }\n")
html.WriteString(" \n")
html.WriteString(" return await response.json();\n")
html.WriteString(" } catch (error) {\n")
html.WriteString(" console.error('API request failed:', error);\n")
html.WriteString(" alert('Error: ' + error.message);\n")
html.WriteString(" throw error;\n")
html.WriteString(" }\n")
html.WriteString(" }\n")
html.WriteString(" \n")
html.WriteString(" // Entity-specific API functions\n")
// Generate API functions for each entity
for entityName := range hi.entities {
entityLower := strings.ToLower(entityName)
entityPlural := entityLower + "s"
html.WriteString(fmt.Sprintf(" async function list%s() {\n", entityName))
html.WriteString(fmt.Sprintf(" return await apiRequest('GET', '/%s');\n", entityPlural))
html.WriteString(" }\n")
html.WriteString(" \n")
html.WriteString(fmt.Sprintf(" async function get%s(id) {\n", entityName))
html.WriteString(fmt.Sprintf(" return await apiRequest('GET', '/%s/' + id);\n", entityPlural))
html.WriteString(" }\n")
html.WriteString(" \n")
html.WriteString(fmt.Sprintf(" async function create%s(data) {\n", entityName))
html.WriteString(fmt.Sprintf(" return await apiRequest('POST', '/%s', data);\n", entityPlural))
html.WriteString(" }\n")
html.WriteString(" \n")
html.WriteString(fmt.Sprintf(" async function update%s(id, data) {\n", entityName))
html.WriteString(fmt.Sprintf(" return await apiRequest('PUT', '/%s/' + id, data);\n", entityPlural))
html.WriteString(" }\n")
html.WriteString(" \n")
html.WriteString(fmt.Sprintf(" async function delete%s(id) {\n", entityName))
html.WriteString(fmt.Sprintf(" return await apiRequest('DELETE', '/%s/' + id);\n", entityPlural))
html.WriteString(" }\n")
html.WriteString(" \n")
}
html.WriteString(" // Tab functionality\n")
html.WriteString(" function showTab(tabName) {\n")
html.WriteString(" const tabs = document.querySelectorAll('.tab-content');\n")
html.WriteString(" tabs.forEach(tab => tab.classList.remove('active'));\n")
html.WriteString(" const buttons = document.querySelectorAll('.tab-button');\n")
html.WriteString(" buttons.forEach(btn => btn.classList.remove('active'));\n")
html.WriteString(" document.getElementById(tabName).classList.add('active');\n")
html.WriteString(" event.target.classList.add('active');\n")
html.WriteString(" }\n")
html.WriteString(" \n")
html.WriteString(" // Modal functionality\n")
html.WriteString(" function showModal(modalId) {\n")
html.WriteString(" document.getElementById(modalId).style.display = 'block';\n")
html.WriteString(" }\n")
html.WriteString(" function hideModal(modalId) {\n")
html.WriteString(" document.getElementById(modalId).style.display = 'none';\n")
html.WriteString(" }\n")
html.WriteString(" \n")
html.WriteString(" // Enhanced form submission with API integration\n")
html.WriteString(" async function submitForm(formId, entityType = null) {\n")
html.WriteString(" const form = document.getElementById(formId);\n")
html.WriteString(" const formData = new FormData(form);\n")
html.WriteString(" const data = Object.fromEntries(formData);\n")
html.WriteString(" \n")
html.WriteString(" console.log('Form data:', data);\n")
html.WriteString(" \n")
html.WriteString(" if (entityType) {\n")
html.WriteString(" try {\n")
html.WriteString(" const result = await window['create' + entityType](data);\n")
html.WriteString(" console.log('Created:', result);\n")
html.WriteString(" alert(entityType + ' created successfully!');\n")
html.WriteString(" form.reset();\n")
html.WriteString(" // Refresh any tables showing this entity type\n")
html.WriteString(" await refreshTables(entityType);\n")
html.WriteString(" } catch (error) {\n")
html.WriteString(" console.error('Form submission failed:', error);\n")
html.WriteString(" }\n")
html.WriteString(" } else {\n")
html.WriteString(" alert('Form submitted successfully!');\n")
html.WriteString(" }\n")
html.WriteString(" }\n")
html.WriteString(" \n")
html.WriteString(" // Load data into tables\n")
html.WriteString(" async function loadTableData(tableId, entityType) {\n")
html.WriteString(" try {\n")
html.WriteString(" const data = await window['list' + entityType]();\n")
html.WriteString(" const table = document.getElementById(tableId);\n")
html.WriteString(" const tbody = table.querySelector('tbody');\n")
html.WriteString(" \n")
html.WriteString(" if (!data || data.length === 0) {\n")
html.WriteString(" tbody.innerHTML = '<tr><td colspan=\"100%\">No data found</td></tr>';\n")
html.WriteString(" return;\n")
html.WriteString(" }\n")
html.WriteString(" \n")
html.WriteString(" tbody.innerHTML = '';\n")
html.WriteString(" data.forEach(item => {\n")
html.WriteString(" const row = document.createElement('tr');\n")
html.WriteString(" const headers = table.querySelectorAll('thead th');\n")
html.WriteString(" \n")
html.WriteString(" headers.forEach((header, index) => {\n")
html.WriteString(" const cell = document.createElement('td');\n")
html.WriteString(" const fieldName = header.textContent.toLowerCase().replace(/\\s+/g, '_');\n")
html.WriteString(" cell.textContent = item[fieldName] || item[header.textContent.toLowerCase()] || '';\n")
html.WriteString(" row.appendChild(cell);\n")
html.WriteString(" });\n")
html.WriteString(" \n")
html.WriteString(" // Add action buttons\n")
html.WriteString(" const actionCell = document.createElement('td');\n")
html.WriteString(" actionCell.innerHTML = `\n")
html.WriteString(" <button onclick=\"editItem('${item.id}', '${entityType}')\" class=\"button button-primary\">Edit</button>\n")
html.WriteString(" <button onclick=\"deleteItem('${item.id}', '${entityType}')\" class=\"button button-secondary\">Delete</button>\n")
html.WriteString(" `;\n")
html.WriteString(" row.appendChild(actionCell);\n")
html.WriteString(" \n")
html.WriteString(" tbody.appendChild(row);\n")
html.WriteString(" });\n")
html.WriteString(" } catch (error) {\n")
html.WriteString(" console.error('Failed to load table data:', error);\n")
html.WriteString(" }\n")
html.WriteString(" }\n")
html.WriteString(" \n")
html.WriteString(" // Refresh all tables of a specific entity type\n")
html.WriteString(" async function refreshTables(entityType) {\n")
html.WriteString(" const tables = document.querySelectorAll(`table[data-entity=\"${entityType}\"]`);\n")
html.WriteString(" tables.forEach(table => {\n")
html.WriteString(" loadTableData(table.id, entityType);\n")
html.WriteString(" });\n")
html.WriteString(" }\n")
html.WriteString(" \n")
html.WriteString(" // Edit and delete functions\n")
html.WriteString(" async function editItem(id, entityType) {\n")
html.WriteString(" try {\n")
html.WriteString(" const item = await window['get' + entityType](id);\n")
html.WriteString(" console.log('Edit item:', item);\n")
html.WriteString(" // TODO: Populate form with item data for editing\n")
html.WriteString(" alert('Edit functionality coming soon!');\n")
html.WriteString(" } catch (error) {\n")
html.WriteString(" console.error('Failed to fetch item for editing:', error);\n")
html.WriteString(" }\n")
html.WriteString(" }\n")
html.WriteString(" \n")
html.WriteString(" async function deleteItem(id, entityType) {\n")
html.WriteString(" if (confirm('Are you sure you want to delete this item?')) {\n")
html.WriteString(" try {\n")
html.WriteString(" await window['delete' + entityType](id);\n")
html.WriteString(" alert('Item deleted successfully!');\n")
html.WriteString(" await refreshTables(entityType);\n")
html.WriteString(" } catch (error) {\n")
html.WriteString(" console.error('Failed to delete item:', error);\n")
html.WriteString(" }\n")
html.WriteString(" }\n")
html.WriteString(" }\n")
html.WriteString(" \n")
html.WriteString(" // Initialize page - load data when page loads\n")
html.WriteString(" document.addEventListener('DOMContentLoaded', function() {\n")
html.WriteString(" // Auto-load data for all tables with data-entity attribute\n")
html.WriteString(" const tables = document.querySelectorAll('table[data-entity]');\n")
html.WriteString(" tables.forEach(table => {\n")
html.WriteString(" const entityType = table.getAttribute('data-entity');\n")
html.WriteString(" loadTableData(table.id, entityType);\n")
html.WriteString(" });\n")
html.WriteString(" });\n")
html.WriteString(" </script>\n")
html.WriteString("</body>\n</html>")
return html.String(), nil
}
// generateSectionHTML creates HTML for a section
func (hi *HTMLInterpreter) generateSectionHTML(section *lang.Section, indent int) (string, error) {
var html strings.Builder
indentStr := strings.Repeat(" ", indent)
sectionType := "container"
if section.Type != nil {
sectionType = *section.Type
}
switch sectionType {
case "tab":
html.WriteString(fmt.Sprintf("%s<div class=\"tabs\" id=\"%s\">\n", indentStr, section.Name))
// Generate tab buttons
for _, element := range section.Elements {
if element.Section != nil && element.Section.Label != nil {
activeClass := ""
if element.Section.Active {
activeClass = " active"
}
cleanLabel := hi.cleanString(*element.Section.Label)
html.WriteString(fmt.Sprintf("%s <button class=\"tab-button%s\" onclick=\"showTab('%s')\">%s</button>\n",
indentStr, activeClass, element.Section.Name, hi.escapeHTML(cleanLabel)))
}
}
// Generate tab content
for _, element := range section.Elements {
if element.Section != nil {
activeClass := ""
if element.Section.Active {
activeClass = " active"
}
html.WriteString(fmt.Sprintf("%s <div class=\"tab-content%s\" id=\"%s\">\n",
indentStr, activeClass, element.Section.Name))
sectionHTML, err := hi.generateSectionHTML(element.Section, indent+2)
if err != nil {
return "", err
}
html.WriteString(sectionHTML)
html.WriteString(fmt.Sprintf("%s </div>\n", indentStr))
}
}
html.WriteString(fmt.Sprintf("%s</div>\n", indentStr))
case "modal":
html.WriteString(fmt.Sprintf("%s<div class=\"modal\" id=\"%s\">\n", indentStr, section.Name))
html.WriteString(fmt.Sprintf("%s <div class=\"modal-content\">\n", indentStr))
if section.Label != nil {
cleanLabel := hi.cleanString(*section.Label)
html.WriteString(fmt.Sprintf("%s <h3>%s</h3>\n", indentStr, hi.escapeHTML(cleanLabel)))
}
// Generate modal content
for _, element := range section.Elements {
elementHTML, err := hi.generateSectionElementHTML(&element, indent+2)
if err != nil {
return "", err
}
html.WriteString(elementHTML)
}
html.WriteString(fmt.Sprintf("%s <button class=\"button button-secondary\" onclick=\"hideModal('%s')\">Close</button>\n", indentStr, section.Name))
html.WriteString(fmt.Sprintf("%s </div>\n", indentStr))
html.WriteString(fmt.Sprintf("%s</div>\n", indentStr))
default: // container, panel, master, detail
cssClass := "section"
if section.Class != nil {
cssClass = hi.cleanString(*section.Class)
}
html.WriteString(fmt.Sprintf("%s<div class=\"%s\" id=\"%s\">\n", indentStr, hi.escapeHTML(cssClass), section.Name))
if section.Label != nil {
cleanLabel := hi.cleanString(*section.Label)
html.WriteString(fmt.Sprintf("%s <h3>%s</h3>\n", indentStr, hi.escapeHTML(cleanLabel)))
}
// Generate section content
for _, element := range section.Elements {
elementHTML, err := hi.generateSectionElementHTML(&element, indent+1)
if err != nil {
return "", err
}
html.WriteString(elementHTML)
}
html.WriteString(fmt.Sprintf("%s</div>\n", indentStr))
}
return html.String(), nil
}
// generateSectionElementHTML creates HTML for section elements
func (hi *HTMLInterpreter) generateSectionElementHTML(element *lang.SectionElement, indent int) (string, error) {
if element.Component != nil {
return hi.generateComponentHTML(element.Component, indent)
}
if element.Section != nil {
return hi.generateSectionHTML(element.Section, indent)
}
if element.When != nil {
return hi.generateWhenConditionHTML(element.When, indent)
}
return "", nil
}
// generateComponentHTML creates HTML for a component
func (hi *HTMLInterpreter) generateComponentHTML(component *lang.Component, indent int) (string, error) {
var html strings.Builder
indentStr := strings.Repeat(" ", indent)
switch component.Type {
case "form":
formId := fmt.Sprintf("form_%s", component.Type)
if component.Entity != nil {
formId = fmt.Sprintf("form_%s", *component.Entity)
}
html.WriteString(fmt.Sprintf("%s<form id=\"%s\" onsubmit=\"event.preventDefault(); submitForm('%s');\">\n", indentStr, formId, formId))
// Generate form fields
for _, element := range component.Elements {
if element.Field != nil {
fieldHTML, err := hi.generateFieldHTML(element.Field, indent+1)
if err != nil {
return "", err
}
html.WriteString(fieldHTML)
}
if element.Button != nil {
buttonHTML, err := hi.generateButtonHTML(element.Button, indent+1)
if err != nil {
return "", err
}
html.WriteString(buttonHTML)
}
}
html.WriteString(fmt.Sprintf("%s <button type=\"submit\" class=\"button button-primary\">Submit</button>\n", indentStr))
html.WriteString(fmt.Sprintf("%s</form>\n", indentStr))
case "table":
html.WriteString(fmt.Sprintf("%s<table class=\"table\">\n", indentStr))
html.WriteString(fmt.Sprintf("%s <thead><tr>\n", indentStr))
// Generate table headers from fields
for _, element := range component.Elements {
if element.Field != nil {
label := element.Field.Name
for _, attr := range element.Field.Attributes {
if attr.Label != nil {
label = hi.cleanString(*attr.Label)
break
}
}
html.WriteString(fmt.Sprintf("%s <th>%s</th>\n", indentStr, hi.escapeHTML(label)))
}
}
html.WriteString(fmt.Sprintf("%s </tr></thead>\n", indentStr))
html.WriteString(fmt.Sprintf("%s <tbody>\n", indentStr))
html.WriteString(fmt.Sprintf("%s <tr><td colspan=\"100%%\">Data will be loaded here...</td></tr>\n", indentStr))
html.WriteString(fmt.Sprintf("%s </tbody>\n", indentStr))
html.WriteString(fmt.Sprintf("%s</table>\n", indentStr))
default:
html.WriteString(fmt.Sprintf("%s<div class=\"component-%s\">\n", indentStr, component.Type))
// Generate component content
for _, element := range component.Elements {
if element.Field != nil {
fieldHTML, err := hi.generateFieldHTML(element.Field, indent+1)
if err != nil {
return "", err
}
html.WriteString(fieldHTML)
}
if element.Button != nil {
buttonHTML, err := hi.generateButtonHTML(element.Button, indent+1)
if err != nil {
return "", err
}
html.WriteString(buttonHTML)
}
}
html.WriteString(fmt.Sprintf("%s</div>\n", indentStr))
}
return html.String(), nil
}
// generateFieldHTML creates HTML for a field
func (hi *HTMLInterpreter) generateFieldHTML(field *lang.ComponentField, indent int) (string, error) {
var html strings.Builder
indentStr := strings.Repeat(" ", indent)
// Get field attributes
var label, placeholder, defaultValue string
var required bool
var options []string
label = field.Name
for _, attr := range field.Attributes {
if attr.Label != nil {
label = hi.cleanString(*attr.Label)
}
if attr.Placeholder != nil {
placeholder = hi.cleanString(*attr.Placeholder)
}
if attr.Default != nil {
defaultValue = hi.cleanString(*attr.Default)
}
if attr.Required {
required = true
}
if attr.Options != nil {
// Clean each option in the array
options = make([]string, len(attr.Options))
for i, opt := range attr.Options {
options[i] = hi.cleanString(opt)
}
}
}
html.WriteString(fmt.Sprintf("%s<div class=\"form-group\">\n", indentStr))
requiredAttr := ""
if required {
requiredAttr = " required"
}
html.WriteString(fmt.Sprintf("%s <label for=\"%s\">%s</label>\n", indentStr, field.Name, hi.escapeHTML(label)))
switch field.Type {
case "text", "email", "password", "number", "tel", "url":
html.WriteString(fmt.Sprintf("%s <input type=\"%s\" id=\"%s\" name=\"%s\" placeholder=\"%s\" value=\"%s\"%s>\n",
indentStr, field.Type, field.Name, field.Name, hi.escapeHTML(placeholder), hi.escapeHTML(defaultValue), requiredAttr))
case "textarea":
html.WriteString(fmt.Sprintf("%s <textarea id=\"%s\" name=\"%s\" placeholder=\"%s\"%s>%s</textarea>\n",
indentStr, field.Name, field.Name, hi.escapeHTML(placeholder), requiredAttr, hi.escapeHTML(defaultValue)))
case "select":
html.WriteString(fmt.Sprintf("%s <select id=\"%s\" name=\"%s\"%s>\n", indentStr, field.Name, field.Name, requiredAttr))
if !required {
html.WriteString(fmt.Sprintf("%s <option value=\"\">Select an option</option>\n", indentStr))
}
for _, option := range options {
selected := ""
if option == defaultValue {
selected = " selected"
}
html.WriteString(fmt.Sprintf("%s <option value=\"%s\"%s>%s</option>\n",
indentStr, hi.escapeHTML(option), selected, hi.escapeHTML(option)))
}
html.WriteString(fmt.Sprintf("%s </select>\n", indentStr))
case "checkbox":
checked := ""
if defaultValue == "true" {
checked = " checked"
}
html.WriteString(fmt.Sprintf("%s <input type=\"checkbox\" id=\"%s\" name=\"%s\" value=\"true\"%s%s>\n",
indentStr, field.Name, field.Name, checked, requiredAttr))
case "file":
html.WriteString(fmt.Sprintf("%s <input type=\"file\" id=\"%s\" name=\"%s\"%s>\n",
indentStr, field.Name, field.Name, requiredAttr))
default:
html.WriteString(fmt.Sprintf("%s <input type=\"text\" id=\"%s\" name=\"%s\" placeholder=\"%s\" value=\"%s\"%s>\n",
indentStr, field.Type, field.Name, field.Name, hi.escapeHTML(placeholder), hi.escapeHTML(defaultValue), requiredAttr))
}
html.WriteString(fmt.Sprintf("%s</div>\n", indentStr))
return html.String(), nil
}
// generateButtonHTML creates HTML for a button
func (hi *HTMLInterpreter) generateButtonHTML(button *lang.ComponentButton, indent int) (string, error) {
var html strings.Builder
indentStr := strings.Repeat(" ", indent)
buttonClass := "button button-primary"
onclick := ""
// Process button attributes
for _, attr := range button.Attributes {
if attr.Style != nil {
// Handle button style
}
if attr.Target != nil {
// Handle button target/onclick
}
}
cleanLabel := hi.cleanString(button.Label)
html.WriteString(fmt.Sprintf("%s<button type=\"button\" class=\"%s\" onclick=\"%s\">%s</button>\n",
indentStr, buttonClass, onclick, hi.escapeHTML(cleanLabel)))
return html.String(), nil
}
// generateWhenConditionHTML creates HTML for conditional content
func (hi *HTMLInterpreter) generateWhenConditionHTML(when *lang.WhenCondition, indent int) (string, error) {
var html strings.Builder
indentStr := strings.Repeat(" ", indent)
// For now, we'll render the content as visible (real implementation would include JavaScript logic)
html.WriteString(fmt.Sprintf("%s<div class=\"conditional-content\" data-when-field=\"%s\" data-when-operator=\"%s\" data-when-value=\"%s\">\n",
indentStr, when.Field, when.Operator, when.Value))
// Generate conditional content
for _, field := range when.Fields {
fieldHTML, err := hi.generateFieldHTML(&field, indent+1)
if err != nil {
return "", err
}
html.WriteString(fieldHTML)
}
for _, section := range when.Sections {
sectionHTML, err := hi.generateSectionHTML(&section, indent+1)
if err != nil {
return "", err
}
html.WriteString(sectionHTML)
}
for _, component := range when.Components {
componentHTML, err := hi.generateComponentHTML(&component, indent+1)
if err != nil {
return "", err
}
html.WriteString(componentHTML)
}
for _, button := range when.Buttons {
buttonHTML, err := hi.generateButtonHTML(&button, indent+1)
if err != nil {
return "", err
}
html.WriteString(buttonHTML)
}
html.WriteString(fmt.Sprintf("%s</div>\n", indentStr))
return html.String(), nil
}

View File

@ -0,0 +1,386 @@
package interpreter
import (
"fmt"
"go/format"
"masonry/lang"
"strings"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
// ServerInterpreter converts Masonry AST to a simple Go HTTP server
type ServerInterpreter struct {
entities map[string]*lang.Entity
endpoints map[string]*lang.Endpoint
server *lang.Server
}
// NewServerInterpreter creates a new server interpreter
func NewServerInterpreter() *ServerInterpreter {
return &ServerInterpreter{
entities: make(map[string]*lang.Entity),
endpoints: make(map[string]*lang.Endpoint),
}
}
// Interpret processes the AST and generates server code
func (si *ServerInterpreter) Interpret(ast lang.AST) (string, error) {
// First pass: collect all definitions
for _, def := range ast.Definitions {
if def.Server != nil {
si.server = def.Server
}
if def.Entity != nil {
si.entities[def.Entity.Name] = def.Entity
}
if def.Endpoint != nil {
key := fmt.Sprintf("%s_%s", def.Endpoint.Method, strings.ReplaceAll(def.Endpoint.Path, "/", "_"))
si.endpoints[key] = def.Endpoint
}
}
// Generate the server code
rawCode, err := si.generateServer()
if err != nil {
return "", err
}
// Format the code to remove unused imports and fix formatting
formattedCode, err := format.Source([]byte(rawCode))
if err != nil {
// If formatting fails, return the raw code with a warning comment
return "// Warning: Code formatting failed, but code should still be functional\n" + rawCode, nil
}
return string(formattedCode), nil
}
// generateServer creates the complete server code
func (si *ServerInterpreter) generateServer() (string, error) {
var code strings.Builder
// Package and imports - only include what's actually used
code.WriteString(`package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"sync"
"time"
"github.com/google/uuid"
"github.com/gorilla/mux"
)
`)
// Generate entity structs
code.WriteString("// Entity definitions\n")
for _, entity := range si.entities {
code.WriteString(si.generateEntityStruct(entity))
code.WriteString("\n")
}
// Generate in-memory database
code.WriteString(si.generateInMemoryDB())
// Generate handlers
code.WriteString("// HTTP Handlers\n")
for _, entity := range si.entities {
code.WriteString(si.generateEntityHandlers(entity))
}
// Generate custom endpoint handlers
for _, endpoint := range si.endpoints {
if endpoint.Entity == nil {
code.WriteString(si.generateCustomEndpointHandler(endpoint))
}
}
// Generate main function
code.WriteString(si.generateMainFunction())
return code.String(), nil
}
// generateEntityStruct creates a Go struct for an entity
func (si *ServerInterpreter) generateEntityStruct(entity *lang.Entity) string {
var code strings.Builder
if entity.Description != nil {
code.WriteString(fmt.Sprintf("// %s - %s\n", entity.Name, *entity.Description))
}
code.WriteString(fmt.Sprintf("type %s struct {\n", entity.Name))
// Always add ID field
code.WriteString("\tID string `json:\"id\"`\n")
code.WriteString("\tCreatedAt time.Time `json:\"created_at\"`\n")
code.WriteString("\tUpdatedAt time.Time `json:\"updated_at\"`\n")
for _, field := range entity.Fields {
goType := si.convertToGoType(field.Type)
jsonTag := strings.ToLower(field.Name)
if !field.Required {
goType = "*" + goType
}
code.WriteString(fmt.Sprintf("\t%s %s `json:\"%s\"`\n",
cases.Title(language.English).String(field.Name), goType, jsonTag))
}
code.WriteString("}\n")
return code.String()
}
// convertToGoType maps Masonry types to Go types
func (si *ServerInterpreter) convertToGoType(masonryType string) string {
switch masonryType {
case "string", "text", "email", "url":
return "string"
case "int", "number":
return "int"
case "float":
return "float64"
case "boolean":
return "bool"
case "uuid":
return "string"
case "timestamp":
return "time.Time"
default:
return "string" // default fallback
}
}
// generateInMemoryDB creates the in-memory database structure
func (si *ServerInterpreter) generateInMemoryDB() string {
var code strings.Builder
code.WriteString("// In-memory database\n")
code.WriteString("type InMemoryDB struct {\n")
code.WriteString("\tmu sync.RWMutex\n")
for entityName := range si.entities {
code.WriteString(fmt.Sprintf("\t%s map[string]*%s\n",
strings.ToLower(entityName)+"s", entityName))
}
code.WriteString("}\n\n")
code.WriteString("var db = &InMemoryDB{\n")
for entityName := range si.entities {
code.WriteString(fmt.Sprintf("\t%s: make(map[string]*%s),\n",
strings.ToLower(entityName)+"s", entityName))
}
code.WriteString("}\n\n")
return code.String()
}
// generateEntityHandlers creates CRUD handlers for an entity
func (si *ServerInterpreter) generateEntityHandlers(entity *lang.Entity) string {
var code strings.Builder
entityName := entity.Name
entityLower := strings.ToLower(entityName)
entityPlural := entityLower + "s"
// List handler
code.WriteString(fmt.Sprintf(`func list%sHandler(w http.ResponseWriter, r *http.Request) {
db.mu.RLock()
defer db.mu.RUnlock()
var items []*%s
for _, item := range db.%s {
items = append(items, item)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(items)
}
`, entityName, entityName, entityPlural))
// Get by ID handler
code.WriteString(fmt.Sprintf(`func get%sHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
db.mu.RLock()
item, exists := db.%s[id]
db.mu.RUnlock()
if !exists {
http.Error(w, "%s not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(item)
}
`, entityName, entityPlural, entityName))
// Create handler
code.WriteString(fmt.Sprintf(`func create%sHandler(w http.ResponseWriter, r *http.Request) {
var item %s
if err := json.NewDecoder(r.Body).Decode(&item); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
item.ID = uuid.New().String()
item.CreatedAt = time.Now()
item.UpdatedAt = time.Now()
db.mu.Lock()
db.%s[item.ID] = &item
db.mu.Unlock()
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(item)
}
`, entityName, entityName, entityPlural))
// Update handler
code.WriteString(fmt.Sprintf(`func update%sHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
db.mu.Lock()
defer db.mu.Unlock()
existing, exists := db.%s[id]
if !exists {
http.Error(w, "%s not found", http.StatusNotFound)
return
}
var updates %s
if err := json.NewDecoder(r.Body).Decode(&updates); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
// Preserve ID and timestamps
updates.ID = existing.ID
updates.CreatedAt = existing.CreatedAt
updates.UpdatedAt = time.Now()
db.%s[id] = &updates
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(updates)
}
`, entityName, entityPlural, entityName, entityName, entityPlural))
// Delete handler
code.WriteString(fmt.Sprintf(`func delete%sHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
db.mu.Lock()
defer db.mu.Unlock()
if _, exists := db.%s[id]; !exists {
http.Error(w, "%s not found", http.StatusNotFound)
return
}
delete(db.%s, id)
w.WriteHeader(http.StatusNoContent)
}
`, entityName, entityPlural, entityName, entityPlural))
return code.String()
}
// generateCustomEndpointHandler creates handlers for custom endpoints
func (si *ServerInterpreter) generateCustomEndpointHandler(endpoint *lang.Endpoint) string {
var code strings.Builder
handlerName := fmt.Sprintf("%s%sHandler",
strings.ToLower(endpoint.Method),
strings.ReplaceAll(cases.Title(language.English).String(endpoint.Path), "/", ""))
code.WriteString(fmt.Sprintf(`func %s(w http.ResponseWriter, r *http.Request) {
// Custom endpoint: %s %s
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"message": "Custom endpoint %s %s",
"status": "success",
})
}
`, handlerName, endpoint.Method, endpoint.Path, endpoint.Method, endpoint.Path))
return code.String()
}
// generateMainFunction creates the main function with routing
func (si *ServerInterpreter) generateMainFunction() string {
var code strings.Builder
code.WriteString("func main() {\n")
code.WriteString("\tr := mux.NewRouter()\n\n")
// Add routes for each entity
for entityName := range si.entities {
entityLower := strings.ToLower(entityName)
entityPlural := entityLower + "s"
code.WriteString(fmt.Sprintf("\t// %s routes\n", entityName))
code.WriteString(fmt.Sprintf("\tr.HandleFunc(\"/%s\", list%sHandler).Methods(\"GET\")\n",
entityPlural, entityName))
code.WriteString(fmt.Sprintf("\tr.HandleFunc(\"/%s/{id}\", get%sHandler).Methods(\"GET\")\n",
entityPlural, entityName))
code.WriteString(fmt.Sprintf("\tr.HandleFunc(\"/%s\", create%sHandler).Methods(\"POST\")\n",
entityPlural, entityName))
code.WriteString(fmt.Sprintf("\tr.HandleFunc(\"/%s/{id}\", update%sHandler).Methods(\"PUT\")\n",
entityPlural, entityName))
code.WriteString(fmt.Sprintf("\tr.HandleFunc(\"/%s/{id}\", delete%sHandler).Methods(\"DELETE\")\n",
entityPlural, entityName))
code.WriteString("\n")
}
// Add custom endpoint routes
for _, endpoint := range si.endpoints {
if endpoint.Entity == nil {
handlerName := fmt.Sprintf("%s%sHandler",
strings.ToLower(endpoint.Method),
strings.ReplaceAll(cases.Title(language.English).String(endpoint.Path), "/", ""))
code.WriteString(fmt.Sprintf("\tr.HandleFunc(\"%s\", %s).Methods(\"%s\")\n",
endpoint.Path, handlerName, endpoint.Method))
}
}
// Server configuration
host := "localhost"
port := 8080
if si.server != nil {
for _, setting := range si.server.Settings {
if setting.Host != nil {
host = *setting.Host
}
if setting.Port != nil {
port = *setting.Port
}
}
}
code.WriteString(fmt.Sprintf("\n\taddr := \"%s:%d\"\n", host, port))
code.WriteString("\tfmt.Printf(\"Server starting on %s\\n\", addr)\n")
code.WriteString("\tlog.Fatal(http.ListenAndServe(addr, r))\n")
code.WriteString("}\n")
return code.String()
}

281
lang/lang.go Normal file
View File

@ -0,0 +1,281 @@
package lang
import (
"github.com/alecthomas/participle/v2"
)
// Root AST node containing all definitions
type AST struct {
Definitions []Definition `parser:"@@*"`
}
// Union type for top-level definitions
type Definition struct {
Server *Server `parser:"@@"`
Entity *Entity `parser:"| @@"`
Endpoint *Endpoint `parser:"| @@"`
Page *Page `parser:"| @@"`
}
// Clean server syntax
type Server struct {
Name string `parser:"'server' @Ident"`
Settings []ServerSetting `parser:"('{' @@* '}')?"` // Block-delimited settings for consistency
}
type ServerSetting struct {
Host *string `parser:"('host' @String)"`
Port *int `parser:"| ('port' @Int)"`
}
// Clean entity syntax with better readability
type Entity struct {
Name string `parser:"'entity' @Ident"`
Description *string `parser:"('desc' @String)?"`
Fields []Field `parser:"('{' @@* '}')?"` // Block-delimited fields for consistency
}
// Much cleaner field syntax
type Field struct {
Name string `parser:"@Ident ':'"`
Type string `parser:"@Ident"`
Required bool `parser:"@'required'?"`
Unique bool `parser:"@'unique'?"`
Index bool `parser:"@'indexed'?"`
Default *string `parser:"('default' @String)?"`
Validations []Validation `parser:"@@*"`
Relationship *Relationship `parser:"@@?"`
}
// Simple validation syntax
type Validation struct {
Type string `parser:"'validate' @Ident"`
Value *string `parser:"@String?"`
}
// Clear relationship syntax
type Relationship struct {
Type string `parser:"'relates' 'to' @Ident"`
Cardinality string `parser:"'as' @('one' | 'many')"`
ForeignKey *string `parser:"('via' @String)?"`
Through *string `parser:"('through' @String)?"`
}
// Endpoint definitions with clean, readable syntax
type Endpoint struct {
Method string `parser:"'endpoint' @('GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH')"`
Path string `parser:"@String"`
Entity *string `parser:"('for' @Ident)?"`
Description *string `parser:"('desc' @String)?"`
Auth bool `parser:"@'auth'?"`
Params []EndpointParam `parser:"('{' @@*"` // Block-delimited parameters
Response *ResponseSpec `parser:"@@?"`
CustomLogic *string `parser:"('custom' @String)? '}')?"` // Close block after all content
}
// Clean parameter syntax
type EndpointParam struct {
Name string `parser:"'param' @Ident ':'"`
Type string `parser:"@Ident"`
Required bool `parser:"@'required'?"`
Source string `parser:"'from' @('path' | 'query' | 'body')"`
}
// Response specification
type ResponseSpec struct {
Type string `parser:"'returns' @Ident"`
Format *string `parser:"('as' @String)?"`
Fields []string `parser:"('fields' '[' @Ident (',' @Ident)* ']')?"`
}
// Enhanced Page definitions with unified section model
type Page struct {
Name string `parser:"'page' @Ident"`
Path string `parser:"'at' @String"`
Layout string `parser:"'layout' @Ident"`
Title *string `parser:"('title' @String)?"`
Description *string `parser:"('desc' @String)?"`
Auth bool `parser:"@'auth'?"`
Meta []MetaTag `parser:"('{' @@*"` // Block-delimited content
Sections []Section `parser:"@@*"` // Unified sections replace containers/tabs/panels/modals
Components []Component `parser:"@@* '}')?"` // Direct components within the block
}
// Meta tags for SEO
type MetaTag struct {
Name string `parser:"'meta' @Ident"`
Content string `parser:"@String"`
}
// Unified Section type that replaces Container, Tab, Panel, Modal, MasterDetail
type Section struct {
Name string `parser:"'section' @Ident"`
Type *string `parser:"('type' @('container' | 'tab' | 'panel' | 'modal' | 'master' | 'detail'))?"`
Class *string `parser:"('class' @String)?"`
Label *string `parser:"('label' @String)?"` // for tabs
Active bool `parser:"@'active'?"` // for tabs
Trigger *string `parser:"('trigger' @String)?"` // for panels/modals/detail
Position *string `parser:"('position' @String)?"` // for panels
Entity *string `parser:"('for' @Ident)?"` // for panels
Elements []SectionElement `parser:"('{' @@* '}')?"` // Block-delimited elements for unambiguous nesting
}
// New unified element type for sections
type SectionElement struct {
Attribute *SectionAttribute `parser:"@@"`
Component *Component `parser:"| @@"`
Section *Section `parser:"| @@"`
When *WhenCondition `parser:"| @@"`
}
// Flexible section attributes (replaces complex config types)
type SectionAttribute struct {
DataSource *string `parser:"('data' 'from' @String)"`
Style *string `parser:"| ('style' @String)"`
Classes *string `parser:"| ('classes' @String)"`
Size *int `parser:"| ('size' @Int)"` // for pagination, etc.
Theme *string `parser:"| ('theme' @String)"`
}
// Simplified Component with unified attributes - reordered for better parsing
type Component struct {
Type string `parser:"'component' @Ident"`
Entity *string `parser:"('for' @Ident)?"`
Elements []ComponentElement `parser:"('{' @@* '}')?"` // Parse everything inside the block
}
// Enhanced ComponentElement with recursive section support - now includes attributes
type ComponentElement struct {
Attribute *ComponentAttr `parser:"@@"` // Component attributes can be inside the block
Field *ComponentField `parser:"| @@"`
Section *Section `parser:"| @@"` // Sections can be nested in components
Button *ComponentButton `parser:"| @@"`
When *WhenCondition `parser:"| @@"`
}
// Simplified component attributes using key-value pattern - reordered for precedence
type ComponentAttr struct {
DataSource *string `parser:"('data' 'from' @String)"`
Fields []string `parser:"| ('fields' '[' @Ident (',' @Ident)* ']')"`
Actions []string `parser:"| ('actions' '[' @Ident (',' @Ident)* ']')"`
Style *string `parser:"| ('style' @String)"`
Classes *string `parser:"| ('classes' @String)"`
PageSize *int `parser:"| ('pagination' 'size' @Int)"`
Validate bool `parser:"| @'validate'"`
}
// Enhanced component field with detailed configuration using flexible attributes
type ComponentField struct {
Name string `parser:"'field' @Ident"`
Type string `parser:"'type' @Ident"`
Attributes []ComponentFieldAttribute `parser:"@@* ('{' @@* '}')?"` // Support both inline and block attributes
}
// Flexible field attribute system
type ComponentFieldAttribute struct {
Label *string `parser:"('label' @String)"`
Placeholder *string `parser:"| ('placeholder' @String)"`
Required bool `parser:"| @'required'"`
Sortable bool `parser:"| @'sortable'"`
Searchable bool `parser:"| @'searchable'"`
Thumbnail bool `parser:"| @'thumbnail'"`
Default *string `parser:"| ('default' @String)"`
Options []string `parser:"| ('options' '[' @String (',' @String)* ']')"`
Accept *string `parser:"| ('accept' @String)"`
Rows *int `parser:"| ('rows' @Int)"`
Format *string `parser:"| ('format' @String)"`
Size *string `parser:"| ('size' @String)"`
Display *string `parser:"| ('display' @String)"`
Value *string `parser:"| ('value' @String)"`
Source *string `parser:"| ('source' @String)"`
Relates *FieldRelation `parser:"| @@"`
Validation *ComponentValidation `parser:"| @@"`
}
// Field relationship for autocomplete and select fields
type FieldRelation struct {
Type string `parser:"'relates' 'to' @Ident"`
}
// Component validation
type ComponentValidation struct {
Type string `parser:"'validate' @Ident"`
Value *string `parser:"@String?"`
}
// Enhanced WhenCondition with recursive support for both sections and components
type WhenCondition struct {
Field string `parser:"'when' @Ident"`
Operator string `parser:"@('equals' | 'not_equals' | 'contains')"`
Value string `parser:"@String"`
Fields []ComponentField `parser:"('{' @@*"`
Sections []Section `parser:"@@*"` // Can contain sections
Components []Component `parser:"@@*"` // Can contain components
Buttons []ComponentButton `parser:"@@* '}')?"` // Block-delimited for unambiguous nesting
}
// Simplified button with flexible attribute ordering
type ComponentButton struct {
Name string `parser:"'button' @Ident"`
Label string `parser:"'label' @String"`
Attributes []ComponentButtonAttr `parser:"@@*"`
}
// Flexible button attribute system - each attribute is a separate alternative
type ComponentButtonAttr struct {
Style *ComponentButtonStyle `parser:"@@"`
Icon *ComponentButtonIcon `parser:"| @@"`
Loading *ComponentButtonLoading `parser:"| @@"`
Disabled *ComponentButtonDisabled `parser:"| @@"`
Confirm *ComponentButtonConfirm `parser:"| @@"`
Target *ComponentButtonTarget `parser:"| @@"`
Position *ComponentButtonPosition `parser:"| @@"`
Via *ComponentButtonVia `parser:"| @@"`
}
// Individual button attribute types
type ComponentButtonStyle struct {
Value string `parser:"'style' @String"`
}
type ComponentButtonIcon struct {
Value string `parser:"'icon' @String"`
}
type ComponentButtonLoading struct {
Value string `parser:"'loading' @String"`
}
type ComponentButtonDisabled struct {
Value string `parser:"'disabled' 'when' @Ident"`
}
type ComponentButtonConfirm struct {
Value string `parser:"'confirm' @String"`
}
type ComponentButtonTarget struct {
Value string `parser:"'target' @Ident"`
}
type ComponentButtonPosition struct {
Value string `parser:"'position' @String"`
}
type ComponentButtonVia struct {
Value string `parser:"'via' @String"`
}
func ParseInput(input string) (AST, error) {
parser, err := participle.Build[AST](
participle.Unquote("String"),
)
if err != nil {
return AST{}, err
}
ast, err := parser.ParseString("", input)
if err != nil {
return AST{}, err
}
return *ast, nil
}

3
lang/lang_test.go Normal file
View File

@ -0,0 +1,3 @@
package lang
// Various parts of the language and parser are tested in specialized files

131
lang/parser_entity_test.go Normal file
View File

@ -0,0 +1,131 @@
package lang
import (
"testing"
)
func TestParseEntityDefinitions(t *testing.T) {
tests := []struct {
name string
input string
want AST
wantErr bool
}{
{
name: "entity with enhanced fields and relationships",
input: `entity User desc "User management" {
id: uuid required unique
email: string required validate email validate min_length "5"
name: string default "Anonymous"
profile_id: uuid relates to Profile as one via "user_id"
}`,
want: AST{
Definitions: []Definition{
{
Entity: &Entity{
Name: "User",
Description: stringPtr("User management"),
Fields: []Field{
{
Name: "id",
Type: "uuid",
Required: true,
Unique: true,
},
{
Name: "email",
Type: "string",
Required: true,
Validations: []Validation{
{Type: "email"},
{Type: "min_length", Value: stringPtr("5")},
},
},
{
Name: "name",
Type: "string",
Default: stringPtr("Anonymous"),
},
{
Name: "profile_id",
Type: "uuid",
Relationship: &Relationship{
Type: "Profile",
Cardinality: "one",
ForeignKey: stringPtr("user_id"),
},
},
},
},
},
},
},
wantErr: false,
},
{
name: "simple entity with basic fields",
input: `entity Product {
id: uuid required unique
name: string required
price: decimal default "0.00"
}`,
want: AST{
Definitions: []Definition{
{
Entity: &Entity{
Name: "Product",
Fields: []Field{
{
Name: "id",
Type: "uuid",
Required: true,
Unique: true,
},
{
Name: "name",
Type: "string",
Required: true,
},
{
Name: "price",
Type: "decimal",
Default: stringPtr("0.00"),
},
},
},
},
},
},
wantErr: false,
},
{
name: "entity without fields block",
input: `entity SimpleEntity`,
want: AST{
Definitions: []Definition{
{
Entity: &Entity{
Name: "SimpleEntity",
Fields: []Field{},
},
},
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseInput(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("ParseInput() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr && !astEqual(got, tt.want) {
t.Errorf("ParseInput() mismatch.\nGot: %+v\nWant: %+v", got, tt.want)
}
})
}
}

103
lang/parser_server_test.go Normal file
View File

@ -0,0 +1,103 @@
package lang
import (
"testing"
)
func TestParseServerDefinitions(t *testing.T) {
tests := []struct {
name string
input string
want AST
wantErr bool
}{
{
name: "simple server definition with block delimiters",
input: `server MyApp {
host "localhost"
port 8080
}`,
want: AST{
Definitions: []Definition{
{
Server: &Server{
Name: "MyApp",
Settings: []ServerSetting{
{Host: stringPtr("localhost")},
{Port: intPtr(8080)},
},
},
},
},
},
wantErr: false,
},
{
name: "server with only host setting",
input: `server WebApp {
host "0.0.0.0"
}`,
want: AST{
Definitions: []Definition{
{
Server: &Server{
Name: "WebApp",
Settings: []ServerSetting{
{Host: stringPtr("0.0.0.0")},
},
},
},
},
},
wantErr: false,
},
{
name: "server with only port setting",
input: `server APIServer {
port 3000
}`,
want: AST{
Definitions: []Definition{
{
Server: &Server{
Name: "APIServer",
Settings: []ServerSetting{
{Port: intPtr(3000)},
},
},
},
},
},
wantErr: false,
},
{
name: "server without settings block",
input: `server SimpleServer`,
want: AST{
Definitions: []Definition{
{
Server: &Server{
Name: "SimpleServer",
Settings: []ServerSetting{},
},
},
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseInput(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("ParseInput() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr && !astEqual(got, tt.want) {
t.Errorf("ParseInput() mismatch.\nGot: %+v\nWant: %+v", got, tt.want)
}
})
}
}

View File

@ -0,0 +1,575 @@
package lang
import (
"testing"
)
func TestParseAdvancedUIFeatures(t *testing.T) {
tests := []struct {
name string
input string
want AST
wantErr bool
}{
{
name: "complex conditional rendering with multiple operators",
input: `page Test at "/test" layout main {
component form for User {
field status type select options ["active", "inactive", "pending"]
when status equals "active" {
field last_login type datetime
field permissions type multiselect
button deactivate label "Deactivate User" style "warning"
}
when status not_equals "active" {
field reason type textarea placeholder "Reason for status"
button activate label "Activate User" style "success"
}
when status contains "pending" {
field approval_date type date
button approve label "Approve" style "primary"
button reject label "Reject" style "danger"
}
}
}`,
want: AST{
Definitions: []Definition{
{
Page: &Page{
Name: "Test",
Path: "/test",
Layout: "main",
Components: []Component{
{
Type: "form",
Entity: stringPtr("User"),
Elements: []ComponentElement{
{
Field: &ComponentField{
Name: "status",
Type: "select",
Attributes: []ComponentFieldAttribute{
{Options: []string{"active", "inactive", "pending"}},
},
},
},
{
When: &WhenCondition{
Field: "status",
Operator: "equals",
Value: "active",
Fields: []ComponentField{
{Name: "last_login", Type: "datetime"},
{Name: "permissions", Type: "multiselect"},
},
Buttons: []ComponentButton{
{
Name: "deactivate",
Label: "Deactivate User",
Attributes: []ComponentButtonAttr{
{Style: &ComponentButtonStyle{Value: "warning"}},
},
},
},
},
},
{
When: &WhenCondition{
Field: "status",
Operator: "not_equals",
Value: "active",
Fields: []ComponentField{
{
Name: "reason",
Type: "textarea",
Attributes: []ComponentFieldAttribute{
{Placeholder: stringPtr("Reason for status")},
},
},
},
Buttons: []ComponentButton{
{
Name: "activate",
Label: "Activate User",
Attributes: []ComponentButtonAttr{
{Style: &ComponentButtonStyle{Value: "success"}},
},
},
},
},
},
{
When: &WhenCondition{
Field: "status",
Operator: "contains",
Value: "pending",
Fields: []ComponentField{
{Name: "approval_date", Type: "date"},
},
Buttons: []ComponentButton{
{
Name: "approve",
Label: "Approve",
Attributes: []ComponentButtonAttr{
{Style: &ComponentButtonStyle{Value: "primary"}},
},
},
{
Name: "reject",
Label: "Reject",
Attributes: []ComponentButtonAttr{
{Style: &ComponentButtonStyle{Value: "danger"}},
},
},
},
},
},
},
},
},
},
},
},
},
},
{
name: "field attributes with all possible options",
input: `page Test at "/test" layout main {
component form for Product {
field name type text {
label "Product Name"
placeholder "Enter product name"
required
default "New Product"
validate min_length "3"
size "large"
display "block"
}
field price type number {
label "Price ($)"
format "currency"
validate min "0"
validate max "10000"
}
field category type autocomplete {
label "Category"
placeholder "Start typing..."
relates to Category
searchable
source "categories/search"
}
field tags type multiselect {
label "Tags"
options ["electronics", "clothing", "books", "home"]
source "tags/popular"
}
field description type richtext {
label "Description"
rows 10
placeholder "Describe your product..."
}
field thumbnail type image {
label "Product Image"
accept "image/jpeg,image/png"
thumbnail
}
field featured type checkbox {
label "Featured Product"
default "false"
value "true"
}
field availability type select {
label "Availability"
options ["in_stock", "out_of_stock", "pre_order"]
default "in_stock"
sortable
}
}
}`,
want: AST{
Definitions: []Definition{
{
Page: &Page{
Name: "Test",
Path: "/test",
Layout: "main",
Components: []Component{
{
Type: "form",
Entity: stringPtr("Product"),
Elements: []ComponentElement{
{
Field: &ComponentField{
Name: "name",
Type: "text",
Attributes: []ComponentFieldAttribute{
{Label: stringPtr("Product Name")},
{Placeholder: stringPtr("Enter product name")},
{Required: true},
{Default: stringPtr("New Product")},
{Validation: &ComponentValidation{Type: "min_length", Value: stringPtr("3")}},
{Size: stringPtr("large")},
{Display: stringPtr("block")},
},
},
},
{
Field: &ComponentField{
Name: "price",
Type: "number",
Attributes: []ComponentFieldAttribute{
{Label: stringPtr("Price ($)")},
{Format: stringPtr("currency")},
{Validation: &ComponentValidation{Type: "min", Value: stringPtr("0")}},
{Validation: &ComponentValidation{Type: "max", Value: stringPtr("10000")}},
},
},
},
{
Field: &ComponentField{
Name: "category",
Type: "autocomplete",
Attributes: []ComponentFieldAttribute{
{Label: stringPtr("Category")},
{Placeholder: stringPtr("Start typing...")},
{Relates: &FieldRelation{Type: "Category"}},
{Searchable: true},
{Source: stringPtr("categories/search")},
},
},
},
{
Field: &ComponentField{
Name: "tags",
Type: "multiselect",
Attributes: []ComponentFieldAttribute{
{Label: stringPtr("Tags")},
{Options: []string{"electronics", "clothing", "books", "home"}},
{Source: stringPtr("tags/popular")},
},
},
},
{
Field: &ComponentField{
Name: "description",
Type: "richtext",
Attributes: []ComponentFieldAttribute{
{Label: stringPtr("Description")},
{Rows: intPtr(10)},
{Placeholder: stringPtr("Describe your product...")},
},
},
},
{
Field: &ComponentField{
Name: "thumbnail",
Type: "image",
Attributes: []ComponentFieldAttribute{
{Label: stringPtr("Product Image")},
{Accept: stringPtr("image/jpeg,image/png")},
{Thumbnail: true},
},
},
},
{
Field: &ComponentField{
Name: "featured",
Type: "checkbox",
Attributes: []ComponentFieldAttribute{
{Label: stringPtr("Featured Product")},
{Default: stringPtr("false")},
{Value: stringPtr("true")},
},
},
},
{
Field: &ComponentField{
Name: "availability",
Type: "select",
Attributes: []ComponentFieldAttribute{
{Label: stringPtr("Availability")},
{Options: []string{"in_stock", "out_of_stock", "pre_order"}},
{Default: stringPtr("in_stock")},
{Sortable: true},
},
},
},
},
},
},
},
},
},
},
},
{
name: "complex button configurations",
input: `page Test at "/test" layout main {
component form for Order {
field status type select options ["draft", "submitted", "approved"]
button save label "Save Draft" style "secondary" icon "save" position "left"
button submit label "Submit Order" style "primary" icon "send" loading "Submitting..." confirm "Submit this order?"
button approve label "Approve" style "success" loading "Approving..." disabled when status confirm "Approve this order?" target approval_modal via "api/orders/approve"
button reject label "Reject" style "danger" icon "x" confirm "Are you sure you want to reject this order?"
button print label "Print" style "outline" icon "printer" position "right"
}
}`,
want: AST{
Definitions: []Definition{
{
Page: &Page{
Name: "Test",
Path: "/test",
Layout: "main",
Components: []Component{
{
Type: "form",
Entity: stringPtr("Order"),
Elements: []ComponentElement{
{
Field: &ComponentField{
Name: "status",
Type: "select",
Attributes: []ComponentFieldAttribute{
{Options: []string{"draft", "submitted", "approved"}},
},
},
},
{
Button: &ComponentButton{
Name: "save",
Label: "Save Draft",
Attributes: []ComponentButtonAttr{
{Style: &ComponentButtonStyle{Value: "secondary"}},
{Icon: &ComponentButtonIcon{Value: "save"}},
{Position: &ComponentButtonPosition{Value: "left"}},
},
},
},
{
Button: &ComponentButton{
Name: "submit",
Label: "Submit Order",
Attributes: []ComponentButtonAttr{
{Style: &ComponentButtonStyle{Value: "primary"}},
{Icon: &ComponentButtonIcon{Value: "send"}},
{Loading: &ComponentButtonLoading{Value: "Submitting..."}},
{Confirm: &ComponentButtonConfirm{Value: "Submit this order?"}},
},
},
},
{
Button: &ComponentButton{
Name: "approve",
Label: "Approve",
Attributes: []ComponentButtonAttr{
{Style: &ComponentButtonStyle{Value: "success"}},
{Loading: &ComponentButtonLoading{Value: "Approving..."}},
{Disabled: &ComponentButtonDisabled{Value: "status"}},
{Confirm: &ComponentButtonConfirm{Value: "Approve this order?"}},
{Target: &ComponentButtonTarget{Value: "approval_modal"}},
{Via: &ComponentButtonVia{Value: "api/orders/approve"}},
},
},
},
{
Button: &ComponentButton{
Name: "reject",
Label: "Reject",
Attributes: []ComponentButtonAttr{
{Style: &ComponentButtonStyle{Value: "danger"}},
{Icon: &ComponentButtonIcon{Value: "x"}},
{Confirm: &ComponentButtonConfirm{Value: "Are you sure you want to reject this order?"}},
},
},
},
{
Button: &ComponentButton{
Name: "print",
Label: "Print",
Attributes: []ComponentButtonAttr{
{Style: &ComponentButtonStyle{Value: "outline"}},
{Icon: &ComponentButtonIcon{Value: "printer"}},
{Position: &ComponentButtonPosition{Value: "right"}},
},
},
},
},
},
},
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseInput(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("ParseInput() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !astEqual(got, tt.want) {
t.Errorf("ParseInput() got = %v, want = %v", got, tt.want)
}
})
}
}
func TestParseFieldValidationTypes(t *testing.T) {
validationTypes := []struct {
validation string
hasValue bool
}{
{"email", false},
{"required", false},
{"min_length", true},
{"max_length", true},
{"min", true},
{"max", true},
{"pattern", true},
{"numeric", false},
{"alpha", false},
{"alphanumeric", false},
{"url", false},
{"date", false},
{"datetime", false},
{"time", false},
{"phone", false},
{"postal_code", false},
{"credit_card", false},
}
for _, vt := range validationTypes {
t.Run("validation_"+vt.validation, func(t *testing.T) {
var input string
if vt.hasValue {
input = `page Test at "/test" layout main {
component form {
field test_field type text validate ` + vt.validation + ` "test_value"
}
}`
} else {
input = `page Test at "/test" layout main {
component form {
field test_field type text validate ` + vt.validation + `
}
}`
}
got, err := ParseInput(input)
if err != nil {
t.Errorf("ParseInput() failed for validation %s: %v", vt.validation, err)
return
}
if len(got.Definitions) != 1 || got.Definitions[0].Page == nil {
t.Errorf("ParseInput() failed to parse page for validation %s", vt.validation)
return
}
page := got.Definitions[0].Page
if len(page.Components) != 1 || len(page.Components[0].Elements) != 1 {
t.Errorf("ParseInput() failed to parse component for validation %s", vt.validation)
return
}
element := page.Components[0].Elements[0]
if element.Field == nil || len(element.Field.Attributes) != 1 {
t.Errorf("ParseInput() failed to parse field attributes for validation %s", vt.validation)
return
}
attr := element.Field.Attributes[0]
if attr.Validation == nil || attr.Validation.Type != vt.validation {
t.Errorf("ParseInput() validation type mismatch: got %v, want %s", attr.Validation, vt.validation)
}
if vt.hasValue && (attr.Validation.Value == nil || *attr.Validation.Value != "test_value") {
t.Errorf("ParseInput() validation value mismatch for %s", vt.validation)
}
})
}
}
func TestParseConditionalOperators(t *testing.T) {
operators := []string{"equals", "not_equals", "contains"}
for _, op := range operators {
t.Run("operator_"+op, func(t *testing.T) {
input := `page Test at "/test" layout main {
component form {
field test_field type text
when test_field ` + op + ` "test_value" {
field conditional_field type text
}
}
}`
got, err := ParseInput(input)
if err != nil {
t.Errorf("ParseInput() failed for operator %s: %v", op, err)
return
}
// Verify the when condition was parsed correctly
page := got.Definitions[0].Page
component := page.Components[0]
whenElement := component.Elements[1].When
if whenElement == nil || whenElement.Operator != op {
t.Errorf("ParseInput() operator mismatch: got %v, want %s", whenElement, op)
}
})
}
}
func TestParseAdvancedUIErrors(t *testing.T) {
tests := []struct {
name string
input string
}{
{
name: "invalid conditional operator",
input: `page Test at "/test" layout main {
component form {
when field invalid_operator "value" {
field test type text
}
}
}`,
},
{
name: "missing field attribute block closure",
input: `page Test at "/test" layout main {
component form {
field test type text {
label "Test"
required
}
}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := ParseInput(tt.input)
if err == nil {
t.Errorf("ParseInput() expected error for invalid syntax, got nil")
}
})
}
}

View File

@ -0,0 +1,548 @@
package lang
import (
"testing"
)
func TestParseComponentDefinitions(t *testing.T) {
tests := []struct {
name string
input string
want AST
wantErr bool
}{
{
name: "basic component with entity",
input: `page Test at "/test" layout main {
component table for User
}`,
want: AST{
Definitions: []Definition{
{
Page: &Page{
Name: "Test",
Path: "/test",
Layout: "main",
Components: []Component{
{
Type: "table",
Entity: stringPtr("User"),
},
},
},
},
},
},
},
{
name: "form component with fields",
input: `page Test at "/test" layout main {
component form for User {
field name type text label "Full Name" placeholder "Enter your name" required
field email type email label "Email Address" required
field bio type textarea rows 5 placeholder "Tell us about yourself"
field avatar type file accept "image/*"
field role type select options ["admin", "user", "guest"] default "user"
}
}`,
want: AST{
Definitions: []Definition{
{
Page: &Page{
Name: "Test",
Path: "/test",
Layout: "main",
Components: []Component{
{
Type: "form",
Entity: stringPtr("User"),
Elements: []ComponentElement{
{
Field: &ComponentField{
Name: "name",
Type: "text",
Attributes: []ComponentFieldAttribute{
{Label: stringPtr("Full Name")},
{Placeholder: stringPtr("Enter your name")},
{Required: true},
},
},
},
{
Field: &ComponentField{
Name: "email",
Type: "email",
Attributes: []ComponentFieldAttribute{
{Label: stringPtr("Email Address")},
{Required: true},
},
},
},
{
Field: &ComponentField{
Name: "bio",
Type: "textarea",
Attributes: []ComponentFieldAttribute{
{Rows: intPtr(5)},
{Placeholder: stringPtr("Tell us about yourself")},
},
},
},
{
Field: &ComponentField{
Name: "avatar",
Type: "file",
Attributes: []ComponentFieldAttribute{
{Accept: stringPtr("image/*")},
},
},
},
{
Field: &ComponentField{
Name: "role",
Type: "select",
Attributes: []ComponentFieldAttribute{
{Options: []string{"admin", "user", "guest"}},
{Default: stringPtr("user")},
},
},
},
},
},
},
},
},
},
},
},
{
name: "component with field attributes and validation",
input: `page Test at "/test" layout main {
component form for Product {
field name type text required validate min_length "3"
field price type number format "currency" validate min "0"
field category type autocomplete relates to Category
field tags type multiselect source "tags/popular"
field description type richtext
field featured type checkbox default "false"
field thumbnail type image thumbnail
}
}`,
want: AST{
Definitions: []Definition{
{
Page: &Page{
Name: "Test",
Path: "/test",
Layout: "main",
Components: []Component{
{
Type: "form",
Entity: stringPtr("Product"),
Elements: []ComponentElement{
{
Field: &ComponentField{
Name: "name",
Type: "text",
Attributes: []ComponentFieldAttribute{
{Required: true},
{Validation: &ComponentValidation{Type: "min_length", Value: stringPtr("3")}},
},
},
},
{
Field: &ComponentField{
Name: "price",
Type: "number",
Attributes: []ComponentFieldAttribute{
{Format: stringPtr("currency")},
{Validation: &ComponentValidation{Type: "min", Value: stringPtr("0")}},
},
},
},
{
Field: &ComponentField{
Name: "category",
Type: "autocomplete",
Attributes: []ComponentFieldAttribute{
{Relates: &FieldRelation{Type: "Category"}},
},
},
},
{
Field: &ComponentField{
Name: "tags",
Type: "multiselect",
Attributes: []ComponentFieldAttribute{
{Source: stringPtr("tags/popular")},
},
},
},
{
Field: &ComponentField{
Name: "description",
Type: "richtext",
},
},
{
Field: &ComponentField{
Name: "featured",
Type: "checkbox",
Attributes: []ComponentFieldAttribute{
{Default: stringPtr("false")},
},
},
},
{
Field: &ComponentField{
Name: "thumbnail",
Type: "image",
Attributes: []ComponentFieldAttribute{
{Thumbnail: true},
},
},
},
},
},
},
},
},
},
},
},
{
name: "component with buttons",
input: `page Test at "/test" layout main {
component form for User {
field name type text
button save label "Save User" style "primary" icon "save"
button cancel label "Cancel" style "secondary"
button delete label "Delete" style "danger" confirm "Are you sure?" disabled when is_protected
}
}`,
want: AST{
Definitions: []Definition{
{
Page: &Page{
Name: "Test",
Path: "/test",
Layout: "main",
Components: []Component{
{
Type: "form",
Entity: stringPtr("User"),
Elements: []ComponentElement{
{
Field: &ComponentField{
Name: "name",
Type: "text",
},
},
{
Button: &ComponentButton{
Name: "save",
Label: "Save User",
Attributes: []ComponentButtonAttr{
{Style: &ComponentButtonStyle{Value: "primary"}},
{Icon: &ComponentButtonIcon{Value: "save"}},
},
},
},
{
Button: &ComponentButton{
Name: "cancel",
Label: "Cancel",
Attributes: []ComponentButtonAttr{
{Style: &ComponentButtonStyle{Value: "secondary"}},
},
},
},
{
Button: &ComponentButton{
Name: "delete",
Label: "Delete",
Attributes: []ComponentButtonAttr{
{Style: &ComponentButtonStyle{Value: "danger"}},
{Confirm: &ComponentButtonConfirm{Value: "Are you sure?"}},
{Disabled: &ComponentButtonDisabled{Value: "is_protected"}},
},
},
},
},
},
},
},
},
},
},
},
{
name: "component with conditional fields",
input: `page Test at "/test" layout main {
component form for User {
field account_type type select options ["personal", "business"]
when account_type equals "business" {
field company_name type text required
field tax_id type text
button verify_business label "Verify Business"
}
when account_type equals "personal" {
field date_of_birth type date
}
}
}`,
want: AST{
Definitions: []Definition{
{
Page: &Page{
Name: "Test",
Path: "/test",
Layout: "main",
Components: []Component{
{
Type: "form",
Entity: stringPtr("User"),
Elements: []ComponentElement{
{
Field: &ComponentField{
Name: "account_type",
Type: "select",
Attributes: []ComponentFieldAttribute{
{Options: []string{"personal", "business"}},
},
},
},
{
When: &WhenCondition{
Field: "account_type",
Operator: "equals",
Value: "business",
Fields: []ComponentField{
{
Name: "company_name",
Type: "text",
Attributes: []ComponentFieldAttribute{
{Required: true},
},
},
{
Name: "tax_id",
Type: "text",
},
},
Buttons: []ComponentButton{
{
Name: "verify_business",
Label: "Verify Business",
},
},
},
},
{
When: &WhenCondition{
Field: "account_type",
Operator: "equals",
Value: "personal",
Fields: []ComponentField{
{
Name: "date_of_birth",
Type: "date",
},
},
},
},
},
},
},
},
},
},
},
},
{
name: "component with nested sections",
input: `page Test at "/test" layout main {
component dashboard {
section stats type container class "stats-grid" {
component metric {
field total_users type display value "1,234"
field revenue type display format "currency" value "45,678"
}
}
section charts type container {
component chart for Analytics {
data from "analytics/monthly"
}
}
}
}`,
want: AST{
Definitions: []Definition{
{
Page: &Page{
Name: "Test",
Path: "/test",
Layout: "main",
Components: []Component{
{
Type: "dashboard",
Elements: []ComponentElement{
{
Section: &Section{
Name: "stats",
Type: stringPtr("container"),
Class: stringPtr("stats-grid"),
Elements: []SectionElement{
{
Component: &Component{
Type: "metric",
Elements: []ComponentElement{
{
Field: &ComponentField{
Name: "total_users",
Type: "display",
Attributes: []ComponentFieldAttribute{
{Value: stringPtr("1,234")},
},
},
},
{
Field: &ComponentField{
Name: "revenue",
Type: "display",
Attributes: []ComponentFieldAttribute{
{Format: stringPtr("currency")},
{Value: stringPtr("45,678")},
},
},
},
},
},
},
},
},
},
{
Section: &Section{
Name: "charts",
Type: stringPtr("container"),
Elements: []SectionElement{
{
Component: &Component{
Type: "chart",
Entity: stringPtr("Analytics"),
Elements: []ComponentElement{
{
Attribute: &ComponentAttr{
DataSource: stringPtr("analytics/monthly"),
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseInput(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("ParseInput() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !astEqual(got, tt.want) {
t.Errorf("ParseInput() got = %v, want %v", got, tt.want)
}
})
}
}
func TestParseComponentFieldTypes(t *testing.T) {
fieldTypes := []string{
"text", "email", "password", "number", "date", "datetime", "time",
"textarea", "richtext", "select", "multiselect", "checkbox", "radio",
"file", "image", "autocomplete", "range", "color", "url", "tel",
"hidden", "display", "json", "code",
}
for _, fieldType := range fieldTypes {
t.Run("field_type_"+fieldType, func(t *testing.T) {
input := `page Test at "/test" layout main {
component form {
field test_field type ` + fieldType + `
}
}`
got, err := ParseInput(input)
if err != nil {
t.Errorf("ParseInput() failed for field type %s: %v", fieldType, err)
return
}
if len(got.Definitions) != 1 || got.Definitions[0].Page == nil {
t.Errorf("ParseInput() failed to parse page for field type %s", fieldType)
return
}
page := got.Definitions[0].Page
if len(page.Components) != 1 || len(page.Components[0].Elements) != 1 {
t.Errorf("ParseInput() failed to parse component for field type %s", fieldType)
return
}
element := page.Components[0].Elements[0]
if element.Field == nil || element.Field.Type != fieldType {
t.Errorf("ParseInput() field type mismatch: got %v, want %s", element.Field, fieldType)
}
})
}
}
func TestParseComponentErrors(t *testing.T) {
tests := []struct {
name string
input string
}{
{
name: "missing component type",
input: `page Test at "/test" layout main {
component
}`,
},
{
name: "invalid field syntax",
input: `page Test at "/test" layout main {
component form {
field name
}
}`,
},
{
name: "invalid button syntax",
input: `page Test at "/test" layout main {
component form {
button
}
}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := ParseInput(tt.input)
if err == nil {
t.Errorf("ParseInput() expected error for invalid syntax, got nil")
}
})
}
}

300
lang/parser_ui_page_test.go Normal file
View File

@ -0,0 +1,300 @@
package lang
import (
"testing"
)
func TestParsePageDefinitions(t *testing.T) {
tests := []struct {
name string
input string
want AST
wantErr bool
}{
{
name: "basic page with minimal fields",
input: `page Dashboard at "/dashboard" layout main`,
want: AST{
Definitions: []Definition{
{
Page: &Page{
Name: "Dashboard",
Path: "/dashboard",
Layout: "main",
},
},
},
},
},
{
name: "page with all optional fields",
input: `page UserProfile at "/profile" layout main title "User Profile" desc "Manage user profile settings" auth`,
want: AST{
Definitions: []Definition{
{
Page: &Page{
Name: "UserProfile",
Path: "/profile",
Layout: "main",
Title: stringPtr("User Profile"),
Description: stringPtr("Manage user profile settings"),
Auth: true,
},
},
},
},
},
{
name: "page with meta tags",
input: `page HomePage at "/" layout main {
meta description "Welcome to our application"
meta keywords "app, dashboard, management"
}`,
want: AST{
Definitions: []Definition{
{
Page: &Page{
Name: "HomePage",
Path: "/",
Layout: "main",
Meta: []MetaTag{
{Name: "description", Content: "Welcome to our application"},
{Name: "keywords", Content: "app, dashboard, management"},
},
},
},
},
},
},
{
name: "page with nested sections",
input: `page Settings at "/settings" layout main {
section tabs type tab {
section profile label "Profile" active {
component form for User {
field name type text
}
}
section security label "Security" {
component form for User {
field password type password
}
}
}
}`,
want: AST{
Definitions: []Definition{
{
Page: &Page{
Name: "Settings",
Path: "/settings",
Layout: "main",
Sections: []Section{
{
Name: "tabs",
Type: stringPtr("tab"),
Elements: []SectionElement{
{
Section: &Section{
Name: "profile",
Label: stringPtr("Profile"),
Active: true,
Elements: []SectionElement{
{
Component: &Component{
Type: "form",
Entity: stringPtr("User"),
Elements: []ComponentElement{
{
Field: &ComponentField{
Name: "name",
Type: "text",
},
},
},
},
},
},
},
},
{
Section: &Section{
Name: "security",
Label: stringPtr("Security"),
Elements: []SectionElement{
{
Component: &Component{
Type: "form",
Entity: stringPtr("User"),
Elements: []ComponentElement{
{
Field: &ComponentField{
Name: "password",
Type: "password",
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
{
name: "page with modal and panel sections",
input: `page ProductList at "/products" layout main {
section main type container {
component table for Product
}
section editModal type modal trigger "edit-product" {
component form for Product {
field name type text required
button save label "Save Changes" style "primary"
}
}
section filters type panel position "left" {
component form {
field category type select
field price_range type range
}
}
}`,
want: AST{
Definitions: []Definition{
{
Page: &Page{
Name: "ProductList",
Path: "/products",
Layout: "main",
Sections: []Section{
{
Name: "main",
Type: stringPtr("container"),
Elements: []SectionElement{
{
Component: &Component{
Type: "table",
Entity: stringPtr("Product"),
},
},
},
},
{
Name: "editModal",
Type: stringPtr("modal"),
Trigger: stringPtr("edit-product"),
Elements: []SectionElement{
{
Component: &Component{
Type: "form",
Entity: stringPtr("Product"),
Elements: []ComponentElement{
{
Field: &ComponentField{
Name: "name",
Type: "text",
Attributes: []ComponentFieldAttribute{
{Required: true},
},
},
},
{
Button: &ComponentButton{
Name: "save",
Label: "Save Changes",
Attributes: []ComponentButtonAttr{
{Style: &ComponentButtonStyle{Value: "primary"}},
},
},
},
},
},
},
},
},
{
Name: "filters",
Type: stringPtr("panel"),
Position: stringPtr("left"),
Elements: []SectionElement{
{
Component: &Component{
Type: "form",
Elements: []ComponentElement{
{
Field: &ComponentField{
Name: "category",
Type: "select",
},
},
{
Field: &ComponentField{
Name: "price_range",
Type: "range",
},
},
},
},
},
},
},
},
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseInput(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("ParseInput() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !astEqual(got, tt.want) {
t.Errorf("ParseInput() got = %v, want %v", got, tt.want)
}
})
}
}
func TestParsePageErrors(t *testing.T) {
tests := []struct {
name string
input string
}{
{
name: "missing layout",
input: `page Dashboard at "/dashboard"`,
},
{
name: "missing path",
input: `page Dashboard layout main`,
},
{
name: "invalid path format",
input: `page Dashboard at dashboard layout main`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := ParseInput(tt.input)
if err == nil {
t.Errorf("ParseInput() expected error for invalid syntax, got nil")
}
})
}
}

View File

@ -0,0 +1,599 @@
package lang
import (
"testing"
)
func TestParseSectionDefinitions(t *testing.T) {
tests := []struct {
name string
input string
want AST
wantErr bool
}{
{
name: "basic container section",
input: `page Test at "/test" layout main {
section main type container
}`,
want: AST{
Definitions: []Definition{
{
Page: &Page{
Name: "Test",
Path: "/test",
Layout: "main",
Sections: []Section{
{
Name: "main",
Type: stringPtr("container"),
},
},
},
},
},
},
},
{
name: "section with all attributes",
input: `page Test at "/test" layout main {
section sidebar type panel class "sidebar-nav" label "Navigation" trigger "toggle-sidebar" position "left" for User
}`,
want: AST{
Definitions: []Definition{
{
Page: &Page{
Name: "Test",
Path: "/test",
Layout: "main",
Sections: []Section{
{
Name: "sidebar",
Type: stringPtr("panel"),
Position: stringPtr("left"),
Class: stringPtr("sidebar-nav"),
Label: stringPtr("Navigation"),
Trigger: stringPtr("toggle-sidebar"),
Entity: stringPtr("User"),
},
},
},
},
},
},
},
{
name: "tab sections with active state",
input: `page Test at "/test" layout main {
section tabs type tab {
section overview label "Overview" active
section details label "Details"
section settings label "Settings"
}
}`,
want: AST{
Definitions: []Definition{
{
Page: &Page{
Name: "Test",
Path: "/test",
Layout: "main",
Sections: []Section{
{
Name: "tabs",
Type: stringPtr("tab"),
Elements: []SectionElement{
{
Section: &Section{
Name: "overview",
Label: stringPtr("Overview"),
Active: true,
},
},
{
Section: &Section{
Name: "details",
Label: stringPtr("Details"),
},
},
{
Section: &Section{
Name: "settings",
Label: stringPtr("Settings"),
},
},
},
},
},
},
},
},
},
},
{
name: "modal section with content",
input: `page Test at "/test" layout main {
section userModal type modal trigger "edit-user" {
component form for User {
field name type text required
field email type email required
button save label "Save Changes" style "primary"
button cancel label "Cancel" style "secondary"
}
}
}`,
want: AST{
Definitions: []Definition{
{
Page: &Page{
Name: "Test",
Path: "/test",
Layout: "main",
Sections: []Section{
{
Name: "userModal",
Type: stringPtr("modal"),
Trigger: stringPtr("edit-user"),
Elements: []SectionElement{
{
Component: &Component{
Type: "form",
Entity: stringPtr("User"),
Elements: []ComponentElement{
{
Field: &ComponentField{
Name: "name",
Type: "text",
Attributes: []ComponentFieldAttribute{
{Required: true},
},
},
},
{
Field: &ComponentField{
Name: "email",
Type: "email",
Attributes: []ComponentFieldAttribute{
{Required: true},
},
},
},
{
Button: &ComponentButton{
Name: "save",
Label: "Save Changes",
Attributes: []ComponentButtonAttr{
{Style: &ComponentButtonStyle{Value: "primary"}},
},
},
},
{
Button: &ComponentButton{
Name: "cancel",
Label: "Cancel",
Attributes: []ComponentButtonAttr{
{Style: &ComponentButtonStyle{Value: "secondary"}},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
{
name: "master-detail sections",
input: `page Test at "/test" layout main {
section masterDetail type master {
section userList type container {
component table for User {
fields [name, email]
}
}
section userDetail type detail trigger "user-selected" for User {
component form for User {
field name type text
field email type email
field bio type textarea
}
}
}
}`,
want: AST{
Definitions: []Definition{
{
Page: &Page{
Name: "Test",
Path: "/test",
Layout: "main",
Sections: []Section{
{
Name: "masterDetail",
Type: stringPtr("master"),
Elements: []SectionElement{
{
Section: &Section{
Name: "userList",
Type: stringPtr("container"),
Elements: []SectionElement{
{
Component: &Component{
Type: "table",
Entity: stringPtr("User"),
Elements: []ComponentElement{
{
Attribute: &ComponentAttr{
Fields: []string{"name", "email"},
},
},
},
},
},
},
},
},
{
Section: &Section{
Name: "userDetail",
Type: stringPtr("detail"),
Trigger: stringPtr("user-selected"),
Entity: stringPtr("User"),
Elements: []SectionElement{
{
Component: &Component{
Type: "form",
Entity: stringPtr("User"),
Elements: []ComponentElement{
{
Field: &ComponentField{
Name: "name",
Type: "text",
},
},
{
Field: &ComponentField{
Name: "email",
Type: "email",
},
},
{
Field: &ComponentField{
Name: "bio",
Type: "textarea",
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
{
name: "deeply nested sections",
input: `page Test at "/test" layout main {
section mainLayout type container {
section header type container class "header" {
component navbar {
field search type text placeholder "Search..."
}
}
section content type container {
section sidebar type panel position "left" {
component menu {
field navigation type list
}
}
section main type container {
section tabs type tab {
section overview label "Overview" active {
component dashboard {
field stats type metric
}
}
section reports label "Reports" {
component table for Report
}
}
}
}
}
}`,
want: AST{
Definitions: []Definition{
{
Page: &Page{
Name: "Test",
Path: "/test",
Layout: "main",
Sections: []Section{
{
Name: "mainLayout",
Type: stringPtr("container"),
Elements: []SectionElement{
{
Section: &Section{
Name: "header",
Type: stringPtr("container"),
Class: stringPtr("header"),
Elements: []SectionElement{
{
Component: &Component{
Type: "navbar",
Elements: []ComponentElement{
{
Field: &ComponentField{
Name: "search",
Type: "text",
Attributes: []ComponentFieldAttribute{
{Placeholder: stringPtr("Search...")},
},
},
},
},
},
},
},
},
},
{
Section: &Section{
Name: "content",
Type: stringPtr("container"),
Elements: []SectionElement{
{
Section: &Section{
Name: "sidebar",
Type: stringPtr("panel"),
Position: stringPtr("left"),
Elements: []SectionElement{
{
Component: &Component{
Type: "menu",
Elements: []ComponentElement{
{
Field: &ComponentField{
Name: "navigation",
Type: "list",
},
},
},
},
},
},
},
},
{
Section: &Section{
Name: "main",
Type: stringPtr("container"),
Elements: []SectionElement{
{
Section: &Section{
Name: "tabs",
Type: stringPtr("tab"),
Elements: []SectionElement{
{
Section: &Section{
Name: "overview",
Label: stringPtr("Overview"),
Active: true,
Elements: []SectionElement{
{
Component: &Component{
Type: "dashboard",
Elements: []ComponentElement{
{
Field: &ComponentField{
Name: "stats",
Type: "metric",
},
},
},
},
},
},
},
},
{
Section: &Section{
Name: "reports",
Label: stringPtr("Reports"),
Elements: []SectionElement{
{
Component: &Component{
Type: "table",
Entity: stringPtr("Report"),
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
{
name: "section with conditional content",
input: `page Test at "/test" layout main {
section adminPanel type container {
when user_role equals "admin" {
section userManagement type container {
component table for User
}
section systemSettings type container {
component form for Settings
}
}
}
}`,
want: AST{
Definitions: []Definition{
{
Page: &Page{
Name: "Test",
Path: "/test",
Layout: "main",
Sections: []Section{
{
Name: "adminPanel",
Type: stringPtr("container"),
Elements: []SectionElement{
{
When: &WhenCondition{
Field: "user_role",
Operator: "equals",
Value: "admin",
Sections: []Section{
{
Name: "userManagement",
Type: stringPtr("container"),
Elements: []SectionElement{
{
Component: &Component{
Type: "table",
Entity: stringPtr("User"),
},
},
},
},
{
Name: "systemSettings",
Type: stringPtr("container"),
Elements: []SectionElement{
{
Component: &Component{
Type: "form",
Entity: stringPtr("Settings"),
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseInput(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("ParseInput() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !astEqual(got, tt.want) {
t.Errorf("ParseInput() got = %v, want %v", got, tt.want)
}
})
}
}
func TestParseSectionTypes(t *testing.T) {
sectionTypes := []string{
"container", "tab", "panel", "modal", "master", "detail",
}
for _, sectionType := range sectionTypes {
t.Run("section_type_"+sectionType, func(t *testing.T) {
input := `page Test at "/test" layout main {
section test_section type ` + sectionType + `
}`
got, err := ParseInput(input)
if err != nil {
t.Errorf("ParseInput() failed for section type %s: %v", sectionType, err)
return
}
if len(got.Definitions) != 1 || got.Definitions[0].Page == nil {
t.Errorf("ParseInput() failed to parse page for section type %s", sectionType)
return
}
page := got.Definitions[0].Page
if len(page.Sections) != 1 {
t.Errorf("ParseInput() failed to parse section for type %s", sectionType)
return
}
section := page.Sections[0]
if section.Type == nil || *section.Type != sectionType {
t.Errorf("ParseInput() section type mismatch: got %v, want %s", section.Type, sectionType)
}
})
}
}
func TestParseSectionErrors(t *testing.T) {
tests := []struct {
name string
input string
}{
{
name: "missing section name",
input: `page Test at "/test" layout main {
section type container
}`,
},
{
name: "invalid section type",
input: `page Test at "/test" layout main {
section test type invalid_type
}`,
},
{
name: "unclosed section block",
input: `page Test at "/test" layout main {
section test type container {
component form
}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := ParseInput(tt.input)
if err == nil {
t.Errorf("ParseInput() expected error for invalid syntax, got nil")
}
})
}
}

View File

@ -0,0 +1,59 @@
package lang
// AST and definition comparison functions for parser tests
// Custom comparison functions (simplified for the new structure)
func astEqual(got, want AST) bool {
if len(got.Definitions) != len(want.Definitions) {
return false
}
for i := range got.Definitions {
if !definitionEqual(got.Definitions[i], want.Definitions[i]) {
return false
}
}
return true
}
func definitionEqual(got, want Definition) bool {
// Server comparison
if (got.Server == nil) != (want.Server == nil) {
return false
}
if got.Server != nil && want.Server != nil {
if !serverEqual(*got.Server, *want.Server) {
return false
}
}
// Entity comparison
if (got.Entity == nil) != (want.Entity == nil) {
return false
}
if got.Entity != nil && want.Entity != nil {
if !entityEqual(*got.Entity, *want.Entity) {
return false
}
}
// Endpoint comparison
if (got.Endpoint == nil) != (want.Endpoint == nil) {
return false
}
if got.Endpoint != nil && want.Endpoint != nil {
if !endpointEqual(*got.Endpoint, *want.Endpoint) {
return false
}
}
// Page comparison (enhanced)
if (got.Page == nil) != (want.Page == nil) {
return false
}
if got.Page != nil && want.Page != nil {
return pageEqual(*got.Page, *want.Page)
}
return true
}

View File

@ -0,0 +1,46 @@
package lang
// Basic comparison utilities for parser tests
// Helper functions for creating pointers
func stringPtr(s string) *string {
return &s
}
func intPtr(i int) *int {
return &i
}
// Pointer comparison functions
func stringPtrEqual(got, want *string) bool {
if (got == nil) != (want == nil) {
return false
}
if got != nil && want != nil {
return *got == *want
}
return true
}
func intPtrEqual(got, want *int) bool {
if (got == nil) != (want == nil) {
return false
}
if got != nil && want != nil {
return *got == *want
}
return true
}
// Slice comparison functions
func stringSliceEqual(got, want []string) bool {
if len(got) != len(want) {
return false
}
for i, s := range got {
if s != want[i] {
return false
}
}
return true
}

View File

@ -0,0 +1,69 @@
package lang
// Field and validation comparison functions for parser tests
func fieldEqual(got, want Field) bool {
if got.Name != want.Name || got.Type != want.Type {
return false
}
if got.Required != want.Required || got.Unique != want.Unique || got.Index != want.Index {
return false
}
if !stringPtrEqual(got.Default, want.Default) {
return false
}
if len(got.Validations) != len(want.Validations) {
return false
}
for i, validation := range got.Validations {
if !validationEqual(validation, want.Validations[i]) {
return false
}
}
if (got.Relationship == nil) != (want.Relationship == nil) {
return false
}
if got.Relationship != nil && want.Relationship != nil {
if !relationshipEqual(*got.Relationship, *want.Relationship) {
return false
}
}
return true
}
func validationEqual(got, want Validation) bool {
return got.Type == want.Type && stringPtrEqual(got.Value, want.Value)
}
func relationshipEqual(got, want Relationship) bool {
return got.Type == want.Type &&
got.Cardinality == want.Cardinality &&
stringPtrEqual(got.ForeignKey, want.ForeignKey) &&
stringPtrEqual(got.Through, want.Through)
}
func fieldRelationEqual(got, want *FieldRelation) bool {
if (got == nil) != (want == nil) {
return false
}
if got != nil && want != nil {
return got.Type == want.Type
}
return true
}
func componentValidationEqual(got, want *ComponentValidation) bool {
if (got == nil) != (want == nil) {
return false
}
if got != nil && want != nil {
return got.Type == want.Type && stringPtrEqual(got.Value, want.Value)
}
return true
}

View File

@ -0,0 +1,57 @@
package lang
// Server and entity comparison functions for parser tests
func serverEqual(got, want Server) bool {
if got.Name != want.Name {
return false
}
if len(got.Settings) != len(want.Settings) {
return false
}
for i, setting := range got.Settings {
if !serverSettingEqual(setting, want.Settings[i]) {
return false
}
}
return true
}
func serverSettingEqual(got, want ServerSetting) bool {
return stringPtrEqual(got.Host, want.Host) && intPtrEqual(got.Port, want.Port)
}
func entityEqual(got, want Entity) bool {
if got.Name != want.Name {
return false
}
if !stringPtrEqual(got.Description, want.Description) {
return false
}
if len(got.Fields) != len(want.Fields) {
return false
}
for i, field := range got.Fields {
if !fieldEqual(field, want.Fields[i]) {
return false
}
}
return true
}
func endpointEqual(got, want Endpoint) bool {
return got.Method == want.Method &&
got.Path == want.Path &&
stringPtrEqual(got.Entity, want.Entity) &&
stringPtrEqual(got.Description, want.Description) &&
got.Auth == want.Auth &&
stringPtrEqual(got.CustomLogic, want.CustomLogic)
// TODO: Add params and response comparison if needed
}

421
lang/test_ui_comparisons.go Normal file
View File

@ -0,0 +1,421 @@
package lang
// Page and UI component comparison functions for parser tests
func pageEqual(got, want Page) bool {
if got.Name != want.Name || got.Path != want.Path || got.Layout != want.Layout {
return false
}
if !stringPtrEqual(got.Title, want.Title) {
return false
}
if !stringPtrEqual(got.Description, want.Description) {
return false
}
if got.Auth != want.Auth {
return false
}
// Compare meta tags
if len(got.Meta) != len(want.Meta) {
return false
}
for i, meta := range got.Meta {
if !metaTagEqual(meta, want.Meta[i]) {
return false
}
}
// Compare sections (unified model)
if len(got.Sections) != len(want.Sections) {
return false
}
for i, section := range got.Sections {
if !sectionEqual(section, want.Sections[i]) {
return false
}
}
// Compare components
if len(got.Components) != len(want.Components) {
return false
}
for i, component := range got.Components {
if !componentEqual(component, want.Components[i]) {
return false
}
}
return true
}
func metaTagEqual(got, want MetaTag) bool {
return got.Name == want.Name && got.Content == want.Content
}
func sectionEqual(got, want Section) bool {
if got.Name != want.Name {
return false
}
if !stringPtrEqual(got.Type, want.Type) {
return false
}
if !stringPtrEqual(got.Class, want.Class) {
return false
}
if !stringPtrEqual(got.Label, want.Label) {
return false
}
if got.Active != want.Active {
return false
}
if !stringPtrEqual(got.Trigger, want.Trigger) {
return false
}
if !stringPtrEqual(got.Position, want.Position) {
return false
}
if !stringPtrEqual(got.Entity, want.Entity) {
return false
}
// Extract different element types from the unified elements
gotAttributes := extractSectionAttributes(got.Elements)
gotComponents := extractSectionComponents(got.Elements)
gotSections := extractSectionSections(got.Elements)
gotWhen := extractSectionWhen(got.Elements)
wantAttributes := extractSectionAttributes(want.Elements)
wantComponents := extractSectionComponents(want.Elements)
wantSections := extractSectionSections(want.Elements)
wantWhen := extractSectionWhen(want.Elements)
// Compare attributes
if len(gotAttributes) != len(wantAttributes) {
return false
}
for i, attr := range gotAttributes {
if !sectionAttributeEqual(attr, wantAttributes[i]) {
return false
}
}
// Compare components
if len(gotComponents) != len(wantComponents) {
return false
}
for i, comp := range gotComponents {
if !componentEqual(comp, wantComponents[i]) {
return false
}
}
// Compare nested sections
if len(gotSections) != len(wantSections) {
return false
}
for i, sect := range gotSections {
if !sectionEqual(sect, wantSections[i]) {
return false
}
}
// Compare when conditions
if len(gotWhen) != len(wantWhen) {
return false
}
for i, when := range gotWhen {
if !whenConditionEqual(when, wantWhen[i]) {
return false
}
}
return true
}
// Helper functions to extract different element types from unified elements
func extractSectionAttributes(elements []SectionElement) []SectionAttribute {
var attrs []SectionAttribute
for _, elem := range elements {
if elem.Attribute != nil {
attrs = append(attrs, *elem.Attribute)
}
}
return attrs
}
func extractSectionComponents(elements []SectionElement) []Component {
var comps []Component
for _, elem := range elements {
if elem.Component != nil {
comps = append(comps, *elem.Component)
}
}
return comps
}
func extractSectionSections(elements []SectionElement) []Section {
var sects []Section
for _, elem := range elements {
if elem.Section != nil {
sects = append(sects, *elem.Section)
}
}
return sects
}
func extractSectionWhen(elements []SectionElement) []WhenCondition {
var whens []WhenCondition
for _, elem := range elements {
if elem.When != nil {
whens = append(whens, *elem.When)
}
}
return whens
}
func sectionAttributeEqual(got, want SectionAttribute) bool {
return stringPtrEqual(got.DataSource, want.DataSource) &&
stringPtrEqual(got.Style, want.Style) &&
stringPtrEqual(got.Classes, want.Classes) &&
intPtrEqual(got.Size, want.Size) &&
stringPtrEqual(got.Theme, want.Theme)
}
func componentEqual(got, want Component) bool {
if got.Type != want.Type {
return false
}
if !stringPtrEqual(got.Entity, want.Entity) {
return false
}
if len(got.Elements) != len(want.Elements) {
return false
}
for i, elem := range got.Elements {
if !componentElementEqual(elem, want.Elements[i]) {
return false
}
}
return true
}
func componentElementEqual(got, want ComponentElement) bool {
// Compare attributes
if (got.Attribute == nil) != (want.Attribute == nil) {
return false
}
if got.Attribute != nil && want.Attribute != nil {
if !componentAttrEqual(*got.Attribute, *want.Attribute) {
return false
}
}
// Compare fields
if (got.Field == nil) != (want.Field == nil) {
return false
}
if got.Field != nil && want.Field != nil {
if !componentFieldEqual(*got.Field, *want.Field) {
return false
}
}
// Compare sections
if (got.Section == nil) != (want.Section == nil) {
return false
}
if got.Section != nil && want.Section != nil {
if !sectionEqual(*got.Section, *want.Section) {
return false
}
}
// Compare buttons
if (got.Button == nil) != (want.Button == nil) {
return false
}
if got.Button != nil && want.Button != nil {
if !componentButtonEqual(*got.Button, *want.Button) {
return false
}
}
// Compare when conditions
if (got.When == nil) != (want.When == nil) {
return false
}
if got.When != nil && want.When != nil {
if !whenConditionEqual(*got.When, *want.When) {
return false
}
}
return true
}
func componentAttrEqual(got, want ComponentAttr) bool {
return stringPtrEqual(got.DataSource, want.DataSource) &&
stringSliceEqual(got.Fields, want.Fields) &&
stringSliceEqual(got.Actions, want.Actions) &&
stringPtrEqual(got.Style, want.Style) &&
stringPtrEqual(got.Classes, want.Classes) &&
intPtrEqual(got.PageSize, want.PageSize) &&
got.Validate == want.Validate
}
func componentFieldEqual(got, want ComponentField) bool {
if got.Name != want.Name || got.Type != want.Type {
return false
}
if len(got.Attributes) != len(want.Attributes) {
return false
}
for i, attr := range got.Attributes {
if !componentFieldAttributeEqual(attr, want.Attributes[i]) {
return false
}
}
return true
}
func componentFieldAttributeEqual(got, want ComponentFieldAttribute) bool {
return stringPtrEqual(got.Label, want.Label) &&
stringPtrEqual(got.Placeholder, want.Placeholder) &&
got.Required == want.Required &&
got.Sortable == want.Sortable &&
got.Searchable == want.Searchable &&
got.Thumbnail == want.Thumbnail &&
stringPtrEqual(got.Default, want.Default) &&
stringSliceEqual(got.Options, want.Options) &&
stringPtrEqual(got.Accept, want.Accept) &&
intPtrEqual(got.Rows, want.Rows) &&
stringPtrEqual(got.Format, want.Format) &&
stringPtrEqual(got.Size, want.Size) &&
stringPtrEqual(got.Display, want.Display) &&
stringPtrEqual(got.Value, want.Value) &&
stringPtrEqual(got.Source, want.Source) &&
fieldRelationEqual(got.Relates, want.Relates) &&
componentValidationEqual(got.Validation, want.Validation)
}
func componentButtonEqual(got, want ComponentButton) bool {
if got.Name != want.Name || got.Label != want.Label {
return false
}
// Extract attributes from both buttons for comparison
gotStyle, gotIcon, gotLoading, gotDisabled, gotConfirm, gotTarget, gotPosition, gotVia := extractButtonAttributesNew(got.Attributes)
wantStyle, wantIcon, wantLoading, wantDisabled, wantConfirm, wantTarget, wantPosition, wantVia := extractButtonAttributesNew(want.Attributes)
return stringPtrEqual(gotStyle, wantStyle) &&
stringPtrEqual(gotIcon, wantIcon) &&
stringPtrEqual(gotLoading, wantLoading) &&
stringPtrEqual(gotDisabled, wantDisabled) &&
stringPtrEqual(gotConfirm, wantConfirm) &&
stringPtrEqual(gotTarget, wantTarget) &&
stringPtrEqual(gotPosition, wantPosition) &&
stringPtrEqual(gotVia, wantVia)
}
// Helper function to extract button attributes from the new structure
func extractButtonAttributesNew(attrs []ComponentButtonAttr) (*string, *string, *string, *string, *string, *string, *string, *string) {
var style, icon, loading, disabled, confirm, target, position, via *string
for _, attr := range attrs {
if attr.Style != nil {
style = &attr.Style.Value
}
if attr.Icon != nil {
icon = &attr.Icon.Value
}
if attr.Loading != nil {
loading = &attr.Loading.Value
}
if attr.Disabled != nil {
disabled = &attr.Disabled.Value
}
if attr.Confirm != nil {
confirm = &attr.Confirm.Value
}
if attr.Target != nil {
target = &attr.Target.Value
}
if attr.Position != nil {
position = &attr.Position.Value
}
if attr.Via != nil {
via = &attr.Via.Value
}
}
return style, icon, loading, disabled, confirm, target, position, via
}
func whenConditionEqual(got, want WhenCondition) bool {
if got.Field != want.Field || got.Operator != want.Operator || got.Value != want.Value {
return false
}
// Compare fields
if len(got.Fields) != len(want.Fields) {
return false
}
for i, field := range got.Fields {
if !componentFieldEqual(field, want.Fields[i]) {
return false
}
}
// Compare sections
if len(got.Sections) != len(want.Sections) {
return false
}
for i, section := range got.Sections {
if !sectionEqual(section, want.Sections[i]) {
return false
}
}
// Compare components
if len(got.Components) != len(want.Components) {
return false
}
for i, component := range got.Components {
if !componentEqual(component, want.Components[i]) {
return false
}
}
// Compare buttons
if len(got.Buttons) != len(want.Buttons) {
return false
}
for i, button := range got.Buttons {
if !componentButtonEqual(button, want.Buttons[i]) {
return false
}
}
return true
}