From 4b28828f24659fea0e62a0675150c4f338cc63bb Mon Sep 17 00:00:00 2001 From: sujit Date: Tue, 5 Aug 2025 12:06:42 +0545 Subject: [PATCH] update --- examples/app/complex.json | 1 + examples/validation-demo/README.md | 103 ---------- examples/validation-demo/main.go | 173 ---------------- renderer/schema.go | 317 +++++++---------------------- 4 files changed, 74 insertions(+), 520 deletions(-) delete mode 100644 examples/validation-demo/README.md delete mode 100644 examples/validation-demo/main.go diff --git a/examples/app/complex.json b/examples/app/complex.json index a16a686..debea8b 100644 --- a/examples/app/complex.json +++ b/examples/app/complex.json @@ -12,6 +12,7 @@ "ui": { "element": "input", "type": "text", + "name": "name", "class": "form-group", "order": 1 } diff --git a/examples/validation-demo/README.md b/examples/validation-demo/README.md deleted file mode 100644 index 40ad82f..0000000 --- a/examples/validation-demo/README.md +++ /dev/null @@ -1,103 +0,0 @@ -# JSON Schema Validation Examples - -This directory contains comprehensive examples demonstrating all the validation features implemented in the JSON Schema form builder. - -## Files - -### Schema Examples - -1. **`comprehensive-validation.json`** - Complete example showcasing all JSON Schema 2020-12 validation features: - - Personal information with string validations (minLength, maxLength, pattern, format) - - Address with conditional validations based on country - - Preferences with array validations and enum constraints - - Financial information with numeric validations and conditional requirements - - Skills array with object validation - - Portfolio with anyOf compositions - - Terms acceptance with const validation - -2. **`validation-features.json`** - Organized examples by validation category: - - **String Validations**: Basic length limits, pattern matching, format validation (email, URL, date), enum selection - - **Numeric Validations**: Integer ranges, number with exclusive bounds, multipleOf, range sliders - - **Array Validations**: Multi-select with constraints, dynamic object arrays - - **Conditional Validations**: if/then/else logic based on user type selection - - **Dependent Fields**: dependentRequired, conditional field requirements - - **Composition Validations**: anyOf for flexible contact methods, oneOf for exclusive payment methods - - **Advanced Features**: const fields, boolean checkboxes, read-only fields, textarea with maxLength - -3. **`complex.json`** - Original company registration form with nested objects and grouped fields - -### Demo Server - -**`main.go`** - HTTP server that renders the schema examples into interactive HTML forms with: -- Real-time client-side validation -- HTML5 form controls with validation attributes -- Tailwind CSS styling -- JavaScript validation feedback -- Form submission handling - -## Validation Features Demonstrated - -### String Validations -- `minLength` / `maxLength` - Length constraints -- `pattern` - Regular expression validation -- `format` - Built-in formats (email, uri, date, etc.) -- `enum` - Predefined value lists - -### Numeric Validations -- `minimum` / `maximum` - Inclusive bounds -- `exclusiveMinimum` / `exclusiveMaximum` - Exclusive bounds -- `multipleOf` - Value must be multiple of specified number -- Integer vs Number types - -### Array Validations -- `minItems` / `maxItems` - Size constraints -- `uniqueItems` - No duplicate values -- `items` - Schema for array elements -- Multi-select form controls - -### Object Validations -- `required` - Required properties -- `dependentRequired` - Fields required based on other fields -- `dependentSchemas` - Schema changes based on other fields -- Nested object structures - -### Conditional Logic -- `if` / `then` / `else` - Conditional schema application -- `allOf` - Must satisfy all sub-schemas -- `anyOf` - Must satisfy at least one sub-schema -- `oneOf` - Must satisfy exactly one sub-schema - -### Advanced Features -- `const` - Constant values -- `default` - Default values -- `readOnly` - Read-only fields -- Boolean validation -- HTML form grouping and styling - -## Running the Demo - -1. Navigate to the validation-demo directory: - ```bash - cd examples/validation-demo - ``` - -2. Run the server: - ```bash - go run main.go - ``` - -3. Open your browser to `http://localhost:8080` - -4. Explore the different validation examples: - - Comprehensive Validation Demo - Complete real-world form - - Validation Features Demo - Organized by validation type - - Complex Form Demo - Original nested object example - -## Implementation Details - -The validation system is implemented in: -- `/renderer/validation.go` - Core validation logic and HTML attribute generation -- `/renderer/schema.go` - Form rendering with validation integration -- Client-side JavaScript - Real-time validation feedback - -All examples demonstrate both server-side schema validation and client-side HTML5 validation attributes working together. diff --git a/examples/validation-demo/main.go b/examples/validation-demo/main.go deleted file mode 100644 index e5dea08..0000000 --- a/examples/validation-demo/main.go +++ /dev/null @@ -1,173 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "log" - "net/http" - "os" - - "github.com/oarkflow/jsonschema" - "github.com/oarkflow/mq/renderer" -) - -func main() { - // Setup routes - http.HandleFunc("/", indexHandler) - http.HandleFunc("/comprehensive", func(w http.ResponseWriter, r *http.Request) { - renderFormHandler(w, r, "comprehensive-validation.json", "Comprehensive Validation Demo") - }) - http.HandleFunc("/features", func(w http.ResponseWriter, r *http.Request) { - renderFormHandler(w, r, "validation-features.json", "Validation Features Demo") - }) - http.HandleFunc("/complex", func(w http.ResponseWriter, r *http.Request) { - renderFormHandler(w, r, "complex.json", "Complex Form Demo") - }) - - fmt.Println("Server starting on :8080") - fmt.Println("Available examples:") - fmt.Println(" - http://localhost:8080/comprehensive - Comprehensive validation demo") - fmt.Println(" - http://localhost:8080/features - Validation features demo") - fmt.Println(" - http://localhost:8080/complex - Complex form demo") - - log.Fatal(http.ListenAndServe(":8080", nil)) -} - -func renderFormHandler(w http.ResponseWriter, r *http.Request, schemaFile, title string) { - // Load and compile schema - schemaData, err := os.ReadFile(schemaFile) - if err != nil { - http.Error(w, fmt.Sprintf("Error reading schema file: %v", err), http.StatusInternalServerError) - return - } - - // Parse JSON schema - var schemaMap map[string]interface{} - if err := json.Unmarshal(schemaData, &schemaMap); err != nil { - http.Error(w, fmt.Sprintf("Error parsing JSON schema: %v", err), http.StatusInternalServerError) - return - } - - // Compile schema - compiler := jsonschema.NewCompiler() - schema, err := compiler.Compile(schemaData) - if err != nil { - http.Error(w, fmt.Sprintf("Error compiling schema: %v", err), http.StatusInternalServerError) - return - } - - // Create renderer with basic template - basicTemplate := ` -
- {{.FieldsHTML}} -
- {{.ButtonsHTML}} -
-
- ` - - renderer := renderer.NewJSONSchemaRenderer(schema, basicTemplate) - - // Render form with empty data - html, err := renderer.RenderFields(map[string]any{}) - if err != nil { - http.Error(w, fmt.Sprintf("Error rendering form: %v", err), http.StatusInternalServerError) - return - } - - // Create complete HTML page - fullHTML := createFullHTMLPage(title, html) - w.Header().Set("Content-Type", "text/html") - w.Write([]byte(fullHTML)) -} - -func indexHandler(w http.ResponseWriter, r *http.Request) { - html := "" + - "" + - "" + - "JSON Schema Form Builder - Validation Examples" + - "" + - "" + - "" + - "

JSON Schema Form Builder - Validation Examples

" + - "

This demo showcases comprehensive JSON Schema 2020-12 validation support with:

" + - "" + - "

Available Examples:

" + - "" + - "" + - "" - - w.Header().Set("Content-Type", "text/html") - w.Write([]byte(html)) -} - -func createFullHTMLPage(title, formHTML string) string { - template := "" + - "" + - "" + - "" + title + "" + - "" + - "" + - "" + - "" + - "" + - "" + - "
" + - "← Back to Examples" + - "
" + - "

" + title + "

" + - "

This form demonstrates comprehensive JSON Schema validation.

" + - formHTML + - "
" + - "" + - "" - - return template -} diff --git a/renderer/schema.go b/renderer/schema.go index 62930c2..b346a45 100644 --- a/renderer/schema.go +++ b/renderer/schema.go @@ -481,8 +481,24 @@ func (r *JSONSchemaRenderer) extractFieldsFromPath(fieldPath, parentPath string) return fields } - // Check if this field is required - isRequired := r.isFieldRequired(fieldPath) + // For nested paths like "company.address.street", we need to check if the final part + // is required in its immediate parent's schema + var fieldName string + var parentSchemaPath string + + pathParts := strings.Split(fieldPath, ".") + if len(pathParts) > 1 { + // Extract the field name (last part) and parent path (all but last) + fieldName = pathParts[len(pathParts)-1] + parentSchemaPath = strings.Join(pathParts[:len(pathParts)-1], ".") + } else { + // Single level field + fieldName = fieldPath + parentSchemaPath = parentPath + } + + // Check if this field is required at the parent level + isRequired := r.isFieldRequiredAtPath(fieldName, parentSchemaPath) // If this schema has properties, it's a nested object if schema.Properties != nil && len(*schema.Properties) > 0 { @@ -499,7 +515,7 @@ func (r *JSONSchemaRenderer) extractFieldsFromPath(fieldPath, parentPath string) } fields = append(fields, FieldInfo{ - Name: fieldPath, + Name: fieldName, FieldPath: fullPath, Order: order, Schema: schema, @@ -512,7 +528,7 @@ func (r *JSONSchemaRenderer) extractFieldsFromPath(fieldPath, parentPath string) } // extractFieldsFromNestedSchema processes nested schema properties -func (r *JSONSchemaRenderer) extractFieldsFromNestedSchema(propName, parentPath string, propSchema *jsonschema.Schema, parentRequired bool) []FieldInfo { +func (r *JSONSchemaRenderer) extractFieldsFromNestedSchema(propName, parentPath string, propSchema *jsonschema.Schema, _ bool) []FieldInfo { var fields []FieldInfo fullPath := propName @@ -520,8 +536,9 @@ func (r *JSONSchemaRenderer) extractFieldsFromNestedSchema(propName, parentPath fullPath = parentPath + "." + propName } - // Check if this nested field is required - isRequired := parentRequired || contains(r.Schema.Required, propName) + // Check if this field is required at its immediate parent level + // The parent schema path is the current parentPath, not one level up + isRequired := r.isFieldRequiredAtPath(propName, parentPath) // If this property has nested properties, recurse if propSchema.Properties != nil && len(*propSchema.Properties) > 0 { @@ -558,24 +575,57 @@ func (r *JSONSchemaRenderer) getSchemaAtPath(path string) *jsonschema.Schema { parts := strings.Split(path, ".") currentSchema := r.Schema - for _, part := range parts { + fmt.Printf("DEBUG: Navigating to path '%s', parts: %v\n", path, parts) + + for i, part := range parts { if currentSchema.Properties == nil { + fmt.Printf("DEBUG: No properties at part %d ('%s')\n", i, part) return nil } if propSchema, exists := (*currentSchema.Properties)[part]; exists { currentSchema = propSchema + fmt.Printf("DEBUG: Found part '%s' at level %d\n", part, i) } else { + fmt.Printf("DEBUG: Part '%s' not found at level %d. Available: %v\n", part, i, func() []string { + var keys []string + for k := range *currentSchema.Properties { + keys = append(keys, k) + } + return keys + }()) return nil } } + fmt.Printf("DEBUG: Successfully navigated to path '%s', found required: %v\n", path, currentSchema.Required) 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) +// isFieldRequiredAtPath checks if a field is required at a specific schema path +func (r *JSONSchemaRenderer) isFieldRequiredAtPath(fieldName, schemaPath string) bool { + var schema *jsonschema.Schema + + if schemaPath == "" { + // Check at root level + schema = r.Schema + } else { + // Navigate to the schema at the given path + schema = r.getSchemaAtPath(schemaPath) + } + + if schema == nil { + // Debug: schema not found + fmt.Printf("DEBUG: Schema not found for path '%s'\n", schemaPath) + return false + } + + isRequired := contains(schema.Required, fieldName) + // Debug: show what we're checking + fmt.Printf("DEBUG: Checking if '%s' is required in schema at path '%s'. Required fields: %v. Result: %v\n", + fieldName, schemaPath, schema.Required, isRequired) + + return isRequired } // renderGroup generates HTML for a single group @@ -736,10 +786,20 @@ func buildAllAttributesWithValidation(field FieldInfo) string { var builder strings.Builder builder.Grow(512) // Pre-allocate capacity - // Use the field path as the name attribute for nested fields - fieldName := field.FieldPath + // Check if UI specifies a custom name, otherwise use field path for nested fields + var fieldName string + if field.Schema.UI != nil { + if customName, exists := field.Schema.UI["name"].(string); exists && customName != "" { + fieldName = customName + } + } + + // Fallback to field path or field name if fieldName == "" { - fieldName = field.Name + fieldName = field.FieldPath + if fieldName == "" { + fieldName = field.Name + } } // Add name attribute @@ -895,184 +955,6 @@ func getPlaceholder(schema *jsonschema.Schema) string { return "" } -// renderFieldOptimized uses pre-compiled templates and optimized attribute building -func renderFieldOptimized(field FieldInfo) string { - // Use the new comprehensive rendering logic - return renderField(field) -} - -func buildAllAttributes(field FieldInfo) string { - var attributes []string - - // 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 { - element, _ := field.Schema.UI["element"].(string) - 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 - } - // For select, input, textarea, add class to element itself - if attr == "class" && (element == "select" || element == "input" || element == "textarea") { - attributes = append(attributes, fmt.Sprintf(`class="%v"`, value)) - continue - } - // For other elements, do not add class here (it will be handled in the wrapper div) - if attr == "class" { - continue - } - 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)) - } - } - } - - // Handle required field - if field.IsRequired { - if !containsAttribute(attributes, "required=") { - attributes = append(attributes, `required="required"`) - } - } - - // Handle input type based on field type - element, _ := field.Schema.UI["element"].(string) - if element == "input" { - if inputType := getInputType(field.Schema); inputType != "" { - if !containsAttribute(attributes, "type=") { - attributes = append(attributes, fmt.Sprintf(`type="%s"`, inputType)) - } - } - } - - return strings.Join(attributes, " ") -} - -// buildAllAttributesOptimized uses string builder for better performance -func buildAllAttributesOptimized(field FieldInfo) string { - var builder strings.Builder - builder.Grow(256) // Pre-allocate reasonable capacity - - // Use the field path as the name attribute for nested fields - fieldName := field.FieldPath - if fieldName == "" { - fieldName = field.Name - } - - // Add name attribute - builder.WriteString(`name="`) - builder.WriteString(fieldName) - builder.WriteString(`"`) - - // Add standard attributes from UI - if field.Schema.UI != nil { - element, _ := field.Schema.UI["element"].(string) - 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 - } - // For select, input, textarea, add class to element itself - if attr == "class" && (element == "select" || element == "input" || element == "textarea") { - builder.WriteString(` class="`) - builder.WriteString(fmt.Sprintf("%v", value)) - builder.WriteString(`"`) - continue - } - // For other elements, do not add class here (it will be handled in the wrapper div) - if attr == "class" { - continue - } - builder.WriteString(` `) - builder.WriteString(attr) - builder.WriteString(`="`) - builder.WriteString(fmt.Sprintf("%v", value)) - builder.WriteString(`"`) - } - } - - // Add data-* and aria-* attributes - for key, value := range field.Schema.UI { - if strings.HasPrefix(key, "data-") || strings.HasPrefix(key, "aria-") { - builder.WriteString(` `) - builder.WriteString(key) - builder.WriteString(`="`) - builder.WriteString(fmt.Sprintf("%v", value)) - builder.WriteString(`"`) - } - } - } - - // Handle required field - if field.IsRequired { - currentAttrs := builder.String() - if !strings.Contains(currentAttrs, "required=") { - builder.WriteString(` required="required"`) - } - } - - // Handle input type based on field type - element, _ := field.Schema.UI["element"].(string) - if element == "input" { - if inputType := getInputType(field.Schema); inputType != "" { - currentAttrs := builder.String() - if !strings.Contains(currentAttrs, "type=") { - builder.WriteString(` type="`) - builder.WriteString(inputType) - builder.WriteString(`"`) - } - } - } - - return builder.String() -} - -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 - 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 { @@ -1157,50 +1039,6 @@ func generateLabel(field FieldInfo) string { return fmt.Sprintf(``, fieldName, title, requiredSpan) } -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]interface{}); ok { - // Complex option with attributes - value := getMapValue(optionMap, "value", "") - text := getMapValue(optionMap, "text", value) - selected := "" - if isSelected, ok := optionMap["selected"].(bool); ok && isSelected { - selected = ` selected="selected"` - } - disabled := "" - if isDisabled, ok := optionMap["disabled"].(bool); ok && isDisabled { - disabled = ` disabled="disabled"` - } - optionsHTML.WriteString(fmt.Sprintf(``, - value, selected, disabled, text)) - } else { - // Simple option (just value) - optionsHTML.WriteString(fmt.Sprintf(``, option, option)) - } - } - return optionsHTML.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]interface{}, key, defaultValue string) string { if value, ok := m[key].(string); ok { return value @@ -1217,15 +1055,6 @@ func contains(slice []string, item string) bool { return false } -func containsAttribute(attributes []string, prefix string) bool { - for _, attr := range attributes { - if strings.HasPrefix(attr, prefix) { - return true - } - } - return false -} - // renderButtons generates HTML for form buttons func (r *JSONSchemaRenderer) renderButtons() string { if r.Schema.Form == nil {