add bracket syntax replace tests
This commit is contained in:
60
.idea/copilotDiffState.xml
generated
60
.idea/copilotDiffState.xml
generated
File diff suppressed because one or more lines are too long
@ -7,8 +7,8 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Read the example.masonry file
|
||||
content, err := os.ReadFile("example.masonry")
|
||||
// Read the example.masonry file from the correct path
|
||||
content, err := os.ReadFile("examples/lang/example.masonry")
|
||||
if err != nil {
|
||||
fmt.Printf("Error reading example.masonry: %v\n", err)
|
||||
return
|
||||
@ -16,456 +16,133 @@ func main() {
|
||||
|
||||
input := string(content)
|
||||
|
||||
// Try to parse the DSL
|
||||
ast, err := lang.ParseInput(input)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("🎉 Successfully parsed enhanced DSL with containers and detailed fields!\n\n")
|
||||
fmt.Printf("❌ Parse Error: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, def := range ast.Definitions {
|
||||
if def.Server != nil {
|
||||
fmt.Printf("📡 Server: %s\n", def.Server.Name)
|
||||
for _, setting := range def.Server.Settings {
|
||||
if setting.Host != nil {
|
||||
fmt.Printf(" host: %s\n", *setting.Host)
|
||||
}
|
||||
if setting.Port != nil {
|
||||
fmt.Printf(" port: %d\n", *setting.Port)
|
||||
}
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
// If we get here, parsing was successful!
|
||||
fmt.Printf("🎉 Successfully parsed DSL with block delimiters!\n\n")
|
||||
|
||||
// Count what we parsed
|
||||
var servers, entities, endpoints, pages int
|
||||
for _, def := range ast.Definitions {
|
||||
if def.Server != nil {
|
||||
servers++
|
||||
}
|
||||
if def.Entity != nil {
|
||||
entities++
|
||||
}
|
||||
if def.Endpoint != nil {
|
||||
endpoints++
|
||||
}
|
||||
if def.Page != nil {
|
||||
pages++
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("📊 Parsing Summary:\n")
|
||||
fmt.Printf(" Servers: %d\n", servers)
|
||||
fmt.Printf(" Entities: %d\n", entities)
|
||||
fmt.Printf(" Endpoints: %d\n", endpoints)
|
||||
fmt.Printf(" Pages: %d\n", pages)
|
||||
fmt.Printf(" Total Definitions: %d\n", len(ast.Definitions))
|
||||
|
||||
// Verify key structures parsed correctly
|
||||
fmt.Printf("\n✅ Validation Results:\n")
|
||||
|
||||
// Check server has settings in block
|
||||
for _, def := range ast.Definitions {
|
||||
if def.Server != nil {
|
||||
if len(def.Server.Settings) > 0 {
|
||||
fmt.Printf(" ✓ Server '%s' has %d settings (block syntax working)\n", def.Server.Name, len(def.Server.Settings))
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if def.Entity != nil {
|
||||
entity := def.Entity
|
||||
fmt.Printf("🏗️ Entity: %s", entity.Name)
|
||||
if entity.Description != nil {
|
||||
fmt.Printf(" - %s", *entity.Description)
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
|
||||
for _, field := range entity.Fields {
|
||||
fmt.Printf(" %s: %s", field.Name, field.Type)
|
||||
if field.Required {
|
||||
fmt.Printf(" (required)")
|
||||
}
|
||||
if field.Unique {
|
||||
fmt.Printf(" (unique)")
|
||||
}
|
||||
if field.Default != nil {
|
||||
fmt.Printf(" default=%s", *field.Default)
|
||||
}
|
||||
if field.Relationship != nil {
|
||||
fmt.Printf(" relates to %s as %s", field.Relationship.Type, field.Relationship.Cardinality)
|
||||
if field.Relationship.ForeignKey != nil {
|
||||
fmt.Printf(" via %s", *field.Relationship.ForeignKey)
|
||||
}
|
||||
if field.Relationship.Through != nil {
|
||||
fmt.Printf(" through %s", *field.Relationship.Through)
|
||||
}
|
||||
}
|
||||
if len(field.Validations) > 0 {
|
||||
fmt.Printf(" validates: ")
|
||||
for i, val := range field.Validations {
|
||||
if i > 0 {
|
||||
fmt.Printf(", ")
|
||||
}
|
||||
fmt.Printf("%s", val.Type)
|
||||
if val.Value != nil {
|
||||
fmt.Printf("(%s)", *val.Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
// Check entities have fields in blocks
|
||||
entityCount := 0
|
||||
for _, def := range ast.Definitions {
|
||||
if def.Entity != nil {
|
||||
entityCount++
|
||||
if len(def.Entity.Fields) > 0 {
|
||||
fmt.Printf(" ✓ Entity '%s' has %d fields (block syntax working)\n", def.Entity.Name, len(def.Entity.Fields))
|
||||
}
|
||||
|
||||
if def.Endpoint != nil {
|
||||
endpoint := def.Endpoint
|
||||
fmt.Printf("🚀 Endpoint: %s %s", endpoint.Method, endpoint.Path)
|
||||
if endpoint.Entity != nil {
|
||||
fmt.Printf(" (for %s)", *endpoint.Entity)
|
||||
}
|
||||
if endpoint.Description != nil {
|
||||
fmt.Printf(" - %s", *endpoint.Description)
|
||||
}
|
||||
if endpoint.Auth {
|
||||
fmt.Printf(" [AUTH]")
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
|
||||
for _, param := range endpoint.Params {
|
||||
fmt.Printf(" param %s: %s from %s", param.Name, param.Type, param.Source)
|
||||
if param.Required {
|
||||
fmt.Printf(" (required)")
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
|
||||
if endpoint.Response != nil {
|
||||
fmt.Printf(" returns %s", endpoint.Response.Type)
|
||||
if endpoint.Response.Format != nil {
|
||||
fmt.Printf(" as %s", *endpoint.Response.Format)
|
||||
}
|
||||
if len(endpoint.Response.Fields) > 0 {
|
||||
fmt.Printf(" fields: %v", endpoint.Response.Fields)
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
|
||||
if endpoint.CustomLogic != nil {
|
||||
fmt.Printf(" custom logic: %s\n", *endpoint.CustomLogic)
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
|
||||
if def.Page != nil {
|
||||
page := def.Page
|
||||
fmt.Printf("🎨 Page: %s at %s", page.Name, page.Path)
|
||||
if page.Title != nil {
|
||||
fmt.Printf(" - %s", *page.Title)
|
||||
}
|
||||
if page.Auth {
|
||||
fmt.Printf(" [AUTH]")
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
fmt.Printf(" Layout: %s\n", page.Layout)
|
||||
|
||||
if page.LayoutType != nil {
|
||||
fmt.Printf(" Layout Type: %s\n", *page.LayoutType)
|
||||
}
|
||||
|
||||
for _, meta := range page.Meta {
|
||||
fmt.Printf(" Meta %s: %s\n", meta.Name, meta.Content)
|
||||
}
|
||||
|
||||
// Display containers
|
||||
for _, container := range page.Containers {
|
||||
fmt.Printf(" 📦 Container: %s", container.Type)
|
||||
if container.Class != nil {
|
||||
fmt.Printf(" class=\"%s\"", *container.Class)
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
|
||||
for _, section := range container.Sections {
|
||||
fmt.Printf(" 📂 Section: %s", section.Name)
|
||||
if section.Class != nil {
|
||||
fmt.Printf(" class=\"%s\"", *section.Class)
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
|
||||
for _, comp := range section.Components {
|
||||
displayComponent(comp, " ")
|
||||
}
|
||||
|
||||
for _, panel := range section.Panels {
|
||||
fmt.Printf(" 🗂️ Panel: %s trigger=\"%s\"", panel.Name, panel.Trigger)
|
||||
if panel.Position != nil {
|
||||
fmt.Printf(" position=\"%s\"", *panel.Position)
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
for _, comp := range panel.Components {
|
||||
displayComponent(comp, " ")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, tab := range container.Tabs {
|
||||
fmt.Printf(" 📋 Tab: %s label=\"%s\"", tab.Name, tab.Label)
|
||||
if tab.Active {
|
||||
fmt.Printf(" (active)")
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
for _, comp := range tab.Components {
|
||||
displayComponent(comp, " ")
|
||||
}
|
||||
}
|
||||
|
||||
for _, comp := range container.Components {
|
||||
displayComponent(comp, " ")
|
||||
}
|
||||
}
|
||||
|
||||
// Display direct components
|
||||
for _, comp := range page.Components {
|
||||
displayComponent(comp, " ")
|
||||
}
|
||||
|
||||
// Display modals
|
||||
for _, modal := range page.Modals {
|
||||
fmt.Printf(" 🪟 Modal: %s trigger=\"%s\"\n", modal.Name, modal.Trigger)
|
||||
for _, comp := range modal.Components {
|
||||
displayComponent(comp, " ")
|
||||
}
|
||||
}
|
||||
|
||||
// Display master-detail layout
|
||||
if page.MasterDetail != nil {
|
||||
fmt.Printf(" 🔄 Master-Detail Layout\n")
|
||||
if page.MasterDetail.Master != nil {
|
||||
fmt.Printf(" 📋 Master: %s\n", page.MasterDetail.Master.Name)
|
||||
for _, comp := range page.MasterDetail.Master.Components {
|
||||
displayComponent(comp, " ")
|
||||
}
|
||||
}
|
||||
if page.MasterDetail.Detail != nil {
|
||||
fmt.Printf(" 📄 Detail: %s", page.MasterDetail.Detail.Name)
|
||||
if page.MasterDetail.Detail.Trigger != nil {
|
||||
fmt.Printf(" trigger=\"%s\"", *page.MasterDetail.Detail.Trigger)
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
for _, comp := range page.MasterDetail.Detail.Components {
|
||||
displayComponent(comp, " ")
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
if entityCount >= 2 { // Just show first couple
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check endpoints have params in blocks
|
||||
endpointCount := 0
|
||||
for _, def := range ast.Definitions {
|
||||
if def.Endpoint != nil {
|
||||
endpointCount++
|
||||
if len(def.Endpoint.Params) > 0 {
|
||||
fmt.Printf(" ✓ Endpoint '%s %s' has %d params (block syntax working)\n", def.Endpoint.Method, def.Endpoint.Path, len(def.Endpoint.Params))
|
||||
}
|
||||
if endpointCount >= 2 { // Just show first couple
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check pages have content in blocks
|
||||
pageCount := 0
|
||||
for _, def := range ast.Definitions {
|
||||
if def.Page != nil {
|
||||
pageCount++
|
||||
totalContent := len(def.Page.Meta) + len(def.Page.Sections) + len(def.Page.Components)
|
||||
if totalContent > 0 {
|
||||
fmt.Printf(" ✓ Page '%s' has %d content items (block syntax working)\n", def.Page.Name, totalContent)
|
||||
}
|
||||
if pageCount >= 2 { // Just show first couple
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for nested sections (complex structures)
|
||||
var totalSections, nestedSections int
|
||||
for _, def := range ast.Definitions {
|
||||
if def.Page != nil {
|
||||
totalSections += len(def.Page.Sections)
|
||||
for _, section := range def.Page.Sections {
|
||||
nestedSections += countNestedSections(section)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if totalSections > 0 {
|
||||
fmt.Printf(" ✓ Found %d sections with %d nested levels (recursive parsing working)\n", totalSections, nestedSections)
|
||||
}
|
||||
|
||||
fmt.Printf("\n🎯 Block delimiter syntax is working correctly!\n")
|
||||
fmt.Printf(" All constructs (server, entity, endpoint, page, section, component) now use { } blocks\n")
|
||||
fmt.Printf(" No more ambiguous whitespace-dependent parsing\n")
|
||||
fmt.Printf(" Language is now unambiguous and consistent\n")
|
||||
}
|
||||
|
||||
func displayComponent(comp lang.Component, indent string) {
|
||||
fmt.Printf("%s🧩 Component: %s", indent, comp.Type)
|
||||
if comp.Entity != nil {
|
||||
fmt.Printf(" for %s", *comp.Entity)
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
|
||||
// Process elements to extract fields, config, conditions, sections, and actions
|
||||
var fields []lang.ComponentField
|
||||
var config []lang.ComponentAttr
|
||||
var conditions []lang.WhenCondition
|
||||
var sections []lang.ComponentSection
|
||||
var actions []lang.ComponentButtonAttr
|
||||
|
||||
for _, element := range comp.Elements {
|
||||
if element.Field != nil {
|
||||
fields = append(fields, *element.Field)
|
||||
}
|
||||
if element.Config != nil {
|
||||
config = append(config, *element.Config)
|
||||
}
|
||||
if element.Condition != nil {
|
||||
conditions = append(conditions, *element.Condition)
|
||||
}
|
||||
// Helper function to count nested sections recursively
|
||||
func countNestedSections(section lang.Section) int {
|
||||
count := 0
|
||||
for _, element := range section.Elements {
|
||||
if element.Section != nil {
|
||||
sections = append(sections, *element.Section)
|
||||
count++
|
||||
count += countNestedSections(*element.Section)
|
||||
}
|
||||
if element.Action != nil {
|
||||
actions = append(actions, *element.Action)
|
||||
if element.Component != nil {
|
||||
for _, compElement := range element.Component.Elements {
|
||||
if compElement.Section != nil {
|
||||
count++
|
||||
count += countNestedSections(*compElement.Section)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Display config attributes
|
||||
for _, attr := range config {
|
||||
if attr.Fields != nil {
|
||||
fmt.Printf("%s fields: %v\n", indent, attr.Fields.Fields)
|
||||
}
|
||||
if attr.Actions != nil {
|
||||
fmt.Printf("%s actions: ", indent)
|
||||
for i, action := range attr.Actions.Actions {
|
||||
if i > 0 {
|
||||
fmt.Printf(", ")
|
||||
}
|
||||
fmt.Printf("%s", action.Name)
|
||||
if action.Endpoint != nil {
|
||||
fmt.Printf(" via %s", *action.Endpoint)
|
||||
}
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
if attr.DataSource != nil {
|
||||
fmt.Printf("%s data from: %s\n", indent, attr.DataSource.Endpoint)
|
||||
}
|
||||
if attr.Style != nil {
|
||||
fmt.Printf("%s style: %s", indent, *attr.Style.Theme)
|
||||
if len(attr.Style.Classes) > 0 {
|
||||
fmt.Printf(" classes: %v", attr.Style.Classes)
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
if attr.Pagination != nil {
|
||||
fmt.Printf("%s pagination: enabled", indent)
|
||||
if attr.Pagination.PageSize != nil {
|
||||
fmt.Printf(" size %d", *attr.Pagination.PageSize)
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
if attr.Filters != nil {
|
||||
fmt.Printf("%s filters: ", indent)
|
||||
for i, filter := range attr.Filters.Filters {
|
||||
if i > 0 {
|
||||
fmt.Printf(", ")
|
||||
}
|
||||
fmt.Printf("%s as %s", filter.Field, filter.Type)
|
||||
if filter.Label != nil {
|
||||
fmt.Printf(" (%s)", *filter.Label)
|
||||
}
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
if attr.Validation {
|
||||
fmt.Printf("%s validation: enabled\n", indent)
|
||||
}
|
||||
}
|
||||
|
||||
// Display enhanced fields
|
||||
for _, field := range fields {
|
||||
fmt.Printf("%s 📝 Field: %s type %s", indent, field.Name, field.Type)
|
||||
|
||||
// Process attributes
|
||||
for _, attr := range field.Attributes {
|
||||
if attr.Label != nil {
|
||||
fmt.Printf(" label=\"%s\"", *attr.Label)
|
||||
}
|
||||
if attr.Placeholder != nil {
|
||||
fmt.Printf(" placeholder=\"%s\"", *attr.Placeholder)
|
||||
}
|
||||
if attr.Required {
|
||||
fmt.Printf(" (required)")
|
||||
}
|
||||
if attr.Sortable {
|
||||
fmt.Printf(" (sortable)")
|
||||
}
|
||||
if attr.Searchable {
|
||||
fmt.Printf(" (searchable)")
|
||||
}
|
||||
if attr.Thumbnail {
|
||||
fmt.Printf(" (thumbnail)")
|
||||
}
|
||||
if attr.Default != nil {
|
||||
fmt.Printf(" default=\"%s\"", *attr.Default)
|
||||
}
|
||||
if len(attr.Options) > 0 {
|
||||
fmt.Printf(" options=%v", attr.Options)
|
||||
}
|
||||
if attr.Accept != nil {
|
||||
fmt.Printf(" accept=\"%s\"", *attr.Accept)
|
||||
}
|
||||
if attr.Rows != nil {
|
||||
fmt.Printf(" rows=%d", *attr.Rows)
|
||||
}
|
||||
if attr.Format != nil {
|
||||
fmt.Printf(" format=\"%s\"", *attr.Format)
|
||||
}
|
||||
if attr.Size != nil {
|
||||
fmt.Printf(" size=\"%s\"", *attr.Size)
|
||||
}
|
||||
if attr.Source != nil {
|
||||
fmt.Printf(" source=\"%s\"", *attr.Source)
|
||||
}
|
||||
if attr.Display != nil {
|
||||
fmt.Printf(" display=\"%s\"", *attr.Display)
|
||||
}
|
||||
if attr.Value != nil {
|
||||
fmt.Printf(" value=\"%s\"", *attr.Value)
|
||||
}
|
||||
if attr.Relates != nil {
|
||||
fmt.Printf(" relates to %s", attr.Relates.Type)
|
||||
}
|
||||
if attr.Validation != nil {
|
||||
fmt.Printf(" validates: %s", attr.Validation.Type)
|
||||
if attr.Validation.Value != nil {
|
||||
fmt.Printf("(%s)", *attr.Validation.Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
|
||||
// Display conditions
|
||||
for _, condition := range conditions {
|
||||
fmt.Printf("%s 🔀 When %s %s \"%s\":\n", indent, condition.Field, condition.Operator, condition.Value)
|
||||
for _, field := range condition.Fields {
|
||||
fmt.Printf("%s 📝 Field: %s type %s", indent, field.Name, field.Type)
|
||||
// Process attributes for conditional fields
|
||||
for _, attr := range field.Attributes {
|
||||
if attr.Label != nil {
|
||||
fmt.Printf(" label=\"%s\"", *attr.Label)
|
||||
}
|
||||
if len(attr.Options) > 0 {
|
||||
fmt.Printf(" options=%v", attr.Options)
|
||||
}
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
for _, section := range condition.Sections {
|
||||
fmt.Printf("%s 📂 Section: %s\n", indent, section.Name)
|
||||
}
|
||||
for _, button := range condition.Buttons {
|
||||
fmt.Printf("%s 🔘 Button: %s label=\"%s\"", indent, button.Name, button.Label)
|
||||
if button.Style != nil {
|
||||
fmt.Printf(" style=\"%s\"", *button.Style)
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
}
|
||||
|
||||
// Display sections
|
||||
for _, section := range sections {
|
||||
fmt.Printf("%s 📂 Section: %s", indent, section.Name)
|
||||
if section.Class != nil {
|
||||
fmt.Printf(" class=\"%s\"", *section.Class)
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
for _, field := range section.Fields {
|
||||
fmt.Printf("%s 📝 Field: %s type %s", indent, field.Name, field.Type)
|
||||
// Process attributes for section fields
|
||||
for _, attr := range field.Attributes {
|
||||
if attr.Label != nil {
|
||||
fmt.Printf(" label=\"%s\"", *attr.Label)
|
||||
}
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
for _, button := range section.Buttons {
|
||||
fmt.Printf("%s 🔘 Button: %s label=\"%s\"", indent, button.Name, button.Label)
|
||||
if button.Style != nil {
|
||||
fmt.Printf(" style=\"%s\"", *button.Style)
|
||||
}
|
||||
if button.Via != nil {
|
||||
fmt.Printf(" via=\"%s\"", *button.Via)
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
for _, action := range section.Actions {
|
||||
fmt.Printf("%s ⚡ Action: %s label=\"%s\"", indent, action.Name, action.Label)
|
||||
if action.Style != nil {
|
||||
fmt.Printf(" style=\"%s\"", *action.Style)
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
}
|
||||
|
||||
// Display direct actions/buttons
|
||||
for _, action := range actions {
|
||||
fmt.Printf("%s 🔘 Button: %s label=\"%s\"", indent, action.Name, action.Label)
|
||||
if action.Style != nil {
|
||||
fmt.Printf(" style=\"%s\"", *action.Style)
|
||||
}
|
||||
if action.Icon != nil {
|
||||
fmt.Printf(" icon=\"%s\"", *action.Icon)
|
||||
}
|
||||
if action.Loading != nil {
|
||||
fmt.Printf(" loading=\"%s\"", *action.Loading)
|
||||
}
|
||||
if action.Disabled != nil {
|
||||
fmt.Printf(" disabled when %s", *action.Disabled)
|
||||
}
|
||||
if action.Confirm != nil {
|
||||
fmt.Printf(" confirm=\"%s\"", *action.Confirm)
|
||||
}
|
||||
if action.Target != nil {
|
||||
fmt.Printf(" target=\"%s\"", *action.Target)
|
||||
}
|
||||
if action.Position != nil {
|
||||
fmt.Printf(" position=\"%s\"", *action.Position)
|
||||
}
|
||||
if action.Via != nil {
|
||||
fmt.Printf(" via=\"%s\"", *action.Via)
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
@ -1,186 +1,295 @@
|
||||
// Enhanced Masonry DSL example demonstrating new features
|
||||
// This shows the comprehensive language structure with containers, detailed fields, and layouts
|
||||
// Enhanced Masonry DSL example demonstrating simplified unified structure
|
||||
// This shows how containers, tabs, panels, modals, and master-detail are now unified as sections
|
||||
|
||||
// Server configuration
|
||||
server MyApp host "localhost" port 8080
|
||||
server MyApp {
|
||||
host "localhost"
|
||||
port 8080
|
||||
}
|
||||
|
||||
// Entity definitions with various field types and relationships
|
||||
entity User desc "User account management"
|
||||
id: uuid required unique
|
||||
email: string required validate email validate min_length "5"
|
||||
name: string default "Anonymous"
|
||||
created_at: timestamp default "now()"
|
||||
profile_id: uuid relates to Profile as one via "user_id"
|
||||
entity User desc "User account management" {
|
||||
id: uuid required unique
|
||||
email: string required validate email validate min_length "5"
|
||||
name: string default "Anonymous"
|
||||
created_at: timestamp default "now()"
|
||||
profile_id: uuid relates to Profile as one via "user_id"
|
||||
}
|
||||
|
||||
entity Profile desc "User profile information"
|
||||
id: uuid required unique
|
||||
user_id: uuid required relates to User as one
|
||||
bio: text validate max_length "500"
|
||||
avatar_url: string validate url
|
||||
updated_at: timestamp
|
||||
posts: uuid relates to Post as many
|
||||
entity Profile desc "User profile information" {
|
||||
id: uuid required unique
|
||||
user_id: uuid required relates to User as one
|
||||
bio: text validate max_length "500"
|
||||
avatar_url: string validate url
|
||||
updated_at: timestamp
|
||||
posts: uuid relates to Post as many
|
||||
}
|
||||
|
||||
entity Post desc "Blog posts"
|
||||
id: uuid required unique
|
||||
title: string required validate min_length "1" validate max_length "200"
|
||||
content: text required
|
||||
author_id: uuid required relates to User as one
|
||||
published: boolean default "false"
|
||||
created_at: timestamp default "now()"
|
||||
tags: uuid relates to Tag as many through "post_tags"
|
||||
entity Post desc "Blog posts" {
|
||||
id: uuid required unique
|
||||
title: string required validate min_length "1" validate max_length "200"
|
||||
content: text required
|
||||
author_id: uuid required relates to User as one
|
||||
published: boolean default "false"
|
||||
created_at: timestamp default "now()"
|
||||
tags: uuid relates to Tag as many through "post_tags"
|
||||
}
|
||||
|
||||
entity Tag desc "Content tags"
|
||||
id: uuid required unique
|
||||
name: string required unique validate min_length "1" validate max_length "50"
|
||||
slug: string required unique indexed
|
||||
created_at: timestamp default "now()"
|
||||
entity Tag desc "Content tags" {
|
||||
id: uuid required unique
|
||||
name: string required unique validate min_length "1" validate max_length "50"
|
||||
slug: string required unique indexed
|
||||
created_at: timestamp default "now()"
|
||||
}
|
||||
|
||||
// API Endpoints with different HTTP methods and parameter sources
|
||||
endpoint GET "/users" for User desc "List users" auth
|
||||
param page: int from query
|
||||
param limit: int required from query
|
||||
returns list as "json" fields [id, email, name]
|
||||
endpoint GET "/users" for User desc "List users" auth {
|
||||
param page: int from query
|
||||
param limit: int required from query
|
||||
returns list as "json" fields [id, email, name]
|
||||
}
|
||||
|
||||
endpoint POST "/users" for User desc "Create user"
|
||||
param user_data: object required from body
|
||||
returns object as "json" fields [id, email, name]
|
||||
endpoint POST "/users" for User desc "Create user" {
|
||||
param user_data: object required from body
|
||||
returns object as "json" fields [id, email, name]
|
||||
}
|
||||
|
||||
endpoint PUT "/users/{id}" for User desc "Update user"
|
||||
param id: uuid required from path
|
||||
param user_data: object required from body
|
||||
returns object
|
||||
custom "update_user_logic"
|
||||
endpoint PUT "/users/{id}" for User desc "Update user" {
|
||||
param id: uuid required from path
|
||||
param user_data: object required from body
|
||||
returns object
|
||||
custom "update_user_logic"
|
||||
}
|
||||
|
||||
endpoint DELETE "/users/{id}" for User desc "Delete user" auth
|
||||
param id: uuid required from path
|
||||
returns object
|
||||
endpoint DELETE "/users/{id}" for User desc "Delete user" auth {
|
||||
param id: uuid required from path
|
||||
returns object
|
||||
}
|
||||
|
||||
endpoint GET "/posts" for Post desc "List posts"
|
||||
param author_id: uuid from query
|
||||
param published: boolean from query
|
||||
param page: int from query
|
||||
returns list as "json" fields [id, title, author_id, published]
|
||||
endpoint GET "/posts" for Post desc "List posts" {
|
||||
param author_id: uuid from query
|
||||
param published: boolean from query
|
||||
param page: int from query
|
||||
returns list as "json" fields [id, title, author_id, published]
|
||||
}
|
||||
|
||||
endpoint POST "/posts" for Post desc "Create post" auth
|
||||
param post_data: object required from body
|
||||
returns object fields [id, title, content, author_id]
|
||||
endpoint POST "/posts" for Post desc "Create post" auth {
|
||||
param post_data: object required from body
|
||||
returns object fields [id, title, content, author_id]
|
||||
}
|
||||
|
||||
// Enhanced User Management page with container layout
|
||||
page UserManagement at "/admin/users" layout AdminLayout title "User Management" auth
|
||||
meta description "Manage system users"
|
||||
meta keywords "users, admin, management"
|
||||
// Enhanced User Management page with unified section layout
|
||||
page UserManagement at "/admin/users" layout AdminLayout title "User Management" auth {
|
||||
meta description "Manage system users"
|
||||
meta keywords "users, admin, management"
|
||||
|
||||
container main class "grid grid-cols-3 gap-4"
|
||||
section sidebar class "col-span-1"
|
||||
component UserStats for User
|
||||
data from "/users/stats"
|
||||
section main type container class "grid grid-cols-3 gap-4" {
|
||||
section sidebar class "col-span-1" {
|
||||
component UserStats for User {
|
||||
data from "/users/stats"
|
||||
}
|
||||
}
|
||||
|
||||
section content class "col-span-2"
|
||||
component UserTable for User
|
||||
fields [email, name, role, created_at]
|
||||
actions [edit, delete, view]
|
||||
data from "/users"
|
||||
section content class "col-span-2" {
|
||||
component UserTable for User {
|
||||
fields [email, name, role, created_at]
|
||||
actions [edit, delete, view]
|
||||
data from "/users"
|
||||
}
|
||||
|
||||
panel UserEditPanel for User trigger "edit" position "slide-right"
|
||||
component UserForm for User
|
||||
field email type text label "Email" required
|
||||
field name type text label "Name" required
|
||||
field role type select options ["admin", "user"]
|
||||
button save label "Save User" style "primary" via "/users/{id}"
|
||||
button cancel label "Cancel" style "secondary"
|
||||
section editPanel type panel trigger "edit" position "slide-right" for User {
|
||||
component UserForm for User {
|
||||
field email type text label "Email" required
|
||||
field name type text label "Name" required
|
||||
field role type select options ["admin", "user"]
|
||||
button save label "Save User" style "primary" via "/users/{id}"
|
||||
button cancel label "Cancel" style "secondary"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enhanced Form component with detailed field configurations
|
||||
page UserFormPage at "/admin/users/new" layout AdminLayout title "Create User" auth
|
||||
component Form for User
|
||||
field email type text label "Email Address" placeholder "Enter your email" required validate email
|
||||
field name type text label "Full Name" placeholder "Enter your full name" required
|
||||
field role type select label "User Role" options ["admin", "user", "moderator"] default "user"
|
||||
field avatar type file label "Profile Picture" accept "image/*"
|
||||
field bio type textarea label "Biography" placeholder "Tell us about yourself" rows 4
|
||||
page UserFormPage at "/admin/users/new" layout AdminLayout title "Create User" auth {
|
||||
component Form for User {
|
||||
field email type text label "Email Address" placeholder "Enter your email" required validate email
|
||||
field name type text label "Full Name" placeholder "Enter your full name" required
|
||||
field role type select label "User Role" options ["admin", "user", "moderator"] default "user"
|
||||
field avatar type file label "Profile Picture" accept "image/*"
|
||||
field bio type textarea label "Biography" placeholder "Tell us about yourself" rows 4
|
||||
|
||||
when role equals "admin"
|
||||
field permissions type multiselect label "Permissions"
|
||||
options ["users.manage", "posts.manage", "system.config"]
|
||||
when role equals "admin" {
|
||||
component AdminPermissions {
|
||||
field permissions type multiselect label "Permissions" {
|
||||
options ["users.manage", "posts.manage", "system.config"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section actions
|
||||
button save label "Save User" style "primary" loading "Saving..." via "/users"
|
||||
button cancel label "Cancel" style "secondary"
|
||||
section actions {
|
||||
component ActionButtons {
|
||||
button save label "Save User" style "primary" loading "Saving..." via "/users"
|
||||
button cancel label "Cancel" style "secondary"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dashboard with tabbed interface
|
||||
page Dashboard at "/dashboard" layout MainLayout title "Dashboard"
|
||||
container tabs
|
||||
tab overview label "Overview" active
|
||||
component StatsCards
|
||||
component RecentActivity
|
||||
// Dashboard with tabbed interface using unified sections
|
||||
page Dashboard at "/dashboard" layout MainLayout title "Dashboard" {
|
||||
section tabs type container {
|
||||
section overview type tab label "Overview" active {
|
||||
component StatsCards
|
||||
component RecentActivity
|
||||
}
|
||||
|
||||
tab users label "Users"
|
||||
component UserTable for User
|
||||
data from "/users"
|
||||
section users type tab label "Users" {
|
||||
component UserTable for User {
|
||||
data from "/users"
|
||||
}
|
||||
}
|
||||
|
||||
tab posts label "Posts"
|
||||
component PostTable for Post
|
||||
data from "/posts"
|
||||
section posts type tab label "Posts" {
|
||||
component PostTable for Post {
|
||||
data from "/posts"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
modal CreateUserModal trigger "create-user"
|
||||
component UserForm for User
|
||||
field email type text label "Email" required
|
||||
field name type text label "Name" required
|
||||
button save label "Create" via "/users"
|
||||
button cancel label "Cancel"
|
||||
section createUserModal type modal trigger "create-user" {
|
||||
component UserForm for User {
|
||||
field email type text label "Email" required
|
||||
field name type text label "Name" required
|
||||
button save label "Create" via "/users"
|
||||
button cancel label "Cancel"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Post Management with master-detail layout
|
||||
page PostManagement at "/admin/posts" layout AdminLayout title "Post Management" auth
|
||||
layout "master-detail"
|
||||
// Post Management with master-detail layout using unified sections
|
||||
page PostManagement at "/admin/posts" layout AdminLayout title "Post Management" auth {
|
||||
section master type master {
|
||||
component PostTable for Post {
|
||||
field title type text label "Title" sortable
|
||||
field author type relation label "Author" display "name" relates to User
|
||||
field status type badge label "Status"
|
||||
}
|
||||
}
|
||||
|
||||
master PostList
|
||||
component Table for Post
|
||||
field title type text label "Title" sortable
|
||||
field author type relation label "Author" display "name" relates to User
|
||||
field status type badge label "Status"
|
||||
section detail type detail trigger "edit" {
|
||||
component PostForm for Post {
|
||||
section basic class "mb-4" {
|
||||
component BasicFields {
|
||||
field title type text label "Post Title" required
|
||||
field content type richtext label "Content" required
|
||||
}
|
||||
}
|
||||
|
||||
detail PostEditor trigger "edit"
|
||||
component Form for Post
|
||||
section basic class "mb-4"
|
||||
field title type text label "Post Title" required
|
||||
field content type richtext label "Content" required
|
||||
|
||||
section metadata class "grid grid-cols-2 gap-4"
|
||||
field author_id type autocomplete label "Author"
|
||||
source "/users" display "name" value "id"
|
||||
field published type toggle label "Published" default "false"
|
||||
field tags type multiselect label "Tags"
|
||||
source "/tags" display "name" value "id"
|
||||
section metadata class "grid grid-cols-2 gap-4" {
|
||||
component MetadataFields {
|
||||
field author_id type autocomplete label "Author" {
|
||||
source "/users" display "name" value "id"
|
||||
}
|
||||
field published type toggle label "Published" default "false"
|
||||
field tags type multiselect label "Tags" {
|
||||
source "/tags" display "name" value "id"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Simple table component with smart defaults
|
||||
page SimpleUserList at "/users" layout MainLayout title "Users"
|
||||
component SimpleTable for User
|
||||
fields [email, name, created_at]
|
||||
actions [edit, delete]
|
||||
data from "/users"
|
||||
page SimpleUserList at "/users" layout MainLayout title "Users" {
|
||||
component SimpleTable for User {
|
||||
fields [email, name, created_at]
|
||||
actions [edit, delete]
|
||||
data from "/users"
|
||||
}
|
||||
}
|
||||
|
||||
// Detailed table when more control is needed
|
||||
page DetailedUserList at "/admin/users/detailed" layout AdminLayout title "Detailed User Management" auth
|
||||
component DetailedTable for User
|
||||
field email type text label "Email Address"
|
||||
field name type text label "Full Name"
|
||||
// Detailed table with simplified component attributes
|
||||
page DetailedUserList at "/admin/users/detailed" layout AdminLayout title "Detailed User Management" auth {
|
||||
component DetailedTable for User {
|
||||
data from "/users"
|
||||
pagination size 20
|
||||
field email type text label "Email Address"
|
||||
field name type text label "Full Name"
|
||||
}
|
||||
}
|
||||
|
||||
data from "/users"
|
||||
pagination size 20
|
||||
// Complex nested sections example
|
||||
page ComplexLayout at "/complex" layout MainLayout title "Complex Layout" {
|
||||
section mainContainer type container class "flex h-screen" {
|
||||
section sidebar type container class "w-64 bg-gray-100" {
|
||||
section navigation {
|
||||
component NavMenu
|
||||
}
|
||||
|
||||
// Conditional rendering example
|
||||
page ConditionalForm at "/conditional" layout MainLayout title "Conditional Form"
|
||||
component UserForm for User
|
||||
field email type text label "Email" required
|
||||
field role type select options ["admin", "user", "moderator"]
|
||||
section userInfo type panel trigger "profile" position "bottom" {
|
||||
component UserProfile
|
||||
}
|
||||
}
|
||||
|
||||
when role equals "admin"
|
||||
field permissions type multiselect label "Admin Permissions"
|
||||
options ["users.manage", "posts.manage", "system.config"]
|
||||
section content type container class "flex-1" {
|
||||
section header class "h-16 border-b" {
|
||||
component PageHeader
|
||||
}
|
||||
|
||||
when role equals "moderator"
|
||||
field moderation_level type select label "Moderation Level"
|
||||
options ["basic", "advanced", "full"]
|
||||
section body class "flex-1 p-4" {
|
||||
section tabs type container {
|
||||
section overview type tab label "Overview" active {
|
||||
section metrics class "grid grid-cols-3 gap-4" {
|
||||
component MetricCard
|
||||
component MetricCard
|
||||
component MetricCard
|
||||
}
|
||||
}
|
||||
|
||||
section actions
|
||||
button save label "Save User" style "primary" loading "Saving..."
|
||||
button cancel label "Cancel" style "secondary"
|
||||
section details type tab label "Details" {
|
||||
component DetailView
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Conditional rendering with sections and components
|
||||
page ConditionalForm at "/conditional" layout MainLayout title "Conditional Form" {
|
||||
component UserForm for User {
|
||||
field email type text label "Email" required
|
||||
field role type select options ["admin", "user", "moderator"]
|
||||
|
||||
when role equals "admin" {
|
||||
section adminSection class "border-l-4 border-red-500 pl-4" {
|
||||
component AdminPermissions {
|
||||
field permissions type multiselect label "Admin Permissions" {
|
||||
options ["users.manage", "posts.manage", "system.config"]
|
||||
}
|
||||
}
|
||||
|
||||
component AdminSettings {
|
||||
field max_users type number label "Max Users"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when role equals "moderator" {
|
||||
component ModeratorSettings {
|
||||
field moderation_level type select label "Moderation Level" {
|
||||
options ["basic", "advanced", "full"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section actions {
|
||||
component ActionButtons {
|
||||
button save label "Save User" style "primary" loading "Saving..."
|
||||
button cancel label "Cancel" style "secondary"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
2
go.mod
2
go.mod
@ -3,12 +3,12 @@ module masonry
|
||||
go 1.23
|
||||
|
||||
require (
|
||||
github.com/alecthomas/participle/v2 v2.1.4
|
||||
github.com/urfave/cli/v2 v2.27.5
|
||||
golang.org/x/text v0.22.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/alecthomas/participle/v2 v2.1.4 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
||||
|
6
go.sum
6
go.sum
@ -1,7 +1,13 @@
|
||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/participle/v2 v2.1.4 h1:W/H79S8Sat/krZ3el6sQMvMaahJ+XcM9WSI2naI7w2U=
|
||||
github.com/alecthomas/participle/v2 v2.1.4/go.mod h1:8tqVbpTX20Ru4NfYQgZf4mP18eXPTBViyMWiArNEgGI=
|
||||
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
||||
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
|
||||
|
241
lang/lang.go
241
lang/lang.go
@ -20,7 +20,7 @@ type Definition struct {
|
||||
// Clean server syntax
|
||||
type Server struct {
|
||||
Name string `parser:"'server' @Ident"`
|
||||
Settings []ServerSetting `parser:"@@*"`
|
||||
Settings []ServerSetting `parser:"('{' @@* '}')?"` // Block-delimited settings for consistency
|
||||
}
|
||||
|
||||
type ServerSetting struct {
|
||||
@ -32,7 +32,7 @@ type ServerSetting struct {
|
||||
type Entity struct {
|
||||
Name string `parser:"'entity' @Ident"`
|
||||
Description *string `parser:"('desc' @String)?"`
|
||||
Fields []Field `parser:"@@*"`
|
||||
Fields []Field `parser:"('{' @@* '}')?"` // Block-delimited fields for consistency
|
||||
}
|
||||
|
||||
// Much cleaner field syntax
|
||||
@ -68,9 +68,9 @@ type Endpoint struct {
|
||||
Entity *string `parser:"('for' @Ident)?"`
|
||||
Description *string `parser:"('desc' @String)?"`
|
||||
Auth bool `parser:"@'auth'?"`
|
||||
Params []EndpointParam `parser:"@@*"`
|
||||
Params []EndpointParam `parser:"('{' @@*"` // Block-delimited parameters
|
||||
Response *ResponseSpec `parser:"@@?"`
|
||||
CustomLogic *string `parser:"('custom' @String)?"`
|
||||
CustomLogic *string `parser:"('custom' @String)? '}')?"` // Close block after all content
|
||||
}
|
||||
|
||||
// Clean parameter syntax
|
||||
@ -88,20 +88,17 @@ type ResponseSpec struct {
|
||||
Fields []string `parser:"('fields' '[' @Ident (',' @Ident)* ']')?"`
|
||||
}
|
||||
|
||||
// Enhanced Page definitions with layout containers and composition
|
||||
// Enhanced Page definitions with unified section model
|
||||
type Page struct {
|
||||
Name string `parser:"'page' @Ident"`
|
||||
Path string `parser:"'at' @String"`
|
||||
Layout string `parser:"'layout' @Ident"`
|
||||
Title *string `parser:"('title' @String)?"`
|
||||
Description *string `parser:"('desc' @String)?"`
|
||||
Auth bool `parser:"@'auth'?"`
|
||||
LayoutType *string `parser:"('layout' @String)?"`
|
||||
Meta []MetaTag `parser:"@@*"`
|
||||
MasterDetail *MasterDetail `parser:"@@?"`
|
||||
Containers []Container `parser:"@@*"`
|
||||
Components []Component `parser:"@@*"`
|
||||
Modals []Modal `parser:"@@*"`
|
||||
Name string `parser:"'page' @Ident"`
|
||||
Path string `parser:"'at' @String"`
|
||||
Layout string `parser:"'layout' @Ident"`
|
||||
Title *string `parser:"('title' @String)?"`
|
||||
Description *string `parser:"('desc' @String)?"`
|
||||
Auth bool `parser:"@'auth'?"`
|
||||
Meta []MetaTag `parser:"('{' @@*"` // Block-delimited content
|
||||
Sections []Section `parser:"@@*"` // Unified sections replace containers/tabs/panels/modals
|
||||
Components []Component `parser:"@@* '}')?"` // Direct components within the block
|
||||
}
|
||||
|
||||
// Meta tags for SEO
|
||||
@ -110,85 +107,68 @@ type MetaTag struct {
|
||||
Content string `parser:"@String"`
|
||||
}
|
||||
|
||||
// Container types for layout organization
|
||||
type Container struct {
|
||||
Type string `parser:"'container' @Ident"`
|
||||
Class *string `parser:"('class' @String)?"`
|
||||
Sections []Section `parser:"@@*"`
|
||||
Tabs []Tab `parser:"@@*"`
|
||||
Components []Component `parser:"@@*"`
|
||||
}
|
||||
|
||||
// Sections within containers
|
||||
// Unified Section type that replaces Container, Tab, Panel, Modal, MasterDetail
|
||||
type Section struct {
|
||||
Name string `parser:"'section' @Ident"`
|
||||
Class *string `parser:"('class' @String)?"`
|
||||
Components []Component `parser:"@@*"`
|
||||
Panels []Panel `parser:"@@*"`
|
||||
Name string `parser:"'section' @Ident"`
|
||||
Type *string `parser:"('type' @('container' | 'tab' | 'panel' | 'modal' | 'master' | 'detail'))?"`
|
||||
Class *string `parser:"('class' @String)?"`
|
||||
Label *string `parser:"('label' @String)?"` // for tabs
|
||||
Active bool `parser:"@'active'?"` // for tabs
|
||||
Trigger *string `parser:"('trigger' @String)?"` // for panels/modals/detail
|
||||
Position *string `parser:"('position' @String)?"` // for panels
|
||||
Entity *string `parser:"('for' @Ident)?"` // for panels
|
||||
Elements []SectionElement `parser:"('{' @@* '}')?"` // Block-delimited elements for unambiguous nesting
|
||||
}
|
||||
|
||||
// Tab definitions for tabbed interfaces
|
||||
type Tab struct {
|
||||
Name string `parser:"'tab' @Ident"`
|
||||
Label string `parser:"'label' @String"`
|
||||
Active bool `parser:"@'active'?"`
|
||||
Components []Component `parser:"@@*"`
|
||||
// New unified element type for sections
|
||||
type SectionElement struct {
|
||||
Attribute *SectionAttribute `parser:"@@"`
|
||||
Component *Component `parser:"| @@"`
|
||||
Section *Section `parser:"| @@"`
|
||||
When *WhenCondition `parser:"| @@"`
|
||||
}
|
||||
|
||||
// Panel definitions for slide-out or overlay interfaces
|
||||
type Panel struct {
|
||||
Name string `parser:"'panel' @Ident"`
|
||||
Entity *string `parser:"('for' @Ident)?"`
|
||||
Trigger string `parser:"'trigger' @String"`
|
||||
Position *string `parser:"('position' @String)?"`
|
||||
Components []Component `parser:"@@*"`
|
||||
// Flexible section attributes (replaces complex config types)
|
||||
type SectionAttribute struct {
|
||||
DataSource *string `parser:"('data' 'from' @String)"`
|
||||
Style *string `parser:"| ('style' @String)"`
|
||||
Classes *string `parser:"| ('classes' @String)"`
|
||||
Size *int `parser:"| ('size' @Int)"` // for pagination, etc.
|
||||
Theme *string `parser:"| ('theme' @String)"`
|
||||
}
|
||||
|
||||
// Modal definitions
|
||||
type Modal struct {
|
||||
Name string `parser:"'modal' @Ident"`
|
||||
Trigger string `parser:"'trigger' @String"`
|
||||
Components []Component `parser:"@@*"`
|
||||
}
|
||||
|
||||
// Master-detail layout components - simplified parsing
|
||||
type MasterDetail struct {
|
||||
Master *MasterSection `parser:"'master' @@"`
|
||||
Detail *DetailSection `parser:"'detail' @@"`
|
||||
}
|
||||
|
||||
type MasterSection struct {
|
||||
Name string `parser:"@Ident"`
|
||||
Components []Component `parser:"@@*"`
|
||||
}
|
||||
|
||||
type DetailSection struct {
|
||||
Name string `parser:"@Ident"`
|
||||
Trigger *string `parser:"('trigger' @String)?"`
|
||||
Components []Component `parser:"@@*"`
|
||||
}
|
||||
|
||||
// Enhanced Component definitions with detailed field configurations
|
||||
// Simplified Component with unified attributes - reordered for better parsing
|
||||
type Component struct {
|
||||
Type string `parser:"'component' @Ident"`
|
||||
Entity *string `parser:"('for' @Ident)?"`
|
||||
Elements []ComponentElement `parser:"@@*"`
|
||||
Elements []ComponentElement `parser:"('{' @@* '}')?"` // Parse everything inside the block
|
||||
}
|
||||
|
||||
// Union type for component elements to allow flexible ordering
|
||||
// Enhanced ComponentElement with recursive section support - now includes attributes
|
||||
type ComponentElement struct {
|
||||
Config *ComponentAttr `parser:"@@"`
|
||||
Field *ComponentField `parser:"| @@"`
|
||||
Condition *WhenCondition `parser:"| @@"`
|
||||
Section *ComponentSection `parser:"| @@"`
|
||||
Action *ComponentButtonAttr `parser:"| @@"`
|
||||
Attribute *ComponentAttr `parser:"@@"` // Component attributes can be inside the block
|
||||
Field *ComponentField `parser:"| @@"`
|
||||
Section *Section `parser:"| @@"` // Sections can be nested in components
|
||||
Button *ComponentButton `parser:"| @@"`
|
||||
When *WhenCondition `parser:"| @@"`
|
||||
}
|
||||
|
||||
// Simplified component attributes using key-value pattern - reordered for precedence
|
||||
type ComponentAttr struct {
|
||||
DataSource *string `parser:"('data' 'from' @String)"`
|
||||
Fields []string `parser:"| ('fields' '[' @Ident (',' @Ident)* ']')"`
|
||||
Actions []string `parser:"| ('actions' '[' @Ident (',' @Ident)* ']')"`
|
||||
Style *string `parser:"| ('style' @String)"`
|
||||
Classes *string `parser:"| ('classes' @String)"`
|
||||
PageSize *int `parser:"| ('pagination' 'size' @Int)"`
|
||||
Validate bool `parser:"| @'validate'"`
|
||||
}
|
||||
|
||||
// Enhanced component field with detailed configuration using flexible attributes
|
||||
type ComponentField struct {
|
||||
Name string `parser:"'field' @Ident"`
|
||||
Type string `parser:"'type' @Ident"`
|
||||
Attributes []ComponentFieldAttribute `parser:"@@*"`
|
||||
Attributes []ComponentFieldAttribute `parser:"@@* ('{' @@* '}')?"` // Support both inline and block attributes
|
||||
}
|
||||
|
||||
// Flexible field attribute system
|
||||
@ -223,96 +203,67 @@ type ComponentValidation struct {
|
||||
Value *string `parser:"@String?"`
|
||||
}
|
||||
|
||||
// Conditional rendering
|
||||
// Enhanced WhenCondition with recursive support for both sections and components
|
||||
type WhenCondition struct {
|
||||
Field string `parser:"'when' @Ident"`
|
||||
Operator string `parser:"@('equals' | 'not_equals' | 'contains')"`
|
||||
Value string `parser:"@String"`
|
||||
Fields []ComponentField `parser:"@@*"`
|
||||
Sections []ComponentSection `parser:"@@*"`
|
||||
Buttons []ComponentButtonAttr `parser:"@@*"`
|
||||
Field string `parser:"'when' @Ident"`
|
||||
Operator string `parser:"@('equals' | 'not_equals' | 'contains')"`
|
||||
Value string `parser:"@String"`
|
||||
Fields []ComponentField `parser:"('{' @@*"`
|
||||
Sections []Section `parser:"@@*"` // Can contain sections
|
||||
Components []Component `parser:"@@*"` // Can contain components
|
||||
Buttons []ComponentButton `parser:"@@* '}')?"` // Block-delimited for unambiguous nesting
|
||||
}
|
||||
|
||||
// Component sections for grouping
|
||||
type ComponentSection struct {
|
||||
Name string `parser:"'section' @Ident"`
|
||||
Class *string `parser:"('class' @String)?"`
|
||||
Fields []ComponentField `parser:"@@*"`
|
||||
Buttons []ComponentButtonAttr `parser:"@@*"`
|
||||
Actions []ComponentButtonAttr `parser:"@@*"`
|
||||
// Simplified button with flexible attribute ordering
|
||||
type ComponentButton struct {
|
||||
Name string `parser:"'button' @Ident"`
|
||||
Label string `parser:"'label' @String"`
|
||||
Attributes []ComponentButtonAttr `parser:"@@*"`
|
||||
}
|
||||
|
||||
// Enhanced component buttons/actions with detailed configuration
|
||||
// Flexible button attribute system - each attribute is a separate alternative
|
||||
type ComponentButtonAttr struct {
|
||||
Name string `parser:"'button' @Ident"`
|
||||
Label string `parser:"'label' @String"`
|
||||
Style *string `parser:"('style' @String)?"`
|
||||
Icon *string `parser:"('icon' @String)?"`
|
||||
Loading *string `parser:"('loading' @String)?"`
|
||||
Disabled *string `parser:"('disabled' 'when' @Ident)?"`
|
||||
Confirm *string `parser:"('confirm' @String)?"`
|
||||
Target *string `parser:"('target' @Ident)?"`
|
||||
Position *string `parser:"('position' @String)?"`
|
||||
Via *string `parser:"('via' @String)?"`
|
||||
Style *ComponentButtonStyle `parser:"@@"`
|
||||
Icon *ComponentButtonIcon `parser:"| @@"`
|
||||
Loading *ComponentButtonLoading `parser:"| @@"`
|
||||
Disabled *ComponentButtonDisabled `parser:"| @@"`
|
||||
Confirm *ComponentButtonConfirm `parser:"| @@"`
|
||||
Target *ComponentButtonTarget `parser:"| @@"`
|
||||
Position *ComponentButtonPosition `parser:"| @@"`
|
||||
Via *ComponentButtonVia `parser:"| @@"`
|
||||
}
|
||||
|
||||
// Component attributes and configurations (keeping existing for backward compatibility)
|
||||
type ComponentAttr struct {
|
||||
Fields *ComponentFields `parser:"@@"`
|
||||
Actions *ComponentActions `parser:"| @@"`
|
||||
DataSource *ComponentDataSource `parser:"| @@"`
|
||||
Style *ComponentStyle `parser:"| @@"`
|
||||
Pagination *ComponentPagination `parser:"| @@"`
|
||||
Filters *ComponentFilters `parser:"| @@"`
|
||||
Validation bool `parser:"| @'validate'"`
|
||||
// Individual button attribute types
|
||||
type ComponentButtonStyle struct {
|
||||
Value string `parser:"'style' @String"`
|
||||
}
|
||||
|
||||
// Component field specification (simple version for backward compatibility)
|
||||
type ComponentFields struct {
|
||||
Fields []string `parser:"'fields' '[' @Ident (',' @Ident)* ']'"`
|
||||
type ComponentButtonIcon struct {
|
||||
Value string `parser:"'icon' @String"`
|
||||
}
|
||||
|
||||
// Enhanced component actions
|
||||
type ComponentActions struct {
|
||||
Actions []ComponentAction `parser:"'actions' '[' @@ (',' @@)* ']'"`
|
||||
type ComponentButtonLoading struct {
|
||||
Value string `parser:"'loading' @String"`
|
||||
}
|
||||
|
||||
type ComponentAction struct {
|
||||
Name string `parser:"@Ident"`
|
||||
Label *string `parser:"('label' @String)?"`
|
||||
Icon *string `parser:"('icon' @String)?"`
|
||||
Style *string `parser:"('style' @String)?"`
|
||||
Endpoint *string `parser:"('via' @String)?"`
|
||||
Target *string `parser:"('target' @Ident)?"`
|
||||
Position *string `parser:"('position' @String)?"`
|
||||
Confirm *string `parser:"('confirm' @String)?"`
|
||||
type ComponentButtonDisabled struct {
|
||||
Value string `parser:"'disabled' 'when' @Ident"`
|
||||
}
|
||||
|
||||
// Data source configuration (can reference endpoints)
|
||||
type ComponentDataSource struct {
|
||||
Endpoint string `parser:"'data' 'from' @String"`
|
||||
type ComponentButtonConfirm struct {
|
||||
Value string `parser:"'confirm' @String"`
|
||||
}
|
||||
|
||||
// Component styling
|
||||
type ComponentStyle struct {
|
||||
Theme *string `parser:"'style' @Ident"`
|
||||
Classes []string `parser:"('classes' '[' @String (',' @String)* ']')?"`
|
||||
type ComponentButtonTarget struct {
|
||||
Value string `parser:"'target' @Ident"`
|
||||
}
|
||||
|
||||
// Pagination configuration
|
||||
type ComponentPagination struct {
|
||||
PageSize *int `parser:"'pagination' ('size' @Int)?"`
|
||||
type ComponentButtonPosition struct {
|
||||
Value string `parser:"'position' @String"`
|
||||
}
|
||||
|
||||
// Filter specifications
|
||||
type ComponentFilters struct {
|
||||
Filters []ComponentFilter `parser:"'filters' '[' @@ (',' @@)* ']'"`
|
||||
}
|
||||
|
||||
type ComponentFilter struct {
|
||||
Field string `parser:"@Ident"`
|
||||
Type string `parser:"'as' @('text' | 'select' | 'date' | 'number')"`
|
||||
Label *string `parser:"('label' @String)?"`
|
||||
type ComponentButtonVia struct {
|
||||
Value string `parser:"'via' @String"`
|
||||
}
|
||||
|
||||
func ParseInput(input string) (AST, error) {
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
575
lang/parser_ui_advanced_test.go
Normal file
575
lang/parser_ui_advanced_test.go
Normal 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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
548
lang/parser_ui_component_test.go
Normal file
548
lang/parser_ui_component_test.go
Normal 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
300
lang/parser_ui_page_test.go
Normal 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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
599
lang/parser_ui_section_test.go
Normal file
599
lang/parser_ui_section_test.go
Normal 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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
59
lang/test_ast_comparisons.go
Normal file
59
lang/test_ast_comparisons.go
Normal 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
|
||||
}
|
46
lang/test_comparison_utils.go
Normal file
46
lang/test_comparison_utils.go
Normal 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
|
||||
}
|
69
lang/test_field_comparisons.go
Normal file
69
lang/test_field_comparisons.go
Normal 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
|
||||
}
|
57
lang/test_server_entity_comparisons.go
Normal file
57
lang/test_server_entity_comparisons.go
Normal 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
421
lang/test_ui_comparisons.go
Normal 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
|
||||
}
|
Reference in New Issue
Block a user