split tests into separate files
This commit is contained in:
@ -1,932 +1,12 @@
|
|||||||
package lang
|
package lang
|
||||||
|
|
||||||
import (
|
// Parser tests have been organized into specialized files:
|
||||||
"testing"
|
// - parser_server_test.go - Server definition parsing tests
|
||||||
)
|
// - parser_entity_test.go - Entity definition parsing tests
|
||||||
|
// - parser_page_test.go - Page definition parsing tests
|
||||||
func TestParseInput(t *testing.T) {
|
// - parser_component_test.go - Component and field parsing tests
|
||||||
tests := []struct {
|
// - parser_advanced_test.go - Advanced features (conditionals, tabs, modals, master-detail)
|
||||||
name string
|
// - parser_test_helpers.go - Shared helper functions and comparison utilities
|
||||||
input string
|
//
|
||||||
want AST
|
// This organization allows for easier maintenance and addition of new test categories
|
||||||
wantErr bool
|
// for future language interpretation features.
|
||||||
}{
|
|
||||||
{
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
304
lang/parser_advanced_test.go
Normal file
304
lang/parser_advanced_test.go
Normal file
@ -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))
|
||||||
|
}
|
||||||
|
}
|
344
lang/parser_component_test.go
Normal file
344
lang/parser_component_test.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
79
lang/parser_entity_test.go
Normal file
79
lang/parser_entity_test.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
139
lang/parser_page_test.go
Normal file
139
lang/parser_page_test.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
47
lang/parser_server_test.go
Normal file
47
lang/parser_server_test.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
138
lang/parser_test_helpers.go
Normal file
138
lang/parser_test_helpers.go
Normal file
@ -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
|
||||||
|
}
|
Reference in New Issue
Block a user