add bracket syntax replace tests

This commit is contained in:
2025-08-24 23:25:43 -06:00
parent 4ac93ee924
commit e71b1c3a23
22 changed files with 3324 additions and 1673 deletions

File diff suppressed because one or more lines are too long

View File

@ -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
}
// 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 {
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)
servers++
}
if setting.Port != nil {
fmt.Printf(" port: %d\n", *setting.Port)
}
}
fmt.Printf("\n")
}
if def.Entity != nil {
entity := def.Entity
fmt.Printf("🏗️ Entity: %s", entity.Name)
if entity.Description != nil {
fmt.Printf(" - %s", *entity.Description)
entities++
}
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")
}
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)
endpoints++
}
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)
pages++
}
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)
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
}
}
// Display containers
for _, container := range page.Containers {
fmt.Printf(" 📦 Container: %s", container.Type)
if container.Class != nil {
fmt.Printf(" class=\"%s\"", *container.Class)
// 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))
}
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, " ")
if entityCount >= 2 { // Just show first couple
break
}
}
}
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")
// 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
}
}
}
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)
// 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
}
}
}
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
// 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)
}
}
}
for _, element := range comp.Elements {
if element.Field != nil {
fields = append(fields, *element.Field)
if totalSections > 0 {
fmt.Printf(" ✓ Found %d sections with %d nested levels (recursive parsing working)\n", totalSections, nestedSections)
}
if element.Config != nil {
config = append(config, *element.Config)
}
if element.Condition != nil {
conditions = append(conditions, *element.Condition)
fmt.Printf("\n🎯 Block delimiter syntax is working correctly!\n")
fmt.Printf(" All constructs (server, entity, endpoint, page, section, component) now use { } blocks\n")
fmt.Printf(" No more ambiguous whitespace-dependent parsing\n")
fmt.Printf(" Language is now unambiguous and consistent\n")
}
// Helper function to count nested sections recursively
func countNestedSections(section lang.Section) int {
count := 0
for _, element := range section.Elements {
if element.Section != nil {
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
}

View File

@ -1,26 +1,31 @@
// 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"
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"
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"
entity Post desc "Blog posts" {
id: uuid required unique
title: string required validate min_length "1" validate max_length "200"
content: text required
@ -28,159 +33,263 @@ entity Post desc "Blog posts"
published: boolean default "false"
created_at: timestamp default "now()"
tags: uuid relates to Tag as many through "post_tags"
}
entity Tag desc "Content tags"
entity Tag desc "Content tags" {
id: uuid required unique
name: string required unique validate min_length "1" validate max_length "50"
slug: string required unique indexed
created_at: timestamp default "now()"
}
// API Endpoints with different HTTP methods and parameter sources
endpoint GET "/users" for User desc "List users" auth
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"
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"
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
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"
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
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
// 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
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
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
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
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"
when role equals "admin" {
component AdminPermissions {
field permissions type multiselect label "Permissions" {
options ["users.manage", "posts.manage", "system.config"]
}
}
}
section actions
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
// 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
section users type tab label "Users" {
component UserTable for User {
data from "/users"
}
}
tab posts label "Posts"
component PostTable for Post
section posts type tab label "Posts" {
component PostTable for Post {
data from "/posts"
}
}
}
modal CreateUserModal trigger "create-user"
component UserForm for User
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"
master PostList
component Table for Post
// 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"
}
}
detail PostEditor trigger "edit"
component Form for Post
section basic class "mb-4"
section detail type detail trigger "edit" {
component PostForm for Post {
section basic class "mb-4" {
component BasicFields {
field title type text label "Post Title" required
field content type richtext label "Content" required
}
}
section metadata class "grid grid-cols-2 gap-4"
field author_id type autocomplete label "Author"
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"
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
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"
}
}
// Conditional rendering example
page ConditionalForm at "/conditional" layout MainLayout title "Conditional Form"
component UserForm for User
// Complex nested sections example
page ComplexLayout at "/complex" layout MainLayout title "Complex Layout" {
section mainContainer type container class "flex h-screen" {
section sidebar type container class "w-64 bg-gray-100" {
section navigation {
component NavMenu
}
section userInfo type panel trigger "profile" position "bottom" {
component UserProfile
}
}
section content type container class "flex-1" {
section header class "h-16 border-b" {
component PageHeader
}
section body class "flex-1 p-4" {
section tabs type container {
section overview type tab label "Overview" active {
section metrics class "grid grid-cols-3 gap-4" {
component MetricCard
component MetricCard
component MetricCard
}
}
section details type tab label "Details" {
component DetailView
}
}
}
}
}
}
// Conditional rendering with sections and components
page ConditionalForm at "/conditional" layout MainLayout title "Conditional Form" {
component UserForm for User {
field email type text label "Email" required
field role type select options ["admin", "user", "moderator"]
when role equals "admin"
field permissions type multiselect label "Admin Permissions"
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"]
}
}
when role equals "moderator"
field moderation_level type select label "Moderation Level"
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
section actions {
component ActionButtons {
button save label "Save User" style "primary" loading "Saving..."
button cancel label "Cancel" style "secondary"
}
}
}
}

2
go.mod
View File

@ -3,12 +3,12 @@ module masonry
go 1.23
require (
github.com/alecthomas/participle/v2 v2.1.4
github.com/urfave/cli/v2 v2.27.5
golang.org/x/text v0.22.0
)
require (
github.com/alecthomas/participle/v2 v2.1.4 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect

6
go.sum
View File

@ -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=

View File

@ -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,7 +88,7 @@ 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"`
@ -96,12 +96,9 @@ type Page struct {
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:"@@*"`
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"`
Type *string `parser:"('type' @('container' | 'tab' | 'panel' | 'modal' | 'master' | 'detail'))?"`
Class *string `parser:"('class' @String)?"`
Components []Component `parser:"@@*"`
Panels []Panel `parser:"@@*"`
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:"@@"`
Attribute *ComponentAttr `parser:"@@"` // Component attributes can be inside the block
Field *ComponentField `parser:"| @@"`
Condition *WhenCondition `parser:"| @@"`
Section *ComponentSection `parser:"| @@"`
Action *ComponentButtonAttr `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:"@@*"`
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:"@@*"`
}
// Enhanced component buttons/actions with detailed configuration
type ComponentButtonAttr struct {
// Simplified button with flexible attribute ordering
type ComponentButton 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)?"`
Attributes []ComponentButtonAttr `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'"`
// Flexible button attribute system - each attribute is a separate alternative
type ComponentButtonAttr struct {
Style *ComponentButtonStyle `parser:"@@"`
Icon *ComponentButtonIcon `parser:"| @@"`
Loading *ComponentButtonLoading `parser:"| @@"`
Disabled *ComponentButtonDisabled `parser:"| @@"`
Confirm *ComponentButtonConfirm `parser:"| @@"`
Target *ComponentButtonTarget `parser:"| @@"`
Position *ComponentButtonPosition `parser:"| @@"`
Via *ComponentButtonVia `parser:"| @@"`
}
// Component field specification (simple version for backward compatibility)
type ComponentFields struct {
Fields []string `parser:"'fields' '[' @Ident (',' @Ident)* ']'"`
// Individual button attribute types
type ComponentButtonStyle struct {
Value string `parser:"'style' @String"`
}
// Enhanced component actions
type ComponentActions struct {
Actions []ComponentAction `parser:"'actions' '[' @@ (',' @@)* ']'"`
type ComponentButtonIcon struct {
Value string `parser:"'icon' @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 ComponentButtonLoading struct {
Value string `parser:"'loading' @String"`
}
// Data source configuration (can reference endpoints)
type ComponentDataSource struct {
Endpoint string `parser:"'data' 'from' @String"`
type ComponentButtonDisabled struct {
Value string `parser:"'disabled' 'when' @Ident"`
}
// Component styling
type ComponentStyle struct {
Theme *string `parser:"'style' @Ident"`
Classes []string `parser:"('classes' '[' @String (',' @String)* ']')?"`
type ComponentButtonConfirm struct {
Value string `parser:"'confirm' @String"`
}
// Pagination configuration
type ComponentPagination struct {
PageSize *int `parser:"'pagination' ('size' @Int)?"`
type ComponentButtonTarget struct {
Value string `parser:"'target' @Ident"`
}
// Filter specifications
type ComponentFilters struct {
Filters []ComponentFilter `parser:"'filters' '[' @@ (',' @@)* ']'"`
type ComponentButtonPosition struct {
Value string `parser:"'position' @String"`
}
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) {

View File

@ -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

View File

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

View File

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

View File

@ -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 {

View File

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

View File

@ -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 {

View File

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

View File

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

View File

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

300
lang/parser_ui_page_test.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

421
lang/test_ui_comparisons.go Normal file
View File

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