Compare commits
1 Commits
e71b1c3a23
...
dsl-lib
Author | SHA1 | Date | |
---|---|---|---|
9b209dfe63 |
78
.idea/copilotDiffState.xml
generated
78
.idea/copilotDiffState.xml
generated
File diff suppressed because one or more lines are too long
@ -1,148 +0,0 @@
|
||||
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
|
||||
}
|
@ -1,295 +0,0 @@
|
||||
// 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
157
examples/langV1/debug.go
Normal file
157
examples/langV1/debug.go
Normal file
@ -0,0 +1,157 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"masonry/lang"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Read the example.masonry file
|
||||
content, err := ioutil.ReadFile("example.masonry")
|
||||
if err != nil {
|
||||
fmt.Printf("Error reading example.masonry: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
input := string(content)
|
||||
|
||||
ast, err := lang.ParseInput(input)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("🎉 Successfully parsed complete DSL with pages!\n\n")
|
||||
|
||||
for _, def := range ast.Definitions {
|
||||
if def.Server != nil {
|
||||
fmt.Printf("📡 Server: %s\n", def.Server.Name)
|
||||
for _, setting := range def.Server.Settings {
|
||||
if setting.Host != nil {
|
||||
fmt.Printf(" host: %s\n", *setting.Host)
|
||||
}
|
||||
if setting.Port != nil {
|
||||
fmt.Printf(" port: %d\n", *setting.Port)
|
||||
}
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
|
||||
if def.Entity != nil {
|
||||
entity := def.Entity
|
||||
fmt.Printf("🏗️ Entity: %s", entity.Name)
|
||||
if entity.Description != nil {
|
||||
fmt.Printf(" - %s", *entity.Description)
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
|
||||
for _, field := range entity.Fields {
|
||||
fmt.Printf(" %s: %s", field.Name, field.Type)
|
||||
if field.Required {
|
||||
fmt.Printf(" (required)")
|
||||
}
|
||||
if field.Unique {
|
||||
fmt.Printf(" (unique)")
|
||||
}
|
||||
if field.Default != nil {
|
||||
fmt.Printf(" default=%s", *field.Default)
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
|
||||
if def.Endpoint != nil {
|
||||
endpoint := def.Endpoint
|
||||
fmt.Printf("🚀 Endpoint: %s %s", endpoint.Method, endpoint.Path)
|
||||
if endpoint.Entity != nil {
|
||||
fmt.Printf(" (for %s)", *endpoint.Entity)
|
||||
}
|
||||
if endpoint.Description != nil {
|
||||
fmt.Printf(" - %s", *endpoint.Description)
|
||||
}
|
||||
if endpoint.Auth {
|
||||
fmt.Printf(" [AUTH]")
|
||||
}
|
||||
fmt.Printf("\n\n")
|
||||
}
|
||||
|
||||
if def.Page != nil {
|
||||
page := def.Page
|
||||
fmt.Printf("🎨 Page: %s at %s", page.Name, page.Path)
|
||||
if page.Title != nil {
|
||||
fmt.Printf(" - %s", *page.Title)
|
||||
}
|
||||
if page.Auth {
|
||||
fmt.Printf(" [AUTH]")
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
fmt.Printf(" Layout: %s\n", page.Layout)
|
||||
|
||||
for _, meta := range page.Meta {
|
||||
fmt.Printf(" Meta %s: %s\n", meta.Name, meta.Content)
|
||||
}
|
||||
|
||||
for _, comp := range page.Components {
|
||||
fmt.Printf(" 📦 Component: %s", comp.Type)
|
||||
if comp.Entity != nil {
|
||||
fmt.Printf(" for %s", *comp.Entity)
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
|
||||
for _, attr := range comp.Config {
|
||||
if attr.Fields != nil {
|
||||
fmt.Printf(" fields: %v\n", attr.Fields.Fields)
|
||||
}
|
||||
if attr.Actions != nil {
|
||||
fmt.Printf(" actions: ")
|
||||
for i, action := range attr.Actions.Actions {
|
||||
if i > 0 {
|
||||
fmt.Printf(", ")
|
||||
}
|
||||
fmt.Printf("%s", action.Name)
|
||||
if action.Endpoint != nil {
|
||||
fmt.Printf(" via %s", *action.Endpoint)
|
||||
}
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
if attr.DataSource != nil {
|
||||
fmt.Printf(" data from: %s\n", attr.DataSource.Endpoint)
|
||||
}
|
||||
if attr.Style != nil {
|
||||
fmt.Printf(" style: %s", *attr.Style.Theme)
|
||||
if len(attr.Style.Classes) > 0 {
|
||||
fmt.Printf(" classes: %v", attr.Style.Classes)
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
if attr.Pagination != nil {
|
||||
fmt.Printf(" pagination: enabled")
|
||||
if attr.Pagination.PageSize != nil {
|
||||
fmt.Printf(" size %d", *attr.Pagination.PageSize)
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
if attr.Filters != nil {
|
||||
fmt.Printf(" filters: ")
|
||||
for i, filter := range attr.Filters.Filters {
|
||||
if i > 0 {
|
||||
fmt.Printf(", ")
|
||||
}
|
||||
fmt.Printf("%s as %s", filter.Field, filter.Type)
|
||||
if filter.Label != nil {
|
||||
fmt.Printf(" (%s)", *filter.Label)
|
||||
}
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
if attr.Validation {
|
||||
fmt.Printf(" validation: enabled\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
125
examples/langV1/example.masonry
Normal file
125
examples/langV1/example.masonry
Normal file
@ -0,0 +1,125 @@
|
||||
// Example Masonry DSL definition
|
||||
// This demonstrates the comprehensive language structure
|
||||
|
||||
// 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]
|
||||
|
||||
// Frontend pages with components
|
||||
page UserManagement at "/admin/users" layout AdminLayout title "User Management" auth
|
||||
meta description "Manage system users"
|
||||
meta keywords "users, admin, management"
|
||||
|
||||
component Table for User
|
||||
fields [email, name, id]
|
||||
actions [edit via "/users/{id}", delete via "/users/{id}", create via "/users"]
|
||||
data from "/users"
|
||||
style modern classes ["table-striped", "table-hover"]
|
||||
pagination size 20
|
||||
filters [email as text label "Search email", name as text label "Search name"]
|
||||
validate
|
||||
|
||||
component Form for User
|
||||
fields [email, name]
|
||||
actions [save via "/users", cancel]
|
||||
style clean
|
||||
validate
|
||||
|
||||
page UserList at "/users" layout MainLayout title "Users"
|
||||
meta description "Browse all users"
|
||||
|
||||
component Table for User
|
||||
fields [email, name]
|
||||
data from "/users"
|
||||
pagination size 10
|
||||
filters [name as text label "Search by name"]
|
||||
|
||||
page PostManagement at "/admin/posts" layout AdminLayout title "Post Management" auth
|
||||
meta description "Manage blog posts"
|
||||
meta keywords "posts, blog, content"
|
||||
|
||||
component Table for Post
|
||||
fields [title, author_id, published, created_at]
|
||||
actions [edit via "/posts/{id}", delete via "/posts/{id}", create via "/posts"]
|
||||
data from "/posts"
|
||||
style modern
|
||||
pagination size 15
|
||||
filters [title as text label "Search title", published as select label "Published status"]
|
||||
validate
|
||||
|
||||
page CreatePost at "/posts/new" layout MainLayout title "Create Post" auth
|
||||
component Form for Post
|
||||
fields [title, content]
|
||||
actions [save via "/posts", cancel]
|
||||
style clean
|
||||
validate
|
||||
|
||||
page BlogList at "/blog" layout PublicLayout title "Blog Posts"
|
||||
meta description "Read our latest blog posts"
|
||||
meta keywords "blog, articles, content"
|
||||
|
||||
component Table for Post
|
||||
fields [title, created_at]
|
||||
data from "/posts"
|
||||
pagination size 5
|
||||
filters [title as text label "Search posts"]
|
33
examples/langV2/debug.go
Normal file
33
examples/langV2/debug.go
Normal file
@ -0,0 +1,33 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"masonry/langV2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Read the example.masonry file
|
||||
content, err := ioutil.ReadFile("example.masonry2")
|
||||
if err != nil {
|
||||
fmt.Printf("Error reading example.masonry: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
input := string(content)
|
||||
|
||||
ast, err := langV2.ParseInput(input)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("🎉 Successfully parsed complete DSL with pages!\n\n")
|
||||
|
||||
out, err := json.MarshalIndent(ast, "", " ")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
println(string(out))
|
||||
|
||||
}
|
||||
}
|
39
examples/langV2/example.masonry2
Normal file
39
examples/langV2/example.masonry2
Normal file
@ -0,0 +1,39 @@
|
||||
entity User {
|
||||
id: string required unique
|
||||
email: string required unique validate email
|
||||
name: string required
|
||||
age: int validate min "18"
|
||||
created_at: datetime
|
||||
}
|
||||
|
||||
entity Post {
|
||||
id: string required unique
|
||||
title: string required validate maxlen "100"
|
||||
content: string required
|
||||
author_id: string required relates to User as one via "user_id"
|
||||
published: bool default "false"
|
||||
}
|
||||
|
||||
server UserService {
|
||||
GET "/users" entity User
|
||||
POST "/users" entity User
|
||||
GET "/users/{id}" entity User
|
||||
PUT "/users/{id}" entity User
|
||||
}
|
||||
|
||||
server BlogService {
|
||||
GET "/posts" entity Post
|
||||
POST "/posts" entity Post
|
||||
GET "/posts/{id}" entity Post
|
||||
DELETE "/posts/{id}" entity Post
|
||||
}
|
||||
|
||||
page UserProfile {
|
||||
User from UserService
|
||||
Post from BlogService
|
||||
}
|
||||
|
||||
page Dashboard {
|
||||
User from UserService
|
||||
Post from BlogService
|
||||
}
|
136
examples/sdk/debug_sdk.go
Normal file
136
examples/sdk/debug_sdk.go
Normal file
@ -0,0 +1,136 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"masonry/sdk"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
userEntity := sdk.Entity{
|
||||
Name: "User",
|
||||
Description: nil,
|
||||
Fields: []sdk.Field{
|
||||
{
|
||||
Name: "id",
|
||||
Type: "int",
|
||||
Required: true,
|
||||
Unique: true,
|
||||
Default: nil,
|
||||
Validations: []sdk.Validation{
|
||||
{
|
||||
Type: "validate",
|
||||
Value: nil,
|
||||
},
|
||||
},
|
||||
Relationship: nil,
|
||||
},
|
||||
{
|
||||
Name: "username",
|
||||
Type: "string",
|
||||
Required: true,
|
||||
Unique: true,
|
||||
Default: nil,
|
||||
Validations: []sdk.Validation{
|
||||
{
|
||||
Type: "validate",
|
||||
Value: nil,
|
||||
},
|
||||
},
|
||||
Relationship: nil,
|
||||
},
|
||||
{
|
||||
Name: "email",
|
||||
Type: "string",
|
||||
Required: true,
|
||||
Unique: true,
|
||||
Default: nil,
|
||||
Validations: []sdk.Validation{
|
||||
{
|
||||
Type: "validate",
|
||||
Value: nil,
|
||||
},
|
||||
},
|
||||
Relationship: nil,
|
||||
},
|
||||
{
|
||||
Name: "password",
|
||||
Type: "string",
|
||||
Required: true,
|
||||
Unique: false,
|
||||
Default: nil,
|
||||
Validations: []sdk.Validation{
|
||||
{
|
||||
Type: "validate",
|
||||
Value: nil,
|
||||
},
|
||||
},
|
||||
Relationship: nil,
|
||||
},
|
||||
{
|
||||
Name: "created_at",
|
||||
Type: "datetime",
|
||||
Required: true,
|
||||
Unique: false,
|
||||
Default: nil,
|
||||
Validations: []sdk.Validation{
|
||||
{
|
||||
Type: "validate",
|
||||
Value: nil,
|
||||
},
|
||||
},
|
||||
Relationship: nil,
|
||||
},
|
||||
{
|
||||
Name: "updated_at",
|
||||
Type: "datetime",
|
||||
Required: true,
|
||||
Unique: false,
|
||||
Default: nil,
|
||||
Validations: []sdk.Validation{
|
||||
{
|
||||
Type: "validate",
|
||||
Value: nil,
|
||||
},
|
||||
},
|
||||
Relationship: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
createUserDesc := "Create a new user with username, email, and password. Returns the created user object."
|
||||
createUser := sdk.Endpoint{
|
||||
Method: "POST",
|
||||
Path: "/user",
|
||||
Entity: userEntity,
|
||||
Description: &createUserDesc,
|
||||
Auth: true,
|
||||
Permissions: nil,
|
||||
}
|
||||
|
||||
server := sdk.Server{
|
||||
Name: "TestServer",
|
||||
Settings: []sdk.ServerSetting{
|
||||
{
|
||||
Host: "localhost",
|
||||
Port: 8000,
|
||||
},
|
||||
},
|
||||
Endpoints: []sdk.Endpoint{
|
||||
createUser,
|
||||
},
|
||||
}
|
||||
|
||||
def := sdk.Definition{
|
||||
Server: []sdk.Server{
|
||||
server,
|
||||
},
|
||||
}
|
||||
|
||||
out, err := json.MarshalIndent(def, "", " ")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
println(string(out))
|
||||
println("🎉 Successfully built SDK definition!")
|
||||
println("You can now use this SDK definition in your application.")
|
||||
}
|
2
go.mod
2
go.mod
@ -3,12 +3,12 @@ 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
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/alecthomas/participle/v2 v2.1.4 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
||||
|
6
go.sum
6
go.sum
@ -1,13 +1,7 @@
|
||||
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=
|
||||
|
190
lang/lang.go
190
lang/lang.go
@ -20,7 +20,7 @@ type Definition struct {
|
||||
// Clean server syntax
|
||||
type Server struct {
|
||||
Name string `parser:"'server' @Ident"`
|
||||
Settings []ServerSetting `parser:"('{' @@* '}')?"` // Block-delimited settings for consistency
|
||||
Settings []ServerSetting `parser:"@@*"`
|
||||
}
|
||||
|
||||
type ServerSetting struct {
|
||||
@ -32,7 +32,7 @@ type ServerSetting struct {
|
||||
type Entity struct {
|
||||
Name string `parser:"'entity' @Ident"`
|
||||
Description *string `parser:"('desc' @String)?"`
|
||||
Fields []Field `parser:"('{' @@* '}')?"` // Block-delimited fields for consistency
|
||||
Fields []Field `parser:"@@*"`
|
||||
}
|
||||
|
||||
// Much cleaner field syntax
|
||||
@ -68,9 +68,9 @@ type Endpoint struct {
|
||||
Entity *string `parser:"('for' @Ident)?"`
|
||||
Description *string `parser:"('desc' @String)?"`
|
||||
Auth bool `parser:"@'auth'?"`
|
||||
Params []EndpointParam `parser:"('{' @@*"` // Block-delimited parameters
|
||||
Params []EndpointParam `parser:"@@*"`
|
||||
Response *ResponseSpec `parser:"@@?"`
|
||||
CustomLogic *string `parser:"('custom' @String)? '}')?"` // Close block after all content
|
||||
CustomLogic *string `parser:"('custom' @String)?"`
|
||||
}
|
||||
|
||||
// Clean parameter syntax
|
||||
@ -88,7 +88,7 @@ type ResponseSpec struct {
|
||||
Fields []string `parser:"('fields' '[' @Ident (',' @Ident)* ']')?"`
|
||||
}
|
||||
|
||||
// Enhanced Page definitions with unified section model
|
||||
// Page definitions for frontend with clean syntax
|
||||
type Page struct {
|
||||
Name string `parser:"'page' @Ident"`
|
||||
Path string `parser:"'at' @String"`
|
||||
@ -96,9 +96,8 @@ type Page struct {
|
||||
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 []MetaTag `parser:"@@*"`
|
||||
Components []Component `parser:"@@*"`
|
||||
}
|
||||
|
||||
// Meta tags for SEO
|
||||
@ -107,163 +106,64 @@ type MetaTag struct {
|
||||
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
|
||||
// Component definitions with endpoint references
|
||||
type Component struct {
|
||||
Type string `parser:"'component' @Ident"`
|
||||
Entity *string `parser:"('for' @Ident)?"`
|
||||
Elements []ComponentElement `parser:"('{' @@* '}')?"` // Parse everything inside the block
|
||||
Type string `parser:"'component' @Ident"`
|
||||
Entity *string `parser:"('for' @Ident)?"`
|
||||
Config []ComponentAttr `parser:"@@*"`
|
||||
}
|
||||
|
||||
// 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
|
||||
// Component attributes and configurations
|
||||
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'"`
|
||||
Fields *ComponentFields `parser:"@@"`
|
||||
Actions *ComponentActions `parser:"| @@"`
|
||||
DataSource *ComponentDataSource `parser:"| @@"`
|
||||
Style *ComponentStyle `parser:"| @@"`
|
||||
Pagination *ComponentPagination `parser:"| @@"`
|
||||
Filters *ComponentFilters `parser:"| @@"`
|
||||
Validation 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
|
||||
// Component field specification
|
||||
type ComponentFields struct {
|
||||
Fields []string `parser:"'fields' '[' @Ident (',' @Ident)* ']'"`
|
||||
}
|
||||
|
||||
// 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:"| @@"`
|
||||
// Component actions (can reference endpoints)
|
||||
type ComponentActions struct {
|
||||
Actions []ComponentAction `parser:"'actions' '[' @@ (',' @@)* ']'"`
|
||||
}
|
||||
|
||||
// Field relationship for autocomplete and select fields
|
||||
type FieldRelation struct {
|
||||
Type string `parser:"'relates' 'to' @Ident"`
|
||||
type ComponentAction struct {
|
||||
Name string `parser:"@Ident"`
|
||||
Endpoint *string `parser:"('via' @String)?"`
|
||||
}
|
||||
|
||||
// Component validation
|
||||
type ComponentValidation struct {
|
||||
Type string `parser:"'validate' @Ident"`
|
||||
Value *string `parser:"@String?"`
|
||||
// Data source configuration (can reference endpoints)
|
||||
type ComponentDataSource struct {
|
||||
Endpoint string `parser:"'data' 'from' @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
|
||||
// Component styling
|
||||
type ComponentStyle struct {
|
||||
Theme *string `parser:"'style' @Ident"`
|
||||
Classes []string `parser:"('classes' '[' @String (',' @String)* ']')?"`
|
||||
}
|
||||
|
||||
// Simplified button with flexible attribute ordering
|
||||
type ComponentButton struct {
|
||||
Name string `parser:"'button' @Ident"`
|
||||
Label string `parser:"'label' @String"`
|
||||
Attributes []ComponentButtonAttr `parser:"@@*"`
|
||||
// Pagination configuration
|
||||
type ComponentPagination struct {
|
||||
PageSize *int `parser:"'pagination' ('size' @Int)?"`
|
||||
}
|
||||
|
||||
// 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:"| @@"`
|
||||
// Filter specifications
|
||||
type ComponentFilters struct {
|
||||
Filters []ComponentFilter `parser:"'filters' '[' @@ (',' @@)* ']'"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
type ComponentFilter struct {
|
||||
Field string `parser:"@Ident"`
|
||||
Type string `parser:"'as' @('text' | 'select' | 'date' | 'number')"`
|
||||
Label *string `parser:"('label' @String)?"`
|
||||
}
|
||||
|
||||
func ParseInput(input string) (AST, error) {
|
||||
|
1113
lang/lang_test.go
1113
lang/lang_test.go
File diff suppressed because it is too large
Load Diff
@ -1,131 +0,0 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -1,103 +0,0 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -1,575 +0,0 @@
|
||||
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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -1,548 +0,0 @@
|
||||
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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -1,300 +0,0 @@
|
||||
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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -1,599 +0,0 @@
|
||||
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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
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
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
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
|
||||
}
|
@ -1,69 +0,0 @@
|
||||
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
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
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
|
||||
}
|
@ -1,421 +0,0 @@
|
||||
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
|
||||
}
|
79
langV2/lang.go
Normal file
79
langV2/lang.go
Normal file
@ -0,0 +1,79 @@
|
||||
package langV2
|
||||
|
||||
import (
|
||||
participle "github.com/alecthomas/participle/v2"
|
||||
)
|
||||
|
||||
// Root AST node containing all definitions
|
||||
type AST struct {
|
||||
Definitions []Definition `parser:"@@*"`
|
||||
}
|
||||
|
||||
type Definition struct {
|
||||
Entity *EntityDef `parser:"@@"`
|
||||
Server *ServerDef `parser:"| @@"`
|
||||
Page *PageDef `parser:"| @@"`
|
||||
Component *ComponentDef `parser:"| @@"`
|
||||
}
|
||||
|
||||
type EntityDef struct {
|
||||
Name string `parser:"'entity' @Ident"`
|
||||
Fields []Field `parser:"'{' @@* '}'"`
|
||||
}
|
||||
|
||||
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:"@@?"`
|
||||
}
|
||||
|
||||
type Validation struct {
|
||||
Type string `parser:"'validate' @Ident"`
|
||||
Value *string `parser:"@String?"`
|
||||
}
|
||||
|
||||
type Relationship struct {
|
||||
Type string `parser:"'relates' 'to' @Ident"`
|
||||
Cardinality string `parser:"'as' @('one' | 'many')"`
|
||||
ForeignKey *string `parser:"('via' @String)?"`
|
||||
}
|
||||
|
||||
type ServerDef struct {
|
||||
Name string `parser:"'server' @Ident"`
|
||||
Endpoints []EndpointDef `parser:"'{' @@* '}'"`
|
||||
}
|
||||
|
||||
type EndpointDef struct {
|
||||
Method string `parser:"@('GET'|'POST'|'PUT'|'DELETE')"`
|
||||
Path string `parser:"@String"`
|
||||
EntityRef string `parser:"'entity' @Ident"`
|
||||
}
|
||||
|
||||
type ComponentDef struct {
|
||||
EntityRef string `parser:"@Ident"`
|
||||
ServerRef string `parser:"('from' @Ident)?"`
|
||||
}
|
||||
|
||||
type PageDef struct {
|
||||
Name string `parser:"'page' @Ident"`
|
||||
Components []ComponentDef `parser:"'{' @@* '}'"`
|
||||
}
|
||||
|
||||
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
|
||||
}
|
91
sdk/sdk.go
Normal file
91
sdk/sdk.go
Normal file
@ -0,0 +1,91 @@
|
||||
package sdk
|
||||
|
||||
type Definition struct {
|
||||
Server []Server `json:"server,omitempty"`
|
||||
Entity []Entity `json:"entity,omitempty"`
|
||||
Endpoint []Endpoint `json:"endpoint,omitempty"`
|
||||
Page []Page `json:"page,omitempty"`
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
Name string `json:"name"`
|
||||
Settings []ServerSetting `json:"settings,omitempty"`
|
||||
Endpoints []Endpoint `json:"endpoints,omitempty"` // Optional endpoints for the server
|
||||
}
|
||||
|
||||
type ServerSetting struct {
|
||||
Host string `json:"host,omitempty"`
|
||||
Port int `json:"port,omitempty"`
|
||||
// TODO: Add more settings as needed e.g. TLS, CORS, etc.
|
||||
}
|
||||
|
||||
type Entity struct {
|
||||
Name string `json:"name"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Fields []Field `json:"fields,omitempty"`
|
||||
}
|
||||
|
||||
type Field struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Required bool `json:"required,omitempty"`
|
||||
Unique bool `json:"unique,omitempty"`
|
||||
Index bool `json:"index,omitempty"`
|
||||
Default *string `json:"default,omitempty"`
|
||||
Validations []Validation `json:"validations,omitempty"`
|
||||
Relationship *Relationship `json:"relationship,omitempty"`
|
||||
}
|
||||
|
||||
type Validation struct {
|
||||
Type string `json:"type"`
|
||||
Value *string `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
type Relationship struct {
|
||||
Type string `json:"type"`
|
||||
Cardinality string `json:"cardinality"`
|
||||
ForeignKey *string `json:"foreign_key,omitempty"`
|
||||
Through *string `json:"through,omitempty"`
|
||||
}
|
||||
|
||||
type Endpoint struct {
|
||||
Method string `json:"method"`
|
||||
Path string `json:"path"`
|
||||
Entity Entity `json:"entity,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Auth bool `json:"auth,omitempty"`
|
||||
Permissions []string `json:"permissions,omitempty"`
|
||||
}
|
||||
|
||||
type Page struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Title *string `json:"title,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Auth bool `json:"auth,omitempty"`
|
||||
Permissions []string `json:"permissions,omitempty"`
|
||||
Components []Component `json:"components,omitempty"` // List of components used in the page
|
||||
Styles []string `json:"styles,omitempty"` // List of stylesheets or CSS
|
||||
Script []string `json:"script,omitempty"` // List of scripts or JS files
|
||||
Metadata map[string]string `json:"metadata,omitempty"` // Additional metadata for the page
|
||||
}
|
||||
|
||||
type Component struct {
|
||||
Name string `json:"name"` // Name of the component
|
||||
Description *string `json:"description,omitempty"` // Optional description of the component
|
||||
Props []Prop `json:"props,omitempty"` // List of properties for the component
|
||||
Type string `json:"type"` // Type of the component (e.g., "form", "table", etc.)
|
||||
Entity Entity `json:"entity,omitempty"` // Optional entity this component is associated with
|
||||
Styles []string `json:"styles,omitempty"` // List of stylesheets or CSS for the component
|
||||
Script []string `json:"script,omitempty"` // List of scripts or JS files for the component
|
||||
}
|
||||
|
||||
type Prop struct {
|
||||
Name string `json:"name"` // Name of the property
|
||||
Description *string `json:"description,omitempty"` // Optional description of the property
|
||||
Type string `json:"type"` // Type of the property (e.g., "string", "number", "boolean", etc.)
|
||||
Required bool `json:"required,omitempty"` // Whether the property is required
|
||||
Default *string `json:"default,omitempty"` // Default value for the property
|
||||
Validations []Validation `json:"validations,omitempty"` // List of validations for the property
|
||||
Relationship *Relationship `json:"relationship,omitempty"` // Optional relationship
|
||||
}
|
Reference in New Issue
Block a user