From b601da07d84cac8c1c0ebe90f4167ac7ac49b37d Mon Sep 17 00:00:00 2001 From: sujit Date: Tue, 5 Aug 2025 08:05:20 +0545 Subject: [PATCH] update --- examples/app/server.go | 10 +- go.mod | 12 +- go.sum | 16 ++ renderer/schema.go | 495 ++++++++++++++++++++++++++--------------- 4 files changed, 348 insertions(+), 185 deletions(-) diff --git a/examples/app/server.go b/examples/app/server.go index c2dd27f..ae98077 100644 --- a/examples/app/server.go +++ b/examples/app/server.go @@ -1,11 +1,11 @@ package main import ( - "encoding/json" "fmt" "net/http" "os" + "github.com/oarkflow/jsonschema" "github.com/oarkflow/mq/renderer" ) @@ -15,12 +15,12 @@ func main() { fmt.Printf("Error reading schema file: %v\n", err) return } - var schema map[string]interface{} - if err := json.Unmarshal(schemaContent, &schema); err != nil { - fmt.Printf("Error parsing schema: %v\n", err) + compiler := jsonschema.NewCompiler() + schema, err := compiler.Compile(schemaContent) + if err != nil { + fmt.Printf("Error compiling schema: %v\n", err) return } - http.Handle("/form.css", http.FileServer(http.Dir("templates"))) http.HandleFunc("/render", func(w http.ResponseWriter, r *http.Request) { templateName := r.URL.Query().Get("template") diff --git a/go.mod b/go.mod index 06c53a0..faae29c 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/oarkflow/mq -go 1.24.0 +go 1.24.2 require ( github.com/gofiber/fiber/v2 v2.52.6 @@ -20,6 +20,15 @@ require ( golang.org/x/time v0.11.0 ) +require ( + github.com/goccy/go-json v0.10.5 // indirect + github.com/goccy/go-yaml v1.18.0 // indirect + github.com/gotnospirit/makeplural v0.0.0-20180622080156-a5f48d94d976 // indirect + github.com/gotnospirit/messageformat v0.0.0-20221001023931-dfe49f1eb092 // indirect + github.com/kaptinlin/go-i18n v0.1.4 // indirect + golang.org/x/text v0.25.0 // indirect +) + require ( github.com/andybalholm/brotli v1.1.1 // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -31,6 +40,7 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/oarkflow/jsonschema v0.0.4 github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.63.0 // indirect github.com/prometheus/procfs v0.16.0 // indirect diff --git a/go.sum b/go.sum index 92ade79..1419666 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,12 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-reflect v1.2.0 h1:O0T8rZCuNmGXewnATuKYnkL0xm6o8UNOJZd/gOkb9ms= github.com/goccy/go-reflect v1.2.0/go.mod h1:n0oYZn8VcV2CkWTxi8B9QjkCoq6GTtCEdfmR66YhFtE= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI= github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -16,6 +20,12 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gotnospirit/makeplural v0.0.0-20180622080156-a5f48d94d976 h1:b70jEaX2iaJSPZULSUxKtm73LBfsCrMsIlYCUgNGSIs= +github.com/gotnospirit/makeplural v0.0.0-20180622080156-a5f48d94d976/go.mod h1:ZGQeOwybjD8lkCjIyJfqR5LD2wMVHJ31d6GdPxoTsWY= +github.com/gotnospirit/messageformat v0.0.0-20221001023931-dfe49f1eb092 h1:c7gcNWTSr1gtLp6PyYi3wzvFCEcHJ4YRobDgqmIgf7Q= +github.com/gotnospirit/messageformat v0.0.0-20221001023931-dfe49f1eb092/go.mod h1:ZZAN4fkkful3l1lpJwF8JbW41ZiG9TwJ2ZlqzQovBNU= +github.com/kaptinlin/go-i18n v0.1.4 h1:wCiwAn1LOcvymvWIVAM4m5dUAMiHunTdEubLDk4hTGs= +github.com/kaptinlin/go-i18n v0.1.4/go.mod h1:g1fn1GvTgT4CiLE8/fFE1hboHWJ6erivrDpiDtCcFKg= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= @@ -42,10 +52,14 @@ github.com/oarkflow/jet v0.0.4 h1:rs0nTzodye/9zhrSX7FlR80Gjaty6ei2Ln0pmaUrdwg= github.com/oarkflow/jet v0.0.4/go.mod h1:YXIc47aYyx1xKpnmuz1Z9o88cxxa47r7X3lfUAxZ0Qg= github.com/oarkflow/json v0.0.21 h1:tBx4ufwC48UAd3fUCqLVH/dERpnZ85Dgw5/h7H2HMoM= github.com/oarkflow/json v0.0.21/go.mod h1:maoLmQZJ/8pF1MugtpVqzHJ59dH1Z7xFSNkhl9BQjYo= +github.com/oarkflow/jsonschema v0.0.4 h1:n5Sb7WVb7NNQzn/ei9++4VPqKXCPJhhsHeTGJkIuwmM= +github.com/oarkflow/jsonschema v0.0.4/go.mod h1:AxNG3Nk7KZxnnjRJlHLmS1wE9brtARu5caTFuicCtnA= github.com/oarkflow/log v1.0.79 h1:DxhtkBGG+pUu6cudSVw5g75FbKEQJkij5w7n5AEN00M= github.com/oarkflow/log v1.0.79/go.mod h1:U/4chr1DyOiQvS6JiQpjYTCJhK7RGR8xrXPsGlouLzM= github.com/oarkflow/xid v1.2.8 h1:uCIX61Binq2RPMsqImZM6pPGzoZTmRyD6jguxF9aAA0= github.com/oarkflow/xid v1.2.8/go.mod h1:jG4YBh+swbjlWApGWDBYnsJEa7hi3CCpmuqhB3RAxVo= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk= @@ -74,6 +88,8 @@ golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= diff --git a/renderer/schema.go b/renderer/schema.go index 9c543aa..8baf7e0 100644 --- a/renderer/schema.go +++ b/renderer/schema.go @@ -6,6 +6,8 @@ import ( "html/template" "sort" "strings" + + "github.com/oarkflow/jsonschema" ) // A single template for the entire group structure @@ -152,8 +154,10 @@ var standardAttrs = []string{ // FieldInfo represents metadata for a field extracted from JSONSchema type FieldInfo struct { Name string + FieldPath string // Full path for nested fields (e.g., "user.address.street") Order int - Definition map[string]any + Schema *jsonschema.Schema + IsRequired bool } // GroupInfo represents metadata for a group extracted from JSONSchema @@ -171,14 +175,14 @@ type GroupTitle struct { // JSONSchemaRenderer is responsible for rendering HTML fields based on JSONSchema type JSONSchemaRenderer struct { - Schema map[string]any + Schema *jsonschema.Schema HTMLLayout string TemplateData map[string]any // Data for template interpolation cachedHTML string // Cached rendered HTML } // NewJSONSchemaRenderer creates a new instance of JSONSchemaRenderer -func NewJSONSchemaRenderer(schema map[string]any, htmlLayout string) *JSONSchemaRenderer { +func NewJSONSchemaRenderer(schema *jsonschema.Schema, htmlLayout string) *JSONSchemaRenderer { return &JSONSchemaRenderer{ Schema: schema, HTMLLayout: htmlLayout, @@ -204,7 +208,7 @@ func (r *JSONSchemaRenderer) interpolateTemplate(templateStr string) string { for key, value := range r.TemplateData { placeholder := fmt.Sprintf("{{%s}}", key) if valueStr, ok := value.(string); ok { - result = fmt.Sprintf("%s", bytes.ReplaceAll([]byte(result), []byte(placeholder), []byte(valueStr))) + result = strings.ReplaceAll(result, placeholder, valueStr) } } return result @@ -226,21 +230,30 @@ func (r *JSONSchemaRenderer) RenderFields() (string, error) { return r.cachedHTML, nil } - groups := parseGroupsFromSchema(r.Schema) + groups := r.parseGroupsFromSchema() var groupHTML bytes.Buffer for _, group := range groups { groupHTML.WriteString(renderGroup(group)) } - formConfig := r.Schema["form"].(map[string]any) - formClass, _ := formConfig["class"].(string) - formAction, _ := formConfig["action"].(string) - formMethod, _ := formConfig["method"].(string) - formEnctype, _ := formConfig["enctype"].(string) + // Extract form configuration + var formClass, formAction, formMethod, formEnctype string + if r.Schema.Form != nil { + if class, ok := r.Schema.Form["class"].(string); ok { + formClass = class + } + if action, ok := r.Schema.Form["action"].(string); ok { + formAction = r.interpolateTemplate(action) + } + if method, ok := r.Schema.Form["method"].(string); ok { + formMethod = method + } + if enctype, ok := r.Schema.Form["enctype"].(string); ok { + formEnctype = enctype + } + } - // Interpolate template data into form action - formAction = r.interpolateTemplate(formAction) - buttonsHTML := renderButtons(formConfig) + buttonsHTML := r.renderButtons() // Create a new template with the layout and functions tmpl, err := template.New("layout").Funcs(template.FuncMap{ @@ -269,32 +282,30 @@ func (r *JSONSchemaRenderer) RenderFields() (string, error) { } // parseGroupsFromSchema extracts and sorts groups and fields from schema -func parseGroupsFromSchema(schema map[string]any) []GroupInfo { - formConfig, ok := schema["form"].(map[string]any) +func (r *JSONSchemaRenderer) parseGroupsFromSchema() []GroupInfo { + if r.Schema.Form == nil { + return nil + } + + groupsData, ok := r.Schema.Form["groups"] if !ok { return nil } - properties, ok := schema["properties"].(map[string]any) + groups, ok := groupsData.([]interface{}) if !ok { return nil } - var requiredFields map[string]bool = make(map[string]bool) - if reqFields, ok := schema["required"].([]any); ok { - for _, field := range reqFields { - if fieldName, ok := field.(string); ok { - requiredFields[fieldName] = true - } + var result []GroupInfo + for _, group := range groups { + groupMap, ok := group.(map[string]interface{}) + if !ok { + continue } - } - - var groups []GroupInfo - for _, group := range formConfig["groups"].([]any) { - groupMap := group.(map[string]any) var groupTitle GroupTitle - if titleMap, ok := groupMap["title"].(map[string]any); ok { + if titleMap, ok := groupMap["title"].(map[string]interface{}); ok { if text, ok := titleMap["text"].(string); ok { groupTitle.Text = text } @@ -304,37 +315,152 @@ func parseGroupsFromSchema(schema map[string]any) []GroupInfo { } groupClass, _ := groupMap["class"].(string) + if groupClass == "" { + groupClass = "form-group-fields" + } var fields []FieldInfo - for _, fieldName := range groupMap["fields"].([]any) { - fieldDef := properties[fieldName.(string)].(map[string]any) - order := 0 - if ord, exists := fieldDef["order"].(int); exists { - order = ord + if fieldsData, ok := groupMap["fields"].([]interface{}); ok { + for _, fieldName := range fieldsData { + if fieldNameStr, ok := fieldName.(string); ok { + // Handle nested field paths + fieldInfos := r.extractFieldsFromPath(fieldNameStr, "") + fields = append(fields, fieldInfos...) + } } - - fieldDefCopy := make(map[string]any) - for k, v := range fieldDef { - fieldDefCopy[k] = v - } - fieldDefCopy["isRequired"] = requiredFields[fieldName.(string)] - - fields = append(fields, FieldInfo{ - Name: fieldName.(string), - Order: order, - Definition: fieldDefCopy, - }) } + + // Sort fields by order sort.Slice(fields, func(i, j int) bool { - return fields[i].Order < fields[j].Order + orderI := 0 + orderJ := 0 + if fields[i].Schema.Order != nil { + orderI = *fields[i].Schema.Order + } + if fields[j].Schema.Order != nil { + orderJ = *fields[j].Schema.Order + } + return orderI < orderJ }) - groups = append(groups, GroupInfo{ + + result = append(result, GroupInfo{ Title: groupTitle, Fields: fields, GroupClass: groupClass, }) } - return groups + return result +} + +// extractFieldsFromPath recursively extracts fields from a path, handling nested properties +func (r *JSONSchemaRenderer) extractFieldsFromPath(fieldPath, parentPath string) []FieldInfo { + var fields []FieldInfo + + // Build the full path + fullPath := fieldPath + if parentPath != "" { + fullPath = parentPath + "." + fieldPath + } + + // Navigate to the schema at this path + schema := r.getSchemaAtPath(fieldPath) + if schema == nil { + return fields + } + + // Check if this field is required + isRequired := r.isFieldRequired(fieldPath) + + // If this schema has properties, it's a nested object + if schema.Properties != nil && len(*schema.Properties) > 0 { + // Recursively process nested properties + for propName, propSchema := range *schema.Properties { + nestedFields := r.extractFieldsFromNestedSchema(propName, fullPath, propSchema, isRequired) + fields = append(fields, nestedFields...) + } + } else { + // This is a leaf field + order := 0 + if schema.Order != nil { + order = *schema.Order + } + + fields = append(fields, FieldInfo{ + Name: fieldPath, + FieldPath: fullPath, + Order: order, + Schema: schema, + IsRequired: isRequired, + }) + } + + return fields +} + +// extractFieldsFromNestedSchema processes nested schema properties +func (r *JSONSchemaRenderer) extractFieldsFromNestedSchema(propName, parentPath string, propSchema *jsonschema.Schema, parentRequired bool) []FieldInfo { + var fields []FieldInfo + + fullPath := propName + if parentPath != "" { + fullPath = parentPath + "." + propName + } + + // Check if this nested field is required + isRequired := parentRequired || contains(r.Schema.Required, propName) + + // If this property has nested properties, recurse + if propSchema.Properties != nil && len(*propSchema.Properties) > 0 { + for nestedPropName, nestedPropSchema := range *propSchema.Properties { + nestedFields := r.extractFieldsFromNestedSchema(nestedPropName, fullPath, nestedPropSchema, isRequired) + fields = append(fields, nestedFields...) + } + } else { + // This is a leaf field + order := 0 + if propSchema.Order != nil { + order = *propSchema.Order + } + + fields = append(fields, FieldInfo{ + Name: propName, + FieldPath: fullPath, + Order: order, + Schema: propSchema, + IsRequired: isRequired, + }) + } + + return fields +} + +// getSchemaAtPath navigates to a schema at a given path +func (r *JSONSchemaRenderer) getSchemaAtPath(path string) *jsonschema.Schema { + if r.Schema.Properties == nil { + return nil + } + + parts := strings.Split(path, ".") + currentSchema := r.Schema + + for _, part := range parts { + if currentSchema.Properties == nil { + return nil + } + + if propSchema, exists := (*currentSchema.Properties)[part]; exists { + currentSchema = propSchema + } else { + return nil + } + } + + return currentSchema +} + +// isFieldRequired checks if a field is required at the current schema level +func (r *JSONSchemaRenderer) isFieldRequired(fieldName string) bool { + return contains(r.Schema.Required, fieldName) } // renderGroup generates HTML for a single group @@ -352,9 +478,6 @@ func renderGroup(group GroupInfo) string { "GroupClass": group.GroupClass, "FieldsHTML": template.HTML(fieldsHTML.String()), } - if group.GroupClass == "" { - data["GroupClass"] = "form-group-fields" - } if err := tmpl.Execute(&groupHTML, data); err != nil { return "" // Return empty string on error @@ -364,25 +487,24 @@ func renderGroup(group GroupInfo) string { } func renderField(field FieldInfo) string { - ui, ok := field.Definition["ui"].(map[string]any) - if !ok { + if field.Schema.UI == nil { return "" } - element, _ := ui["element"].(string) - if element == "" { + element, ok := field.Schema.UI["element"].(string) + if !ok || element == "" { return "" } // Build all attributes - allAttributes := buildAllAttributes(field, ui) + allAttributes := buildAllAttributes(field) // Get content - content := getFieldContent(field.Definition, ui) - contentHTML := getFieldContentHTML(field.Definition, ui) + content := getFieldContent(field) + contentHTML := getFieldContentHTML(field) // Generate label if needed - labelHTML := generateLabel(field, ui) + labelHTML := generateLabel(field) data := map[string]any{ "Element": element, @@ -390,8 +512,8 @@ func renderField(field FieldInfo) string { "Content": content, "ContentHTML": template.HTML(contentHTML), "LabelHTML": template.HTML(labelHTML), - "Class": getUIValue(ui, "class"), - "OptionsHTML": template.HTML(generateOptions(ui)), + "Class": getUIValue(field.Schema.UI, "class"), + "OptionsHTML": template.HTML(generateOptions(field.Schema.UI)), } // Use specific template if available, otherwise use generic @@ -412,142 +534,133 @@ func renderField(field FieldInfo) string { return buf.String() } -func buildAllAttributes(field FieldInfo, ui map[string]any) string { +func buildAllAttributes(field FieldInfo) string { var attributes []string - // Add standard attributes from ui - for _, attr := range standardAttrs { - if value, exists := ui[attr]; exists { - if attr == "class" && value == "" { - continue // Skip empty class + // Use the field path as the name attribute for nested fields + fieldName := field.FieldPath + if fieldName == "" { + fieldName = field.Name + } + + // Add name attribute + attributes = append(attributes, fmt.Sprintf(`name="%s"`, fieldName)) + + // Add standard attributes from UI + if field.Schema.UI != nil { + for _, attr := range standardAttrs { + if attr == "name" { + continue // Already handled above + } + if value, exists := field.Schema.UI[attr]; exists { + if attr == "class" && value == "" { + continue // Skip empty class + } + attributes = append(attributes, fmt.Sprintf(`%s="%v"`, attr, value)) + } + } + + // Add data-* and aria-* attributes + for key, value := range field.Schema.UI { + if strings.HasPrefix(key, "data-") || strings.HasPrefix(key, "aria-") { + attributes = append(attributes, fmt.Sprintf(`%s="%v"`, key, value)) } - attributes = append(attributes, fmt.Sprintf(`%s="%v"`, attr, value)) } } // Handle required field - if isRequired, ok := field.Definition["isRequired"].(bool); ok && isRequired { - if !contains(attributes, "required=") { + if field.IsRequired { + if !containsAttribute(attributes, "required=") { attributes = append(attributes, `required="required"`) } } // Handle input type based on field type - element, _ := ui["element"].(string) + element, _ := field.Schema.UI["element"].(string) if element == "input" { - if inputType := getInputType(field.Definition, ui); inputType != "" { - if !contains(attributes, "type=") { + if inputType := getInputType(field.Schema); inputType != "" { + if !containsAttribute(attributes, "type=") { attributes = append(attributes, fmt.Sprintf(`type="%s"`, inputType)) } } } - // Add data-* and aria-* attributes - for key, value := range ui { - if strings.HasPrefix(key, "data-") || strings.HasPrefix(key, "aria-") { - attributes = append(attributes, fmt.Sprintf(`%s="%v"`, key, value)) - } - } - - // Add custom attributes from field definition (excluding known schema properties) - excludeFields := map[string]bool{ - "type": true, "title": true, "ui": true, "placeholder": true, - "order": true, "isRequired": true, "content": true, "children": true, - } - - for key, value := range field.Definition { - if !excludeFields[key] && !strings.HasPrefix(key, "ui") { - attributes = append(attributes, fmt.Sprintf(`%s="%v"`, key, value)) - } - } - return strings.Join(attributes, " ") } -func getInputType(fieldDef map[string]any, ui map[string]any) string { - // Check ui type first - if uiType, ok := ui["type"].(string); ok { - return uiType +func getInputType(schema *jsonschema.Schema) string { + // Check UI type first + if schema.UI != nil { + if uiType, ok := schema.UI["type"].(string); ok { + return uiType + } + } + var typeStr string + if len(schema.Type) > 0 { + typeStr = schema.Type[0] + } else { + typeStr = "string" } - // Map schema types to input types - if fieldType, ok := fieldDef["type"].(string); ok { - switch fieldType { - case "email": - return "email" - case "password": - return "password" - case "number", "integer": - return "number" - case "boolean": - return "checkbox" - case "date": - return "date" - case "time": - return "time" - case "datetime": - return "datetime-local" - case "url": - return "url" - case "tel": - return "tel" - case "color": - return "color" - case "range": - return "range" - case "file": - return "file" - case "hidden": - return "hidden" - default: - return "text" + switch typeStr { + case "string", "text": + return "text" + case "number", "integer": + return "number" + case "boolean": + return "checkbox" + default: + return "text" + } +} + +func getFieldContent(field FieldInfo) string { + // Check for content in UI first + if field.Schema.UI != nil { + if content, ok := field.Schema.UI["content"].(string); ok { + return content } } - return "text" -} - -func getFieldContent(fieldDef map[string]any, ui map[string]any) string { - // Check for content in ui first - if content, ok := ui["content"].(string); ok { - return content - } - - // Check for content in field definition - if content, ok := fieldDef["content"].(string); ok { - return content - } - // Use title as fallback for some elements - if title, ok := fieldDef["title"].(string); ok { - return title + if field.Schema.Title != nil { + return *field.Schema.Title } return "" } -func getFieldContentHTML(fieldDef map[string]any, ui map[string]any) string { - // Check for HTML content in ui - if contentHTML, ok := ui["contentHTML"].(string); ok { - return contentHTML - } +func getFieldContentHTML(field FieldInfo) string { + // Check for HTML content in UI + if field.Schema.UI != nil { + if contentHTML, ok := field.Schema.UI["contentHTML"].(string); ok { + return contentHTML + } - // Check for children elements - if children, ok := ui["children"].([]any); ok { - return renderChildren(children) + // Check for children elements + if children, ok := field.Schema.UI["children"].([]interface{}); ok { + return renderChildren(children) + } } return "" } -func renderChildren(children []any) string { +func renderChildren(children []interface{}) string { var result strings.Builder for _, child := range children { - if childMap, ok := child.(map[string]any); ok { + if childMap, ok := child.(map[string]interface{}); ok { // Create a temporary field info for the child + childSchema := &jsonschema.Schema{ + UI: childMap, + } + if title, ok := childMap["title"].(string); ok { + childSchema.Title = &title + } + childField := FieldInfo{ - Name: getMapValue(childMap, "name", ""), - Definition: childMap, + Name: getMapValue(childMap, "name", ""), + Schema: childSchema, } result.WriteString(renderField(childField)) } @@ -555,41 +668,49 @@ func renderChildren(children []any) string { return result.String() } -func generateLabel(field FieldInfo, ui map[string]any) string { +func generateLabel(field FieldInfo) string { // Check if label should be generated - if showLabel, ok := ui["showLabel"].(bool); !showLabel && ok { - return "" + if field.Schema.UI != nil { + if showLabel, ok := field.Schema.UI["showLabel"].(bool); !showLabel && ok { + return "" + } } - title, _ := field.Definition["title"].(string) + var title string + if field.Schema.Title != nil { + title = *field.Schema.Title + } if title == "" { return "" } - name := getUIValue(ui, "name") - if name == "" { - name = field.Name + fieldName := field.FieldPath + if fieldName == "" { + fieldName = field.Name } // Check if field is required - isRequired, _ := field.Definition["isRequired"].(bool) requiredSpan := "" - if isRequired { + if field.IsRequired { requiredSpan = ` *` } - return fmt.Sprintf(``, name, title, requiredSpan) + return fmt.Sprintf(``, fieldName, title, requiredSpan) } -func generateOptions(ui map[string]any) string { - options, ok := ui["options"].([]any) +func generateOptions(ui map[string]interface{}) string { + if ui == nil { + return "" + } + + options, ok := ui["options"].([]interface{}) if !ok { return "" } var optionsHTML strings.Builder for _, option := range options { - if optionMap, ok := option.(map[string]any); ok { + if optionMap, ok := option.(map[string]interface{}); ok { // Complex option with attributes value := getMapValue(optionMap, "value", "") text := getMapValue(optionMap, "text", value) @@ -611,23 +732,35 @@ func generateOptions(ui map[string]any) string { return optionsHTML.String() } -func getUIValue(ui map[string]any, key string) string { +func getUIValue(ui map[string]interface{}, key string) string { + if ui == nil { + return "" + } if value, ok := ui[key].(string); ok { return value } return "" } -func getMapValue(m map[string]any, key, defaultValue string) string { +func getMapValue(m map[string]interface{}, key, defaultValue string) string { if value, ok := m[key].(string); ok { return value } return defaultValue } -func contains(slice []string, substr string) bool { - for _, item := range slice { - if strings.Contains(item, substr) { +func contains(slice []string, item string) bool { + for _, s := range slice { + if s == item { + return true + } + } + return false +} + +func containsAttribute(attributes []string, prefix string) bool { + for _, attr := range attributes { + if strings.HasPrefix(attr, prefix) { return true } } @@ -635,23 +768,27 @@ func contains(slice []string, substr string) bool { } // renderButtons generates HTML for form buttons -func renderButtons(formConfig map[string]any) string { +func (r *JSONSchemaRenderer) renderButtons() string { + if r.Schema.Form == nil { + return "" + } + var buttonsHTML bytes.Buffer - if submitConfig, ok := formConfig["submit"].(map[string]any); ok { + if submitConfig, ok := r.Schema.Form["submit"].(map[string]interface{}); ok { buttonHTML := renderButtonFromConfig(submitConfig, "submit") buttonsHTML.WriteString(buttonHTML) } - if resetConfig, ok := formConfig["reset"].(map[string]any); ok { + if resetConfig, ok := r.Schema.Form["reset"].(map[string]interface{}); ok { buttonHTML := renderButtonFromConfig(resetConfig, "reset") buttonsHTML.WriteString(buttonHTML) } // Support for additional custom buttons - if buttons, ok := formConfig["buttons"].([]any); ok { + if buttons, ok := r.Schema.Form["buttons"].([]interface{}); ok { for _, button := range buttons { - if buttonMap, ok := button.(map[string]any); ok { + if buttonMap, ok := button.(map[string]interface{}); ok { buttonType := getMapValue(buttonMap, "type", "button") buttonHTML := renderButtonFromConfig(buttonMap, buttonType) buttonsHTML.WriteString(buttonHTML) @@ -662,7 +799,7 @@ func renderButtons(formConfig map[string]any) string { return buttonsHTML.String() } -func renderButtonFromConfig(config map[string]any, defaultType string) string { +func renderButtonFromConfig(config map[string]interface{}, defaultType string) string { var attributes []string buttonType := getMapValue(config, "type", defaultType)