From 1ee8de23da9f282f143ddde3e57d95ad235c93b9 Mon Sep 17 00:00:00 2001 From: Mason Payne Date: Fri, 22 Aug 2025 00:59:14 -0600 Subject: [PATCH] split tests into separate files --- lang/lang_test.go | 940 +--------------------------------- lang/parser_advanced_test.go | 304 +++++++++++ lang/parser_component_test.go | 344 +++++++++++++ lang/parser_entity_test.go | 79 +++ lang/parser_page_test.go | 139 +++++ lang/parser_server_test.go | 47 ++ lang/parser_test_helpers.go | 138 +++++ 7 files changed, 1061 insertions(+), 930 deletions(-) create mode 100644 lang/parser_advanced_test.go create mode 100644 lang/parser_component_test.go create mode 100644 lang/parser_entity_test.go create mode 100644 lang/parser_page_test.go create mode 100644 lang/parser_server_test.go create mode 100644 lang/parser_test_helpers.go diff --git a/lang/lang_test.go b/lang/lang_test.go index 6e7c8ee..607a16b 100644 --- a/lang/lang_test.go +++ b/lang/lang_test.go @@ -1,932 +1,12 @@ package lang -import ( - "testing" -) - -func TestParseInput(t *testing.T) { - tests := []struct { - name string - input string - want AST - wantErr bool - }{ - { - name: "simple server definition", - input: `server MyApp host "localhost" port 8080`, - want: AST{ - Definitions: []Definition{ - { - Server: &Server{ - Name: "MyApp", - Settings: []ServerSetting{ - {Host: stringPtr("localhost")}, - {Port: intPtr(8080)}, - }, - }, - }, - }, - }, - wantErr: false, - }, - { - name: "entity with enhanced fields and relationships", - input: `entity User desc "User management" - id: uuid required unique - email: string required validate email validate min_length "5" - name: string default "Anonymous" - profile_id: uuid relates to Profile as one via "user_id"`, - want: AST{ - Definitions: []Definition{ - { - Entity: &Entity{ - Name: "User", - Description: stringPtr("User management"), - Fields: []Field{ - { - Name: "id", - Type: "uuid", - Required: true, - Unique: true, - }, - { - Name: "email", - Type: "string", - Required: true, - Validations: []Validation{ - {Type: "email"}, - {Type: "min_length", Value: stringPtr("5")}, - }, - }, - { - Name: "name", - Type: "string", - Default: stringPtr("Anonymous"), - }, - { - Name: "profile_id", - Type: "uuid", - Relationship: &Relationship{ - Type: "Profile", - Cardinality: "one", - ForeignKey: stringPtr("user_id"), - }, - }, - }, - }, - }, - }, - }, - wantErr: false, - }, - { - name: "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: "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 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: "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, - }, - { - name: "panel with trigger and position", - 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, - }, - { - 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 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 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)) - } -} - -// 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) - } -} - -// 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 -} +// 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. diff --git a/lang/parser_advanced_test.go b/lang/parser_advanced_test.go new file mode 100644 index 0000000..b4d1d8f --- /dev/null +++ b/lang/parser_advanced_test.go @@ -0,0 +1,304 @@ +package lang + +import ( + "testing" +) + +func TestAdvancedParsingFeatures(t *testing.T) { + tests := []struct { + name string + input string + want AST + wantErr bool + }{ + { + name: "component with conditional rendering", + input: `page ConditionalForm at "/conditional" layout MainLayout + component UserForm for User + field role type select options ["admin", "user"] + + when role equals "admin" + field permissions type multiselect label "Admin Permissions" + options ["users.manage", "posts.manage"]`, + want: AST{ + Definitions: []Definition{ + { + Page: &Page{ + Name: "ConditionalForm", + Path: "/conditional", + Layout: "MainLayout", + Components: []Component{ + { + Type: "UserForm", + Entity: stringPtr("User"), + Elements: []ComponentElement{ + { + Field: &ComponentField{ + Name: "role", + Type: "select", + Attributes: []ComponentFieldAttribute{ + {Options: []string{"admin", "user"}}, + }, + }, + }, + { + Condition: &WhenCondition{ + Field: "role", + Operator: "equals", + Value: "admin", + Fields: []ComponentField{ + { + Name: "permissions", + Type: "multiselect", + Attributes: []ComponentFieldAttribute{ + {Label: stringPtr("Admin Permissions")}, + {Options: []string{"users.manage", "posts.manage"}}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + wantErr: false, + }, + { + name: "page with tabs container", + input: `page Dashboard at "/dashboard" layout MainLayout + container tabs + tab overview label "Overview" active + component StatsCards + tab users label "Users" + component UserTable for User`, + want: AST{ + Definitions: []Definition{ + { + Page: &Page{ + Name: "Dashboard", + Path: "/dashboard", + Layout: "MainLayout", + Containers: []Container{ + { + Type: "tabs", + Tabs: []Tab{ + { + Name: "overview", + Label: "Overview", + Active: true, + Components: []Component{ + { + Type: "StatsCards", + Elements: []ComponentElement{}, + }, + }, + }, + { + Name: "users", + Label: "Users", + Components: []Component{ + { + Type: "UserTable", + Entity: stringPtr("User"), + Elements: []ComponentElement{}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + wantErr: false, + }, + { + name: "page with modal", + input: `page UserList at "/users" layout MainLayout + modal CreateUserModal trigger "create-user" + component UserForm for User + field email type text required + button save label "Create" via "/users"`, + want: AST{ + Definitions: []Definition{ + { + Page: &Page{ + Name: "UserList", + Path: "/users", + Layout: "MainLayout", + Modals: []Modal{ + { + Name: "CreateUserModal", + Trigger: "create-user", + Components: []Component{ + { + Type: "UserForm", + Entity: stringPtr("User"), + Elements: []ComponentElement{ + { + Field: &ComponentField{ + Name: "email", + Type: "text", + Attributes: []ComponentFieldAttribute{ + {Required: true}, + }, + }, + }, + { + Action: &ComponentButtonAttr{ + Name: "save", + Label: "Create", + Via: stringPtr("/users"), + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + wantErr: false, + }, + { + name: "page with master-detail layout", + input: `page PostManagement at "/admin/posts" layout AdminLayout + layout "master-detail" + + master PostList + component Table for Post + field title type text sortable + + detail PostEditor trigger "edit" + component Form for Post + field title type text required`, + want: AST{ + Definitions: []Definition{ + { + Page: &Page{ + Name: "PostManagement", + Path: "/admin/posts", + Layout: "AdminLayout", + LayoutType: stringPtr("master-detail"), + MasterDetail: &MasterDetail{ + Master: &MasterSection{ + Name: "PostList", + Components: []Component{ + { + Type: "Table", + Entity: stringPtr("Post"), + Elements: []ComponentElement{ + { + Field: &ComponentField{ + Name: "title", + Type: "text", + Attributes: []ComponentFieldAttribute{ + {Sortable: true}, + }, + }, + }, + }, + }, + }, + }, + Detail: &DetailSection{ + Name: "PostEditor", + Trigger: stringPtr("edit"), + Components: []Component{ + { + Type: "Form", + Entity: stringPtr("Post"), + Elements: []ComponentElement{ + { + Field: &ComponentField{ + Name: "title", + Type: "text", + Attributes: []ComponentFieldAttribute{ + {Required: true}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + wantErr: false, + }, + { + name: "invalid syntax should error", + input: `invalid syntax here`, + want: AST{}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseInput(tt.input) + if (err != nil) != tt.wantErr { + t.Errorf("ParseInput() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if !tt.wantErr && !astEqual(got, tt.want) { + t.Errorf("ParseInput() mismatch.\nGot: %+v\nWant: %+v", got, tt.want) + } + }) + } +} + +// Test for conditional rendering +func TestConditionalRendering(t *testing.T) { + input := `page ConditionalTest at "/test" layout MainLayout + component Form for User + field role type select options ["admin", "user"] + + when role equals "admin" + field permissions type multiselect options ["manage_users", "manage_posts"] + section admin_tools + field audit_log type toggle` + + ast, err := ParseInput(input) + if err != nil { + t.Fatalf("Failed to parse: %v", err) + } + + component := ast.Definitions[0].Page.Components[0] + + // Extract conditions from elements + var conditions []WhenCondition + for _, element := range component.Elements { + if element.Condition != nil { + conditions = append(conditions, *element.Condition) + } + } + + if len(conditions) != 1 { + t.Errorf("Expected 1 condition, got %d", len(conditions)) + } + + condition := conditions[0] + if condition.Field != "role" || condition.Operator != "equals" || condition.Value != "admin" { + t.Error("Condition parameters incorrect") + } + + if len(condition.Fields) != 1 { + t.Errorf("Expected 1 conditional field, got %d", len(condition.Fields)) + } + + if len(condition.Sections) != 1 { + t.Errorf("Expected 1 conditional section, got %d", len(condition.Sections)) + } +} diff --git a/lang/parser_component_test.go b/lang/parser_component_test.go new file mode 100644 index 0000000..9e79716 --- /dev/null +++ b/lang/parser_component_test.go @@ -0,0 +1,344 @@ +package lang + +import ( + "testing" +) + +func TestParseComponentDefinitions(t *testing.T) { + tests := []struct { + name string + input string + want AST + wantErr bool + }{ + { + name: "component with enhanced field configurations", + input: `page UserForm at "/users/new" layout MainLayout + component Form for User + field email type text label "Email Address" placeholder "Enter email" required validate email + field role type select options ["admin", "user"] default "user" + field avatar type file accept "image/*" + field bio type textarea rows 4`, + want: AST{ + Definitions: []Definition{ + { + Page: &Page{ + Name: "UserForm", + Path: "/users/new", + Layout: "MainLayout", + Components: []Component{ + { + Type: "Form", + Entity: stringPtr("User"), + Elements: []ComponentElement{ + { + Field: &ComponentField{ + Name: "email", + Type: "text", + Attributes: []ComponentFieldAttribute{ + {Label: stringPtr("Email Address")}, + {Placeholder: stringPtr("Enter email")}, + {Required: true}, + {Validation: &ComponentValidation{Type: "email"}}, + }, + }, + }, + { + Field: &ComponentField{ + Name: "role", + Type: "select", + Attributes: []ComponentFieldAttribute{ + {Options: []string{"admin", "user"}}, + {Default: stringPtr("user")}, + }, + }, + }, + { + Field: &ComponentField{ + Name: "avatar", + Type: "file", + Attributes: []ComponentFieldAttribute{ + {Accept: stringPtr("image/*")}, + }, + }, + }, + { + Field: &ComponentField{ + Name: "bio", + Type: "textarea", + Attributes: []ComponentFieldAttribute{ + {Rows: intPtr(4)}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + wantErr: false, + }, + { + name: "component with sections and buttons", + input: `page FormWithSections at "/form" layout MainLayout + component UserForm for User + section basic class "mb-4" + field email type text required + field name type text required + + section actions + button save label "Save" style "primary" loading "Saving..." via "/users" + button cancel label "Cancel" style "secondary"`, + want: AST{ + Definitions: []Definition{ + { + Page: &Page{ + Name: "FormWithSections", + Path: "/form", + Layout: "MainLayout", + Components: []Component{ + { + Type: "UserForm", + Entity: stringPtr("User"), + Elements: []ComponentElement{ + { + Section: &ComponentSection{ + Name: "basic", + Class: stringPtr("mb-4"), + Fields: []ComponentField{ + { + Name: "email", + Type: "text", + Attributes: []ComponentFieldAttribute{ + {Required: true}, + }, + }, + { + Name: "name", + Type: "text", + Attributes: []ComponentFieldAttribute{ + {Required: true}, + }, + }, + }, + }, + }, + { + Section: &ComponentSection{ + Name: "actions", + Buttons: []ComponentButtonAttr{ + { + Name: "save", + Label: "Save", + Style: stringPtr("primary"), + Loading: stringPtr("Saving..."), + Via: stringPtr("/users"), + }, + { + Name: "cancel", + Label: "Cancel", + Style: stringPtr("secondary"), + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + wantErr: false, + }, + { + name: "detailed field configurations", + input: `page DetailedTable at "/detailed" layout MainLayout + component Table for User + field email type text label "Email" sortable searchable + field avatar type image thumbnail size "32x32" + field created_at type datetime format "MMM dd, yyyy" + field author_id type autocomplete source "/users" display "name" value "id"`, + want: AST{ + Definitions: []Definition{ + { + Page: &Page{ + Name: "DetailedTable", + Path: "/detailed", + Layout: "MainLayout", + Components: []Component{ + { + Type: "Table", + Entity: stringPtr("User"), + Elements: []ComponentElement{ + { + Field: &ComponentField{ + Name: "email", + Type: "text", + Attributes: []ComponentFieldAttribute{ + {Label: stringPtr("Email")}, + {Sortable: true}, + {Searchable: true}, + }, + }, + }, + { + Field: &ComponentField{ + Name: "avatar", + Type: "image", + Attributes: []ComponentFieldAttribute{ + {Thumbnail: true}, + {Size: stringPtr("32x32")}, + }, + }, + }, + { + Field: &ComponentField{ + Name: "created_at", + Type: "datetime", + Attributes: []ComponentFieldAttribute{ + {Format: stringPtr("MMM dd, yyyy")}, + }, + }, + }, + { + Field: &ComponentField{ + Name: "author_id", + Type: "autocomplete", + Attributes: []ComponentFieldAttribute{ + {Source: stringPtr("/users")}, + {Display: stringPtr("name")}, + {Value: stringPtr("id")}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseInput(tt.input) + if (err != nil) != tt.wantErr { + t.Errorf("ParseInput() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if !tt.wantErr && !astEqual(got, tt.want) { + t.Errorf("ParseInput() mismatch.\nGot: %+v\nWant: %+v", got, tt.want) + } + }) + } +} + +// Test specifically for enhanced field features - updated for new structure +func TestEnhancedFieldParsing(t *testing.T) { + input := `page TestPage at "/test" layout MainLayout + component Form for User + field email type text label "Email" placeholder "Enter email" required validate email + field role type select label "Role" options ["admin", "user"] default "user" + field avatar type file accept "image/*" + field bio type textarea rows 5 placeholder "Tell us about yourself"` + + ast, err := ParseInput(input) + if err != nil { + t.Fatalf("Failed to parse: %v", err) + } + + page := ast.Definitions[0].Page + component := page.Components[0] + + // Extract fields from elements + var fields []ComponentField + for _, element := range component.Elements { + if element.Field != nil { + fields = append(fields, *element.Field) + } + } + + if len(fields) != 4 { + t.Errorf("Expected 4 fields, got %d", len(fields)) + } + + // Test basic field structure + emailField := fields[0] + if emailField.Name != "email" || emailField.Type != "text" { + t.Errorf("Email field incorrect: name=%s, type=%s", emailField.Name, emailField.Type) + } + + // Test that attributes are populated + if len(emailField.Attributes) == 0 { + t.Error("Email field should have attributes") + } + + // Test role field + roleField := fields[1] + if roleField.Name != "role" || roleField.Type != "select" { + t.Errorf("Role field incorrect: name=%s, type=%s", roleField.Name, roleField.Type) + } + + // Test file field + fileField := fields[2] + if fileField.Name != "avatar" || fileField.Type != "file" { + t.Errorf("File field incorrect: name=%s, type=%s", fileField.Name, fileField.Type) + } + + // Test textarea field + textareaField := fields[3] + if textareaField.Name != "bio" || textareaField.Type != "textarea" { + t.Errorf("Textarea field incorrect: name=%s, type=%s", textareaField.Name, textareaField.Type) + } +} + +// Test for config attributes after fields (reproduces parsing issue) +func TestConfigAfterFields(t *testing.T) { + input := `page TestPage at "/test" layout MainLayout + component DetailedTable for User + field email type text label "Email Address" + field name type text label "Full Name" + + data from "/users" + pagination size 20` + + ast, err := ParseInput(input) + if err != nil { + t.Fatalf("Failed to parse: %v", err) + } + + // Verify the component was parsed correctly + page := ast.Definitions[0].Page + component := page.Components[0] + + if component.Type != "DetailedTable" { + t.Errorf("Expected component type DetailedTable, got %s", component.Type) + } + + // Count fields and config elements + fieldCount := 0 + configCount := 0 + for _, element := range component.Elements { + if element.Field != nil { + fieldCount++ + } + if element.Config != nil { + configCount++ + } + } + + if fieldCount != 2 { + t.Errorf("Expected 2 fields, got %d", fieldCount) + } + + if configCount != 2 { + t.Errorf("Expected 2 config items, got %d", configCount) + } +} diff --git a/lang/parser_entity_test.go b/lang/parser_entity_test.go new file mode 100644 index 0000000..301647f --- /dev/null +++ b/lang/parser_entity_test.go @@ -0,0 +1,79 @@ +package lang + +import ( + "testing" +) + +func TestParseEntityDefinitions(t *testing.T) { + tests := []struct { + name string + input string + want AST + wantErr bool + }{ + { + name: "entity with enhanced fields and relationships", + input: `entity User desc "User management" + id: uuid required unique + email: string required validate email validate min_length "5" + name: string default "Anonymous" + profile_id: uuid relates to Profile as one via "user_id"`, + want: AST{ + Definitions: []Definition{ + { + Entity: &Entity{ + Name: "User", + Description: stringPtr("User management"), + Fields: []Field{ + { + Name: "id", + Type: "uuid", + Required: true, + Unique: true, + }, + { + Name: "email", + Type: "string", + Required: true, + Validations: []Validation{ + {Type: "email"}, + {Type: "min_length", Value: stringPtr("5")}, + }, + }, + { + Name: "name", + Type: "string", + Default: stringPtr("Anonymous"), + }, + { + Name: "profile_id", + Type: "uuid", + Relationship: &Relationship{ + Type: "Profile", + Cardinality: "one", + ForeignKey: stringPtr("user_id"), + }, + }, + }, + }, + }, + }, + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseInput(tt.input) + if (err != nil) != tt.wantErr { + t.Errorf("ParseInput() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if !tt.wantErr && !astEqual(got, tt.want) { + t.Errorf("ParseInput() mismatch.\nGot: %+v\nWant: %+v", got, tt.want) + } + }) + } +} diff --git a/lang/parser_page_test.go b/lang/parser_page_test.go new file mode 100644 index 0000000..002d355 --- /dev/null +++ b/lang/parser_page_test.go @@ -0,0 +1,139 @@ +package lang + +import ( + "testing" +) + +func TestParsePageDefinitions(t *testing.T) { + tests := []struct { + name string + input string + want AST + wantErr bool + }{ + { + name: "page with container and sections", + input: `page UserManagement at "/admin/users" layout AdminLayout title "User Management" auth + meta description "Manage users" + + container main class "grid grid-cols-2" + section sidebar class "col-span-1" + component UserStats for User + data from "/users/stats"`, + want: AST{ + Definitions: []Definition{ + { + Page: &Page{ + Name: "UserManagement", + Path: "/admin/users", + Layout: "AdminLayout", + Title: stringPtr("User Management"), + Auth: true, + Meta: []MetaTag{ + {Name: "description", Content: "Manage users"}, + }, + Containers: []Container{ + { + Type: "main", + Class: stringPtr("grid grid-cols-2"), + Sections: []Section{ + { + Name: "sidebar", + Class: stringPtr("col-span-1"), + Components: []Component{ + { + Type: "UserStats", + Entity: stringPtr("User"), + Elements: []ComponentElement{ + { + Config: &ComponentAttr{ + DataSource: &ComponentDataSource{ + Endpoint: "/users/stats", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + wantErr: false, + }, + { + name: "page with panel", + input: `page PageWithPanel at "/panel" layout MainLayout + container main + section content + panel UserEditPanel for User trigger "edit" position "slide-right" + component UserForm for User + field name type text required`, + want: AST{ + Definitions: []Definition{ + { + Page: &Page{ + Name: "PageWithPanel", + Path: "/panel", + Layout: "MainLayout", + Containers: []Container{ + { + Type: "main", + Sections: []Section{ + { + Name: "content", + Panels: []Panel{ + { + Name: "UserEditPanel", + Entity: stringPtr("User"), + Trigger: "edit", + Position: stringPtr("slide-right"), + Components: []Component{ + { + Type: "UserForm", + Entity: stringPtr("User"), + Elements: []ComponentElement{ + { + Field: &ComponentField{ + Name: "name", + Type: "text", + Attributes: []ComponentFieldAttribute{ + {Required: true}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseInput(tt.input) + if (err != nil) != tt.wantErr { + t.Errorf("ParseInput() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if !tt.wantErr && !astEqual(got, tt.want) { + t.Errorf("ParseInput() mismatch.\nGot: %+v\nWant: %+v", got, tt.want) + } + }) + } +} diff --git a/lang/parser_server_test.go b/lang/parser_server_test.go new file mode 100644 index 0000000..b69ca41 --- /dev/null +++ b/lang/parser_server_test.go @@ -0,0 +1,47 @@ +package lang + +import ( + "testing" +) + +func TestParseServerDefinitions(t *testing.T) { + tests := []struct { + name string + input string + want AST + wantErr bool + }{ + { + name: "simple server definition", + input: `server MyApp host "localhost" port 8080`, + want: AST{ + Definitions: []Definition{ + { + Server: &Server{ + Name: "MyApp", + Settings: []ServerSetting{ + {Host: stringPtr("localhost")}, + {Port: intPtr(8080)}, + }, + }, + }, + }, + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseInput(tt.input) + if (err != nil) != tt.wantErr { + t.Errorf("ParseInput() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if !tt.wantErr && !astEqual(got, tt.want) { + t.Errorf("ParseInput() mismatch.\nGot: %+v\nWant: %+v", got, tt.want) + } + }) + } +} diff --git a/lang/parser_test_helpers.go b/lang/parser_test_helpers.go new file mode 100644 index 0000000..7f3457b --- /dev/null +++ b/lang/parser_test_helpers.go @@ -0,0 +1,138 @@ +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 +}