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