diff --git a/.idea/copilotDiffState.xml b/.idea/copilotDiffState.xml
index cf5d926..0beb31b 100644
--- a/.idea/copilotDiffState.xml
+++ b/.idea/copilotDiffState.xml
@@ -12,6 +12,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/lang/debug.go b/examples/lang/debug.go
index 335a39d..4236175 100644
--- a/examples/lang/debug.go
+++ b/examples/lang/debug.go
@@ -7,8 +7,8 @@ import (
)
func main() {
- // Read the example.masonry file
- content, err := os.ReadFile("example.masonry")
+ // 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
@@ -16,456 +16,133 @@ func main() {
input := string(content)
+ // Try to parse the DSL
ast, err := lang.ParseInput(input)
if err != nil {
- fmt.Printf("Error: %v\n", err)
- } else {
- fmt.Printf("🎉 Successfully parsed enhanced DSL with containers and detailed fields!\n\n")
+ fmt.Printf("❌ Parse Error: %v\n", err)
+ return
+ }
- 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 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
+ }
+ }
- 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)
- }
- if field.Relationship != nil {
- fmt.Printf(" relates to %s as %s", field.Relationship.Type, field.Relationship.Cardinality)
- if field.Relationship.ForeignKey != nil {
- fmt.Printf(" via %s", *field.Relationship.ForeignKey)
- }
- if field.Relationship.Through != nil {
- fmt.Printf(" through %s", *field.Relationship.Through)
- }
- }
- if len(field.Validations) > 0 {
- fmt.Printf(" validates: ")
- for i, val := range field.Validations {
- if i > 0 {
- fmt.Printf(", ")
- }
- fmt.Printf("%s", val.Type)
- if val.Value != nil {
- fmt.Printf("(%s)", *val.Value)
- }
- }
- }
- fmt.Printf("\n")
- }
- fmt.Printf("\n")
+ // 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 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")
-
- for _, param := range endpoint.Params {
- fmt.Printf(" param %s: %s from %s", param.Name, param.Type, param.Source)
- if param.Required {
- fmt.Printf(" (required)")
- }
- fmt.Printf("\n")
- }
-
- if endpoint.Response != nil {
- fmt.Printf(" returns %s", endpoint.Response.Type)
- if endpoint.Response.Format != nil {
- fmt.Printf(" as %s", *endpoint.Response.Format)
- }
- if len(endpoint.Response.Fields) > 0 {
- fmt.Printf(" fields: %v", endpoint.Response.Fields)
- }
- fmt.Printf("\n")
- }
-
- if endpoint.CustomLogic != nil {
- fmt.Printf(" custom logic: %s\n", *endpoint.CustomLogic)
- }
- fmt.Printf("\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)
-
- if page.LayoutType != nil {
- fmt.Printf(" Layout Type: %s\n", *page.LayoutType)
- }
-
- for _, meta := range page.Meta {
- fmt.Printf(" Meta %s: %s\n", meta.Name, meta.Content)
- }
-
- // Display containers
- for _, container := range page.Containers {
- fmt.Printf(" 📦 Container: %s", container.Type)
- if container.Class != nil {
- fmt.Printf(" class=\"%s\"", *container.Class)
- }
- fmt.Printf("\n")
-
- for _, section := range container.Sections {
- fmt.Printf(" 📂 Section: %s", section.Name)
- if section.Class != nil {
- fmt.Printf(" class=\"%s\"", *section.Class)
- }
- fmt.Printf("\n")
-
- for _, comp := range section.Components {
- displayComponent(comp, " ")
- }
-
- for _, panel := range section.Panels {
- fmt.Printf(" 🗂️ Panel: %s trigger=\"%s\"", panel.Name, panel.Trigger)
- if panel.Position != nil {
- fmt.Printf(" position=\"%s\"", *panel.Position)
- }
- fmt.Printf("\n")
- for _, comp := range panel.Components {
- displayComponent(comp, " ")
- }
- }
- }
-
- for _, tab := range container.Tabs {
- fmt.Printf(" 📋 Tab: %s label=\"%s\"", tab.Name, tab.Label)
- if tab.Active {
- fmt.Printf(" (active)")
- }
- fmt.Printf("\n")
- for _, comp := range tab.Components {
- displayComponent(comp, " ")
- }
- }
-
- for _, comp := range container.Components {
- displayComponent(comp, " ")
- }
- }
-
- // Display direct components
- for _, comp := range page.Components {
- displayComponent(comp, " ")
- }
-
- // Display modals
- for _, modal := range page.Modals {
- fmt.Printf(" 🪟 Modal: %s trigger=\"%s\"\n", modal.Name, modal.Trigger)
- for _, comp := range modal.Components {
- displayComponent(comp, " ")
- }
- }
-
- // Display master-detail layout
- if page.MasterDetail != nil {
- fmt.Printf(" 🔄 Master-Detail Layout\n")
- if page.MasterDetail.Master != nil {
- fmt.Printf(" 📋 Master: %s\n", page.MasterDetail.Master.Name)
- for _, comp := range page.MasterDetail.Master.Components {
- displayComponent(comp, " ")
- }
- }
- if page.MasterDetail.Detail != nil {
- fmt.Printf(" 📄 Detail: %s", page.MasterDetail.Detail.Name)
- if page.MasterDetail.Detail.Trigger != nil {
- fmt.Printf(" trigger=\"%s\"", *page.MasterDetail.Detail.Trigger)
- }
- fmt.Printf("\n")
- for _, comp := range page.MasterDetail.Detail.Components {
- displayComponent(comp, " ")
- }
- }
- }
- fmt.Printf("\n")
+ 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")
}
-func displayComponent(comp lang.Component, indent string) {
- fmt.Printf("%s🧩 Component: %s", indent, comp.Type)
- if comp.Entity != nil {
- fmt.Printf(" for %s", *comp.Entity)
- }
- fmt.Printf("\n")
-
- // Process elements to extract fields, config, conditions, sections, and actions
- var fields []lang.ComponentField
- var config []lang.ComponentAttr
- var conditions []lang.WhenCondition
- var sections []lang.ComponentSection
- var actions []lang.ComponentButtonAttr
-
- for _, element := range comp.Elements {
- if element.Field != nil {
- fields = append(fields, *element.Field)
- }
- if element.Config != nil {
- config = append(config, *element.Config)
- }
- if element.Condition != nil {
- conditions = append(conditions, *element.Condition)
- }
+// Helper function to count nested sections recursively
+func countNestedSections(section lang.Section) int {
+ count := 0
+ for _, element := range section.Elements {
if element.Section != nil {
- sections = append(sections, *element.Section)
+ count++
+ count += countNestedSections(*element.Section)
}
- if element.Action != nil {
- actions = append(actions, *element.Action)
+ if element.Component != nil {
+ for _, compElement := range element.Component.Elements {
+ if compElement.Section != nil {
+ count++
+ count += countNestedSections(*compElement.Section)
+ }
+ }
}
}
-
- // Display config attributes
- for _, attr := range config {
- if attr.Fields != nil {
- fmt.Printf("%s fields: %v\n", indent, attr.Fields.Fields)
- }
- if attr.Actions != nil {
- fmt.Printf("%s actions: ", indent)
- 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("%s data from: %s\n", indent, attr.DataSource.Endpoint)
- }
- if attr.Style != nil {
- fmt.Printf("%s style: %s", indent, *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("%s pagination: enabled", indent)
- if attr.Pagination.PageSize != nil {
- fmt.Printf(" size %d", *attr.Pagination.PageSize)
- }
- fmt.Printf("\n")
- }
- if attr.Filters != nil {
- fmt.Printf("%s filters: ", indent)
- 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("%s validation: enabled\n", indent)
- }
- }
-
- // Display enhanced fields
- for _, field := range fields {
- fmt.Printf("%s 📝 Field: %s type %s", indent, field.Name, field.Type)
-
- // Process attributes
- for _, attr := range field.Attributes {
- if attr.Label != nil {
- fmt.Printf(" label=\"%s\"", *attr.Label)
- }
- if attr.Placeholder != nil {
- fmt.Printf(" placeholder=\"%s\"", *attr.Placeholder)
- }
- if attr.Required {
- fmt.Printf(" (required)")
- }
- if attr.Sortable {
- fmt.Printf(" (sortable)")
- }
- if attr.Searchable {
- fmt.Printf(" (searchable)")
- }
- if attr.Thumbnail {
- fmt.Printf(" (thumbnail)")
- }
- if attr.Default != nil {
- fmt.Printf(" default=\"%s\"", *attr.Default)
- }
- if len(attr.Options) > 0 {
- fmt.Printf(" options=%v", attr.Options)
- }
- if attr.Accept != nil {
- fmt.Printf(" accept=\"%s\"", *attr.Accept)
- }
- if attr.Rows != nil {
- fmt.Printf(" rows=%d", *attr.Rows)
- }
- if attr.Format != nil {
- fmt.Printf(" format=\"%s\"", *attr.Format)
- }
- if attr.Size != nil {
- fmt.Printf(" size=\"%s\"", *attr.Size)
- }
- if attr.Source != nil {
- fmt.Printf(" source=\"%s\"", *attr.Source)
- }
- if attr.Display != nil {
- fmt.Printf(" display=\"%s\"", *attr.Display)
- }
- if attr.Value != nil {
- fmt.Printf(" value=\"%s\"", *attr.Value)
- }
- if attr.Relates != nil {
- fmt.Printf(" relates to %s", attr.Relates.Type)
- }
- if attr.Validation != nil {
- fmt.Printf(" validates: %s", attr.Validation.Type)
- if attr.Validation.Value != nil {
- fmt.Printf("(%s)", *attr.Validation.Value)
- }
- }
- }
- fmt.Printf("\n")
- }
-
- // Display conditions
- for _, condition := range conditions {
- fmt.Printf("%s 🔀 When %s %s \"%s\":\n", indent, condition.Field, condition.Operator, condition.Value)
- for _, field := range condition.Fields {
- fmt.Printf("%s 📝 Field: %s type %s", indent, field.Name, field.Type)
- // Process attributes for conditional fields
- for _, attr := range field.Attributes {
- if attr.Label != nil {
- fmt.Printf(" label=\"%s\"", *attr.Label)
- }
- if len(attr.Options) > 0 {
- fmt.Printf(" options=%v", attr.Options)
- }
- }
- fmt.Printf("\n")
- }
- for _, section := range condition.Sections {
- fmt.Printf("%s 📂 Section: %s\n", indent, section.Name)
- }
- for _, button := range condition.Buttons {
- fmt.Printf("%s 🔘 Button: %s label=\"%s\"", indent, button.Name, button.Label)
- if button.Style != nil {
- fmt.Printf(" style=\"%s\"", *button.Style)
- }
- fmt.Printf("\n")
- }
- }
-
- // Display sections
- for _, section := range sections {
- fmt.Printf("%s 📂 Section: %s", indent, section.Name)
- if section.Class != nil {
- fmt.Printf(" class=\"%s\"", *section.Class)
- }
- fmt.Printf("\n")
- for _, field := range section.Fields {
- fmt.Printf("%s 📝 Field: %s type %s", indent, field.Name, field.Type)
- // Process attributes for section fields
- for _, attr := range field.Attributes {
- if attr.Label != nil {
- fmt.Printf(" label=\"%s\"", *attr.Label)
- }
- }
- fmt.Printf("\n")
- }
- for _, button := range section.Buttons {
- fmt.Printf("%s 🔘 Button: %s label=\"%s\"", indent, button.Name, button.Label)
- if button.Style != nil {
- fmt.Printf(" style=\"%s\"", *button.Style)
- }
- if button.Via != nil {
- fmt.Printf(" via=\"%s\"", *button.Via)
- }
- fmt.Printf("\n")
- }
- for _, action := range section.Actions {
- fmt.Printf("%s ⚡ Action: %s label=\"%s\"", indent, action.Name, action.Label)
- if action.Style != nil {
- fmt.Printf(" style=\"%s\"", *action.Style)
- }
- fmt.Printf("\n")
- }
- }
-
- // Display direct actions/buttons
- for _, action := range actions {
- fmt.Printf("%s 🔘 Button: %s label=\"%s\"", indent, action.Name, action.Label)
- if action.Style != nil {
- fmt.Printf(" style=\"%s\"", *action.Style)
- }
- if action.Icon != nil {
- fmt.Printf(" icon=\"%s\"", *action.Icon)
- }
- if action.Loading != nil {
- fmt.Printf(" loading=\"%s\"", *action.Loading)
- }
- if action.Disabled != nil {
- fmt.Printf(" disabled when %s", *action.Disabled)
- }
- if action.Confirm != nil {
- fmt.Printf(" confirm=\"%s\"", *action.Confirm)
- }
- if action.Target != nil {
- fmt.Printf(" target=\"%s\"", *action.Target)
- }
- if action.Position != nil {
- fmt.Printf(" position=\"%s\"", *action.Position)
- }
- if action.Via != nil {
- fmt.Printf(" via=\"%s\"", *action.Via)
- }
- fmt.Printf("\n")
- }
+ return count
}
diff --git a/examples/lang/example.masonry b/examples/lang/example.masonry
index 0a9a5ac..15a1299 100644
--- a/examples/lang/example.masonry
+++ b/examples/lang/example.masonry
@@ -1,186 +1,295 @@
-// Enhanced Masonry DSL example demonstrating new features
-// This shows the comprehensive language structure with containers, detailed fields, and layouts
+// 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
+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 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 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 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()"
+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 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 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 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 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 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]
+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 container layout
-page UserManagement at "/admin/users" layout AdminLayout title "User Management" auth
- meta description "Manage system users"
- meta keywords "users, admin, management"
+// 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"
- container main class "grid grid-cols-3 gap-4"
- section sidebar class "col-span-1"
- component UserStats for User
- data from "/users/stats"
+ 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 content class "col-span-2" {
+ component UserTable for User {
+ fields [email, name, role, created_at]
+ actions [edit, delete, view]
+ data from "/users"
+ }
- panel UserEditPanel for User trigger "edit" position "slide-right"
- 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"
+ 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
+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"
- field permissions type multiselect label "Permissions"
- options ["users.manage", "posts.manage", "system.config"]
+ when role equals "admin" {
+ component AdminPermissions {
+ field permissions type multiselect label "Permissions" {
+ options ["users.manage", "posts.manage", "system.config"]
+ }
+ }
+ }
- section actions
- button save label "Save User" style "primary" loading "Saving..." via "/users"
- button cancel label "Cancel" style "secondary"
+ 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
-page Dashboard at "/dashboard" layout MainLayout title "Dashboard"
- container tabs
- tab overview label "Overview" active
- component StatsCards
- component RecentActivity
+// 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
+ }
- tab users label "Users"
- component UserTable for User
- data from "/users"
+ section users type tab label "Users" {
+ component UserTable for User {
+ data from "/users"
+ }
+ }
- tab posts label "Posts"
- component PostTable for Post
- data from "/posts"
+ section posts type tab label "Posts" {
+ component PostTable for Post {
+ data from "/posts"
+ }
+ }
+ }
- modal CreateUserModal 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"
+ 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
-page PostManagement at "/admin/posts" layout AdminLayout title "Post Management" auth
- layout "master-detail"
+// 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"
+ }
+ }
- master PostList
- component Table 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
+ }
+ }
- detail PostEditor trigger "edit"
- component Form for Post
- section basic class "mb-4"
- field title type text label "Post Title" required
- field content type richtext label "Content" required
-
- section metadata class "grid grid-cols-2 gap-4"
- 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"
+ 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"
+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 when more control is needed
-page DetailedUserList at "/admin/users/detailed" layout AdminLayout title "Detailed User Management" auth
- component DetailedTable for User
- field email type text label "Email Address"
- field name type text label "Full Name"
+// 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"
+ }
+}
- data from "/users"
- pagination size 20
+// 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
+ }
-// Conditional rendering example
-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"]
+ section userInfo type panel trigger "profile" position "bottom" {
+ component UserProfile
+ }
+ }
- when role equals "admin"
- field permissions type multiselect label "Admin Permissions"
- options ["users.manage", "posts.manage", "system.config"]
+ section content type container class "flex-1" {
+ section header class "h-16 border-b" {
+ component PageHeader
+ }
- when role equals "moderator"
- field moderation_level type select label "Moderation Level"
- options ["basic", "advanced", "full"]
+ 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 actions
- button save label "Save User" style "primary" loading "Saving..."
- button cancel label "Cancel" style "secondary"
+ 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"
+ }
+ }
+ }
+}
diff --git a/go.mod b/go.mod
index d90a1cf..3bab0aa 100644
--- a/go.mod
+++ b/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
diff --git a/go.sum b/go.sum
index 55ed6ea..5cf5057 100644
--- a/go.sum
+++ b/go.sum
@@ -1,7 +1,13 @@
+github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
+github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/participle/v2 v2.1.4 h1:W/H79S8Sat/krZ3el6sQMvMaahJ+XcM9WSI2naI7w2U=
github.com/alecthomas/participle/v2 v2.1.4/go.mod h1:8tqVbpTX20Ru4NfYQgZf4mP18eXPTBViyMWiArNEgGI=
+github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
+github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
+github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
diff --git a/lang/lang.go b/lang/lang.go
index 3e56235..4ac2346 100644
--- a/lang/lang.go
+++ b/lang/lang.go
@@ -20,7 +20,7 @@ type Definition struct {
// Clean server syntax
type Server struct {
Name string `parser:"'server' @Ident"`
- Settings []ServerSetting `parser:"@@*"`
+ Settings []ServerSetting `parser:"('{' @@* '}')?"` // Block-delimited settings for consistency
}
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:"@@*"`
+ Fields []Field `parser:"('{' @@* '}')?"` // Block-delimited fields for consistency
}
// 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:"@@*"`
+ Params []EndpointParam `parser:"('{' @@*"` // Block-delimited parameters
Response *ResponseSpec `parser:"@@?"`
- CustomLogic *string `parser:"('custom' @String)?"`
+ CustomLogic *string `parser:"('custom' @String)? '}')?"` // Close block after all content
}
// Clean parameter syntax
@@ -88,20 +88,17 @@ type ResponseSpec struct {
Fields []string `parser:"('fields' '[' @Ident (',' @Ident)* ']')?"`
}
-// Enhanced Page definitions with layout containers and composition
+// Enhanced Page definitions with unified section model
type Page struct {
- Name string `parser:"'page' @Ident"`
- Path string `parser:"'at' @String"`
- Layout string `parser:"'layout' @Ident"`
- Title *string `parser:"('title' @String)?"`
- Description *string `parser:"('desc' @String)?"`
- Auth bool `parser:"@'auth'?"`
- LayoutType *string `parser:"('layout' @String)?"`
- Meta []MetaTag `parser:"@@*"`
- MasterDetail *MasterDetail `parser:"@@?"`
- Containers []Container `parser:"@@*"`
- Components []Component `parser:"@@*"`
- Modals []Modal `parser:"@@*"`
+ Name string `parser:"'page' @Ident"`
+ Path string `parser:"'at' @String"`
+ Layout string `parser:"'layout' @Ident"`
+ Title *string `parser:"('title' @String)?"`
+ Description *string `parser:"('desc' @String)?"`
+ Auth bool `parser:"@'auth'?"`
+ Meta []MetaTag `parser:"('{' @@*"` // Block-delimited content
+ Sections []Section `parser:"@@*"` // Unified sections replace containers/tabs/panels/modals
+ Components []Component `parser:"@@* '}')?"` // Direct components within the block
}
// Meta tags for SEO
@@ -110,85 +107,68 @@ type MetaTag struct {
Content string `parser:"@String"`
}
-// Container types for layout organization
-type Container struct {
- Type string `parser:"'container' @Ident"`
- Class *string `parser:"('class' @String)?"`
- Sections []Section `parser:"@@*"`
- Tabs []Tab `parser:"@@*"`
- Components []Component `parser:"@@*"`
-}
-
-// Sections within containers
+// Unified Section type that replaces Container, Tab, Panel, Modal, MasterDetail
type Section struct {
- Name string `parser:"'section' @Ident"`
- Class *string `parser:"('class' @String)?"`
- Components []Component `parser:"@@*"`
- Panels []Panel `parser:"@@*"`
+ 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
}
-// Tab definitions for tabbed interfaces
-type Tab struct {
- Name string `parser:"'tab' @Ident"`
- Label string `parser:"'label' @String"`
- Active bool `parser:"@'active'?"`
- Components []Component `parser:"@@*"`
+// New unified element type for sections
+type SectionElement struct {
+ Attribute *SectionAttribute `parser:"@@"`
+ Component *Component `parser:"| @@"`
+ Section *Section `parser:"| @@"`
+ When *WhenCondition `parser:"| @@"`
}
-// Panel definitions for slide-out or overlay interfaces
-type Panel struct {
- Name string `parser:"'panel' @Ident"`
- Entity *string `parser:"('for' @Ident)?"`
- Trigger string `parser:"'trigger' @String"`
- Position *string `parser:"('position' @String)?"`
- Components []Component `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)"`
}
-// Modal definitions
-type Modal struct {
- Name string `parser:"'modal' @Ident"`
- Trigger string `parser:"'trigger' @String"`
- Components []Component `parser:"@@*"`
-}
-
-// Master-detail layout components - simplified parsing
-type MasterDetail struct {
- Master *MasterSection `parser:"'master' @@"`
- Detail *DetailSection `parser:"'detail' @@"`
-}
-
-type MasterSection struct {
- Name string `parser:"@Ident"`
- Components []Component `parser:"@@*"`
-}
-
-type DetailSection struct {
- Name string `parser:"@Ident"`
- Trigger *string `parser:"('trigger' @String)?"`
- Components []Component `parser:"@@*"`
-}
-
-// Enhanced Component definitions with detailed field configurations
+// Simplified Component with unified attributes - reordered for better parsing
type Component struct {
Type string `parser:"'component' @Ident"`
Entity *string `parser:"('for' @Ident)?"`
- Elements []ComponentElement `parser:"@@*"`
+ Elements []ComponentElement `parser:"('{' @@* '}')?"` // Parse everything inside the block
}
-// Union type for component elements to allow flexible ordering
+// Enhanced ComponentElement with recursive section support - now includes attributes
type ComponentElement struct {
- Config *ComponentAttr `parser:"@@"`
- Field *ComponentField `parser:"| @@"`
- Condition *WhenCondition `parser:"| @@"`
- Section *ComponentSection `parser:"| @@"`
- Action *ComponentButtonAttr `parser:"| @@"`
+ Attribute *ComponentAttr `parser:"@@"` // Component attributes can be inside the block
+ Field *ComponentField `parser:"| @@"`
+ Section *Section `parser:"| @@"` // Sections can be nested in components
+ Button *ComponentButton `parser:"| @@"`
+ When *WhenCondition `parser:"| @@"`
+}
+
+// Simplified component attributes using key-value pattern - reordered for precedence
+type ComponentAttr struct {
+ DataSource *string `parser:"('data' 'from' @String)"`
+ Fields []string `parser:"| ('fields' '[' @Ident (',' @Ident)* ']')"`
+ Actions []string `parser:"| ('actions' '[' @Ident (',' @Ident)* ']')"`
+ Style *string `parser:"| ('style' @String)"`
+ Classes *string `parser:"| ('classes' @String)"`
+ PageSize *int `parser:"| ('pagination' 'size' @Int)"`
+ Validate bool `parser:"| @'validate'"`
}
// Enhanced component field with detailed configuration using flexible attributes
type ComponentField struct {
Name string `parser:"'field' @Ident"`
Type string `parser:"'type' @Ident"`
- Attributes []ComponentFieldAttribute `parser:"@@*"`
+ Attributes []ComponentFieldAttribute `parser:"@@* ('{' @@* '}')?"` // Support both inline and block attributes
}
// Flexible field attribute system
@@ -223,96 +203,67 @@ type ComponentValidation struct {
Value *string `parser:"@String?"`
}
-// Conditional rendering
+// 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 []ComponentSection `parser:"@@*"`
- Buttons []ComponentButtonAttr `parser:"@@*"`
+ 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 sections for grouping
-type ComponentSection struct {
- Name string `parser:"'section' @Ident"`
- Class *string `parser:"('class' @String)?"`
- Fields []ComponentField `parser:"@@*"`
- Buttons []ComponentButtonAttr `parser:"@@*"`
- Actions []ComponentButtonAttr `parser:"@@*"`
+// Simplified button with flexible attribute ordering
+type ComponentButton struct {
+ Name string `parser:"'button' @Ident"`
+ Label string `parser:"'label' @String"`
+ Attributes []ComponentButtonAttr `parser:"@@*"`
}
-// Enhanced component buttons/actions with detailed configuration
+// Flexible button attribute system - each attribute is a separate alternative
type ComponentButtonAttr struct {
- Name string `parser:"'button' @Ident"`
- Label string `parser:"'label' @String"`
- Style *string `parser:"('style' @String)?"`
- Icon *string `parser:"('icon' @String)?"`
- Loading *string `parser:"('loading' @String)?"`
- Disabled *string `parser:"('disabled' 'when' @Ident)?"`
- Confirm *string `parser:"('confirm' @String)?"`
- Target *string `parser:"('target' @Ident)?"`
- Position *string `parser:"('position' @String)?"`
- Via *string `parser:"('via' @String)?"`
+ Style *ComponentButtonStyle `parser:"@@"`
+ Icon *ComponentButtonIcon `parser:"| @@"`
+ Loading *ComponentButtonLoading `parser:"| @@"`
+ Disabled *ComponentButtonDisabled `parser:"| @@"`
+ Confirm *ComponentButtonConfirm `parser:"| @@"`
+ Target *ComponentButtonTarget `parser:"| @@"`
+ Position *ComponentButtonPosition `parser:"| @@"`
+ Via *ComponentButtonVia `parser:"| @@"`
}
-// Component attributes and configurations (keeping existing for backward compatibility)
-type ComponentAttr struct {
- Fields *ComponentFields `parser:"@@"`
- Actions *ComponentActions `parser:"| @@"`
- DataSource *ComponentDataSource `parser:"| @@"`
- Style *ComponentStyle `parser:"| @@"`
- Pagination *ComponentPagination `parser:"| @@"`
- Filters *ComponentFilters `parser:"| @@"`
- Validation bool `parser:"| @'validate'"`
+// Individual button attribute types
+type ComponentButtonStyle struct {
+ Value string `parser:"'style' @String"`
}
-// Component field specification (simple version for backward compatibility)
-type ComponentFields struct {
- Fields []string `parser:"'fields' '[' @Ident (',' @Ident)* ']'"`
+type ComponentButtonIcon struct {
+ Value string `parser:"'icon' @String"`
}
-// Enhanced component actions
-type ComponentActions struct {
- Actions []ComponentAction `parser:"'actions' '[' @@ (',' @@)* ']'"`
+type ComponentButtonLoading struct {
+ Value string `parser:"'loading' @String"`
}
-type ComponentAction struct {
- Name string `parser:"@Ident"`
- Label *string `parser:"('label' @String)?"`
- Icon *string `parser:"('icon' @String)?"`
- Style *string `parser:"('style' @String)?"`
- Endpoint *string `parser:"('via' @String)?"`
- Target *string `parser:"('target' @Ident)?"`
- Position *string `parser:"('position' @String)?"`
- Confirm *string `parser:"('confirm' @String)?"`
+type ComponentButtonDisabled struct {
+ Value string `parser:"'disabled' 'when' @Ident"`
}
-// Data source configuration (can reference endpoints)
-type ComponentDataSource struct {
- Endpoint string `parser:"'data' 'from' @String"`
+type ComponentButtonConfirm struct {
+ Value string `parser:"'confirm' @String"`
}
-// Component styling
-type ComponentStyle struct {
- Theme *string `parser:"'style' @Ident"`
- Classes []string `parser:"('classes' '[' @String (',' @String)* ']')?"`
+type ComponentButtonTarget struct {
+ Value string `parser:"'target' @Ident"`
}
-// Pagination configuration
-type ComponentPagination struct {
- PageSize *int `parser:"'pagination' ('size' @Int)?"`
+type ComponentButtonPosition struct {
+ Value string `parser:"'position' @String"`
}
-// Filter specifications
-type ComponentFilters struct {
- Filters []ComponentFilter `parser:"'filters' '[' @@ (',' @@)* ']'"`
-}
-
-type ComponentFilter struct {
- Field string `parser:"@Ident"`
- Type string `parser:"'as' @('text' | 'select' | 'date' | 'number')"`
- Label *string `parser:"('label' @String)?"`
+type ComponentButtonVia struct {
+ Value string `parser:"'via' @String"`
}
func ParseInput(input string) (AST, error) {
diff --git a/lang/lang_test.go b/lang/lang_test.go
index 607a16b..7dd9be0 100644
--- a/lang/lang_test.go
+++ b/lang/lang_test.go
@@ -1,12 +1,3 @@
package lang
-// Parser tests have been organized into specialized files:
-// - parser_server_test.go - Server definition parsing tests
-// - parser_entity_test.go - Entity definition parsing tests
-// - parser_page_test.go - Page definition parsing tests
-// - parser_component_test.go - Component and field parsing tests
-// - parser_advanced_test.go - Advanced features (conditionals, tabs, modals, master-detail)
-// - parser_test_helpers.go - Shared helper functions and comparison utilities
-//
-// This organization allows for easier maintenance and addition of new test categories
-// for future language interpretation features.
+// Various parts of the language and parser are tested in specialized files
diff --git a/lang/parser_advanced_test.go b/lang/parser_advanced_test.go
deleted file mode 100644
index b4d1d8f..0000000
--- a/lang/parser_advanced_test.go
+++ /dev/null
@@ -1,304 +0,0 @@
-package lang
-
-import (
- "testing"
-)
-
-func TestAdvancedParsingFeatures(t *testing.T) {
- tests := []struct {
- name string
- input string
- want AST
- wantErr bool
- }{
- {
- name: "component with conditional rendering",
- input: `page ConditionalForm at "/conditional" layout MainLayout
- component UserForm for User
- field role type select options ["admin", "user"]
-
- when role equals "admin"
- field permissions type multiselect label "Admin Permissions"
- options ["users.manage", "posts.manage"]`,
- want: AST{
- Definitions: []Definition{
- {
- Page: &Page{
- Name: "ConditionalForm",
- Path: "/conditional",
- Layout: "MainLayout",
- Components: []Component{
- {
- Type: "UserForm",
- Entity: stringPtr("User"),
- Elements: []ComponentElement{
- {
- Field: &ComponentField{
- Name: "role",
- Type: "select",
- Attributes: []ComponentFieldAttribute{
- {Options: []string{"admin", "user"}},
- },
- },
- },
- {
- Condition: &WhenCondition{
- Field: "role",
- Operator: "equals",
- Value: "admin",
- Fields: []ComponentField{
- {
- Name: "permissions",
- Type: "multiselect",
- Attributes: []ComponentFieldAttribute{
- {Label: stringPtr("Admin Permissions")},
- {Options: []string{"users.manage", "posts.manage"}},
- },
- },
- },
- },
- },
- },
- },
- },
- },
- },
- },
- },
- wantErr: false,
- },
- {
- name: "page with tabs container",
- input: `page Dashboard at "/dashboard" layout MainLayout
- container tabs
- tab overview label "Overview" active
- component StatsCards
- tab users label "Users"
- component UserTable for User`,
- want: AST{
- Definitions: []Definition{
- {
- Page: &Page{
- Name: "Dashboard",
- Path: "/dashboard",
- Layout: "MainLayout",
- Containers: []Container{
- {
- Type: "tabs",
- Tabs: []Tab{
- {
- Name: "overview",
- Label: "Overview",
- Active: true,
- Components: []Component{
- {
- Type: "StatsCards",
- Elements: []ComponentElement{},
- },
- },
- },
- {
- Name: "users",
- Label: "Users",
- Components: []Component{
- {
- Type: "UserTable",
- Entity: stringPtr("User"),
- Elements: []ComponentElement{},
- },
- },
- },
- },
- },
- },
- },
- },
- },
- },
- wantErr: false,
- },
- {
- name: "page with modal",
- input: `page UserList at "/users" layout MainLayout
- modal CreateUserModal trigger "create-user"
- component UserForm for User
- field email type text required
- button save label "Create" via "/users"`,
- want: AST{
- Definitions: []Definition{
- {
- Page: &Page{
- Name: "UserList",
- Path: "/users",
- Layout: "MainLayout",
- Modals: []Modal{
- {
- Name: "CreateUserModal",
- Trigger: "create-user",
- Components: []Component{
- {
- Type: "UserForm",
- Entity: stringPtr("User"),
- Elements: []ComponentElement{
- {
- Field: &ComponentField{
- Name: "email",
- Type: "text",
- Attributes: []ComponentFieldAttribute{
- {Required: true},
- },
- },
- },
- {
- Action: &ComponentButtonAttr{
- Name: "save",
- Label: "Create",
- Via: stringPtr("/users"),
- },
- },
- },
- },
- },
- },
- },
- },
- },
- },
- },
- wantErr: false,
- },
- {
- name: "page with master-detail layout",
- input: `page PostManagement at "/admin/posts" layout AdminLayout
- layout "master-detail"
-
- master PostList
- component Table for Post
- field title type text sortable
-
- detail PostEditor trigger "edit"
- component Form for Post
- field title type text required`,
- want: AST{
- Definitions: []Definition{
- {
- Page: &Page{
- Name: "PostManagement",
- Path: "/admin/posts",
- Layout: "AdminLayout",
- LayoutType: stringPtr("master-detail"),
- MasterDetail: &MasterDetail{
- Master: &MasterSection{
- Name: "PostList",
- Components: []Component{
- {
- Type: "Table",
- Entity: stringPtr("Post"),
- Elements: []ComponentElement{
- {
- Field: &ComponentField{
- Name: "title",
- Type: "text",
- Attributes: []ComponentFieldAttribute{
- {Sortable: true},
- },
- },
- },
- },
- },
- },
- },
- Detail: &DetailSection{
- Name: "PostEditor",
- Trigger: stringPtr("edit"),
- Components: []Component{
- {
- Type: "Form",
- Entity: stringPtr("Post"),
- Elements: []ComponentElement{
- {
- Field: &ComponentField{
- Name: "title",
- Type: "text",
- Attributes: []ComponentFieldAttribute{
- {Required: true},
- },
- },
- },
- },
- },
- },
- },
- },
- },
- },
- },
- },
- wantErr: false,
- },
- {
- name: "invalid syntax should error",
- input: `invalid syntax here`,
- want: AST{},
- wantErr: true,
- },
- }
-
- 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)
- }
- })
- }
-}
-
-// Test for conditional rendering
-func TestConditionalRendering(t *testing.T) {
- input := `page ConditionalTest at "/test" layout MainLayout
- component Form for User
- field role type select options ["admin", "user"]
-
- when role equals "admin"
- field permissions type multiselect options ["manage_users", "manage_posts"]
- section admin_tools
- field audit_log type toggle`
-
- ast, err := ParseInput(input)
- if err != nil {
- t.Fatalf("Failed to parse: %v", err)
- }
-
- component := ast.Definitions[0].Page.Components[0]
-
- // Extract conditions from elements
- var conditions []WhenCondition
- for _, element := range component.Elements {
- if element.Condition != nil {
- conditions = append(conditions, *element.Condition)
- }
- }
-
- if len(conditions) != 1 {
- t.Errorf("Expected 1 condition, got %d", len(conditions))
- }
-
- condition := conditions[0]
- if condition.Field != "role" || condition.Operator != "equals" || condition.Value != "admin" {
- t.Error("Condition parameters incorrect")
- }
-
- if len(condition.Fields) != 1 {
- t.Errorf("Expected 1 conditional field, got %d", len(condition.Fields))
- }
-
- if len(condition.Sections) != 1 {
- t.Errorf("Expected 1 conditional section, got %d", len(condition.Sections))
- }
-}
diff --git a/lang/parser_component_test.go b/lang/parser_component_test.go
deleted file mode 100644
index 9e79716..0000000
--- a/lang/parser_component_test.go
+++ /dev/null
@@ -1,344 +0,0 @@
-package lang
-
-import (
- "testing"
-)
-
-func TestParseComponentDefinitions(t *testing.T) {
- tests := []struct {
- name string
- input string
- want AST
- wantErr bool
- }{
- {
- name: "component with enhanced field configurations",
- input: `page UserForm at "/users/new" layout MainLayout
- component Form for User
- field email type text label "Email Address" placeholder "Enter email" required validate email
- field role type select options ["admin", "user"] default "user"
- field avatar type file accept "image/*"
- field bio type textarea rows 4`,
- want: AST{
- Definitions: []Definition{
- {
- Page: &Page{
- Name: "UserForm",
- Path: "/users/new",
- Layout: "MainLayout",
- Components: []Component{
- {
- Type: "Form",
- Entity: stringPtr("User"),
- Elements: []ComponentElement{
- {
- Field: &ComponentField{
- Name: "email",
- Type: "text",
- Attributes: []ComponentFieldAttribute{
- {Label: stringPtr("Email Address")},
- {Placeholder: stringPtr("Enter email")},
- {Required: true},
- {Validation: &ComponentValidation{Type: "email"}},
- },
- },
- },
- {
- Field: &ComponentField{
- Name: "role",
- Type: "select",
- Attributes: []ComponentFieldAttribute{
- {Options: []string{"admin", "user"}},
- {Default: stringPtr("user")},
- },
- },
- },
- {
- Field: &ComponentField{
- Name: "avatar",
- Type: "file",
- Attributes: []ComponentFieldAttribute{
- {Accept: stringPtr("image/*")},
- },
- },
- },
- {
- Field: &ComponentField{
- Name: "bio",
- Type: "textarea",
- Attributes: []ComponentFieldAttribute{
- {Rows: intPtr(4)},
- },
- },
- },
- },
- },
- },
- },
- },
- },
- },
- wantErr: false,
- },
- {
- name: "component with sections and buttons",
- input: `page FormWithSections at "/form" layout MainLayout
- component UserForm for User
- section basic class "mb-4"
- field email type text required
- field name type text required
-
- section actions
- button save label "Save" style "primary" loading "Saving..." via "/users"
- button cancel label "Cancel" style "secondary"`,
- want: AST{
- Definitions: []Definition{
- {
- Page: &Page{
- Name: "FormWithSections",
- Path: "/form",
- Layout: "MainLayout",
- Components: []Component{
- {
- Type: "UserForm",
- Entity: stringPtr("User"),
- Elements: []ComponentElement{
- {
- Section: &ComponentSection{
- Name: "basic",
- Class: stringPtr("mb-4"),
- Fields: []ComponentField{
- {
- Name: "email",
- Type: "text",
- Attributes: []ComponentFieldAttribute{
- {Required: true},
- },
- },
- {
- Name: "name",
- Type: "text",
- Attributes: []ComponentFieldAttribute{
- {Required: true},
- },
- },
- },
- },
- },
- {
- Section: &ComponentSection{
- Name: "actions",
- Buttons: []ComponentButtonAttr{
- {
- Name: "save",
- Label: "Save",
- Style: stringPtr("primary"),
- Loading: stringPtr("Saving..."),
- Via: stringPtr("/users"),
- },
- {
- Name: "cancel",
- Label: "Cancel",
- Style: stringPtr("secondary"),
- },
- },
- },
- },
- },
- },
- },
- },
- },
- },
- },
- wantErr: false,
- },
- {
- name: "detailed field configurations",
- input: `page DetailedTable at "/detailed" layout MainLayout
- component Table for User
- field email type text label "Email" sortable searchable
- field avatar type image thumbnail size "32x32"
- field created_at type datetime format "MMM dd, yyyy"
- field author_id type autocomplete source "/users" display "name" value "id"`,
- want: AST{
- Definitions: []Definition{
- {
- Page: &Page{
- Name: "DetailedTable",
- Path: "/detailed",
- Layout: "MainLayout",
- Components: []Component{
- {
- Type: "Table",
- Entity: stringPtr("User"),
- Elements: []ComponentElement{
- {
- Field: &ComponentField{
- Name: "email",
- Type: "text",
- Attributes: []ComponentFieldAttribute{
- {Label: stringPtr("Email")},
- {Sortable: true},
- {Searchable: true},
- },
- },
- },
- {
- Field: &ComponentField{
- Name: "avatar",
- Type: "image",
- Attributes: []ComponentFieldAttribute{
- {Thumbnail: true},
- {Size: stringPtr("32x32")},
- },
- },
- },
- {
- Field: &ComponentField{
- Name: "created_at",
- Type: "datetime",
- Attributes: []ComponentFieldAttribute{
- {Format: stringPtr("MMM dd, yyyy")},
- },
- },
- },
- {
- Field: &ComponentField{
- Name: "author_id",
- Type: "autocomplete",
- Attributes: []ComponentFieldAttribute{
- {Source: stringPtr("/users")},
- {Display: stringPtr("name")},
- {Value: stringPtr("id")},
- },
- },
- },
- },
- },
- },
- },
- },
- },
- },
- 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)
- }
- })
- }
-}
-
-// Test specifically for enhanced field features - updated for new structure
-func TestEnhancedFieldParsing(t *testing.T) {
- input := `page TestPage at "/test" layout MainLayout
- component Form for User
- field email type text label "Email" placeholder "Enter email" required validate email
- field role type select label "Role" options ["admin", "user"] default "user"
- field avatar type file accept "image/*"
- field bio type textarea rows 5 placeholder "Tell us about yourself"`
-
- ast, err := ParseInput(input)
- if err != nil {
- t.Fatalf("Failed to parse: %v", err)
- }
-
- page := ast.Definitions[0].Page
- component := page.Components[0]
-
- // Extract fields from elements
- var fields []ComponentField
- for _, element := range component.Elements {
- if element.Field != nil {
- fields = append(fields, *element.Field)
- }
- }
-
- if len(fields) != 4 {
- t.Errorf("Expected 4 fields, got %d", len(fields))
- }
-
- // Test basic field structure
- emailField := fields[0]
- if emailField.Name != "email" || emailField.Type != "text" {
- t.Errorf("Email field incorrect: name=%s, type=%s", emailField.Name, emailField.Type)
- }
-
- // Test that attributes are populated
- if len(emailField.Attributes) == 0 {
- t.Error("Email field should have attributes")
- }
-
- // Test role field
- roleField := fields[1]
- if roleField.Name != "role" || roleField.Type != "select" {
- t.Errorf("Role field incorrect: name=%s, type=%s", roleField.Name, roleField.Type)
- }
-
- // Test file field
- fileField := fields[2]
- if fileField.Name != "avatar" || fileField.Type != "file" {
- t.Errorf("File field incorrect: name=%s, type=%s", fileField.Name, fileField.Type)
- }
-
- // Test textarea field
- textareaField := fields[3]
- if textareaField.Name != "bio" || textareaField.Type != "textarea" {
- t.Errorf("Textarea field incorrect: name=%s, type=%s", textareaField.Name, textareaField.Type)
- }
-}
-
-// Test for config attributes after fields (reproduces parsing issue)
-func TestConfigAfterFields(t *testing.T) {
- input := `page TestPage at "/test" layout MainLayout
- component DetailedTable for User
- field email type text label "Email Address"
- field name type text label "Full Name"
-
- data from "/users"
- pagination size 20`
-
- ast, err := ParseInput(input)
- if err != nil {
- t.Fatalf("Failed to parse: %v", err)
- }
-
- // Verify the component was parsed correctly
- page := ast.Definitions[0].Page
- component := page.Components[0]
-
- if component.Type != "DetailedTable" {
- t.Errorf("Expected component type DetailedTable, got %s", component.Type)
- }
-
- // Count fields and config elements
- fieldCount := 0
- configCount := 0
- for _, element := range component.Elements {
- if element.Field != nil {
- fieldCount++
- }
- if element.Config != nil {
- configCount++
- }
- }
-
- if fieldCount != 2 {
- t.Errorf("Expected 2 fields, got %d", fieldCount)
- }
-
- if configCount != 2 {
- t.Errorf("Expected 2 config items, got %d", configCount)
- }
-}
diff --git a/lang/parser_entity_test.go b/lang/parser_entity_test.go
index 301647f..3f14cc6 100644
--- a/lang/parser_entity_test.go
+++ b/lang/parser_entity_test.go
@@ -13,11 +13,12 @@ func TestParseEntityDefinitions(t *testing.T) {
}{
{
name: "entity with enhanced fields and relationships",
- input: `entity User desc "User management"
+ 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"`,
+ profile_id: uuid relates to Profile as one via "user_id"
+ }`,
want: AST{
Definitions: []Definition{
{
@@ -61,6 +62,57 @@ func TestParseEntityDefinitions(t *testing.T) {
},
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 {
diff --git a/lang/parser_page_test.go b/lang/parser_page_test.go
deleted file mode 100644
index 002d355..0000000
--- a/lang/parser_page_test.go
+++ /dev/null
@@ -1,139 +0,0 @@
-package lang
-
-import (
- "testing"
-)
-
-func TestParsePageDefinitions(t *testing.T) {
- tests := []struct {
- name string
- input string
- want AST
- wantErr bool
- }{
- {
- name: "page with container and sections",
- input: `page UserManagement at "/admin/users" layout AdminLayout title "User Management" auth
- meta description "Manage users"
-
- container main class "grid grid-cols-2"
- section sidebar class "col-span-1"
- component UserStats for User
- data from "/users/stats"`,
- want: AST{
- Definitions: []Definition{
- {
- Page: &Page{
- Name: "UserManagement",
- Path: "/admin/users",
- Layout: "AdminLayout",
- Title: stringPtr("User Management"),
- Auth: true,
- Meta: []MetaTag{
- {Name: "description", Content: "Manage users"},
- },
- Containers: []Container{
- {
- Type: "main",
- Class: stringPtr("grid grid-cols-2"),
- Sections: []Section{
- {
- Name: "sidebar",
- Class: stringPtr("col-span-1"),
- Components: []Component{
- {
- Type: "UserStats",
- Entity: stringPtr("User"),
- Elements: []ComponentElement{
- {
- Config: &ComponentAttr{
- DataSource: &ComponentDataSource{
- Endpoint: "/users/stats",
- },
- },
- },
- },
- },
- },
- },
- },
- },
- },
- },
- },
- },
- },
- wantErr: false,
- },
- {
- name: "page with panel",
- input: `page PageWithPanel at "/panel" layout MainLayout
- container main
- section content
- panel UserEditPanel for User trigger "edit" position "slide-right"
- component UserForm for User
- field name type text required`,
- want: AST{
- Definitions: []Definition{
- {
- Page: &Page{
- Name: "PageWithPanel",
- Path: "/panel",
- Layout: "MainLayout",
- Containers: []Container{
- {
- Type: "main",
- Sections: []Section{
- {
- Name: "content",
- Panels: []Panel{
- {
- Name: "UserEditPanel",
- Entity: stringPtr("User"),
- Trigger: "edit",
- Position: stringPtr("slide-right"),
- Components: []Component{
- {
- Type: "UserForm",
- Entity: stringPtr("User"),
- Elements: []ComponentElement{
- {
- Field: &ComponentField{
- Name: "name",
- Type: "text",
- Attributes: []ComponentFieldAttribute{
- {Required: true},
- },
- },
- },
- },
- },
- },
- },
- },
- },
- },
- },
- },
- },
- },
- },
- },
- 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)
- }
- })
- }
-}
diff --git a/lang/parser_server_test.go b/lang/parser_server_test.go
index b69ca41..264c230 100644
--- a/lang/parser_server_test.go
+++ b/lang/parser_server_test.go
@@ -12,8 +12,11 @@ func TestParseServerDefinitions(t *testing.T) {
wantErr bool
}{
{
- name: "simple server definition",
- input: `server MyApp host "localhost" port 8080`,
+ name: "simple server definition with block delimiters",
+ input: `server MyApp {
+ host "localhost"
+ port 8080
+ }`,
want: AST{
Definitions: []Definition{
{
@@ -29,6 +32,59 @@ func TestParseServerDefinitions(t *testing.T) {
},
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 {
diff --git a/lang/parser_test_helpers.go b/lang/parser_test_helpers.go
deleted file mode 100644
index 7f3457b..0000000
--- a/lang/parser_test_helpers.go
+++ /dev/null
@@ -1,138 +0,0 @@
-package lang
-
-// Helper functions and comparison utilities 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 got.Server.Name != want.Server.Name {
- return false
- }
- if len(got.Server.Settings) != len(want.Server.Settings) {
- return false
- }
- // Simplified server settings comparison
- }
-
- // Entity comparison
- if (got.Entity == nil) != (want.Entity == nil) {
- return false
- }
- if got.Entity != nil && want.Entity != nil {
- if got.Entity.Name != want.Entity.Name {
- return false
- }
- // Simplified entity comparison
- }
-
- // Endpoint comparison
- if (got.Endpoint == nil) != (want.Endpoint == nil) {
- return false
- }
- if got.Endpoint != nil && want.Endpoint != nil {
- if got.Endpoint.Method != want.Endpoint.Method || got.Endpoint.Path != want.Endpoint.Path {
- 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
-}
-
-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 got.Auth != want.Auth {
- return false
- }
-
- if !stringPtrEqual(got.LayoutType, want.LayoutType) {
- return false
- }
-
- // Compare meta tags
- if len(got.Meta) != len(want.Meta) {
- return false
- }
-
- // Compare containers
- if len(got.Containers) != len(want.Containers) {
- return false
- }
-
- // Compare components
- if len(got.Components) != len(want.Components) {
- return false
- }
-
- // Compare modals
- if len(got.Modals) != len(want.Modals) {
- return false
- }
-
- // Compare master-detail
- if (got.MasterDetail == nil) != (want.MasterDetail == nil) {
- return false
- }
-
- return true
-}
-
-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
-}
-
-// Helper functions for creating pointers
-func stringPtr(s string) *string {
- return &s
-}
-
-func intPtr(i int) *int {
- return &i
-}
diff --git a/lang/parser_ui_advanced_test.go b/lang/parser_ui_advanced_test.go
new file mode 100644
index 0000000..9a2490b
--- /dev/null
+++ b/lang/parser_ui_advanced_test.go
@@ -0,0 +1,575 @@
+package lang
+
+import (
+ "testing"
+)
+
+func TestParseAdvancedUIFeatures(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ want AST
+ wantErr bool
+ }{
+ {
+ name: "complex conditional rendering with multiple operators",
+ input: `page Test at "/test" layout main {
+ component form for User {
+ field status type select options ["active", "inactive", "pending"]
+
+ when status equals "active" {
+ field last_login type datetime
+ field permissions type multiselect
+ button deactivate label "Deactivate User" style "warning"
+ }
+
+ when status not_equals "active" {
+ field reason type textarea placeholder "Reason for status"
+ button activate label "Activate User" style "success"
+ }
+
+ when status contains "pending" {
+ field approval_date type date
+ button approve label "Approve" style "primary"
+ button reject label "Reject" style "danger"
+ }
+ }
+ }`,
+ want: AST{
+ Definitions: []Definition{
+ {
+ Page: &Page{
+ Name: "Test",
+ Path: "/test",
+ Layout: "main",
+ Components: []Component{
+ {
+ Type: "form",
+ Entity: stringPtr("User"),
+ Elements: []ComponentElement{
+ {
+ Field: &ComponentField{
+ Name: "status",
+ Type: "select",
+ Attributes: []ComponentFieldAttribute{
+ {Options: []string{"active", "inactive", "pending"}},
+ },
+ },
+ },
+ {
+ When: &WhenCondition{
+ Field: "status",
+ Operator: "equals",
+ Value: "active",
+ Fields: []ComponentField{
+ {Name: "last_login", Type: "datetime"},
+ {Name: "permissions", Type: "multiselect"},
+ },
+ Buttons: []ComponentButton{
+ {
+ Name: "deactivate",
+ Label: "Deactivate User",
+ Attributes: []ComponentButtonAttr{
+ {Style: &ComponentButtonStyle{Value: "warning"}},
+ },
+ },
+ },
+ },
+ },
+ {
+ When: &WhenCondition{
+ Field: "status",
+ Operator: "not_equals",
+ Value: "active",
+ Fields: []ComponentField{
+ {
+ Name: "reason",
+ Type: "textarea",
+ Attributes: []ComponentFieldAttribute{
+ {Placeholder: stringPtr("Reason for status")},
+ },
+ },
+ },
+ Buttons: []ComponentButton{
+ {
+ Name: "activate",
+ Label: "Activate User",
+ Attributes: []ComponentButtonAttr{
+ {Style: &ComponentButtonStyle{Value: "success"}},
+ },
+ },
+ },
+ },
+ },
+ {
+ When: &WhenCondition{
+ Field: "status",
+ Operator: "contains",
+ Value: "pending",
+ Fields: []ComponentField{
+ {Name: "approval_date", Type: "date"},
+ },
+ Buttons: []ComponentButton{
+ {
+ Name: "approve",
+ Label: "Approve",
+ Attributes: []ComponentButtonAttr{
+ {Style: &ComponentButtonStyle{Value: "primary"}},
+ },
+ },
+ {
+ Name: "reject",
+ Label: "Reject",
+ Attributes: []ComponentButtonAttr{
+ {Style: &ComponentButtonStyle{Value: "danger"}},
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "field attributes with all possible options",
+ input: `page Test at "/test" layout main {
+ component form for Product {
+ field name type text {
+ label "Product Name"
+ placeholder "Enter product name"
+ required
+ default "New Product"
+ validate min_length "3"
+ size "large"
+ display "block"
+ }
+
+ field price type number {
+ label "Price ($)"
+ format "currency"
+ validate min "0"
+ validate max "10000"
+ }
+
+ field category type autocomplete {
+ label "Category"
+ placeholder "Start typing..."
+ relates to Category
+ searchable
+ source "categories/search"
+ }
+
+ field tags type multiselect {
+ label "Tags"
+ options ["electronics", "clothing", "books", "home"]
+ source "tags/popular"
+ }
+
+ field description type richtext {
+ label "Description"
+ rows 10
+ placeholder "Describe your product..."
+ }
+
+ field thumbnail type image {
+ label "Product Image"
+ accept "image/jpeg,image/png"
+ thumbnail
+ }
+
+ field featured type checkbox {
+ label "Featured Product"
+ default "false"
+ value "true"
+ }
+
+ field availability type select {
+ label "Availability"
+ options ["in_stock", "out_of_stock", "pre_order"]
+ default "in_stock"
+ sortable
+ }
+ }
+ }`,
+ want: AST{
+ Definitions: []Definition{
+ {
+ Page: &Page{
+ Name: "Test",
+ Path: "/test",
+ Layout: "main",
+ Components: []Component{
+ {
+ Type: "form",
+ Entity: stringPtr("Product"),
+ Elements: []ComponentElement{
+ {
+ Field: &ComponentField{
+ Name: "name",
+ Type: "text",
+ Attributes: []ComponentFieldAttribute{
+ {Label: stringPtr("Product Name")},
+ {Placeholder: stringPtr("Enter product name")},
+ {Required: true},
+ {Default: stringPtr("New Product")},
+ {Validation: &ComponentValidation{Type: "min_length", Value: stringPtr("3")}},
+ {Size: stringPtr("large")},
+ {Display: stringPtr("block")},
+ },
+ },
+ },
+ {
+ Field: &ComponentField{
+ Name: "price",
+ Type: "number",
+ Attributes: []ComponentFieldAttribute{
+ {Label: stringPtr("Price ($)")},
+ {Format: stringPtr("currency")},
+ {Validation: &ComponentValidation{Type: "min", Value: stringPtr("0")}},
+ {Validation: &ComponentValidation{Type: "max", Value: stringPtr("10000")}},
+ },
+ },
+ },
+ {
+ Field: &ComponentField{
+ Name: "category",
+ Type: "autocomplete",
+ Attributes: []ComponentFieldAttribute{
+ {Label: stringPtr("Category")},
+ {Placeholder: stringPtr("Start typing...")},
+ {Relates: &FieldRelation{Type: "Category"}},
+ {Searchable: true},
+ {Source: stringPtr("categories/search")},
+ },
+ },
+ },
+ {
+ Field: &ComponentField{
+ Name: "tags",
+ Type: "multiselect",
+ Attributes: []ComponentFieldAttribute{
+ {Label: stringPtr("Tags")},
+ {Options: []string{"electronics", "clothing", "books", "home"}},
+ {Source: stringPtr("tags/popular")},
+ },
+ },
+ },
+ {
+ Field: &ComponentField{
+ Name: "description",
+ Type: "richtext",
+ Attributes: []ComponentFieldAttribute{
+ {Label: stringPtr("Description")},
+ {Rows: intPtr(10)},
+ {Placeholder: stringPtr("Describe your product...")},
+ },
+ },
+ },
+ {
+ Field: &ComponentField{
+ Name: "thumbnail",
+ Type: "image",
+ Attributes: []ComponentFieldAttribute{
+ {Label: stringPtr("Product Image")},
+ {Accept: stringPtr("image/jpeg,image/png")},
+ {Thumbnail: true},
+ },
+ },
+ },
+ {
+ Field: &ComponentField{
+ Name: "featured",
+ Type: "checkbox",
+ Attributes: []ComponentFieldAttribute{
+ {Label: stringPtr("Featured Product")},
+ {Default: stringPtr("false")},
+ {Value: stringPtr("true")},
+ },
+ },
+ },
+ {
+ Field: &ComponentField{
+ Name: "availability",
+ Type: "select",
+ Attributes: []ComponentFieldAttribute{
+ {Label: stringPtr("Availability")},
+ {Options: []string{"in_stock", "out_of_stock", "pre_order"}},
+ {Default: stringPtr("in_stock")},
+ {Sortable: true},
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "complex button configurations",
+ input: `page Test at "/test" layout main {
+ component form for Order {
+ field status type select options ["draft", "submitted", "approved"]
+
+ button save label "Save Draft" style "secondary" icon "save" position "left"
+ button submit label "Submit Order" style "primary" icon "send" loading "Submitting..." confirm "Submit this order?"
+ button approve label "Approve" style "success" loading "Approving..." disabled when status confirm "Approve this order?" target approval_modal via "api/orders/approve"
+ button reject label "Reject" style "danger" icon "x" confirm "Are you sure you want to reject this order?"
+ button print label "Print" style "outline" icon "printer" position "right"
+ }
+ }`,
+ want: AST{
+ Definitions: []Definition{
+ {
+ Page: &Page{
+ Name: "Test",
+ Path: "/test",
+ Layout: "main",
+ Components: []Component{
+ {
+ Type: "form",
+ Entity: stringPtr("Order"),
+ Elements: []ComponentElement{
+ {
+ Field: &ComponentField{
+ Name: "status",
+ Type: "select",
+ Attributes: []ComponentFieldAttribute{
+ {Options: []string{"draft", "submitted", "approved"}},
+ },
+ },
+ },
+ {
+ Button: &ComponentButton{
+ Name: "save",
+ Label: "Save Draft",
+ Attributes: []ComponentButtonAttr{
+ {Style: &ComponentButtonStyle{Value: "secondary"}},
+ {Icon: &ComponentButtonIcon{Value: "save"}},
+ {Position: &ComponentButtonPosition{Value: "left"}},
+ },
+ },
+ },
+ {
+ Button: &ComponentButton{
+ Name: "submit",
+ Label: "Submit Order",
+ Attributes: []ComponentButtonAttr{
+ {Style: &ComponentButtonStyle{Value: "primary"}},
+ {Icon: &ComponentButtonIcon{Value: "send"}},
+ {Loading: &ComponentButtonLoading{Value: "Submitting..."}},
+ {Confirm: &ComponentButtonConfirm{Value: "Submit this order?"}},
+ },
+ },
+ },
+ {
+ Button: &ComponentButton{
+ Name: "approve",
+ Label: "Approve",
+ Attributes: []ComponentButtonAttr{
+ {Style: &ComponentButtonStyle{Value: "success"}},
+ {Loading: &ComponentButtonLoading{Value: "Approving..."}},
+ {Disabled: &ComponentButtonDisabled{Value: "status"}},
+ {Confirm: &ComponentButtonConfirm{Value: "Approve this order?"}},
+ {Target: &ComponentButtonTarget{Value: "approval_modal"}},
+ {Via: &ComponentButtonVia{Value: "api/orders/approve"}},
+ },
+ },
+ },
+ {
+ Button: &ComponentButton{
+ Name: "reject",
+ Label: "Reject",
+ Attributes: []ComponentButtonAttr{
+ {Style: &ComponentButtonStyle{Value: "danger"}},
+ {Icon: &ComponentButtonIcon{Value: "x"}},
+ {Confirm: &ComponentButtonConfirm{Value: "Are you sure you want to reject this order?"}},
+ },
+ },
+ },
+ {
+ Button: &ComponentButton{
+ Name: "print",
+ Label: "Print",
+ Attributes: []ComponentButtonAttr{
+ {Style: &ComponentButtonStyle{Value: "outline"}},
+ {Icon: &ComponentButtonIcon{Value: "printer"}},
+ {Position: &ComponentButtonPosition{Value: "right"}},
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := ParseInput(tt.input)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("ParseInput() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if !astEqual(got, tt.want) {
+ t.Errorf("ParseInput() got = %v, want = %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestParseFieldValidationTypes(t *testing.T) {
+ validationTypes := []struct {
+ validation string
+ hasValue bool
+ }{
+ {"email", false},
+ {"required", false},
+ {"min_length", true},
+ {"max_length", true},
+ {"min", true},
+ {"max", true},
+ {"pattern", true},
+ {"numeric", false},
+ {"alpha", false},
+ {"alphanumeric", false},
+ {"url", false},
+ {"date", false},
+ {"datetime", false},
+ {"time", false},
+ {"phone", false},
+ {"postal_code", false},
+ {"credit_card", false},
+ }
+
+ for _, vt := range validationTypes {
+ t.Run("validation_"+vt.validation, func(t *testing.T) {
+ var input string
+ if vt.hasValue {
+ input = `page Test at "/test" layout main {
+ component form {
+ field test_field type text validate ` + vt.validation + ` "test_value"
+ }
+ }`
+ } else {
+ input = `page Test at "/test" layout main {
+ component form {
+ field test_field type text validate ` + vt.validation + `
+ }
+ }`
+ }
+
+ got, err := ParseInput(input)
+ if err != nil {
+ t.Errorf("ParseInput() failed for validation %s: %v", vt.validation, err)
+ return
+ }
+
+ if len(got.Definitions) != 1 || got.Definitions[0].Page == nil {
+ t.Errorf("ParseInput() failed to parse page for validation %s", vt.validation)
+ return
+ }
+
+ page := got.Definitions[0].Page
+ if len(page.Components) != 1 || len(page.Components[0].Elements) != 1 {
+ t.Errorf("ParseInput() failed to parse component for validation %s", vt.validation)
+ return
+ }
+
+ element := page.Components[0].Elements[0]
+ if element.Field == nil || len(element.Field.Attributes) != 1 {
+ t.Errorf("ParseInput() failed to parse field attributes for validation %s", vt.validation)
+ return
+ }
+
+ attr := element.Field.Attributes[0]
+ if attr.Validation == nil || attr.Validation.Type != vt.validation {
+ t.Errorf("ParseInput() validation type mismatch: got %v, want %s", attr.Validation, vt.validation)
+ }
+
+ if vt.hasValue && (attr.Validation.Value == nil || *attr.Validation.Value != "test_value") {
+ t.Errorf("ParseInput() validation value mismatch for %s", vt.validation)
+ }
+ })
+ }
+}
+
+func TestParseConditionalOperators(t *testing.T) {
+ operators := []string{"equals", "not_equals", "contains"}
+
+ for _, op := range operators {
+ t.Run("operator_"+op, func(t *testing.T) {
+ input := `page Test at "/test" layout main {
+ component form {
+ field test_field type text
+ when test_field ` + op + ` "test_value" {
+ field conditional_field type text
+ }
+ }
+ }`
+
+ got, err := ParseInput(input)
+ if err != nil {
+ t.Errorf("ParseInput() failed for operator %s: %v", op, err)
+ return
+ }
+
+ // Verify the when condition was parsed correctly
+ page := got.Definitions[0].Page
+ component := page.Components[0]
+ whenElement := component.Elements[1].When
+
+ if whenElement == nil || whenElement.Operator != op {
+ t.Errorf("ParseInput() operator mismatch: got %v, want %s", whenElement, op)
+ }
+ })
+ }
+}
+
+func TestParseAdvancedUIErrors(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ }{
+ {
+ name: "invalid conditional operator",
+ input: `page Test at "/test" layout main {
+ component form {
+ when field invalid_operator "value" {
+ field test type text
+ }
+ }
+ }`,
+ },
+ {
+ name: "missing field attribute block closure",
+ input: `page Test at "/test" layout main {
+ component form {
+ field test type text {
+ label "Test"
+ required
+ }
+ }`,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ _, err := ParseInput(tt.input)
+ if err == nil {
+ t.Errorf("ParseInput() expected error for invalid syntax, got nil")
+ }
+ })
+ }
+}
diff --git a/lang/parser_ui_component_test.go b/lang/parser_ui_component_test.go
new file mode 100644
index 0000000..eaa9b77
--- /dev/null
+++ b/lang/parser_ui_component_test.go
@@ -0,0 +1,548 @@
+package lang
+
+import (
+ "testing"
+)
+
+func TestParseComponentDefinitions(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ want AST
+ wantErr bool
+ }{
+ {
+ name: "basic component with entity",
+ input: `page Test at "/test" layout main {
+ component table for User
+ }`,
+ want: AST{
+ Definitions: []Definition{
+ {
+ Page: &Page{
+ Name: "Test",
+ Path: "/test",
+ Layout: "main",
+ Components: []Component{
+ {
+ Type: "table",
+ Entity: stringPtr("User"),
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "form component with fields",
+ input: `page Test at "/test" layout main {
+ component form for User {
+ field name type text label "Full Name" placeholder "Enter your name" required
+ field email type email label "Email Address" required
+ field bio type textarea rows 5 placeholder "Tell us about yourself"
+ field avatar type file accept "image/*"
+ field role type select options ["admin", "user", "guest"] default "user"
+ }
+ }`,
+ want: AST{
+ Definitions: []Definition{
+ {
+ Page: &Page{
+ Name: "Test",
+ Path: "/test",
+ Layout: "main",
+ Components: []Component{
+ {
+ Type: "form",
+ Entity: stringPtr("User"),
+ Elements: []ComponentElement{
+ {
+ Field: &ComponentField{
+ Name: "name",
+ Type: "text",
+ Attributes: []ComponentFieldAttribute{
+ {Label: stringPtr("Full Name")},
+ {Placeholder: stringPtr("Enter your name")},
+ {Required: true},
+ },
+ },
+ },
+ {
+ Field: &ComponentField{
+ Name: "email",
+ Type: "email",
+ Attributes: []ComponentFieldAttribute{
+ {Label: stringPtr("Email Address")},
+ {Required: true},
+ },
+ },
+ },
+ {
+ Field: &ComponentField{
+ Name: "bio",
+ Type: "textarea",
+ Attributes: []ComponentFieldAttribute{
+ {Rows: intPtr(5)},
+ {Placeholder: stringPtr("Tell us about yourself")},
+ },
+ },
+ },
+ {
+ Field: &ComponentField{
+ Name: "avatar",
+ Type: "file",
+ Attributes: []ComponentFieldAttribute{
+ {Accept: stringPtr("image/*")},
+ },
+ },
+ },
+ {
+ Field: &ComponentField{
+ Name: "role",
+ Type: "select",
+ Attributes: []ComponentFieldAttribute{
+ {Options: []string{"admin", "user", "guest"}},
+ {Default: stringPtr("user")},
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "component with field attributes and validation",
+ input: `page Test at "/test" layout main {
+ component form for Product {
+ field name type text required validate min_length "3"
+ field price type number format "currency" validate min "0"
+ field category type autocomplete relates to Category
+ field tags type multiselect source "tags/popular"
+ field description type richtext
+ field featured type checkbox default "false"
+ field thumbnail type image thumbnail
+ }
+ }`,
+ want: AST{
+ Definitions: []Definition{
+ {
+ Page: &Page{
+ Name: "Test",
+ Path: "/test",
+ Layout: "main",
+ Components: []Component{
+ {
+ Type: "form",
+ Entity: stringPtr("Product"),
+ Elements: []ComponentElement{
+ {
+ Field: &ComponentField{
+ Name: "name",
+ Type: "text",
+ Attributes: []ComponentFieldAttribute{
+ {Required: true},
+ {Validation: &ComponentValidation{Type: "min_length", Value: stringPtr("3")}},
+ },
+ },
+ },
+ {
+ Field: &ComponentField{
+ Name: "price",
+ Type: "number",
+ Attributes: []ComponentFieldAttribute{
+ {Format: stringPtr("currency")},
+ {Validation: &ComponentValidation{Type: "min", Value: stringPtr("0")}},
+ },
+ },
+ },
+ {
+ Field: &ComponentField{
+ Name: "category",
+ Type: "autocomplete",
+ Attributes: []ComponentFieldAttribute{
+ {Relates: &FieldRelation{Type: "Category"}},
+ },
+ },
+ },
+ {
+ Field: &ComponentField{
+ Name: "tags",
+ Type: "multiselect",
+ Attributes: []ComponentFieldAttribute{
+ {Source: stringPtr("tags/popular")},
+ },
+ },
+ },
+ {
+ Field: &ComponentField{
+ Name: "description",
+ Type: "richtext",
+ },
+ },
+ {
+ Field: &ComponentField{
+ Name: "featured",
+ Type: "checkbox",
+ Attributes: []ComponentFieldAttribute{
+ {Default: stringPtr("false")},
+ },
+ },
+ },
+ {
+ Field: &ComponentField{
+ Name: "thumbnail",
+ Type: "image",
+ Attributes: []ComponentFieldAttribute{
+ {Thumbnail: true},
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "component with buttons",
+ input: `page Test at "/test" layout main {
+ component form for User {
+ field name type text
+ button save label "Save User" style "primary" icon "save"
+ button cancel label "Cancel" style "secondary"
+ button delete label "Delete" style "danger" confirm "Are you sure?" disabled when is_protected
+ }
+ }`,
+ want: AST{
+ Definitions: []Definition{
+ {
+ Page: &Page{
+ Name: "Test",
+ Path: "/test",
+ Layout: "main",
+ Components: []Component{
+ {
+ Type: "form",
+ Entity: stringPtr("User"),
+ Elements: []ComponentElement{
+ {
+ Field: &ComponentField{
+ Name: "name",
+ Type: "text",
+ },
+ },
+ {
+ Button: &ComponentButton{
+ Name: "save",
+ Label: "Save User",
+ Attributes: []ComponentButtonAttr{
+ {Style: &ComponentButtonStyle{Value: "primary"}},
+ {Icon: &ComponentButtonIcon{Value: "save"}},
+ },
+ },
+ },
+ {
+ Button: &ComponentButton{
+ Name: "cancel",
+ Label: "Cancel",
+ Attributes: []ComponentButtonAttr{
+ {Style: &ComponentButtonStyle{Value: "secondary"}},
+ },
+ },
+ },
+ {
+ Button: &ComponentButton{
+ Name: "delete",
+ Label: "Delete",
+ Attributes: []ComponentButtonAttr{
+ {Style: &ComponentButtonStyle{Value: "danger"}},
+ {Confirm: &ComponentButtonConfirm{Value: "Are you sure?"}},
+ {Disabled: &ComponentButtonDisabled{Value: "is_protected"}},
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "component with conditional fields",
+ input: `page Test at "/test" layout main {
+ component form for User {
+ field account_type type select options ["personal", "business"]
+ when account_type equals "business" {
+ field company_name type text required
+ field tax_id type text
+ button verify_business label "Verify Business"
+ }
+ when account_type equals "personal" {
+ field date_of_birth type date
+ }
+ }
+ }`,
+ want: AST{
+ Definitions: []Definition{
+ {
+ Page: &Page{
+ Name: "Test",
+ Path: "/test",
+ Layout: "main",
+ Components: []Component{
+ {
+ Type: "form",
+ Entity: stringPtr("User"),
+ Elements: []ComponentElement{
+ {
+ Field: &ComponentField{
+ Name: "account_type",
+ Type: "select",
+ Attributes: []ComponentFieldAttribute{
+ {Options: []string{"personal", "business"}},
+ },
+ },
+ },
+ {
+ When: &WhenCondition{
+ Field: "account_type",
+ Operator: "equals",
+ Value: "business",
+ Fields: []ComponentField{
+ {
+ Name: "company_name",
+ Type: "text",
+ Attributes: []ComponentFieldAttribute{
+ {Required: true},
+ },
+ },
+ {
+ Name: "tax_id",
+ Type: "text",
+ },
+ },
+ Buttons: []ComponentButton{
+ {
+ Name: "verify_business",
+ Label: "Verify Business",
+ },
+ },
+ },
+ },
+ {
+ When: &WhenCondition{
+ Field: "account_type",
+ Operator: "equals",
+ Value: "personal",
+ Fields: []ComponentField{
+ {
+ Name: "date_of_birth",
+ Type: "date",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "component with nested sections",
+ input: `page Test at "/test" layout main {
+ component dashboard {
+ section stats type container class "stats-grid" {
+ component metric {
+ field total_users type display value "1,234"
+ field revenue type display format "currency" value "45,678"
+ }
+ }
+ section charts type container {
+ component chart for Analytics {
+ data from "analytics/monthly"
+ }
+ }
+ }
+ }`,
+ want: AST{
+ Definitions: []Definition{
+ {
+ Page: &Page{
+ Name: "Test",
+ Path: "/test",
+ Layout: "main",
+ Components: []Component{
+ {
+ Type: "dashboard",
+ Elements: []ComponentElement{
+ {
+ Section: &Section{
+ Name: "stats",
+ Type: stringPtr("container"),
+ Class: stringPtr("stats-grid"),
+ Elements: []SectionElement{
+ {
+ Component: &Component{
+ Type: "metric",
+ Elements: []ComponentElement{
+ {
+ Field: &ComponentField{
+ Name: "total_users",
+ Type: "display",
+ Attributes: []ComponentFieldAttribute{
+ {Value: stringPtr("1,234")},
+ },
+ },
+ },
+ {
+ Field: &ComponentField{
+ Name: "revenue",
+ Type: "display",
+ Attributes: []ComponentFieldAttribute{
+ {Format: stringPtr("currency")},
+ {Value: stringPtr("45,678")},
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ Section: &Section{
+ Name: "charts",
+ Type: stringPtr("container"),
+ Elements: []SectionElement{
+ {
+ Component: &Component{
+ Type: "chart",
+ Entity: stringPtr("Analytics"),
+ Elements: []ComponentElement{
+ {
+ Attribute: &ComponentAttr{
+ DataSource: stringPtr("analytics/monthly"),
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := ParseInput(tt.input)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("ParseInput() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if !astEqual(got, tt.want) {
+ t.Errorf("ParseInput() got = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestParseComponentFieldTypes(t *testing.T) {
+ fieldTypes := []string{
+ "text", "email", "password", "number", "date", "datetime", "time",
+ "textarea", "richtext", "select", "multiselect", "checkbox", "radio",
+ "file", "image", "autocomplete", "range", "color", "url", "tel",
+ "hidden", "display", "json", "code",
+ }
+
+ for _, fieldType := range fieldTypes {
+ t.Run("field_type_"+fieldType, func(t *testing.T) {
+ input := `page Test at "/test" layout main {
+ component form {
+ field test_field type ` + fieldType + `
+ }
+ }`
+
+ got, err := ParseInput(input)
+ if err != nil {
+ t.Errorf("ParseInput() failed for field type %s: %v", fieldType, err)
+ return
+ }
+
+ if len(got.Definitions) != 1 || got.Definitions[0].Page == nil {
+ t.Errorf("ParseInput() failed to parse page for field type %s", fieldType)
+ return
+ }
+
+ page := got.Definitions[0].Page
+ if len(page.Components) != 1 || len(page.Components[0].Elements) != 1 {
+ t.Errorf("ParseInput() failed to parse component for field type %s", fieldType)
+ return
+ }
+
+ element := page.Components[0].Elements[0]
+ if element.Field == nil || element.Field.Type != fieldType {
+ t.Errorf("ParseInput() field type mismatch: got %v, want %s", element.Field, fieldType)
+ }
+ })
+ }
+}
+
+func TestParseComponentErrors(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ }{
+ {
+ name: "missing component type",
+ input: `page Test at "/test" layout main {
+ component
+ }`,
+ },
+ {
+ name: "invalid field syntax",
+ input: `page Test at "/test" layout main {
+ component form {
+ field name
+ }
+ }`,
+ },
+ {
+ name: "invalid button syntax",
+ input: `page Test at "/test" layout main {
+ component form {
+ button
+ }
+ }`,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ _, err := ParseInput(tt.input)
+ if err == nil {
+ t.Errorf("ParseInput() expected error for invalid syntax, got nil")
+ }
+ })
+ }
+}
diff --git a/lang/parser_ui_page_test.go b/lang/parser_ui_page_test.go
new file mode 100644
index 0000000..83efbca
--- /dev/null
+++ b/lang/parser_ui_page_test.go
@@ -0,0 +1,300 @@
+package lang
+
+import (
+ "testing"
+)
+
+func TestParsePageDefinitions(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ want AST
+ wantErr bool
+ }{
+ {
+ name: "basic page with minimal fields",
+ input: `page Dashboard at "/dashboard" layout main`,
+ want: AST{
+ Definitions: []Definition{
+ {
+ Page: &Page{
+ Name: "Dashboard",
+ Path: "/dashboard",
+ Layout: "main",
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "page with all optional fields",
+ input: `page UserProfile at "/profile" layout main title "User Profile" desc "Manage user profile settings" auth`,
+ want: AST{
+ Definitions: []Definition{
+ {
+ Page: &Page{
+ Name: "UserProfile",
+ Path: "/profile",
+ Layout: "main",
+ Title: stringPtr("User Profile"),
+ Description: stringPtr("Manage user profile settings"),
+ Auth: true,
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "page with meta tags",
+ input: `page HomePage at "/" layout main {
+ meta description "Welcome to our application"
+ meta keywords "app, dashboard, management"
+ }`,
+ want: AST{
+ Definitions: []Definition{
+ {
+ Page: &Page{
+ Name: "HomePage",
+ Path: "/",
+ Layout: "main",
+ Meta: []MetaTag{
+ {Name: "description", Content: "Welcome to our application"},
+ {Name: "keywords", Content: "app, dashboard, management"},
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "page with nested sections",
+ input: `page Settings at "/settings" layout main {
+ section tabs type tab {
+ section profile label "Profile" active {
+ component form for User {
+ field name type text
+ }
+ }
+
+ section security label "Security" {
+ component form for User {
+ field password type password
+ }
+ }
+ }
+ }`,
+ want: AST{
+ Definitions: []Definition{
+ {
+ Page: &Page{
+ Name: "Settings",
+ Path: "/settings",
+ Layout: "main",
+ Sections: []Section{
+ {
+ Name: "tabs",
+ Type: stringPtr("tab"),
+ Elements: []SectionElement{
+ {
+ Section: &Section{
+ Name: "profile",
+ Label: stringPtr("Profile"),
+ Active: true,
+ Elements: []SectionElement{
+ {
+ Component: &Component{
+ Type: "form",
+ Entity: stringPtr("User"),
+ Elements: []ComponentElement{
+ {
+ Field: &ComponentField{
+ Name: "name",
+ Type: "text",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ Section: &Section{
+ Name: "security",
+ Label: stringPtr("Security"),
+ Elements: []SectionElement{
+ {
+ Component: &Component{
+ Type: "form",
+ Entity: stringPtr("User"),
+ Elements: []ComponentElement{
+ {
+ Field: &ComponentField{
+ Name: "password",
+ Type: "password",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "page with modal and panel sections",
+ input: `page ProductList at "/products" layout main {
+ section main type container {
+ component table for Product
+ }
+
+ section editModal type modal trigger "edit-product" {
+ component form for Product {
+ field name type text required
+ button save label "Save Changes" style "primary"
+ }
+ }
+
+ section filters type panel position "left" {
+ component form {
+ field category type select
+ field price_range type range
+ }
+ }
+ }`,
+ want: AST{
+ Definitions: []Definition{
+ {
+ Page: &Page{
+ Name: "ProductList",
+ Path: "/products",
+ Layout: "main",
+ Sections: []Section{
+ {
+ Name: "main",
+ Type: stringPtr("container"),
+ Elements: []SectionElement{
+ {
+ Component: &Component{
+ Type: "table",
+ Entity: stringPtr("Product"),
+ },
+ },
+ },
+ },
+ {
+ Name: "editModal",
+ Type: stringPtr("modal"),
+ Trigger: stringPtr("edit-product"),
+ Elements: []SectionElement{
+ {
+ Component: &Component{
+ Type: "form",
+ Entity: stringPtr("Product"),
+ Elements: []ComponentElement{
+ {
+ Field: &ComponentField{
+ Name: "name",
+ Type: "text",
+ Attributes: []ComponentFieldAttribute{
+ {Required: true},
+ },
+ },
+ },
+ {
+ Button: &ComponentButton{
+ Name: "save",
+ Label: "Save Changes",
+ Attributes: []ComponentButtonAttr{
+ {Style: &ComponentButtonStyle{Value: "primary"}},
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ Name: "filters",
+ Type: stringPtr("panel"),
+ Position: stringPtr("left"),
+ Elements: []SectionElement{
+ {
+ Component: &Component{
+ Type: "form",
+ Elements: []ComponentElement{
+ {
+ Field: &ComponentField{
+ Name: "category",
+ Type: "select",
+ },
+ },
+ {
+ Field: &ComponentField{
+ Name: "price_range",
+ Type: "range",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := ParseInput(tt.input)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("ParseInput() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if !astEqual(got, tt.want) {
+ t.Errorf("ParseInput() got = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestParsePageErrors(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ }{
+ {
+ name: "missing layout",
+ input: `page Dashboard at "/dashboard"`,
+ },
+ {
+ name: "missing path",
+ input: `page Dashboard layout main`,
+ },
+ {
+ name: "invalid path format",
+ input: `page Dashboard at dashboard layout main`,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ _, err := ParseInput(tt.input)
+ if err == nil {
+ t.Errorf("ParseInput() expected error for invalid syntax, got nil")
+ }
+ })
+ }
+}
diff --git a/lang/parser_ui_section_test.go b/lang/parser_ui_section_test.go
new file mode 100644
index 0000000..a2bbbc2
--- /dev/null
+++ b/lang/parser_ui_section_test.go
@@ -0,0 +1,599 @@
+package lang
+
+import (
+ "testing"
+)
+
+func TestParseSectionDefinitions(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ want AST
+ wantErr bool
+ }{
+ {
+ name: "basic container section",
+ input: `page Test at "/test" layout main {
+ section main type container
+ }`,
+ want: AST{
+ Definitions: []Definition{
+ {
+ Page: &Page{
+ Name: "Test",
+ Path: "/test",
+ Layout: "main",
+ Sections: []Section{
+ {
+ Name: "main",
+ Type: stringPtr("container"),
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "section with all attributes",
+ input: `page Test at "/test" layout main {
+ section sidebar type panel class "sidebar-nav" label "Navigation" trigger "toggle-sidebar" position "left" for User
+ }`,
+ want: AST{
+ Definitions: []Definition{
+ {
+ Page: &Page{
+ Name: "Test",
+ Path: "/test",
+ Layout: "main",
+ Sections: []Section{
+ {
+ Name: "sidebar",
+ Type: stringPtr("panel"),
+ Position: stringPtr("left"),
+ Class: stringPtr("sidebar-nav"),
+ Label: stringPtr("Navigation"),
+ Trigger: stringPtr("toggle-sidebar"),
+ Entity: stringPtr("User"),
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "tab sections with active state",
+ input: `page Test at "/test" layout main {
+ section tabs type tab {
+ section overview label "Overview" active
+ section details label "Details"
+ section settings label "Settings"
+ }
+ }`,
+ want: AST{
+ Definitions: []Definition{
+ {
+ Page: &Page{
+ Name: "Test",
+ Path: "/test",
+ Layout: "main",
+ Sections: []Section{
+ {
+ Name: "tabs",
+ Type: stringPtr("tab"),
+ Elements: []SectionElement{
+ {
+ Section: &Section{
+ Name: "overview",
+ Label: stringPtr("Overview"),
+ Active: true,
+ },
+ },
+ {
+ Section: &Section{
+ Name: "details",
+ Label: stringPtr("Details"),
+ },
+ },
+ {
+ Section: &Section{
+ Name: "settings",
+ Label: stringPtr("Settings"),
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "modal section with content",
+ input: `page Test at "/test" layout main {
+ section userModal type modal trigger "edit-user" {
+ component form for User {
+ field name type text required
+ field email type email required
+ button save label "Save Changes" style "primary"
+ button cancel label "Cancel" style "secondary"
+ }
+ }
+ }`,
+ want: AST{
+ Definitions: []Definition{
+ {
+ Page: &Page{
+ Name: "Test",
+ Path: "/test",
+ Layout: "main",
+ Sections: []Section{
+ {
+ Name: "userModal",
+ Type: stringPtr("modal"),
+ Trigger: stringPtr("edit-user"),
+ Elements: []SectionElement{
+ {
+ Component: &Component{
+ Type: "form",
+ Entity: stringPtr("User"),
+ Elements: []ComponentElement{
+ {
+ Field: &ComponentField{
+ Name: "name",
+ Type: "text",
+ Attributes: []ComponentFieldAttribute{
+ {Required: true},
+ },
+ },
+ },
+ {
+ Field: &ComponentField{
+ Name: "email",
+ Type: "email",
+ Attributes: []ComponentFieldAttribute{
+ {Required: true},
+ },
+ },
+ },
+ {
+ Button: &ComponentButton{
+ Name: "save",
+ Label: "Save Changes",
+ Attributes: []ComponentButtonAttr{
+ {Style: &ComponentButtonStyle{Value: "primary"}},
+ },
+ },
+ },
+ {
+ Button: &ComponentButton{
+ Name: "cancel",
+ Label: "Cancel",
+ Attributes: []ComponentButtonAttr{
+ {Style: &ComponentButtonStyle{Value: "secondary"}},
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "master-detail sections",
+ input: `page Test at "/test" layout main {
+ section masterDetail type master {
+ section userList type container {
+ component table for User {
+ fields [name, email]
+ }
+ }
+
+ section userDetail type detail trigger "user-selected" for User {
+ component form for User {
+ field name type text
+ field email type email
+ field bio type textarea
+ }
+ }
+ }
+ }`,
+ want: AST{
+ Definitions: []Definition{
+ {
+ Page: &Page{
+ Name: "Test",
+ Path: "/test",
+ Layout: "main",
+ Sections: []Section{
+ {
+ Name: "masterDetail",
+ Type: stringPtr("master"),
+ Elements: []SectionElement{
+ {
+ Section: &Section{
+ Name: "userList",
+ Type: stringPtr("container"),
+ Elements: []SectionElement{
+ {
+ Component: &Component{
+ Type: "table",
+ Entity: stringPtr("User"),
+ Elements: []ComponentElement{
+ {
+ Attribute: &ComponentAttr{
+ Fields: []string{"name", "email"},
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ Section: &Section{
+ Name: "userDetail",
+ Type: stringPtr("detail"),
+ Trigger: stringPtr("user-selected"),
+ Entity: stringPtr("User"),
+ Elements: []SectionElement{
+ {
+ Component: &Component{
+ Type: "form",
+ Entity: stringPtr("User"),
+ Elements: []ComponentElement{
+ {
+ Field: &ComponentField{
+ Name: "name",
+ Type: "text",
+ },
+ },
+ {
+ Field: &ComponentField{
+ Name: "email",
+ Type: "email",
+ },
+ },
+ {
+ Field: &ComponentField{
+ Name: "bio",
+ Type: "textarea",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "deeply nested sections",
+ input: `page Test at "/test" layout main {
+ section mainLayout type container {
+ section header type container class "header" {
+ component navbar {
+ field search type text placeholder "Search..."
+ }
+ }
+
+ section content type container {
+ section sidebar type panel position "left" {
+ component menu {
+ field navigation type list
+ }
+ }
+
+ section main type container {
+ section tabs type tab {
+ section overview label "Overview" active {
+ component dashboard {
+ field stats type metric
+ }
+ }
+
+ section reports label "Reports" {
+ component table for Report
+ }
+ }
+ }
+ }
+ }
+ }`,
+ want: AST{
+ Definitions: []Definition{
+ {
+ Page: &Page{
+ Name: "Test",
+ Path: "/test",
+ Layout: "main",
+ Sections: []Section{
+ {
+ Name: "mainLayout",
+ Type: stringPtr("container"),
+ Elements: []SectionElement{
+ {
+ Section: &Section{
+ Name: "header",
+ Type: stringPtr("container"),
+ Class: stringPtr("header"),
+ Elements: []SectionElement{
+ {
+ Component: &Component{
+ Type: "navbar",
+ Elements: []ComponentElement{
+ {
+ Field: &ComponentField{
+ Name: "search",
+ Type: "text",
+ Attributes: []ComponentFieldAttribute{
+ {Placeholder: stringPtr("Search...")},
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ Section: &Section{
+ Name: "content",
+ Type: stringPtr("container"),
+ Elements: []SectionElement{
+ {
+ Section: &Section{
+ Name: "sidebar",
+ Type: stringPtr("panel"),
+ Position: stringPtr("left"),
+ Elements: []SectionElement{
+ {
+ Component: &Component{
+ Type: "menu",
+ Elements: []ComponentElement{
+ {
+ Field: &ComponentField{
+ Name: "navigation",
+ Type: "list",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ Section: &Section{
+ Name: "main",
+ Type: stringPtr("container"),
+ Elements: []SectionElement{
+ {
+ Section: &Section{
+ Name: "tabs",
+ Type: stringPtr("tab"),
+ Elements: []SectionElement{
+ {
+ Section: &Section{
+ Name: "overview",
+ Label: stringPtr("Overview"),
+ Active: true,
+ Elements: []SectionElement{
+ {
+ Component: &Component{
+ Type: "dashboard",
+ Elements: []ComponentElement{
+ {
+ Field: &ComponentField{
+ Name: "stats",
+ Type: "metric",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ Section: &Section{
+ Name: "reports",
+ Label: stringPtr("Reports"),
+ Elements: []SectionElement{
+ {
+ Component: &Component{
+ Type: "table",
+ Entity: stringPtr("Report"),
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "section with conditional content",
+ input: `page Test at "/test" layout main {
+ section adminPanel type container {
+ when user_role equals "admin" {
+ section userManagement type container {
+ component table for User
+ }
+ section systemSettings type container {
+ component form for Settings
+ }
+ }
+ }
+ }`,
+ want: AST{
+ Definitions: []Definition{
+ {
+ Page: &Page{
+ Name: "Test",
+ Path: "/test",
+ Layout: "main",
+ Sections: []Section{
+ {
+ Name: "adminPanel",
+ Type: stringPtr("container"),
+ Elements: []SectionElement{
+ {
+ When: &WhenCondition{
+ Field: "user_role",
+ Operator: "equals",
+ Value: "admin",
+ Sections: []Section{
+ {
+ Name: "userManagement",
+ Type: stringPtr("container"),
+ Elements: []SectionElement{
+ {
+ Component: &Component{
+ Type: "table",
+ Entity: stringPtr("User"),
+ },
+ },
+ },
+ },
+ {
+ Name: "systemSettings",
+ Type: stringPtr("container"),
+ Elements: []SectionElement{
+ {
+ Component: &Component{
+ Type: "form",
+ Entity: stringPtr("Settings"),
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := ParseInput(tt.input)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("ParseInput() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if !astEqual(got, tt.want) {
+ t.Errorf("ParseInput() got = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestParseSectionTypes(t *testing.T) {
+ sectionTypes := []string{
+ "container", "tab", "panel", "modal", "master", "detail",
+ }
+
+ for _, sectionType := range sectionTypes {
+ t.Run("section_type_"+sectionType, func(t *testing.T) {
+ input := `page Test at "/test" layout main {
+ section test_section type ` + sectionType + `
+ }`
+
+ got, err := ParseInput(input)
+ if err != nil {
+ t.Errorf("ParseInput() failed for section type %s: %v", sectionType, err)
+ return
+ }
+
+ if len(got.Definitions) != 1 || got.Definitions[0].Page == nil {
+ t.Errorf("ParseInput() failed to parse page for section type %s", sectionType)
+ return
+ }
+
+ page := got.Definitions[0].Page
+ if len(page.Sections) != 1 {
+ t.Errorf("ParseInput() failed to parse section for type %s", sectionType)
+ return
+ }
+
+ section := page.Sections[0]
+ if section.Type == nil || *section.Type != sectionType {
+ t.Errorf("ParseInput() section type mismatch: got %v, want %s", section.Type, sectionType)
+ }
+ })
+ }
+}
+
+func TestParseSectionErrors(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ }{
+ {
+ name: "missing section name",
+ input: `page Test at "/test" layout main {
+ section type container
+ }`,
+ },
+ {
+ name: "invalid section type",
+ input: `page Test at "/test" layout main {
+ section test type invalid_type
+ }`,
+ },
+ {
+ name: "unclosed section block",
+ input: `page Test at "/test" layout main {
+ section test type container {
+ component form
+ }`,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ _, err := ParseInput(tt.input)
+ if err == nil {
+ t.Errorf("ParseInput() expected error for invalid syntax, got nil")
+ }
+ })
+ }
+}
diff --git a/lang/test_ast_comparisons.go b/lang/test_ast_comparisons.go
new file mode 100644
index 0000000..fe6debd
--- /dev/null
+++ b/lang/test_ast_comparisons.go
@@ -0,0 +1,59 @@
+package lang
+
+// AST and definition comparison functions for parser tests
+
+// Custom comparison functions (simplified for the new structure)
+func astEqual(got, want AST) bool {
+ if len(got.Definitions) != len(want.Definitions) {
+ return false
+ }
+
+ for i := range got.Definitions {
+ if !definitionEqual(got.Definitions[i], want.Definitions[i]) {
+ return false
+ }
+ }
+ return true
+}
+
+func definitionEqual(got, want Definition) bool {
+ // Server comparison
+ if (got.Server == nil) != (want.Server == nil) {
+ return false
+ }
+ if got.Server != nil && want.Server != nil {
+ if !serverEqual(*got.Server, *want.Server) {
+ return false
+ }
+ }
+
+ // Entity comparison
+ if (got.Entity == nil) != (want.Entity == nil) {
+ return false
+ }
+ if got.Entity != nil && want.Entity != nil {
+ if !entityEqual(*got.Entity, *want.Entity) {
+ return false
+ }
+ }
+
+ // Endpoint comparison
+ if (got.Endpoint == nil) != (want.Endpoint == nil) {
+ return false
+ }
+ if got.Endpoint != nil && want.Endpoint != nil {
+ if !endpointEqual(*got.Endpoint, *want.Endpoint) {
+ return false
+ }
+ }
+
+ // Page comparison (enhanced)
+ if (got.Page == nil) != (want.Page == nil) {
+ return false
+ }
+ if got.Page != nil && want.Page != nil {
+ return pageEqual(*got.Page, *want.Page)
+ }
+
+ return true
+}
diff --git a/lang/test_comparison_utils.go b/lang/test_comparison_utils.go
new file mode 100644
index 0000000..017772d
--- /dev/null
+++ b/lang/test_comparison_utils.go
@@ -0,0 +1,46 @@
+package lang
+
+// Basic comparison utilities for parser tests
+
+// Helper functions for creating pointers
+func stringPtr(s string) *string {
+ return &s
+}
+
+func intPtr(i int) *int {
+ return &i
+}
+
+// Pointer comparison functions
+func stringPtrEqual(got, want *string) bool {
+ if (got == nil) != (want == nil) {
+ return false
+ }
+ if got != nil && want != nil {
+ return *got == *want
+ }
+ return true
+}
+
+func intPtrEqual(got, want *int) bool {
+ if (got == nil) != (want == nil) {
+ return false
+ }
+ if got != nil && want != nil {
+ return *got == *want
+ }
+ return true
+}
+
+// Slice comparison functions
+func stringSliceEqual(got, want []string) bool {
+ if len(got) != len(want) {
+ return false
+ }
+ for i, s := range got {
+ if s != want[i] {
+ return false
+ }
+ }
+ return true
+}
diff --git a/lang/test_field_comparisons.go b/lang/test_field_comparisons.go
new file mode 100644
index 0000000..f2db7d3
--- /dev/null
+++ b/lang/test_field_comparisons.go
@@ -0,0 +1,69 @@
+package lang
+
+// Field and validation comparison functions for parser tests
+
+func fieldEqual(got, want Field) bool {
+ if got.Name != want.Name || got.Type != want.Type {
+ return false
+ }
+
+ if got.Required != want.Required || got.Unique != want.Unique || got.Index != want.Index {
+ return false
+ }
+
+ if !stringPtrEqual(got.Default, want.Default) {
+ return false
+ }
+
+ if len(got.Validations) != len(want.Validations) {
+ return false
+ }
+
+ for i, validation := range got.Validations {
+ if !validationEqual(validation, want.Validations[i]) {
+ return false
+ }
+ }
+
+ if (got.Relationship == nil) != (want.Relationship == nil) {
+ return false
+ }
+ if got.Relationship != nil && want.Relationship != nil {
+ if !relationshipEqual(*got.Relationship, *want.Relationship) {
+ return false
+ }
+ }
+
+ return true
+}
+
+func validationEqual(got, want Validation) bool {
+ return got.Type == want.Type && stringPtrEqual(got.Value, want.Value)
+}
+
+func relationshipEqual(got, want Relationship) bool {
+ return got.Type == want.Type &&
+ got.Cardinality == want.Cardinality &&
+ stringPtrEqual(got.ForeignKey, want.ForeignKey) &&
+ stringPtrEqual(got.Through, want.Through)
+}
+
+func fieldRelationEqual(got, want *FieldRelation) bool {
+ if (got == nil) != (want == nil) {
+ return false
+ }
+ if got != nil && want != nil {
+ return got.Type == want.Type
+ }
+ return true
+}
+
+func componentValidationEqual(got, want *ComponentValidation) bool {
+ if (got == nil) != (want == nil) {
+ return false
+ }
+ if got != nil && want != nil {
+ return got.Type == want.Type && stringPtrEqual(got.Value, want.Value)
+ }
+ return true
+}
diff --git a/lang/test_server_entity_comparisons.go b/lang/test_server_entity_comparisons.go
new file mode 100644
index 0000000..20c4d9a
--- /dev/null
+++ b/lang/test_server_entity_comparisons.go
@@ -0,0 +1,57 @@
+package lang
+
+// Server and entity comparison functions for parser tests
+
+func serverEqual(got, want Server) bool {
+ if got.Name != want.Name {
+ return false
+ }
+
+ if len(got.Settings) != len(want.Settings) {
+ return false
+ }
+
+ for i, setting := range got.Settings {
+ if !serverSettingEqual(setting, want.Settings[i]) {
+ return false
+ }
+ }
+
+ return true
+}
+
+func serverSettingEqual(got, want ServerSetting) bool {
+ return stringPtrEqual(got.Host, want.Host) && intPtrEqual(got.Port, want.Port)
+}
+
+func entityEqual(got, want Entity) bool {
+ if got.Name != want.Name {
+ return false
+ }
+
+ if !stringPtrEqual(got.Description, want.Description) {
+ return false
+ }
+
+ if len(got.Fields) != len(want.Fields) {
+ return false
+ }
+
+ for i, field := range got.Fields {
+ if !fieldEqual(field, want.Fields[i]) {
+ return false
+ }
+ }
+
+ return true
+}
+
+func endpointEqual(got, want Endpoint) bool {
+ return got.Method == want.Method &&
+ got.Path == want.Path &&
+ stringPtrEqual(got.Entity, want.Entity) &&
+ stringPtrEqual(got.Description, want.Description) &&
+ got.Auth == want.Auth &&
+ stringPtrEqual(got.CustomLogic, want.CustomLogic)
+ // TODO: Add params and response comparison if needed
+}
diff --git a/lang/test_ui_comparisons.go b/lang/test_ui_comparisons.go
new file mode 100644
index 0000000..88a191e
--- /dev/null
+++ b/lang/test_ui_comparisons.go
@@ -0,0 +1,421 @@
+package lang
+
+// Page and UI component comparison functions for parser tests
+
+func pageEqual(got, want Page) bool {
+ if got.Name != want.Name || got.Path != want.Path || got.Layout != want.Layout {
+ return false
+ }
+
+ if !stringPtrEqual(got.Title, want.Title) {
+ return false
+ }
+
+ if !stringPtrEqual(got.Description, want.Description) {
+ return false
+ }
+
+ if got.Auth != want.Auth {
+ return false
+ }
+
+ // Compare meta tags
+ if len(got.Meta) != len(want.Meta) {
+ return false
+ }
+
+ for i, meta := range got.Meta {
+ if !metaTagEqual(meta, want.Meta[i]) {
+ return false
+ }
+ }
+
+ // Compare sections (unified model)
+ if len(got.Sections) != len(want.Sections) {
+ return false
+ }
+
+ for i, section := range got.Sections {
+ if !sectionEqual(section, want.Sections[i]) {
+ return false
+ }
+ }
+
+ // Compare components
+ if len(got.Components) != len(want.Components) {
+ return false
+ }
+
+ for i, component := range got.Components {
+ if !componentEqual(component, want.Components[i]) {
+ return false
+ }
+ }
+
+ return true
+}
+
+func metaTagEqual(got, want MetaTag) bool {
+ return got.Name == want.Name && got.Content == want.Content
+}
+
+func sectionEqual(got, want Section) bool {
+ if got.Name != want.Name {
+ return false
+ }
+
+ if !stringPtrEqual(got.Type, want.Type) {
+ return false
+ }
+
+ if !stringPtrEqual(got.Class, want.Class) {
+ return false
+ }
+
+ if !stringPtrEqual(got.Label, want.Label) {
+ return false
+ }
+
+ if got.Active != want.Active {
+ return false
+ }
+
+ if !stringPtrEqual(got.Trigger, want.Trigger) {
+ return false
+ }
+
+ if !stringPtrEqual(got.Position, want.Position) {
+ return false
+ }
+
+ if !stringPtrEqual(got.Entity, want.Entity) {
+ return false
+ }
+
+ // Extract different element types from the unified elements
+ gotAttributes := extractSectionAttributes(got.Elements)
+ gotComponents := extractSectionComponents(got.Elements)
+ gotSections := extractSectionSections(got.Elements)
+ gotWhen := extractSectionWhen(got.Elements)
+
+ wantAttributes := extractSectionAttributes(want.Elements)
+ wantComponents := extractSectionComponents(want.Elements)
+ wantSections := extractSectionSections(want.Elements)
+ wantWhen := extractSectionWhen(want.Elements)
+
+ // Compare attributes
+ if len(gotAttributes) != len(wantAttributes) {
+ return false
+ }
+ for i, attr := range gotAttributes {
+ if !sectionAttributeEqual(attr, wantAttributes[i]) {
+ return false
+ }
+ }
+
+ // Compare components
+ if len(gotComponents) != len(wantComponents) {
+ return false
+ }
+ for i, comp := range gotComponents {
+ if !componentEqual(comp, wantComponents[i]) {
+ return false
+ }
+ }
+
+ // Compare nested sections
+ if len(gotSections) != len(wantSections) {
+ return false
+ }
+ for i, sect := range gotSections {
+ if !sectionEqual(sect, wantSections[i]) {
+ return false
+ }
+ }
+
+ // Compare when conditions
+ if len(gotWhen) != len(wantWhen) {
+ return false
+ }
+ for i, when := range gotWhen {
+ if !whenConditionEqual(when, wantWhen[i]) {
+ return false
+ }
+ }
+
+ return true
+}
+
+// Helper functions to extract different element types from unified elements
+func extractSectionAttributes(elements []SectionElement) []SectionAttribute {
+ var attrs []SectionAttribute
+ for _, elem := range elements {
+ if elem.Attribute != nil {
+ attrs = append(attrs, *elem.Attribute)
+ }
+ }
+ return attrs
+}
+
+func extractSectionComponents(elements []SectionElement) []Component {
+ var comps []Component
+ for _, elem := range elements {
+ if elem.Component != nil {
+ comps = append(comps, *elem.Component)
+ }
+ }
+ return comps
+}
+
+func extractSectionSections(elements []SectionElement) []Section {
+ var sects []Section
+ for _, elem := range elements {
+ if elem.Section != nil {
+ sects = append(sects, *elem.Section)
+ }
+ }
+ return sects
+}
+
+func extractSectionWhen(elements []SectionElement) []WhenCondition {
+ var whens []WhenCondition
+ for _, elem := range elements {
+ if elem.When != nil {
+ whens = append(whens, *elem.When)
+ }
+ }
+ return whens
+}
+
+func sectionAttributeEqual(got, want SectionAttribute) bool {
+ return stringPtrEqual(got.DataSource, want.DataSource) &&
+ stringPtrEqual(got.Style, want.Style) &&
+ stringPtrEqual(got.Classes, want.Classes) &&
+ intPtrEqual(got.Size, want.Size) &&
+ stringPtrEqual(got.Theme, want.Theme)
+}
+
+func componentEqual(got, want Component) bool {
+ if got.Type != want.Type {
+ return false
+ }
+
+ if !stringPtrEqual(got.Entity, want.Entity) {
+ return false
+ }
+
+ if len(got.Elements) != len(want.Elements) {
+ return false
+ }
+
+ for i, elem := range got.Elements {
+ if !componentElementEqual(elem, want.Elements[i]) {
+ return false
+ }
+ }
+
+ return true
+}
+
+func componentElementEqual(got, want ComponentElement) bool {
+ // Compare attributes
+ if (got.Attribute == nil) != (want.Attribute == nil) {
+ return false
+ }
+ if got.Attribute != nil && want.Attribute != nil {
+ if !componentAttrEqual(*got.Attribute, *want.Attribute) {
+ return false
+ }
+ }
+
+ // Compare fields
+ if (got.Field == nil) != (want.Field == nil) {
+ return false
+ }
+ if got.Field != nil && want.Field != nil {
+ if !componentFieldEqual(*got.Field, *want.Field) {
+ return false
+ }
+ }
+
+ // Compare sections
+ if (got.Section == nil) != (want.Section == nil) {
+ return false
+ }
+ if got.Section != nil && want.Section != nil {
+ if !sectionEqual(*got.Section, *want.Section) {
+ return false
+ }
+ }
+
+ // Compare buttons
+ if (got.Button == nil) != (want.Button == nil) {
+ return false
+ }
+ if got.Button != nil && want.Button != nil {
+ if !componentButtonEqual(*got.Button, *want.Button) {
+ return false
+ }
+ }
+
+ // Compare when conditions
+ if (got.When == nil) != (want.When == nil) {
+ return false
+ }
+ if got.When != nil && want.When != nil {
+ if !whenConditionEqual(*got.When, *want.When) {
+ return false
+ }
+ }
+
+ return true
+}
+
+func componentAttrEqual(got, want ComponentAttr) bool {
+ return stringPtrEqual(got.DataSource, want.DataSource) &&
+ stringSliceEqual(got.Fields, want.Fields) &&
+ stringSliceEqual(got.Actions, want.Actions) &&
+ stringPtrEqual(got.Style, want.Style) &&
+ stringPtrEqual(got.Classes, want.Classes) &&
+ intPtrEqual(got.PageSize, want.PageSize) &&
+ got.Validate == want.Validate
+}
+
+func componentFieldEqual(got, want ComponentField) bool {
+ if got.Name != want.Name || got.Type != want.Type {
+ return false
+ }
+
+ if len(got.Attributes) != len(want.Attributes) {
+ return false
+ }
+
+ for i, attr := range got.Attributes {
+ if !componentFieldAttributeEqual(attr, want.Attributes[i]) {
+ return false
+ }
+ }
+
+ return true
+}
+
+func componentFieldAttributeEqual(got, want ComponentFieldAttribute) bool {
+ return stringPtrEqual(got.Label, want.Label) &&
+ stringPtrEqual(got.Placeholder, want.Placeholder) &&
+ got.Required == want.Required &&
+ got.Sortable == want.Sortable &&
+ got.Searchable == want.Searchable &&
+ got.Thumbnail == want.Thumbnail &&
+ stringPtrEqual(got.Default, want.Default) &&
+ stringSliceEqual(got.Options, want.Options) &&
+ stringPtrEqual(got.Accept, want.Accept) &&
+ intPtrEqual(got.Rows, want.Rows) &&
+ stringPtrEqual(got.Format, want.Format) &&
+ stringPtrEqual(got.Size, want.Size) &&
+ stringPtrEqual(got.Display, want.Display) &&
+ stringPtrEqual(got.Value, want.Value) &&
+ stringPtrEqual(got.Source, want.Source) &&
+ fieldRelationEqual(got.Relates, want.Relates) &&
+ componentValidationEqual(got.Validation, want.Validation)
+}
+
+func componentButtonEqual(got, want ComponentButton) bool {
+ if got.Name != want.Name || got.Label != want.Label {
+ return false
+ }
+
+ // Extract attributes from both buttons for comparison
+ gotStyle, gotIcon, gotLoading, gotDisabled, gotConfirm, gotTarget, gotPosition, gotVia := extractButtonAttributesNew(got.Attributes)
+ wantStyle, wantIcon, wantLoading, wantDisabled, wantConfirm, wantTarget, wantPosition, wantVia := extractButtonAttributesNew(want.Attributes)
+
+ return stringPtrEqual(gotStyle, wantStyle) &&
+ stringPtrEqual(gotIcon, wantIcon) &&
+ stringPtrEqual(gotLoading, wantLoading) &&
+ stringPtrEqual(gotDisabled, wantDisabled) &&
+ stringPtrEqual(gotConfirm, wantConfirm) &&
+ stringPtrEqual(gotTarget, wantTarget) &&
+ stringPtrEqual(gotPosition, wantPosition) &&
+ stringPtrEqual(gotVia, wantVia)
+}
+
+// Helper function to extract button attributes from the new structure
+func extractButtonAttributesNew(attrs []ComponentButtonAttr) (*string, *string, *string, *string, *string, *string, *string, *string) {
+ var style, icon, loading, disabled, confirm, target, position, via *string
+
+ for _, attr := range attrs {
+ if attr.Style != nil {
+ style = &attr.Style.Value
+ }
+ if attr.Icon != nil {
+ icon = &attr.Icon.Value
+ }
+ if attr.Loading != nil {
+ loading = &attr.Loading.Value
+ }
+ if attr.Disabled != nil {
+ disabled = &attr.Disabled.Value
+ }
+ if attr.Confirm != nil {
+ confirm = &attr.Confirm.Value
+ }
+ if attr.Target != nil {
+ target = &attr.Target.Value
+ }
+ if attr.Position != nil {
+ position = &attr.Position.Value
+ }
+ if attr.Via != nil {
+ via = &attr.Via.Value
+ }
+ }
+
+ return style, icon, loading, disabled, confirm, target, position, via
+}
+
+func whenConditionEqual(got, want WhenCondition) bool {
+ if got.Field != want.Field || got.Operator != want.Operator || got.Value != want.Value {
+ return false
+ }
+
+ // Compare fields
+ if len(got.Fields) != len(want.Fields) {
+ return false
+ }
+ for i, field := range got.Fields {
+ if !componentFieldEqual(field, want.Fields[i]) {
+ return false
+ }
+ }
+
+ // Compare sections
+ if len(got.Sections) != len(want.Sections) {
+ return false
+ }
+ for i, section := range got.Sections {
+ if !sectionEqual(section, want.Sections[i]) {
+ return false
+ }
+ }
+
+ // Compare components
+ if len(got.Components) != len(want.Components) {
+ return false
+ }
+ for i, component := range got.Components {
+ if !componentEqual(component, want.Components[i]) {
+ return false
+ }
+ }
+
+ // Compare buttons
+ if len(got.Buttons) != len(want.Buttons) {
+ return false
+ }
+ for i, button := range got.Buttons {
+ if !componentButtonEqual(button, want.Buttons[i]) {
+ return false
+ }
+ }
+
+ return true
+}