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

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,20 +88,17 @@ type ResponseSpec struct {
Fields []string `parser:"('fields' '[' @Ident (',' @Ident)* ']')?"`
}
// Enhanced Page definitions with layout containers and composition
// Enhanced Page definitions with unified section model
type Page struct {
Name string `parser:"'page' @Ident"`
Path string `parser:"'at' @String"`
Layout string `parser:"'layout' @Ident"`
Title *string `parser:"('title' @String)?"`
Description *string `parser:"('desc' @String)?"`
Auth bool `parser:"@'auth'?"`
LayoutType *string `parser:"('layout' @String)?"`
Meta []MetaTag `parser:"@@*"`
MasterDetail *MasterDetail `parser:"@@?"`
Containers []Container `parser:"@@*"`
Components []Component `parser:"@@*"`
Modals []Modal `parser:"@@*"`
Name string `parser:"'page' @Ident"`
Path string `parser:"'at' @String"`
Layout string `parser:"'layout' @Ident"`
Title *string `parser:"('title' @String)?"`
Description *string `parser:"('desc' @String)?"`
Auth bool `parser:"@'auth'?"`
Meta []MetaTag `parser:"('{' @@*"` // Block-delimited content
Sections []Section `parser:"@@*"` // Unified sections replace containers/tabs/panels/modals
Components []Component `parser:"@@* '}')?"` // Direct components within the block
}
// Meta tags for SEO
@ -110,85 +107,68 @@ type MetaTag struct {
Content string `parser:"@String"`
}
// Container types for layout organization
type Container struct {
Type string `parser:"'container' @Ident"`
Class *string `parser:"('class' @String)?"`
Sections []Section `parser:"@@*"`
Tabs []Tab `parser:"@@*"`
Components []Component `parser:"@@*"`
}
// Sections within containers
// Unified Section type that replaces Container, Tab, Panel, Modal, MasterDetail
type Section struct {
Name string `parser:"'section' @Ident"`
Class *string `parser:"('class' @String)?"`
Components []Component `parser:"@@*"`
Panels []Panel `parser:"@@*"`
Name string `parser:"'section' @Ident"`
Type *string `parser:"('type' @('container' | 'tab' | 'panel' | 'modal' | 'master' | 'detail'))?"`
Class *string `parser:"('class' @String)?"`
Label *string `parser:"('label' @String)?"` // for tabs
Active bool `parser:"@'active'?"` // for tabs
Trigger *string `parser:"('trigger' @String)?"` // for panels/modals/detail
Position *string `parser:"('position' @String)?"` // for panels
Entity *string `parser:"('for' @Ident)?"` // for panels
Elements []SectionElement `parser:"('{' @@* '}')?"` // Block-delimited elements for unambiguous nesting
}
// Tab definitions for tabbed interfaces
type Tab struct {
Name string `parser:"'tab' @Ident"`
Label string `parser:"'label' @String"`
Active bool `parser:"@'active'?"`
Components []Component `parser:"@@*"`
// New unified element type for sections
type SectionElement struct {
Attribute *SectionAttribute `parser:"@@"`
Component *Component `parser:"| @@"`
Section *Section `parser:"| @@"`
When *WhenCondition `parser:"| @@"`
}
// Panel definitions for slide-out or overlay interfaces
type Panel struct {
Name string `parser:"'panel' @Ident"`
Entity *string `parser:"('for' @Ident)?"`
Trigger string `parser:"'trigger' @String"`
Position *string `parser:"('position' @String)?"`
Components []Component `parser:"@@*"`
// Flexible section attributes (replaces complex config types)
type SectionAttribute struct {
DataSource *string `parser:"('data' 'from' @String)"`
Style *string `parser:"| ('style' @String)"`
Classes *string `parser:"| ('classes' @String)"`
Size *int `parser:"| ('size' @Int)"` // for pagination, etc.
Theme *string `parser:"| ('theme' @String)"`
}
// Modal definitions
type Modal struct {
Name string `parser:"'modal' @Ident"`
Trigger string `parser:"'trigger' @String"`
Components []Component `parser:"@@*"`
}
// Master-detail layout components - simplified parsing
type MasterDetail struct {
Master *MasterSection `parser:"'master' @@"`
Detail *DetailSection `parser:"'detail' @@"`
}
type MasterSection struct {
Name string `parser:"@Ident"`
Components []Component `parser:"@@*"`
}
type DetailSection struct {
Name string `parser:"@Ident"`
Trigger *string `parser:"('trigger' @String)?"`
Components []Component `parser:"@@*"`
}
// Enhanced Component definitions with detailed field configurations
// Simplified Component with unified attributes - reordered for better parsing
type Component struct {
Type string `parser:"'component' @Ident"`
Entity *string `parser:"('for' @Ident)?"`
Elements []ComponentElement `parser:"@@*"`
Elements []ComponentElement `parser:"('{' @@* '}')?"` // Parse everything inside the block
}
// Union type for component elements to allow flexible ordering
// Enhanced ComponentElement with recursive section support - now includes attributes
type ComponentElement struct {
Config *ComponentAttr `parser:"@@"`
Field *ComponentField `parser:"| @@"`
Condition *WhenCondition `parser:"| @@"`
Section *ComponentSection `parser:"| @@"`
Action *ComponentButtonAttr `parser:"| @@"`
Attribute *ComponentAttr `parser:"@@"` // Component attributes can be inside the block
Field *ComponentField `parser:"| @@"`
Section *Section `parser:"| @@"` // Sections can be nested in components
Button *ComponentButton `parser:"| @@"`
When *WhenCondition `parser:"| @@"`
}
// Simplified component attributes using key-value pattern - reordered for precedence
type ComponentAttr struct {
DataSource *string `parser:"('data' 'from' @String)"`
Fields []string `parser:"| ('fields' '[' @Ident (',' @Ident)* ']')"`
Actions []string `parser:"| ('actions' '[' @Ident (',' @Ident)* ']')"`
Style *string `parser:"| ('style' @String)"`
Classes *string `parser:"| ('classes' @String)"`
PageSize *int `parser:"| ('pagination' 'size' @Int)"`
Validate bool `parser:"| @'validate'"`
}
// Enhanced component field with detailed configuration using flexible attributes
type ComponentField struct {
Name string `parser:"'field' @Ident"`
Type string `parser:"'type' @Ident"`
Attributes []ComponentFieldAttribute `parser:"@@*"`
Attributes []ComponentFieldAttribute `parser:"@@* ('{' @@* '}')?"` // Support both inline and block attributes
}
// Flexible field attribute system
@ -223,96 +203,67 @@ type ComponentValidation struct {
Value *string `parser:"@String?"`
}
// Conditional rendering
// Enhanced WhenCondition with recursive support for both sections and components
type WhenCondition struct {
Field string `parser:"'when' @Ident"`
Operator string `parser:"@('equals' | 'not_equals' | 'contains')"`
Value string `parser:"@String"`
Fields []ComponentField `parser:"@@*"`
Sections []ComponentSection `parser:"@@*"`
Buttons []ComponentButtonAttr `parser:"@@*"`
Field string `parser:"'when' @Ident"`
Operator string `parser:"@('equals' | 'not_equals' | 'contains')"`
Value string `parser:"@String"`
Fields []ComponentField `parser:"('{' @@*"`
Sections []Section `parser:"@@*"` // Can contain sections
Components []Component `parser:"@@*"` // Can contain components
Buttons []ComponentButton `parser:"@@* '}')?"` // Block-delimited for unambiguous nesting
}
// Component sections for grouping
type ComponentSection struct {
Name string `parser:"'section' @Ident"`
Class *string `parser:"('class' @String)?"`
Fields []ComponentField `parser:"@@*"`
Buttons []ComponentButtonAttr `parser:"@@*"`
Actions []ComponentButtonAttr `parser:"@@*"`
// Simplified button with flexible attribute ordering
type ComponentButton struct {
Name string `parser:"'button' @Ident"`
Label string `parser:"'label' @String"`
Attributes []ComponentButtonAttr `parser:"@@*"`
}
// Enhanced component buttons/actions with detailed configuration
// Flexible button attribute system - each attribute is a separate alternative
type ComponentButtonAttr struct {
Name string `parser:"'button' @Ident"`
Label string `parser:"'label' @String"`
Style *string `parser:"('style' @String)?"`
Icon *string `parser:"('icon' @String)?"`
Loading *string `parser:"('loading' @String)?"`
Disabled *string `parser:"('disabled' 'when' @Ident)?"`
Confirm *string `parser:"('confirm' @String)?"`
Target *string `parser:"('target' @Ident)?"`
Position *string `parser:"('position' @String)?"`
Via *string `parser:"('via' @String)?"`
Style *ComponentButtonStyle `parser:"@@"`
Icon *ComponentButtonIcon `parser:"| @@"`
Loading *ComponentButtonLoading `parser:"| @@"`
Disabled *ComponentButtonDisabled `parser:"| @@"`
Confirm *ComponentButtonConfirm `parser:"| @@"`
Target *ComponentButtonTarget `parser:"| @@"`
Position *ComponentButtonPosition `parser:"| @@"`
Via *ComponentButtonVia `parser:"| @@"`
}
// Component attributes and configurations (keeping existing for backward compatibility)
type ComponentAttr struct {
Fields *ComponentFields `parser:"@@"`
Actions *ComponentActions `parser:"| @@"`
DataSource *ComponentDataSource `parser:"| @@"`
Style *ComponentStyle `parser:"| @@"`
Pagination *ComponentPagination `parser:"| @@"`
Filters *ComponentFilters `parser:"| @@"`
Validation bool `parser:"| @'validate'"`
// Individual button attribute types
type ComponentButtonStyle struct {
Value string `parser:"'style' @String"`
}
// Component field specification (simple version for backward compatibility)
type ComponentFields struct {
Fields []string `parser:"'fields' '[' @Ident (',' @Ident)* ']'"`
type ComponentButtonIcon struct {
Value string `parser:"'icon' @String"`
}
// Enhanced component actions
type ComponentActions struct {
Actions []ComponentAction `parser:"'actions' '[' @@ (',' @@)* ']'"`
type ComponentButtonLoading struct {
Value string `parser:"'loading' @String"`
}
type ComponentAction struct {
Name string `parser:"@Ident"`
Label *string `parser:"('label' @String)?"`
Icon *string `parser:"('icon' @String)?"`
Style *string `parser:"('style' @String)?"`
Endpoint *string `parser:"('via' @String)?"`
Target *string `parser:"('target' @Ident)?"`
Position *string `parser:"('position' @String)?"`
Confirm *string `parser:"('confirm' @String)?"`
type ComponentButtonDisabled struct {
Value string `parser:"'disabled' 'when' @Ident"`
}
// Data source configuration (can reference endpoints)
type ComponentDataSource struct {
Endpoint string `parser:"'data' 'from' @String"`
type ComponentButtonConfirm struct {
Value string `parser:"'confirm' @String"`
}
// Component styling
type ComponentStyle struct {
Theme *string `parser:"'style' @Ident"`
Classes []string `parser:"('classes' '[' @String (',' @String)* ']')?"`
type ComponentButtonTarget struct {
Value string `parser:"'target' @Ident"`
}
// Pagination configuration
type ComponentPagination struct {
PageSize *int `parser:"'pagination' ('size' @Int)?"`
type ComponentButtonPosition struct {
Value string `parser:"'position' @String"`
}
// Filter specifications
type ComponentFilters struct {
Filters []ComponentFilter `parser:"'filters' '[' @@ (',' @@)* ']'"`
}
type ComponentFilter struct {
Field string `parser:"@Ident"`
Type string `parser:"'as' @('text' | 'select' | 'date' | 'number')"`
Label *string `parser:"('label' @String)?"`
type ComponentButtonVia struct {
Value string `parser:"'via' @String"`
}
func ParseInput(input string) (AST, error) {

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
}