mirror of
https://github.com/oarkflow/mq.git
synced 2025-10-08 20:20:04 +08:00
update
This commit is contained in:
681
examples/app/comprehensive-validation.json
Normal file
681
examples/app/comprehensive-validation.json
Normal file
@@ -0,0 +1,681 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"title": "Comprehensive Validation Demo",
|
||||
"description": "A complete example showcasing all JSON Schema 2020-12 validation features",
|
||||
"properties": {
|
||||
"personalInfo": {
|
||||
"type": "object",
|
||||
"title": "Personal Information",
|
||||
"properties": {
|
||||
"firstName": {
|
||||
"type": "string",
|
||||
"title": "First Name",
|
||||
"minLength": 2,
|
||||
"maxLength": 50,
|
||||
"pattern": "^[A-Za-z\\s]+$",
|
||||
"ui": {
|
||||
"element": "input",
|
||||
"type": "text",
|
||||
"class": "form-group",
|
||||
"order": 1,
|
||||
"placeholder": "Enter your first name"
|
||||
}
|
||||
},
|
||||
"lastName": {
|
||||
"type": "string",
|
||||
"title": "Last Name",
|
||||
"minLength": 2,
|
||||
"maxLength": 50,
|
||||
"pattern": "^[A-Za-z\\s]+$",
|
||||
"ui": {
|
||||
"element": "input",
|
||||
"type": "text",
|
||||
"class": "form-group",
|
||||
"order": 2
|
||||
}
|
||||
},
|
||||
"age": {
|
||||
"type": "integer",
|
||||
"title": "Age",
|
||||
"minimum": 18,
|
||||
"maximum": 120,
|
||||
"ui": {
|
||||
"element": "input",
|
||||
"type": "number",
|
||||
"class": "form-group",
|
||||
"order": 3
|
||||
}
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"format": "email",
|
||||
"title": "Email Address",
|
||||
"ui": {
|
||||
"element": "input",
|
||||
"type": "email",
|
||||
"class": "form-group",
|
||||
"order": 4
|
||||
}
|
||||
},
|
||||
"phone": {
|
||||
"type": "string",
|
||||
"title": "Phone Number",
|
||||
"pattern": "^\\+?[1-9]\\d{1,14}$",
|
||||
"ui": {
|
||||
"element": "input",
|
||||
"type": "tel",
|
||||
"class": "form-group",
|
||||
"order": 5
|
||||
}
|
||||
},
|
||||
"dateOfBirth": {
|
||||
"type": "string",
|
||||
"format": "date",
|
||||
"title": "Date of Birth",
|
||||
"ui": {
|
||||
"element": "input",
|
||||
"type": "date",
|
||||
"class": "form-group",
|
||||
"order": 6
|
||||
}
|
||||
},
|
||||
"website": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"title": "Personal Website",
|
||||
"ui": {
|
||||
"element": "input",
|
||||
"type": "url",
|
||||
"class": "form-group",
|
||||
"order": 7
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [ "firstName", "lastName", "email" ],
|
||||
"dependentRequired": {
|
||||
"phone": [ "email" ],
|
||||
"website": [ "email" ]
|
||||
}
|
||||
},
|
||||
"address": {
|
||||
"type": "object",
|
||||
"title": "Address Information",
|
||||
"properties": {
|
||||
"street": {
|
||||
"type": "string",
|
||||
"title": "Street Address",
|
||||
"minLength": 5,
|
||||
"maxLength": 100,
|
||||
"ui": {
|
||||
"element": "input",
|
||||
"type": "text",
|
||||
"class": "form-group",
|
||||
"order": 1
|
||||
}
|
||||
},
|
||||
"city": {
|
||||
"type": "string",
|
||||
"title": "City",
|
||||
"minLength": 2,
|
||||
"maxLength": 50,
|
||||
"ui": {
|
||||
"element": "input",
|
||||
"type": "text",
|
||||
"class": "form-group",
|
||||
"order": 2
|
||||
}
|
||||
},
|
||||
"state": {
|
||||
"type": "string",
|
||||
"title": "State/Province",
|
||||
"ui": {
|
||||
"element": "input",
|
||||
"type": "text",
|
||||
"class": "form-group",
|
||||
"order": 3
|
||||
}
|
||||
},
|
||||
"country": {
|
||||
"type": "string",
|
||||
"title": "Country",
|
||||
"enum": [ "US", "CA", "UK", "DE", "FR", "AU", "JP", "IN" ],
|
||||
"ui": {
|
||||
"element": "select",
|
||||
"class": "form-group",
|
||||
"order": 4,
|
||||
"options": [
|
||||
{ "value": "US", "text": "United States" },
|
||||
{ "value": "CA", "text": "Canada" },
|
||||
{ "value": "UK", "text": "United Kingdom" },
|
||||
{ "value": "DE", "text": "Germany" },
|
||||
{ "value": "FR", "text": "France" },
|
||||
{ "value": "AU", "text": "Australia" },
|
||||
{ "value": "JP", "text": "Japan" },
|
||||
{ "value": "IN", "text": "India" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"postalCode": {
|
||||
"type": "string",
|
||||
"title": "Postal Code",
|
||||
"ui": {
|
||||
"element": "input",
|
||||
"type": "text",
|
||||
"class": "form-group",
|
||||
"order": 5
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [ "street", "city", "country" ],
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"country": { "const": "US" }
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"postalCode": {
|
||||
"pattern": "^\\d{5}(-\\d{4})?$"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"country": { "const": "CA" }
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"postalCode": {
|
||||
"pattern": "^[A-Z]\\d[A-Z] \\d[A-Z]\\d$"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"preferences": {
|
||||
"type": "object",
|
||||
"title": "User Preferences",
|
||||
"properties": {
|
||||
"newsletter": {
|
||||
"type": "boolean",
|
||||
"title": "Subscribe to Newsletter",
|
||||
"default": false,
|
||||
"ui": {
|
||||
"element": "input",
|
||||
"type": "checkbox",
|
||||
"class": "form-group",
|
||||
"order": 1
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"type": "array",
|
||||
"title": "Notification Types",
|
||||
"minItems": 1,
|
||||
"maxItems": 5,
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [ "email", "sms", "push", "phone", "mail" ]
|
||||
},
|
||||
"ui": {
|
||||
"element": "select",
|
||||
"class": "form-group",
|
||||
"order": 2,
|
||||
"multiple": true,
|
||||
"options": [
|
||||
{ "value": "email", "text": "Email Notifications" },
|
||||
{ "value": "sms", "text": "SMS Notifications" },
|
||||
{ "value": "push", "text": "Push Notifications" },
|
||||
{ "value": "phone", "text": "Phone Calls" },
|
||||
{ "value": "mail", "text": "Physical Mail" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"theme": {
|
||||
"type": "string",
|
||||
"title": "Preferred Theme",
|
||||
"enum": [ "light", "dark", "auto" ],
|
||||
"default": "auto",
|
||||
"ui": {
|
||||
"element": "select",
|
||||
"class": "form-group",
|
||||
"order": 3,
|
||||
"options": [
|
||||
{ "value": "light", "text": "Light Theme" },
|
||||
{ "value": "dark", "text": "Dark Theme" },
|
||||
{ "value": "auto", "text": "Auto (System)" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"language": {
|
||||
"type": "string",
|
||||
"title": "Preferred Language",
|
||||
"enum": [ "en", "es", "fr", "de", "zh", "ja" ],
|
||||
"default": "en",
|
||||
"ui": {
|
||||
"element": "select",
|
||||
"class": "form-group",
|
||||
"order": 4,
|
||||
"options": [
|
||||
{ "value": "en", "text": "English" },
|
||||
{ "value": "es", "text": "Español" },
|
||||
{ "value": "fr", "text": "Français" },
|
||||
{ "value": "de", "text": "Deutsch" },
|
||||
{ "value": "zh", "text": "中文" },
|
||||
{ "value": "ja", "text": "日本語" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"financial": {
|
||||
"type": "object",
|
||||
"title": "Financial Information",
|
||||
"properties": {
|
||||
"annualIncome": {
|
||||
"type": "number",
|
||||
"title": "Annual Income",
|
||||
"minimum": 0,
|
||||
"maximum": 10000000,
|
||||
"multipleOf": 1000,
|
||||
"ui": {
|
||||
"element": "input",
|
||||
"type": "number",
|
||||
"class": "form-group",
|
||||
"order": 1,
|
||||
"step": "1000"
|
||||
}
|
||||
},
|
||||
"creditScore": {
|
||||
"type": "integer",
|
||||
"title": "Credit Score",
|
||||
"minimum": 300,
|
||||
"maximum": 850,
|
||||
"ui": {
|
||||
"element": "input",
|
||||
"type": "range",
|
||||
"class": "form-group",
|
||||
"order": 2,
|
||||
"min": "300",
|
||||
"max": "850"
|
||||
}
|
||||
},
|
||||
"accountType": {
|
||||
"type": "string",
|
||||
"title": "Account Type",
|
||||
"oneOf": [
|
||||
{
|
||||
"const": "basic",
|
||||
"title": "Basic Account",
|
||||
"description": "Free account with basic features"
|
||||
},
|
||||
{
|
||||
"const": "premium",
|
||||
"title": "Premium Account",
|
||||
"description": "Paid account with advanced features"
|
||||
},
|
||||
{
|
||||
"const": "enterprise",
|
||||
"title": "Enterprise Account",
|
||||
"description": "Business account with full features"
|
||||
}
|
||||
],
|
||||
"ui": {
|
||||
"element": "select",
|
||||
"class": "form-group",
|
||||
"order": 3,
|
||||
"options": [
|
||||
{ "value": "basic", "text": "Basic Account" },
|
||||
{ "value": "premium", "text": "Premium Account" },
|
||||
{ "value": "enterprise", "text": "Enterprise Account" }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"if": {
|
||||
"properties": {
|
||||
"accountType": { "const": "premium" }
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"paymentMethod": {
|
||||
"type": "string",
|
||||
"title": "Payment Method",
|
||||
"enum": [ "credit_card", "debit_card", "paypal", "bank_transfer" ],
|
||||
"ui": {
|
||||
"element": "select",
|
||||
"class": "form-group",
|
||||
"order": 4,
|
||||
"options": [
|
||||
{ "value": "credit_card", "text": "Credit Card" },
|
||||
{ "value": "debit_card", "text": "Debit Card" },
|
||||
{ "value": "paypal", "text": "PayPal" },
|
||||
{ "value": "bank_transfer", "text": "Bank Transfer" }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [ "paymentMethod" ]
|
||||
},
|
||||
"else": {
|
||||
"not": {
|
||||
"required": [ "paymentMethod" ]
|
||||
}
|
||||
}
|
||||
},
|
||||
"skills": {
|
||||
"type": "array",
|
||||
"title": "Technical Skills",
|
||||
"minItems": 1,
|
||||
"maxItems": 10,
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"title": "Skill Name",
|
||||
"minLength": 2,
|
||||
"maxLength": 50
|
||||
},
|
||||
"level": {
|
||||
"type": "string",
|
||||
"title": "Proficiency Level",
|
||||
"enum": [ "beginner", "intermediate", "advanced", "expert" ]
|
||||
},
|
||||
"yearsExperience": {
|
||||
"type": "number",
|
||||
"title": "Years of Experience",
|
||||
"minimum": 0,
|
||||
"maximum": 50,
|
||||
"multipleOf": 0.5
|
||||
}
|
||||
},
|
||||
"required": [ "name", "level" ]
|
||||
},
|
||||
"ui": {
|
||||
"element": "fieldset",
|
||||
"class": "skills-section",
|
||||
"order": 1
|
||||
}
|
||||
},
|
||||
"portfolio": {
|
||||
"type": "object",
|
||||
"title": "Portfolio",
|
||||
"properties": {
|
||||
"projects": {
|
||||
"type": "array",
|
||||
"title": "Projects",
|
||||
"maxItems": 5,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"title": "Project Title",
|
||||
"minLength": 3,
|
||||
"maxLength": 100
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"title": "Description",
|
||||
"maxLength": 500,
|
||||
"ui": {
|
||||
"element": "textarea",
|
||||
"rows": 4,
|
||||
"class": "form-group"
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"title": "Project URL"
|
||||
},
|
||||
"technologies": {
|
||||
"type": "array",
|
||||
"title": "Technologies Used",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"minItems": 1,
|
||||
"maxItems": 10,
|
||||
"uniqueItems": true
|
||||
}
|
||||
},
|
||||
"required": [ "title", "description" ]
|
||||
}
|
||||
}
|
||||
},
|
||||
"anyOf": [
|
||||
{
|
||||
"properties": {
|
||||
"githubUrl": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"pattern": "^https://github\\.com/",
|
||||
"title": "GitHub Profile"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"linkedinUrl": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"pattern": "^https://www\\.linkedin\\.com/",
|
||||
"title": "LinkedIn Profile"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"personalWebsite": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"title": "Personal Website"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"comments": {
|
||||
"type": "string",
|
||||
"title": "Additional Comments",
|
||||
"maxLength": 1000,
|
||||
"ui": {
|
||||
"element": "textarea",
|
||||
"class": "form-group",
|
||||
"rows": 5,
|
||||
"placeholder": "Any additional information you'd like to share..."
|
||||
}
|
||||
},
|
||||
"termsAccepted": {
|
||||
"type": "boolean",
|
||||
"title": "I accept the terms and conditions",
|
||||
"const": true,
|
||||
"ui": {
|
||||
"element": "input",
|
||||
"type": "checkbox",
|
||||
"class": "form-group required-checkbox"
|
||||
}
|
||||
},
|
||||
"privacyAccepted": {
|
||||
"type": "boolean",
|
||||
"title": "I accept the privacy policy",
|
||||
"const": true,
|
||||
"ui": {
|
||||
"element": "input",
|
||||
"type": "checkbox",
|
||||
"class": "form-group required-checkbox"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [ "personalInfo", "address", "termsAccepted", "privacyAccepted" ],
|
||||
"dependentSchemas": {
|
||||
"financial": {
|
||||
"properties": {
|
||||
"personalInfo": {
|
||||
"properties": {
|
||||
"age": {
|
||||
"minimum": 21
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
"class": "comprehensive-form",
|
||||
"action": "/api/submit-comprehensive-form",
|
||||
"method": "POST",
|
||||
"enctype": "multipart/form-data",
|
||||
"groups": [
|
||||
{
|
||||
"title": {
|
||||
"text": "Personal Information",
|
||||
"class": "section-header"
|
||||
},
|
||||
"class": "personal-info-section",
|
||||
"fields": [
|
||||
"personalInfo.firstName",
|
||||
"personalInfo.lastName",
|
||||
"personalInfo.age",
|
||||
"personalInfo.email",
|
||||
"personalInfo.phone",
|
||||
"personalInfo.dateOfBirth",
|
||||
"personalInfo.website"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": {
|
||||
"text": "Address Information",
|
||||
"class": "section-header"
|
||||
},
|
||||
"class": "address-section flex flex-wrap gap-4",
|
||||
"fields": [
|
||||
"address.street",
|
||||
"address.city",
|
||||
"address.state",
|
||||
"address.country",
|
||||
"address.postalCode"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": {
|
||||
"text": "Preferences & Settings",
|
||||
"class": "section-header"
|
||||
},
|
||||
"class": "preferences-section",
|
||||
"fields": [
|
||||
"preferences.newsletter",
|
||||
"preferences.notifications",
|
||||
"preferences.theme",
|
||||
"preferences.language"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": {
|
||||
"text": "Financial Information",
|
||||
"class": "section-header"
|
||||
},
|
||||
"class": "financial-section",
|
||||
"fields": [
|
||||
"financial.annualIncome",
|
||||
"financial.creditScore",
|
||||
"financial.accountType"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": {
|
||||
"text": "Skills & Portfolio",
|
||||
"class": "section-header"
|
||||
},
|
||||
"class": "skills-section",
|
||||
"fields": [
|
||||
"skills",
|
||||
"portfolio"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": {
|
||||
"text": "Final Details",
|
||||
"class": "section-header"
|
||||
},
|
||||
"class": "final-section",
|
||||
"fields": [
|
||||
"comments",
|
||||
"termsAccepted",
|
||||
"privacyAccepted"
|
||||
]
|
||||
}
|
||||
],
|
||||
"submit": {
|
||||
"label": "Submit Application",
|
||||
"class": "btn btn-primary btn-lg",
|
||||
"id": "submit-btn"
|
||||
},
|
||||
"reset": {
|
||||
"label": "Reset Form",
|
||||
"class": "btn btn-secondary",
|
||||
"id": "reset-btn"
|
||||
},
|
||||
"buttons": [
|
||||
{
|
||||
"type": "button",
|
||||
"label": "Save Draft",
|
||||
"class": "btn btn-outline-secondary",
|
||||
"id": "save-draft-btn",
|
||||
"onclick": "saveDraft()"
|
||||
},
|
||||
{
|
||||
"type": "button",
|
||||
"label": "Preview",
|
||||
"class": "btn btn-outline-primary",
|
||||
"id": "preview-btn",
|
||||
"onclick": "showPreview()"
|
||||
}
|
||||
]
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"personalInfo": {
|
||||
"firstName": "John",
|
||||
"lastName": "Doe",
|
||||
"age": 30,
|
||||
"email": "john.doe@example.com",
|
||||
"phone": "+1-555-123-4567",
|
||||
"dateOfBirth": "1993-05-15",
|
||||
"website": "https://johndoe.com"
|
||||
},
|
||||
"address": {
|
||||
"street": "123 Main St",
|
||||
"city": "New York",
|
||||
"state": "NY",
|
||||
"country": "US",
|
||||
"postalCode": "10001"
|
||||
},
|
||||
"preferences": {
|
||||
"newsletter": true,
|
||||
"notifications": [ "email", "push" ],
|
||||
"theme": "dark",
|
||||
"language": "en"
|
||||
},
|
||||
"financial": {
|
||||
"annualIncome": 75000,
|
||||
"creditScore": 750,
|
||||
"accountType": "premium",
|
||||
"paymentMethod": "credit_card"
|
||||
},
|
||||
"termsAccepted": true,
|
||||
"privacyAccepted": true
|
||||
}
|
||||
]
|
||||
}
|
586
examples/app/validation-features.json
Normal file
586
examples/app/validation-features.json
Normal file
@@ -0,0 +1,586 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "Advanced Validation Features Demo",
|
||||
"description": "Demonstrates all implemented validation features",
|
||||
"properties": {
|
||||
"stringValidations": {
|
||||
"type": "object",
|
||||
"title": "String Validation Examples",
|
||||
"properties": {
|
||||
"basicString": {
|
||||
"type": "string",
|
||||
"title": "Basic String with Length Limits",
|
||||
"minLength": 3,
|
||||
"maxLength": 20,
|
||||
"ui": {
|
||||
"element": "input",
|
||||
"type": "text",
|
||||
"class": "form-group",
|
||||
"order": 1
|
||||
}
|
||||
},
|
||||
"patternString": {
|
||||
"type": "string",
|
||||
"title": "Pattern Validation (Letters Only)",
|
||||
"pattern": "^[A-Za-z\\s]+$",
|
||||
"ui": {
|
||||
"element": "input",
|
||||
"type": "text",
|
||||
"class": "form-group",
|
||||
"order": 2,
|
||||
"placeholder": "Letters and spaces only"
|
||||
}
|
||||
},
|
||||
"emailField": {
|
||||
"type": "string",
|
||||
"format": "email",
|
||||
"title": "Email Format Validation",
|
||||
"ui": {
|
||||
"element": "input",
|
||||
"type": "email",
|
||||
"class": "form-group",
|
||||
"order": 3
|
||||
}
|
||||
},
|
||||
"urlField": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"title": "URL Format Validation",
|
||||
"ui": {
|
||||
"element": "input",
|
||||
"type": "url",
|
||||
"class": "form-group",
|
||||
"order": 4
|
||||
}
|
||||
},
|
||||
"dateField": {
|
||||
"type": "string",
|
||||
"format": "date",
|
||||
"title": "Date Format Validation",
|
||||
"ui": {
|
||||
"element": "input",
|
||||
"type": "date",
|
||||
"class": "form-group",
|
||||
"order": 5
|
||||
}
|
||||
},
|
||||
"enumField": {
|
||||
"type": "string",
|
||||
"title": "Enum Selection",
|
||||
"enum": [ "option1", "option2", "option3" ],
|
||||
"ui": {
|
||||
"element": "select",
|
||||
"class": "form-group",
|
||||
"order": 6,
|
||||
"options": [
|
||||
{ "value": "option1", "text": "Option 1" },
|
||||
{ "value": "option2", "text": "Option 2" },
|
||||
{ "value": "option3", "text": "Option 3" }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [ "basicString", "emailField" ]
|
||||
},
|
||||
"numericValidations": {
|
||||
"type": "object",
|
||||
"title": "Numeric Validation Examples",
|
||||
"properties": {
|
||||
"integerField": {
|
||||
"type": "integer",
|
||||
"title": "Integer with Range",
|
||||
"minimum": 1,
|
||||
"maximum": 100,
|
||||
"ui": {
|
||||
"element": "input",
|
||||
"type": "number",
|
||||
"class": "form-group",
|
||||
"order": 1
|
||||
}
|
||||
},
|
||||
"numberField": {
|
||||
"type": "number",
|
||||
"title": "Number with Exclusive Range",
|
||||
"exclusiveMinimum": 0,
|
||||
"exclusiveMaximum": 1000,
|
||||
"multipleOf": 0.5,
|
||||
"ui": {
|
||||
"element": "input",
|
||||
"type": "number",
|
||||
"class": "form-group",
|
||||
"order": 2,
|
||||
"step": "0.5"
|
||||
}
|
||||
},
|
||||
"rangeField": {
|
||||
"type": "integer",
|
||||
"title": "Range Slider",
|
||||
"minimum": 0,
|
||||
"maximum": 100,
|
||||
"default": 50,
|
||||
"ui": {
|
||||
"element": "input",
|
||||
"type": "range",
|
||||
"class": "form-group",
|
||||
"order": 3
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"arrayValidations": {
|
||||
"type": "object",
|
||||
"title": "Array Validation Examples",
|
||||
"properties": {
|
||||
"multiSelect": {
|
||||
"type": "array",
|
||||
"title": "Multi-Select with Constraints",
|
||||
"minItems": 2,
|
||||
"maxItems": 4,
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [ "red", "green", "blue", "yellow", "purple", "orange" ]
|
||||
},
|
||||
"ui": {
|
||||
"element": "select",
|
||||
"class": "form-group",
|
||||
"order": 1,
|
||||
"multiple": true,
|
||||
"options": [
|
||||
{ "value": "red", "text": "Red" },
|
||||
{ "value": "green", "text": "Green" },
|
||||
{ "value": "blue", "text": "Blue" },
|
||||
{ "value": "yellow", "text": "Yellow" },
|
||||
{ "value": "purple", "text": "Purple" },
|
||||
{ "value": "orange", "text": "Orange" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"dynamicList": {
|
||||
"type": "array",
|
||||
"title": "Dynamic List of Objects",
|
||||
"minItems": 1,
|
||||
"maxItems": 5,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"title": "Name",
|
||||
"minLength": 2
|
||||
},
|
||||
"value": {
|
||||
"type": "number",
|
||||
"title": "Value",
|
||||
"minimum": 0
|
||||
}
|
||||
},
|
||||
"required": [ "name" ]
|
||||
},
|
||||
"ui": {
|
||||
"element": "fieldset",
|
||||
"class": "dynamic-list",
|
||||
"order": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"conditionalValidations": {
|
||||
"type": "object",
|
||||
"title": "Conditional Validation Examples",
|
||||
"properties": {
|
||||
"userType": {
|
||||
"type": "string",
|
||||
"title": "User Type",
|
||||
"enum": [ "individual", "business", "non-profit" ],
|
||||
"ui": {
|
||||
"element": "select",
|
||||
"class": "form-group",
|
||||
"order": 1,
|
||||
"options": [
|
||||
{ "value": "individual", "text": "Individual" },
|
||||
{ "value": "business", "text": "Business" },
|
||||
{ "value": "non-profit", "text": "Non-Profit" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"individualInfo": {
|
||||
"type": "object",
|
||||
"title": "Individual Information",
|
||||
"properties": {
|
||||
"ssn": {
|
||||
"type": "string",
|
||||
"title": "Social Security Number",
|
||||
"pattern": "^\\d{3}-\\d{2}-\\d{4}$",
|
||||
"ui": {
|
||||
"element": "input",
|
||||
"type": "text",
|
||||
"class": "form-group",
|
||||
"placeholder": "XXX-XX-XXXX"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"businessInfo": {
|
||||
"type": "object",
|
||||
"title": "Business Information",
|
||||
"properties": {
|
||||
"taxId": {
|
||||
"type": "string",
|
||||
"title": "Tax ID",
|
||||
"pattern": "^\\d{2}-\\d{7}$",
|
||||
"ui": {
|
||||
"element": "input",
|
||||
"type": "text",
|
||||
"class": "form-group",
|
||||
"placeholder": "XX-XXXXXXX"
|
||||
}
|
||||
},
|
||||
"businessName": {
|
||||
"type": "string",
|
||||
"title": "Business Name",
|
||||
"minLength": 2,
|
||||
"ui": {
|
||||
"element": "input",
|
||||
"type": "text",
|
||||
"class": "form-group"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"userType": { "const": "individual" }
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"individualInfo": {
|
||||
"required": [ "ssn" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"userType": { "const": "business" }
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"businessInfo": {
|
||||
"required": [ "taxId", "businessName" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"dependentFields": {
|
||||
"type": "object",
|
||||
"title": "Dependent Field Examples",
|
||||
"properties": {
|
||||
"hasPhone": {
|
||||
"type": "boolean",
|
||||
"title": "I have a phone number",
|
||||
"ui": {
|
||||
"element": "input",
|
||||
"type": "checkbox",
|
||||
"class": "form-group",
|
||||
"order": 1
|
||||
}
|
||||
},
|
||||
"phoneNumber": {
|
||||
"type": "string",
|
||||
"title": "Phone Number",
|
||||
"pattern": "^\\+?[1-9]\\d{1,14}$",
|
||||
"ui": {
|
||||
"element": "input",
|
||||
"type": "tel",
|
||||
"class": "form-group",
|
||||
"order": 2
|
||||
}
|
||||
},
|
||||
"preferredContact": {
|
||||
"type": "string",
|
||||
"title": "Preferred Contact Method",
|
||||
"enum": [ "email", "phone", "mail" ],
|
||||
"ui": {
|
||||
"element": "select",
|
||||
"class": "form-group",
|
||||
"order": 3,
|
||||
"options": [
|
||||
{ "value": "email", "text": "Email" },
|
||||
{ "value": "phone", "text": "Phone" },
|
||||
{ "value": "mail", "text": "Physical Mail" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"mailingAddress": {
|
||||
"type": "string",
|
||||
"title": "Mailing Address",
|
||||
"ui": {
|
||||
"element": "textarea",
|
||||
"class": "form-group",
|
||||
"order": 4,
|
||||
"rows": 3
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependentRequired": {
|
||||
"hasPhone": [ "phoneNumber" ],
|
||||
"preferredContact": [ "preferredContact" ]
|
||||
},
|
||||
"if": {
|
||||
"properties": {
|
||||
"preferredContact": { "const": "phone" }
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": [ "phoneNumber" ]
|
||||
},
|
||||
"else": {
|
||||
"if": {
|
||||
"properties": {
|
||||
"preferredContact": { "const": "mail" }
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": [ "mailingAddress" ]
|
||||
}
|
||||
}
|
||||
},
|
||||
"compositionValidations": {
|
||||
"type": "object",
|
||||
"title": "Composition Validation Examples (allOf, anyOf, oneOf)",
|
||||
"properties": {
|
||||
"contactMethod": {
|
||||
"title": "Contact Method Validation",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"format": "email",
|
||||
"title": "Email Address"
|
||||
}
|
||||
},
|
||||
"required": [ "email" ]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"phone": {
|
||||
"type": "string",
|
||||
"pattern": "^\\+?[1-9]\\d{1,14}$",
|
||||
"title": "Phone Number"
|
||||
}
|
||||
},
|
||||
"required": [ "phone" ]
|
||||
}
|
||||
],
|
||||
"ui": {
|
||||
"element": "fieldset",
|
||||
"class": "contact-methods",
|
||||
"order": 1
|
||||
}
|
||||
},
|
||||
"paymentMethod": {
|
||||
"title": "Payment Method (One Of)",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": { "const": "credit_card" },
|
||||
"cardNumber": {
|
||||
"type": "string",
|
||||
"pattern": "^\\d{16}$",
|
||||
"title": "Card Number"
|
||||
},
|
||||
"expiryDate": {
|
||||
"type": "string",
|
||||
"pattern": "^(0[1-9]|1[0-2])\\/\\d{2}$",
|
||||
"title": "Expiry Date (MM/YY)"
|
||||
}
|
||||
},
|
||||
"required": [ "type", "cardNumber", "expiryDate" ]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": { "const": "paypal" },
|
||||
"paypalEmail": {
|
||||
"type": "string",
|
||||
"format": "email",
|
||||
"title": "PayPal Email"
|
||||
}
|
||||
},
|
||||
"required": [ "type", "paypalEmail" ]
|
||||
}
|
||||
],
|
||||
"ui": {
|
||||
"element": "fieldset",
|
||||
"class": "payment-methods",
|
||||
"order": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"advancedFeatures": {
|
||||
"type": "object",
|
||||
"title": "Advanced Features",
|
||||
"properties": {
|
||||
"constField": {
|
||||
"const": "REQUIRED_VALUE",
|
||||
"title": "Constant Value Field",
|
||||
"ui": {
|
||||
"element": "input",
|
||||
"type": "hidden",
|
||||
"value": "REQUIRED_VALUE"
|
||||
}
|
||||
},
|
||||
"booleanField": {
|
||||
"type": "boolean",
|
||||
"title": "Boolean Checkbox",
|
||||
"ui": {
|
||||
"element": "input",
|
||||
"type": "checkbox",
|
||||
"class": "form-group"
|
||||
}
|
||||
},
|
||||
"readOnlyField": {
|
||||
"type": "string",
|
||||
"title": "Read Only Field",
|
||||
"default": "This is read only",
|
||||
"readOnly": true,
|
||||
"ui": {
|
||||
"element": "input",
|
||||
"type": "text",
|
||||
"class": "form-group",
|
||||
"readonly": true
|
||||
}
|
||||
},
|
||||
"textareaField": {
|
||||
"type": "string",
|
||||
"title": "Large Text Area",
|
||||
"maxLength": 500,
|
||||
"ui": {
|
||||
"element": "textarea",
|
||||
"class": "form-group",
|
||||
"rows": 4,
|
||||
"placeholder": "Enter detailed information here..."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [ "stringValidations" ],
|
||||
"form": {
|
||||
"class": "validation-demo-form",
|
||||
"action": "/api/validate-form",
|
||||
"method": "POST",
|
||||
"groups": [
|
||||
{
|
||||
"title": {
|
||||
"text": "String Validations",
|
||||
"class": "section-header"
|
||||
},
|
||||
"class": "string-validation-section",
|
||||
"fields": [
|
||||
"stringValidations.basicString",
|
||||
"stringValidations.patternString",
|
||||
"stringValidations.emailField",
|
||||
"stringValidations.urlField",
|
||||
"stringValidations.dateField",
|
||||
"stringValidations.enumField"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": {
|
||||
"text": "Numeric Validations",
|
||||
"class": "section-header"
|
||||
},
|
||||
"class": "numeric-validation-section",
|
||||
"fields": [
|
||||
"numericValidations.integerField",
|
||||
"numericValidations.numberField",
|
||||
"numericValidations.rangeField"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": {
|
||||
"text": "Array Validations",
|
||||
"class": "section-header"
|
||||
},
|
||||
"class": "array-validation-section",
|
||||
"fields": [
|
||||
"arrayValidations.multiSelect",
|
||||
"arrayValidations.dynamicList"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": {
|
||||
"text": "Conditional Validations",
|
||||
"class": "section-header"
|
||||
},
|
||||
"class": "conditional-validation-section",
|
||||
"fields": [
|
||||
"conditionalValidations.userType",
|
||||
"conditionalValidations.individualInfo",
|
||||
"conditionalValidations.businessInfo"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": {
|
||||
"text": "Dependent Fields",
|
||||
"class": "section-header"
|
||||
},
|
||||
"class": "dependent-validation-section",
|
||||
"fields": [
|
||||
"dependentFields.hasPhone",
|
||||
"dependentFields.phoneNumber",
|
||||
"dependentFields.preferredContact",
|
||||
"dependentFields.mailingAddress"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": {
|
||||
"text": "Composition Validations",
|
||||
"class": "section-header"
|
||||
},
|
||||
"class": "composition-validation-section",
|
||||
"fields": [
|
||||
"compositionValidations.contactMethod",
|
||||
"compositionValidations.paymentMethod"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": {
|
||||
"text": "Advanced Features",
|
||||
"class": "section-header"
|
||||
},
|
||||
"class": "advanced-features-section",
|
||||
"fields": [
|
||||
"advancedFeatures.constField",
|
||||
"advancedFeatures.booleanField",
|
||||
"advancedFeatures.readOnlyField",
|
||||
"advancedFeatures.textareaField"
|
||||
]
|
||||
}
|
||||
],
|
||||
"submit": {
|
||||
"label": "Validate Form",
|
||||
"class": "btn btn-primary"
|
||||
},
|
||||
"reset": {
|
||||
"label": "Reset All",
|
||||
"class": "btn btn-secondary"
|
||||
}
|
||||
}
|
||||
}
|
103
examples/validation-demo/README.md
Normal file
103
examples/validation-demo/README.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# 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.
|
173
examples/validation-demo/main.go
Normal file
173
examples/validation-demo/main.go
Normal file
@@ -0,0 +1,173 @@
|
||||
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 := `
|
||||
<form {{if .Form.Action}}action="{{.Form.Action}}"{{end}} {{if .Form.Method}}method="{{.Form.Method}}"{{end}} {{if .Form.Class}}class="{{.Form.Class}}"{{end}}>
|
||||
{{.FieldsHTML}}
|
||||
<div class="form-buttons">
|
||||
{{.ButtonsHTML}}
|
||||
</div>
|
||||
</form>
|
||||
`
|
||||
|
||||
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 := "<!DOCTYPE html>" +
|
||||
"<html>" +
|
||||
"<head>" +
|
||||
"<title>JSON Schema Form Builder - Validation Examples</title>" +
|
||||
"<style>" +
|
||||
"body { font-family: Arial, sans-serif; margin: 40px; }" +
|
||||
"h1 { color: #333; }" +
|
||||
".example-list { list-style: none; padding: 0; }" +
|
||||
".example-list li { margin: 15px 0; }" +
|
||||
".example-list a { display: inline-block; padding: 10px 20px; background: #007bff; color: white; text-decoration: none; border-radius: 5px; }" +
|
||||
".example-list a:hover { background: #0056b3; }" +
|
||||
".description { color: #666; margin-top: 5px; }" +
|
||||
"</style>" +
|
||||
"</head>" +
|
||||
"<body>" +
|
||||
"<h1>JSON Schema Form Builder - Validation Examples</h1>" +
|
||||
"<p>This demo showcases comprehensive JSON Schema 2020-12 validation support with:</p>" +
|
||||
"<ul>" +
|
||||
"<li>String validations (minLength, maxLength, pattern, format)</li>" +
|
||||
"<li>Numeric validations (minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf)</li>" +
|
||||
"<li>Array validations (minItems, maxItems, uniqueItems)</li>" +
|
||||
"<li>Conditional validations (if/then/else, allOf, anyOf, oneOf)</li>" +
|
||||
"<li>Dependent validations (dependentRequired, dependentSchemas)</li>" +
|
||||
"<li>Client-side and server-side validation</li>" +
|
||||
"<li>Advanced HTML form generation with validation attributes</li>" +
|
||||
"</ul>" +
|
||||
"<h2>Available Examples:</h2>" +
|
||||
"<ul class=\"example-list\">" +
|
||||
"<li>" +
|
||||
"<a href=\"/comprehensive\">Comprehensive Validation Demo</a>" +
|
||||
"<div class=\"description\">Complete example with personal info, address, preferences, financial data, skills, portfolio, and complex conditional logic</div>" +
|
||||
"</li>" +
|
||||
"<li>" +
|
||||
"<a href=\"/features\">Validation Features Demo</a>" +
|
||||
"<div class=\"description\">Focused examples of specific validation features organized by category</div>" +
|
||||
"</li>" +
|
||||
"<li>" +
|
||||
"<a href=\"/complex\">Complex Form Demo</a>" +
|
||||
"<div class=\"description\">Original company registration form with nested objects and grouped fields</div>" +
|
||||
"</li>" +
|
||||
"</ul>" +
|
||||
"</body>" +
|
||||
"</html>"
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte(html))
|
||||
}
|
||||
|
||||
func createFullHTMLPage(title, formHTML string) string {
|
||||
template := "<!DOCTYPE html>" +
|
||||
"<html>" +
|
||||
"<head>" +
|
||||
"<title>" + title + "</title>" +
|
||||
"<meta charset=\"utf-8\">" +
|
||||
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">" +
|
||||
"<script src=\"https://cdn.tailwindcss.com\"></script>" +
|
||||
"<style>" +
|
||||
".form-group { margin-bottom: 1rem; }" +
|
||||
".form-group label { display: block; margin-bottom: 0.25rem; font-weight: 600; }" +
|
||||
".form-group input, .form-group select, .form-group textarea { width: 100%; padding: 0.5rem; border: 1px solid #d1d5db; border-radius: 0.375rem; }" +
|
||||
".btn { padding: 0.5rem 1rem; border-radius: 0.375rem; font-weight: 600; cursor: pointer; border: none; margin-right: 0.5rem; }" +
|
||||
".btn-primary { background-color: #3b82f6; color: white; }" +
|
||||
".validation-error { color: #dc2626; font-size: 0.875rem; margin-top: 0.25rem; }" +
|
||||
".back-link { display: inline-block; margin-bottom: 2rem; color: #3b82f6; text-decoration: none; }" +
|
||||
"</style>" +
|
||||
"</head>" +
|
||||
"<body class=\"bg-gray-50 min-h-screen py-8\">" +
|
||||
"<div class=\"max-w-4xl mx-auto px-4\">" +
|
||||
"<a href=\"/\" class=\"back-link\">← Back to Examples</a>" +
|
||||
"<div class=\"bg-white rounded-lg shadow-lg p-8\">" +
|
||||
"<h1 class=\"text-3xl font-bold text-gray-900 mb-6\">" + title + "</h1>" +
|
||||
"<p class=\"text-gray-600 mb-8\">This form demonstrates comprehensive JSON Schema validation.</p>" +
|
||||
formHTML +
|
||||
"</div></div>" +
|
||||
"<script>" +
|
||||
"document.addEventListener('DOMContentLoaded', function() {" +
|
||||
"const form = document.querySelector('form');" +
|
||||
"if (form) {" +
|
||||
"form.addEventListener('submit', function(e) {" +
|
||||
"e.preventDefault();" +
|
||||
"alert('Form validation demo - would submit data in real application');" +
|
||||
"});" +
|
||||
"}" +
|
||||
"});" +
|
||||
"</script>" +
|
||||
"</body></html>"
|
||||
|
||||
return template
|
||||
}
|
@@ -139,6 +139,32 @@ var voidElements = map[string]bool{
|
||||
"param": true, "source": true, "track": true, "wbr": true,
|
||||
}
|
||||
|
||||
// Template cache for compiled templates
|
||||
var (
|
||||
compiledFieldTemplates = make(map[string]*template.Template)
|
||||
compiledGroupTemplate *template.Template
|
||||
templateCacheMutex sync.RWMutex
|
||||
)
|
||||
|
||||
// Initialize compiled templates once
|
||||
func init() {
|
||||
var err error
|
||||
compiledGroupTemplate, err = template.New("group").Parse(groupTemplateStr)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Failed to compile group template: %v", err))
|
||||
}
|
||||
|
||||
templateCacheMutex.Lock()
|
||||
defer templateCacheMutex.Unlock()
|
||||
|
||||
for element, tmplStr := range fieldTemplates {
|
||||
compiled, err := template.New(element).Parse(tmplStr)
|
||||
if err == nil {
|
||||
compiledFieldTemplates[element] = compiled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var standardAttrs = []string{
|
||||
"id", "class", "name", "type", "value", "placeholder", "href", "src",
|
||||
"alt", "title", "target", "rel", "role", "tabindex", "accesskey",
|
||||
@@ -160,6 +186,7 @@ type FieldInfo struct {
|
||||
Order int
|
||||
Schema *jsonschema.Schema
|
||||
IsRequired bool
|
||||
Validation ValidationInfo // Add validation information
|
||||
}
|
||||
|
||||
// GroupInfo represents metadata for a group extracted from JSONSchema
|
||||
@@ -179,15 +206,77 @@ type GroupTitle struct {
|
||||
type JSONSchemaRenderer struct {
|
||||
Schema *jsonschema.Schema
|
||||
HTMLLayout string
|
||||
cachedHTML string // Cached rendered HTML
|
||||
compiledLayout *template.Template
|
||||
cachedGroups []GroupInfo
|
||||
cachedButtons string
|
||||
formConfig FormConfig
|
||||
cacheMutex sync.RWMutex
|
||||
}
|
||||
|
||||
// FormConfig holds cached form configuration
|
||||
type FormConfig struct {
|
||||
Class string
|
||||
Action string
|
||||
Method string
|
||||
Enctype string
|
||||
}
|
||||
|
||||
// NewJSONSchemaRenderer creates a new instance of JSONSchemaRenderer
|
||||
func NewJSONSchemaRenderer(schema *jsonschema.Schema, htmlLayout string) *JSONSchemaRenderer {
|
||||
return &JSONSchemaRenderer{
|
||||
renderer := &JSONSchemaRenderer{
|
||||
Schema: schema,
|
||||
HTMLLayout: htmlLayout,
|
||||
}
|
||||
|
||||
// Pre-compile layout template
|
||||
renderer.compileLayoutTemplate()
|
||||
|
||||
// Pre-parse and cache groups and form config
|
||||
renderer.precomputeStaticData()
|
||||
|
||||
return renderer
|
||||
}
|
||||
|
||||
// compileLayoutTemplate pre-compiles the layout template
|
||||
func (r *JSONSchemaRenderer) compileLayoutTemplate() {
|
||||
tmpl, err := template.New("layout").Funcs(template.FuncMap{
|
||||
"form_groups": func(groupsHTML string) template.HTML {
|
||||
return template.HTML(groupsHTML)
|
||||
},
|
||||
"form_buttons": func() template.HTML {
|
||||
return template.HTML(r.cachedButtons)
|
||||
},
|
||||
"form_attributes": func(formAction string) template.HTMLAttr {
|
||||
return template.HTMLAttr(fmt.Sprintf(`class="%s" action="%s" method="%s" enctype="%s"`,
|
||||
r.formConfig.Class, formAction, r.formConfig.Method, r.formConfig.Enctype))
|
||||
},
|
||||
}).Parse(r.HTMLLayout)
|
||||
|
||||
if err == nil {
|
||||
r.compiledLayout = tmpl
|
||||
}
|
||||
}
|
||||
|
||||
// precomputeStaticData caches groups and form configuration
|
||||
func (r *JSONSchemaRenderer) precomputeStaticData() {
|
||||
r.cachedGroups = r.parseGroupsFromSchema()
|
||||
r.cachedButtons = r.renderButtons()
|
||||
|
||||
// Cache form configuration
|
||||
if r.Schema.Form != nil {
|
||||
if class, ok := r.Schema.Form["class"].(string); ok {
|
||||
r.formConfig.Class = class
|
||||
}
|
||||
if action, ok := r.Schema.Form["action"].(string); ok {
|
||||
r.formConfig.Action = action
|
||||
}
|
||||
if method, ok := r.Schema.Form["method"].(string); ok {
|
||||
r.formConfig.Method = method
|
||||
}
|
||||
if enctype, ok := r.Schema.Form["enctype"].(string); ok {
|
||||
r.formConfig.Enctype = enctype
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// interpolateTemplate replaces template placeholders with actual values
|
||||
@@ -220,35 +309,65 @@ func (r *JSONSchemaRenderer) interpolateTemplate(templateStr string, data map[st
|
||||
|
||||
// RenderFields generates HTML for fields based on the JSONSchema
|
||||
func (r *JSONSchemaRenderer) RenderFields(data map[string]any) (string, error) {
|
||||
// Return cached HTML if available
|
||||
if r.cachedHTML != "" {
|
||||
return r.cachedHTML, nil
|
||||
r.cacheMutex.RLock()
|
||||
defer r.cacheMutex.RUnlock()
|
||||
|
||||
// Use a string builder for efficient string concatenation
|
||||
var groupHTML strings.Builder
|
||||
groupHTML.Grow(1024) // Pre-allocate reasonable capacity
|
||||
|
||||
for _, group := range r.cachedGroups {
|
||||
groupHTML.WriteString(renderGroupOptimized(group))
|
||||
}
|
||||
|
||||
groups := r.parseGroupsFromSchema()
|
||||
var groupHTML bytes.Buffer
|
||||
for _, group := range groups {
|
||||
groupHTML.WriteString(renderGroup(group))
|
||||
// Interpolate dynamic form action
|
||||
formAction := r.interpolateTemplate(r.formConfig.Action, data)
|
||||
|
||||
// Use pre-compiled template if available
|
||||
if r.compiledLayout != nil {
|
||||
var renderedHTML bytes.Buffer
|
||||
|
||||
templateData := struct {
|
||||
GroupsHTML string
|
||||
FormAction string
|
||||
}{
|
||||
GroupsHTML: groupHTML.String(),
|
||||
FormAction: formAction,
|
||||
}
|
||||
|
||||
// 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, data)
|
||||
}
|
||||
if method, ok := r.Schema.Form["method"].(string); ok {
|
||||
formMethod = method
|
||||
}
|
||||
if enctype, ok := r.Schema.Form["enctype"].(string); ok {
|
||||
formEnctype = enctype
|
||||
}
|
||||
// Update the template functions with current data
|
||||
updatedTemplate := r.compiledLayout.Funcs(template.FuncMap{
|
||||
"form_groups": func() template.HTML {
|
||||
return template.HTML(templateData.GroupsHTML)
|
||||
},
|
||||
"form_buttons": func() template.HTML {
|
||||
return template.HTML(r.cachedButtons)
|
||||
},
|
||||
"form_attributes": func() template.HTMLAttr {
|
||||
return template.HTMLAttr(fmt.Sprintf(`class="%s" action="%s" method="%s" enctype="%s"`,
|
||||
r.formConfig.Class, templateData.FormAction, r.formConfig.Method, r.formConfig.Enctype))
|
||||
},
|
||||
})
|
||||
|
||||
if err := updatedTemplate.Execute(&renderedHTML, nil); err != nil {
|
||||
return "", fmt.Errorf("failed to execute compiled template: %w", err)
|
||||
}
|
||||
|
||||
buttonsHTML := r.renderButtons()
|
||||
return renderedHTML.String(), nil
|
||||
}
|
||||
|
||||
// Fallback to original method if compilation failed
|
||||
return r.renderFieldsFallback(data)
|
||||
}
|
||||
|
||||
// renderFieldsFallback provides fallback rendering when template compilation fails
|
||||
func (r *JSONSchemaRenderer) renderFieldsFallback(data map[string]any) (string, error) {
|
||||
var groupHTML strings.Builder
|
||||
for _, group := range r.cachedGroups {
|
||||
groupHTML.WriteString(renderGroupOptimized(group))
|
||||
}
|
||||
|
||||
formAction := r.interpolateTemplate(r.formConfig.Action, data)
|
||||
|
||||
// Create a new template with the layout and functions
|
||||
tmpl, err := template.New("layout").Funcs(template.FuncMap{
|
||||
@@ -256,11 +375,11 @@ func (r *JSONSchemaRenderer) RenderFields(data map[string]any) (string, error) {
|
||||
return template.HTML(groupHTML.String())
|
||||
},
|
||||
"form_buttons": func() template.HTML {
|
||||
return template.HTML(buttonsHTML)
|
||||
return template.HTML(r.cachedButtons)
|
||||
},
|
||||
"form_attributes": func() template.HTMLAttr {
|
||||
return template.HTMLAttr(fmt.Sprintf(`class="%s" action="%s" method="%s" enctype="%s"`,
|
||||
formClass, formAction, formMethod, formEnctype))
|
||||
r.formConfig.Class, formAction, r.formConfig.Method, r.formConfig.Enctype))
|
||||
},
|
||||
}).Parse(r.HTMLLayout)
|
||||
if err != nil {
|
||||
@@ -272,8 +391,7 @@ func (r *JSONSchemaRenderer) RenderFields(data map[string]any) (string, error) {
|
||||
return "", fmt.Errorf("failed to execute HTML template: %w", err)
|
||||
}
|
||||
|
||||
r.cachedHTML = renderedHTML.String()
|
||||
return r.cachedHTML, nil
|
||||
return renderedHTML.String(), nil
|
||||
}
|
||||
|
||||
// parseGroupsFromSchema extracts and sorts groups and fields from schema
|
||||
@@ -386,6 +504,7 @@ func (r *JSONSchemaRenderer) extractFieldsFromPath(fieldPath, parentPath string)
|
||||
Order: order,
|
||||
Schema: schema,
|
||||
IsRequired: isRequired,
|
||||
Validation: extractValidationInfo(schema, isRequired),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -423,6 +542,7 @@ func (r *JSONSchemaRenderer) extractFieldsFromNestedSchema(propName, parentPath
|
||||
Order: order,
|
||||
Schema: propSchema,
|
||||
IsRequired: isRequired,
|
||||
Validation: extractValidationInfo(propSchema, isRequired),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -481,18 +601,58 @@ func renderGroup(group GroupInfo) string {
|
||||
return groupHTML.String()
|
||||
}
|
||||
|
||||
// renderGroupOptimized uses pre-compiled templates and string builder for better performance
|
||||
func renderGroupOptimized(group GroupInfo) string {
|
||||
// Collect all validations for dependency checking
|
||||
allValidations := make(map[string]ValidationInfo)
|
||||
for _, field := range group.Fields {
|
||||
allValidations[field.FieldPath] = field.Validation
|
||||
}
|
||||
|
||||
// Use string builder for better performance
|
||||
var fieldsHTML strings.Builder
|
||||
fieldsHTML.Grow(512) // Pre-allocate reasonable capacity
|
||||
|
||||
for _, field := range group.Fields {
|
||||
fieldsHTML.WriteString(renderFieldWithContext(field, allValidations))
|
||||
}
|
||||
|
||||
// Use pre-compiled group template
|
||||
templateCacheMutex.RLock()
|
||||
groupTemplate := compiledGroupTemplate
|
||||
templateCacheMutex.RUnlock()
|
||||
|
||||
if groupTemplate != nil {
|
||||
var groupHTML bytes.Buffer
|
||||
data := map[string]any{
|
||||
"Title": group.Title,
|
||||
"GroupClass": group.GroupClass,
|
||||
"FieldsHTML": template.HTML(fieldsHTML.String()),
|
||||
}
|
||||
|
||||
if err := groupTemplate.Execute(&groupHTML, data); err == nil {
|
||||
return groupHTML.String()
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to original method
|
||||
return renderGroup(group)
|
||||
}
|
||||
|
||||
func renderField(field FieldInfo) string {
|
||||
if field.Schema.UI == nil {
|
||||
return renderFieldWithContext(field, make(map[string]ValidationInfo))
|
||||
}
|
||||
|
||||
// renderFieldWithContext renders a field with access to all field validations for dependency checking
|
||||
func renderFieldWithContext(field FieldInfo, allValidations map[string]ValidationInfo) string {
|
||||
// Determine the appropriate element type based on schema
|
||||
element := determineFieldElement(field.Schema)
|
||||
if element == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
element, ok := field.Schema.UI["element"].(string)
|
||||
if !ok || element == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Build all attributes
|
||||
allAttributes := buildAllAttributes(field)
|
||||
// Build all attributes including validation
|
||||
allAttributes := buildAllAttributesWithValidation(field)
|
||||
|
||||
// Get content
|
||||
content := getFieldContent(field)
|
||||
@@ -501,14 +661,20 @@ func renderField(field FieldInfo) string {
|
||||
// Generate label if needed
|
||||
labelHTML := generateLabel(field)
|
||||
|
||||
// Generate options for select/radio elements
|
||||
optionsHTML := generateOptionsFromSchema(field.Schema)
|
||||
|
||||
// Generate client-side validation with context
|
||||
validationJS := generateClientSideValidation(field.FieldPath, field.Validation, allValidations)
|
||||
|
||||
data := map[string]any{
|
||||
"Element": element,
|
||||
"AllAttributes": template.HTMLAttr(allAttributes),
|
||||
"Content": content,
|
||||
"ContentHTML": template.HTML(contentHTML),
|
||||
"ContentHTML": template.HTML(contentHTML + validationJS),
|
||||
"LabelHTML": template.HTML(labelHTML),
|
||||
"Class": getUIValue(field.Schema.UI, "class"),
|
||||
"OptionsHTML": template.HTML(generateOptions(field.Schema.UI)),
|
||||
"Class": getFieldWrapperClass(field.Schema),
|
||||
"OptionsHTML": template.HTML(optionsHTML),
|
||||
}
|
||||
|
||||
// Use specific template if available, otherwise use generic
|
||||
@@ -529,6 +695,212 @@ func renderField(field FieldInfo) string {
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// determineFieldElement determines the appropriate HTML element based on JSON Schema
|
||||
func determineFieldElement(schema *jsonschema.Schema) string {
|
||||
// Check UI element first
|
||||
if schema.UI != nil {
|
||||
if element, ok := schema.UI["element"].(string); ok {
|
||||
return element
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-determine based on schema properties
|
||||
if shouldUseSelect(schema) {
|
||||
return "select"
|
||||
}
|
||||
|
||||
if shouldUseTextarea(schema) {
|
||||
return "textarea"
|
||||
}
|
||||
|
||||
// Default to input for most cases
|
||||
var typeStr string
|
||||
if len(schema.Type) > 0 {
|
||||
typeStr = schema.Type[0]
|
||||
}
|
||||
|
||||
switch typeStr {
|
||||
case "boolean":
|
||||
return "input" // will be type="checkbox"
|
||||
case "array":
|
||||
return "input" // could be enhanced for array inputs
|
||||
case "object":
|
||||
return "fieldset" // for nested objects
|
||||
default:
|
||||
return "input"
|
||||
}
|
||||
}
|
||||
|
||||
// buildAllAttributesWithValidation creates all HTML attributes including validation
|
||||
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
|
||||
if fieldName == "" {
|
||||
fieldName = field.Name
|
||||
}
|
||||
|
||||
// Add name attribute
|
||||
builder.WriteString(`name="`)
|
||||
builder.WriteString(fieldName)
|
||||
builder.WriteString(`"`)
|
||||
|
||||
// Add id attribute for accessibility
|
||||
fieldId := strings.ReplaceAll(fieldName, ".", "_")
|
||||
builder.WriteString(` id="`)
|
||||
builder.WriteString(fieldId)
|
||||
builder.WriteString(`"`)
|
||||
|
||||
// Determine and add type attribute for input elements
|
||||
element := determineFieldElement(field.Schema)
|
||||
if element == "input" {
|
||||
inputType := getInputTypeFromSchema(field.Schema)
|
||||
builder.WriteString(` type="`)
|
||||
builder.WriteString(inputType)
|
||||
builder.WriteString(`"`)
|
||||
}
|
||||
|
||||
// Add validation attributes
|
||||
validationAttrs := generateValidationAttributes(field.Validation)
|
||||
for _, attr := range validationAttrs {
|
||||
builder.WriteString(` `)
|
||||
builder.WriteString(attr)
|
||||
}
|
||||
|
||||
// Add default value
|
||||
if defaultValue := getDefaultValue(field.Schema); defaultValue != "" {
|
||||
builder.WriteString(` value="`)
|
||||
builder.WriteString(defaultValue)
|
||||
builder.WriteString(`"`)
|
||||
}
|
||||
|
||||
// Add placeholder
|
||||
if placeholder := getPlaceholder(field.Schema); placeholder != "" {
|
||||
builder.WriteString(` placeholder="`)
|
||||
builder.WriteString(placeholder)
|
||||
builder.WriteString(`"`)
|
||||
}
|
||||
|
||||
// Add standard attributes from UI
|
||||
if field.Schema.UI != nil {
|
||||
for _, attr := range standardAttrs {
|
||||
if attr == "name" || attr == "id" || attr == "type" || attr == "required" ||
|
||||
attr == "pattern" || attr == "min" || attr == "max" || attr == "minlength" ||
|
||||
attr == "maxlength" || attr == "step" || attr == "value" || attr == "placeholder" {
|
||||
continue // Already handled above or in validation
|
||||
}
|
||||
if value, exists := field.Schema.UI[attr]; exists {
|
||||
if attr == "class" && value == "" {
|
||||
continue // Skip empty class
|
||||
}
|
||||
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(`"`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
// generateOptionsFromSchema generates option HTML from schema enum or UI options
|
||||
func generateOptionsFromSchema(schema *jsonschema.Schema) string {
|
||||
var optionsHTML strings.Builder
|
||||
|
||||
// Check UI options first
|
||||
if schema.UI != nil {
|
||||
if options, ok := schema.UI["options"].([]interface{}); ok {
|
||||
for _, option := range options {
|
||||
if optionMap, ok := option.(map[string]interface{}); ok {
|
||||
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(`<option value="%s"%s%s>%s</option>`,
|
||||
value, selected, disabled, text))
|
||||
} else {
|
||||
optionsHTML.WriteString(fmt.Sprintf(`<option value="%v">%v</option>`, option, option))
|
||||
}
|
||||
}
|
||||
return optionsHTML.String()
|
||||
}
|
||||
}
|
||||
|
||||
// Generate options from enum
|
||||
if len(schema.Enum) > 0 {
|
||||
for _, enumValue := range schema.Enum {
|
||||
optionsHTML.WriteString(fmt.Sprintf(`<option value="%v">%v</option>`, enumValue, enumValue))
|
||||
}
|
||||
}
|
||||
|
||||
return optionsHTML.String()
|
||||
}
|
||||
|
||||
// getFieldWrapperClass determines the CSS class for the field wrapper
|
||||
func getFieldWrapperClass(schema *jsonschema.Schema) string {
|
||||
if schema.UI != nil {
|
||||
if class, ok := schema.UI["class"].(string); ok {
|
||||
return class
|
||||
}
|
||||
}
|
||||
return "form-group" // default class
|
||||
}
|
||||
|
||||
// getDefaultValue extracts default value from schema
|
||||
func getDefaultValue(schema *jsonschema.Schema) string {
|
||||
if schema.Default != nil {
|
||||
return fmt.Sprintf("%v", schema.Default)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// getPlaceholder extracts placeholder from schema
|
||||
func getPlaceholder(schema *jsonschema.Schema) string {
|
||||
if schema.UI != nil {
|
||||
if placeholder, ok := schema.UI["placeholder"].(string); ok {
|
||||
return placeholder
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-generate placeholder from title or description
|
||||
if schema.Title != nil {
|
||||
return fmt.Sprintf("Enter %s", strings.ToLower(*schema.Title))
|
||||
}
|
||||
|
||||
if schema.Description != nil {
|
||||
return *schema.Description
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -593,6 +965,88 @@ func buildAllAttributes(field FieldInfo) string {
|
||||
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 {
|
||||
@@ -835,14 +1289,42 @@ type RequestSchemaTemplate struct {
|
||||
Renderer *JSONSchemaRenderer `json:"template"`
|
||||
}
|
||||
|
||||
var cache = make(map[string]*RequestSchemaTemplate)
|
||||
var mu = &sync.Mutex{}
|
||||
var BaseTemplateDir = "templates"
|
||||
var (
|
||||
cache = make(map[string]*RequestSchemaTemplate)
|
||||
mu = &sync.RWMutex{}
|
||||
BaseTemplateDir = "templates"
|
||||
)
|
||||
|
||||
func Get(schemaPath, template string) (*JSONSchemaRenderer, error) {
|
||||
// ClearCache clears the template cache
|
||||
func ClearCache() {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
cache = make(map[string]*RequestSchemaTemplate)
|
||||
}
|
||||
|
||||
// GetCacheSize returns the current cache size
|
||||
func GetCacheSize() int {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
return len(cache)
|
||||
}
|
||||
|
||||
func Get(schemaPath, template string) (*JSONSchemaRenderer, error) {
|
||||
path := fmt.Sprintf("%s:%s", schemaPath, template)
|
||||
|
||||
// Try to get from cache with read lock first
|
||||
mu.RLock()
|
||||
if cached, exists := cache[path]; exists {
|
||||
mu.RUnlock()
|
||||
return cached.Renderer, nil
|
||||
}
|
||||
mu.RUnlock()
|
||||
|
||||
// If not in cache, acquire write lock and create
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
// Double-check after acquiring write lock
|
||||
if cached, exists := cache[path]; exists {
|
||||
return cached.Renderer, nil
|
||||
}
|
||||
@@ -873,3 +1355,10 @@ func Get(schemaPath, template string) (*JSONSchemaRenderer, error) {
|
||||
|
||||
return cachedTemplate.Renderer, nil
|
||||
}
|
||||
|
||||
// ClearRenderCache clears the rendered HTML cache for a specific renderer
|
||||
func (r *JSONSchemaRenderer) ClearRenderCache() {
|
||||
r.cacheMutex.Lock()
|
||||
defer r.cacheMutex.Unlock()
|
||||
// Since we're not caching static HTML anymore, this is just for API consistency
|
||||
}
|
||||
|
663
renderer/validation.go
Normal file
663
renderer/validation.go
Normal file
@@ -0,0 +1,663 @@
|
||||
package renderer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/oarkflow/jsonschema"
|
||||
)
|
||||
|
||||
// ValidationInfo holds comprehensive validation information for a field
|
||||
type ValidationInfo struct {
|
||||
// Basic validations
|
||||
Required bool
|
||||
MinLength *float64
|
||||
MaxLength *float64
|
||||
Minimum *jsonschema.Rat
|
||||
Maximum *jsonschema.Rat
|
||||
Pattern string
|
||||
Format string
|
||||
Enum []interface{}
|
||||
MultipleOf *jsonschema.Rat
|
||||
ExclusiveMin *jsonschema.Rat
|
||||
ExclusiveMax *jsonschema.Rat
|
||||
MinItems *float64
|
||||
MaxItems *float64
|
||||
UniqueItems bool
|
||||
MinProperties *float64
|
||||
MaxProperties *float64
|
||||
Const interface{}
|
||||
|
||||
// Advanced JSON Schema 2020-12 validations
|
||||
AllOf []*jsonschema.Schema
|
||||
AnyOf []*jsonschema.Schema
|
||||
OneOf []*jsonschema.Schema
|
||||
Not *jsonschema.Schema
|
||||
If *jsonschema.Schema
|
||||
Then *jsonschema.Schema
|
||||
Else *jsonschema.Schema
|
||||
DependentSchemas map[string]*jsonschema.Schema
|
||||
DependentRequired map[string][]string
|
||||
PrefixItems []*jsonschema.Schema
|
||||
Items *jsonschema.Schema
|
||||
Contains *jsonschema.Schema
|
||||
MaxContains *float64
|
||||
MinContains *float64
|
||||
UnevaluatedItems *jsonschema.Schema
|
||||
UnevaluatedProperties *jsonschema.Schema
|
||||
PropertyNames *jsonschema.Schema
|
||||
AdditionalProperties *jsonschema.Schema
|
||||
PatternProperties *jsonschema.SchemaMap
|
||||
|
||||
// Content validations
|
||||
ContentEncoding *string
|
||||
ContentMediaType *string
|
||||
ContentSchema *jsonschema.Schema
|
||||
|
||||
// Metadata
|
||||
Title *string
|
||||
Description *string
|
||||
Default interface{}
|
||||
Examples []interface{}
|
||||
Deprecated *bool
|
||||
ReadOnly *bool
|
||||
WriteOnly *bool
|
||||
}
|
||||
|
||||
// extractValidationInfo extracts comprehensive validation information from JSON Schema
|
||||
func extractValidationInfo(schema *jsonschema.Schema, isRequired bool) ValidationInfo {
|
||||
validation := ValidationInfo{
|
||||
Required: isRequired,
|
||||
}
|
||||
|
||||
// Basic string validations
|
||||
if schema.MinLength != nil {
|
||||
validation.MinLength = schema.MinLength
|
||||
}
|
||||
if schema.MaxLength != nil {
|
||||
validation.MaxLength = schema.MaxLength
|
||||
}
|
||||
if schema.Pattern != nil {
|
||||
validation.Pattern = *schema.Pattern
|
||||
}
|
||||
if schema.Format != nil {
|
||||
validation.Format = *schema.Format
|
||||
}
|
||||
|
||||
// Numeric validations
|
||||
if schema.Minimum != nil {
|
||||
validation.Minimum = schema.Minimum
|
||||
}
|
||||
if schema.Maximum != nil {
|
||||
validation.Maximum = schema.Maximum
|
||||
}
|
||||
if schema.ExclusiveMinimum != nil {
|
||||
validation.ExclusiveMin = schema.ExclusiveMinimum
|
||||
}
|
||||
if schema.ExclusiveMaximum != nil {
|
||||
validation.ExclusiveMax = schema.ExclusiveMaximum
|
||||
}
|
||||
if schema.MultipleOf != nil {
|
||||
validation.MultipleOf = schema.MultipleOf
|
||||
}
|
||||
|
||||
// Array validations
|
||||
if schema.MinItems != nil {
|
||||
validation.MinItems = schema.MinItems
|
||||
}
|
||||
if schema.MaxItems != nil {
|
||||
validation.MaxItems = schema.MaxItems
|
||||
}
|
||||
if schema.UniqueItems != nil {
|
||||
validation.UniqueItems = *schema.UniqueItems
|
||||
}
|
||||
if schema.MaxContains != nil {
|
||||
validation.MaxContains = schema.MaxContains
|
||||
}
|
||||
if schema.MinContains != nil {
|
||||
validation.MinContains = schema.MinContains
|
||||
}
|
||||
|
||||
// Object validations
|
||||
if schema.MinProperties != nil {
|
||||
validation.MinProperties = schema.MinProperties
|
||||
}
|
||||
if schema.MaxProperties != nil {
|
||||
validation.MaxProperties = schema.MaxProperties
|
||||
}
|
||||
|
||||
// Enum and const values
|
||||
if schema.Enum != nil {
|
||||
validation.Enum = schema.Enum
|
||||
}
|
||||
if schema.Const != nil {
|
||||
validation.Const = *schema.Const
|
||||
}
|
||||
|
||||
// Advanced JSON Schema 2020-12 features
|
||||
if schema.AllOf != nil {
|
||||
validation.AllOf = schema.AllOf
|
||||
}
|
||||
if schema.AnyOf != nil {
|
||||
validation.AnyOf = schema.AnyOf
|
||||
}
|
||||
if schema.OneOf != nil {
|
||||
validation.OneOf = schema.OneOf
|
||||
}
|
||||
if schema.Not != nil {
|
||||
validation.Not = schema.Not
|
||||
}
|
||||
|
||||
// Conditional validations
|
||||
if schema.If != nil {
|
||||
validation.If = schema.If
|
||||
}
|
||||
if schema.Then != nil {
|
||||
validation.Then = schema.Then
|
||||
}
|
||||
if schema.Else != nil {
|
||||
validation.Else = schema.Else
|
||||
}
|
||||
|
||||
// Dependent validations
|
||||
if schema.DependentSchemas != nil {
|
||||
validation.DependentSchemas = schema.DependentSchemas
|
||||
}
|
||||
if schema.DependentRequired != nil {
|
||||
validation.DependentRequired = schema.DependentRequired
|
||||
}
|
||||
|
||||
// Array item validations
|
||||
if schema.PrefixItems != nil {
|
||||
validation.PrefixItems = schema.PrefixItems
|
||||
}
|
||||
if schema.Items != nil {
|
||||
validation.Items = schema.Items
|
||||
}
|
||||
if schema.Contains != nil {
|
||||
validation.Contains = schema.Contains
|
||||
}
|
||||
if schema.UnevaluatedItems != nil {
|
||||
validation.UnevaluatedItems = schema.UnevaluatedItems
|
||||
}
|
||||
|
||||
// Property validations
|
||||
if schema.PropertyNames != nil {
|
||||
validation.PropertyNames = schema.PropertyNames
|
||||
}
|
||||
if schema.AdditionalProperties != nil {
|
||||
validation.AdditionalProperties = schema.AdditionalProperties
|
||||
}
|
||||
if schema.PatternProperties != nil {
|
||||
validation.PatternProperties = schema.PatternProperties
|
||||
}
|
||||
if schema.UnevaluatedProperties != nil {
|
||||
validation.UnevaluatedProperties = schema.UnevaluatedProperties
|
||||
}
|
||||
|
||||
// Content validations
|
||||
if schema.ContentEncoding != nil {
|
||||
validation.ContentEncoding = schema.ContentEncoding
|
||||
}
|
||||
if schema.ContentMediaType != nil {
|
||||
validation.ContentMediaType = schema.ContentMediaType
|
||||
}
|
||||
if schema.ContentSchema != nil {
|
||||
validation.ContentSchema = schema.ContentSchema
|
||||
}
|
||||
|
||||
// Metadata
|
||||
if schema.Title != nil {
|
||||
validation.Title = schema.Title
|
||||
}
|
||||
if schema.Description != nil {
|
||||
validation.Description = schema.Description
|
||||
}
|
||||
if schema.Default != nil {
|
||||
validation.Default = schema.Default
|
||||
}
|
||||
if schema.Examples != nil {
|
||||
validation.Examples = schema.Examples
|
||||
}
|
||||
if schema.Deprecated != nil {
|
||||
validation.Deprecated = schema.Deprecated
|
||||
}
|
||||
if schema.ReadOnly != nil {
|
||||
validation.ReadOnly = schema.ReadOnly
|
||||
}
|
||||
if schema.WriteOnly != nil {
|
||||
validation.WriteOnly = schema.WriteOnly
|
||||
}
|
||||
|
||||
return validation
|
||||
}
|
||||
|
||||
// generateValidationAttributes creates comprehensive HTML validation attributes
|
||||
func generateValidationAttributes(validation ValidationInfo) []string {
|
||||
var attrs []string
|
||||
|
||||
if validation.Required {
|
||||
attrs = append(attrs, `required="required"`)
|
||||
}
|
||||
|
||||
// String validations
|
||||
if validation.MinLength != nil {
|
||||
attrs = append(attrs, fmt.Sprintf(`minlength="%.0f"`, *validation.MinLength))
|
||||
}
|
||||
if validation.MaxLength != nil {
|
||||
attrs = append(attrs, fmt.Sprintf(`maxlength="%.0f"`, *validation.MaxLength))
|
||||
}
|
||||
if validation.Pattern != "" {
|
||||
attrs = append(attrs, fmt.Sprintf(`pattern="%s"`, validation.Pattern))
|
||||
}
|
||||
|
||||
// Numeric validations
|
||||
if validation.Minimum != nil {
|
||||
minVal, _ := validation.Minimum.Float64()
|
||||
attrs = append(attrs, fmt.Sprintf(`min="%g"`, minVal))
|
||||
}
|
||||
if validation.Maximum != nil {
|
||||
maxVal, _ := validation.Maximum.Float64()
|
||||
attrs = append(attrs, fmt.Sprintf(`max="%g"`, maxVal))
|
||||
}
|
||||
if validation.ExclusiveMin != nil {
|
||||
exclusiveMinVal, _ := validation.ExclusiveMin.Float64()
|
||||
attrs = append(attrs, fmt.Sprintf(`data-exclusive-min="%g"`, exclusiveMinVal))
|
||||
}
|
||||
if validation.ExclusiveMax != nil {
|
||||
exclusiveMaxVal, _ := validation.ExclusiveMax.Float64()
|
||||
attrs = append(attrs, fmt.Sprintf(`data-exclusive-max="%g"`, exclusiveMaxVal))
|
||||
}
|
||||
if validation.MultipleOf != nil {
|
||||
stepVal, _ := validation.MultipleOf.Float64()
|
||||
attrs = append(attrs, fmt.Sprintf(`step="%g"`, stepVal))
|
||||
}
|
||||
|
||||
// Array validations (for multi-select and array inputs)
|
||||
if validation.MinItems != nil {
|
||||
attrs = append(attrs, fmt.Sprintf(`data-min-items="%.0f"`, *validation.MinItems))
|
||||
}
|
||||
if validation.MaxItems != nil {
|
||||
attrs = append(attrs, fmt.Sprintf(`data-max-items="%.0f"`, *validation.MaxItems))
|
||||
}
|
||||
if validation.UniqueItems {
|
||||
attrs = append(attrs, `data-unique-items="true"`)
|
||||
}
|
||||
|
||||
// Metadata attributes
|
||||
if validation.Title != nil {
|
||||
attrs = append(attrs, fmt.Sprintf(`title="%s"`, *validation.Title))
|
||||
}
|
||||
if validation.Description != nil {
|
||||
attrs = append(attrs, fmt.Sprintf(`data-description="%s"`, *validation.Description))
|
||||
}
|
||||
if validation.Default != nil {
|
||||
attrs = append(attrs, fmt.Sprintf(`data-default="%v"`, validation.Default))
|
||||
}
|
||||
if validation.ReadOnly != nil && *validation.ReadOnly {
|
||||
attrs = append(attrs, `readonly="readonly"`)
|
||||
}
|
||||
if validation.Deprecated != nil && *validation.Deprecated {
|
||||
attrs = append(attrs, `data-deprecated="true"`)
|
||||
}
|
||||
|
||||
// Complex validation indicators (for client-side handling)
|
||||
if len(validation.AllOf) > 0 {
|
||||
attrs = append(attrs, `data-has-allof="true"`)
|
||||
}
|
||||
if len(validation.AnyOf) > 0 {
|
||||
attrs = append(attrs, `data-has-anyof="true"`)
|
||||
}
|
||||
if len(validation.OneOf) > 0 {
|
||||
attrs = append(attrs, `data-has-oneof="true"`)
|
||||
}
|
||||
if validation.Not != nil {
|
||||
attrs = append(attrs, `data-has-not="true"`)
|
||||
}
|
||||
if validation.If != nil {
|
||||
attrs = append(attrs, `data-has-conditional="true"`)
|
||||
}
|
||||
if len(validation.DependentRequired) > 0 {
|
||||
attrs = append(attrs, `data-has-dependent-required="true"`)
|
||||
}
|
||||
|
||||
return attrs
|
||||
}
|
||||
|
||||
// generateClientSideValidation creates comprehensive JavaScript validation functions
|
||||
func generateClientSideValidation(fieldPath string, validation ValidationInfo, allFields map[string]ValidationInfo) string {
|
||||
if !hasValidation(validation) {
|
||||
return ""
|
||||
}
|
||||
|
||||
var validations []string
|
||||
var dependentValidations []string
|
||||
|
||||
// Basic validations
|
||||
if validation.Required {
|
||||
validations = append(validations, `
|
||||
if (!value || (typeof value === 'string' && value.trim() === '')) {
|
||||
return 'This field is required';
|
||||
}`)
|
||||
}
|
||||
|
||||
// String validations
|
||||
if validation.MinLength != nil {
|
||||
validations = append(validations, fmt.Sprintf(`
|
||||
if (value && value.length < %.0f) {
|
||||
return 'Minimum length is %.0f characters';
|
||||
}`, *validation.MinLength, *validation.MinLength))
|
||||
}
|
||||
if validation.MaxLength != nil {
|
||||
validations = append(validations, fmt.Sprintf(`
|
||||
if (value && value.length > %.0f) {
|
||||
return 'Maximum length is %.0f characters';
|
||||
}`, *validation.MaxLength, *validation.MaxLength))
|
||||
}
|
||||
if validation.Pattern != "" {
|
||||
validations = append(validations, fmt.Sprintf(`
|
||||
if (value && !/%s/.test(value)) {
|
||||
return 'Invalid format';
|
||||
}`, strings.ReplaceAll(validation.Pattern, `\`, `\\`)))
|
||||
}
|
||||
|
||||
// Numeric validations
|
||||
if validation.Minimum != nil {
|
||||
minVal, _ := validation.Minimum.Float64()
|
||||
validations = append(validations, fmt.Sprintf(`
|
||||
if (value !== '' && parseFloat(value) < %g) {
|
||||
return 'Minimum value is %g';
|
||||
}`, minVal, minVal))
|
||||
}
|
||||
if validation.Maximum != nil {
|
||||
maxVal, _ := validation.Maximum.Float64()
|
||||
validations = append(validations, fmt.Sprintf(`
|
||||
if (value !== '' && parseFloat(value) > %g) {
|
||||
return 'Maximum value is %g';
|
||||
}`, maxVal, maxVal))
|
||||
}
|
||||
if validation.ExclusiveMin != nil {
|
||||
exclusiveMinVal, _ := validation.ExclusiveMin.Float64()
|
||||
validations = append(validations, fmt.Sprintf(`
|
||||
if (value !== '' && parseFloat(value) <= %g) {
|
||||
return 'Value must be greater than %g';
|
||||
}`, exclusiveMinVal, exclusiveMinVal))
|
||||
}
|
||||
if validation.ExclusiveMax != nil {
|
||||
exclusiveMaxVal, _ := validation.ExclusiveMax.Float64()
|
||||
validations = append(validations, fmt.Sprintf(`
|
||||
if (value !== '' && parseFloat(value) >= %g) {
|
||||
return 'Value must be less than %g';
|
||||
}`, exclusiveMaxVal, exclusiveMaxVal))
|
||||
}
|
||||
|
||||
// Enum validations
|
||||
if len(validation.Enum) > 0 {
|
||||
enumValues := make([]string, len(validation.Enum))
|
||||
for i, v := range validation.Enum {
|
||||
enumValues[i] = fmt.Sprintf("'%v'", v)
|
||||
}
|
||||
validations = append(validations, fmt.Sprintf(`
|
||||
var allowedValues = [%s];
|
||||
if (value && allowedValues.indexOf(value) === -1) {
|
||||
return "Value must be one of: %s";
|
||||
}`, strings.Join(enumValues, ", "), strings.Join(enumValues, ", ")))
|
||||
}
|
||||
|
||||
// Const validation
|
||||
if validation.Const != nil {
|
||||
validations = append(validations, fmt.Sprintf(`
|
||||
if (value !== '%v') {
|
||||
return 'Value must be %v';
|
||||
}`, validation.Const, validation.Const))
|
||||
}
|
||||
|
||||
// Array validations
|
||||
if validation.MinItems != nil {
|
||||
validations = append(validations, fmt.Sprintf(`
|
||||
if (Array.isArray(value) && value.length < %.0f) {
|
||||
return 'Minimum %.0f items required';
|
||||
}`, *validation.MinItems, *validation.MinItems))
|
||||
}
|
||||
if validation.MaxItems != nil {
|
||||
validations = append(validations, fmt.Sprintf(`
|
||||
if (Array.isArray(value) && value.length > %.0f) {
|
||||
return 'Maximum %.0f items allowed';
|
||||
}`, *validation.MaxItems, *validation.MaxItems))
|
||||
}
|
||||
if validation.UniqueItems {
|
||||
validations = append(validations, `
|
||||
if (Array.isArray(value)) {
|
||||
var unique = [...new Set(value)];
|
||||
if (unique.length !== value.length) {
|
||||
return 'All items must be unique';
|
||||
}
|
||||
}`)
|
||||
}
|
||||
|
||||
// Dependent required validations
|
||||
if len(validation.DependentRequired) > 0 {
|
||||
for depField, requiredFields := range validation.DependentRequired {
|
||||
for _, reqField := range requiredFields {
|
||||
dependentValidations = append(dependentValidations, fmt.Sprintf(`
|
||||
// Check if %s has value, then %s is required
|
||||
var depField = document.querySelector('[name="%s"]');
|
||||
var reqField = document.querySelector('[name="%s"]');
|
||||
if (depField && depField.value && (!reqField || !reqField.value)) {
|
||||
return 'Field %s is required when %s has a value';
|
||||
}`, depField, reqField, reqField, depField))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Complex validations warning (these need server-side validation)
|
||||
var complexValidationWarnings []string
|
||||
if len(validation.AllOf) > 0 {
|
||||
complexValidationWarnings = append(complexValidationWarnings, "// AllOf validation requires server-side validation")
|
||||
}
|
||||
if len(validation.AnyOf) > 0 {
|
||||
complexValidationWarnings = append(complexValidationWarnings, "// AnyOf validation requires server-side validation")
|
||||
}
|
||||
if len(validation.OneOf) > 0 {
|
||||
complexValidationWarnings = append(complexValidationWarnings, "// OneOf validation requires server-side validation")
|
||||
}
|
||||
if validation.Not != nil {
|
||||
complexValidationWarnings = append(complexValidationWarnings, "// Not validation requires server-side validation")
|
||||
}
|
||||
if validation.If != nil {
|
||||
complexValidationWarnings = append(complexValidationWarnings, "// Conditional validation requires server-side validation")
|
||||
}
|
||||
|
||||
if len(validations) == 0 && len(dependentValidations) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
fieldId := strings.ReplaceAll(fieldPath, ".", "_")
|
||||
allValidations := append(validations, dependentValidations...)
|
||||
if len(complexValidationWarnings) > 0 {
|
||||
allValidations = append([]string{strings.Join(complexValidationWarnings, "\n\t\t\t")}, allValidations...)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`
|
||||
<script>
|
||||
(function() {
|
||||
var field = document.querySelector('[name="%s"]');
|
||||
if (!field) return;
|
||||
|
||||
function validate%s(value) {
|
||||
%s
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateForm() {
|
||||
// Dependent validations that check other fields
|
||||
%s
|
||||
return null;
|
||||
}
|
||||
|
||||
field.addEventListener('blur', function() {
|
||||
var error = validate%s(this.value) || validateForm();
|
||||
var errorElement = document.getElementById('%s_error');
|
||||
|
||||
if (error) {
|
||||
if (!errorElement) {
|
||||
errorElement = document.createElement('div');
|
||||
errorElement.id = '%s_error';
|
||||
errorElement.className = 'validation-error';
|
||||
errorElement.style.color = 'red';
|
||||
errorElement.style.fontSize = '0.875rem';
|
||||
errorElement.style.marginTop = '0.25rem';
|
||||
this.parentNode.appendChild(errorElement);
|
||||
}
|
||||
errorElement.textContent = error;
|
||||
this.classList.add('invalid');
|
||||
this.style.borderColor = 'red';
|
||||
} else {
|
||||
if (errorElement) {
|
||||
errorElement.remove();
|
||||
}
|
||||
this.classList.remove('invalid');
|
||||
this.style.borderColor = '';
|
||||
}
|
||||
});
|
||||
|
||||
// Also validate on input for immediate feedback
|
||||
field.addEventListener('input', function() {
|
||||
if (this.classList.contains('invalid')) {
|
||||
var error = validate%s(this.value);
|
||||
var errorElement = document.getElementById('%s_error');
|
||||
if (!error && errorElement) {
|
||||
errorElement.remove();
|
||||
this.classList.remove('invalid');
|
||||
this.style.borderColor = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
`, fieldPath, fieldId, strings.Join(allValidations, "\n"),
|
||||
strings.Join(dependentValidations, "\n"), fieldId, fieldId, fieldId, fieldId, fieldId)
|
||||
}
|
||||
|
||||
// hasValidation checks if validation info contains any validation rules
|
||||
func hasValidation(validation ValidationInfo) bool {
|
||||
return validation.Required ||
|
||||
validation.MinLength != nil ||
|
||||
validation.MaxLength != nil ||
|
||||
validation.Minimum != nil ||
|
||||
validation.Maximum != nil ||
|
||||
validation.Pattern != "" ||
|
||||
validation.ExclusiveMin != nil ||
|
||||
validation.ExclusiveMax != nil ||
|
||||
validation.MultipleOf != nil ||
|
||||
len(validation.Enum) > 0 ||
|
||||
validation.Const != nil ||
|
||||
validation.MinItems != nil ||
|
||||
validation.MaxItems != nil ||
|
||||
validation.UniqueItems ||
|
||||
validation.MinProperties != nil ||
|
||||
validation.MaxProperties != nil ||
|
||||
len(validation.AllOf) > 0 ||
|
||||
len(validation.AnyOf) > 0 ||
|
||||
len(validation.OneOf) > 0 ||
|
||||
validation.Not != nil ||
|
||||
validation.If != nil ||
|
||||
validation.Then != nil ||
|
||||
validation.Else != nil ||
|
||||
len(validation.DependentSchemas) > 0 ||
|
||||
len(validation.DependentRequired) > 0 ||
|
||||
len(validation.PrefixItems) > 0 ||
|
||||
validation.Items != nil ||
|
||||
validation.Contains != nil ||
|
||||
validation.MaxContains != nil ||
|
||||
validation.MinContains != nil ||
|
||||
validation.UnevaluatedItems != nil ||
|
||||
validation.UnevaluatedProperties != nil ||
|
||||
validation.PropertyNames != nil ||
|
||||
validation.AdditionalProperties != nil ||
|
||||
validation.PatternProperties != nil ||
|
||||
validation.ContentEncoding != nil ||
|
||||
validation.ContentMediaType != nil ||
|
||||
validation.ContentSchema != nil ||
|
||||
validation.ReadOnly != nil ||
|
||||
validation.WriteOnly != nil ||
|
||||
validation.Deprecated != nil
|
||||
}
|
||||
|
||||
// getInputTypeFromSchema determines the appropriate HTML input type based on JSON Schema
|
||||
func getInputTypeFromSchema(schema *jsonschema.Schema) string {
|
||||
// Check UI type first
|
||||
if schema.UI != nil {
|
||||
if uiType, ok := schema.UI["type"].(string); ok {
|
||||
return uiType
|
||||
}
|
||||
}
|
||||
|
||||
// Check format
|
||||
if schema.Format != nil {
|
||||
switch *schema.Format {
|
||||
case "email":
|
||||
return "email"
|
||||
case "uri", "uri-reference":
|
||||
return "url"
|
||||
case "date":
|
||||
return "date"
|
||||
case "time":
|
||||
return "time"
|
||||
case "date-time":
|
||||
return "datetime-local"
|
||||
case "password":
|
||||
return "password"
|
||||
}
|
||||
}
|
||||
|
||||
// Check type
|
||||
var typeStr string
|
||||
if len(schema.Type) > 0 {
|
||||
typeStr = schema.Type[0]
|
||||
}
|
||||
|
||||
switch typeStr {
|
||||
case "string":
|
||||
return "text"
|
||||
case "number":
|
||||
return "number"
|
||||
case "integer":
|
||||
return "number"
|
||||
case "boolean":
|
||||
return "checkbox"
|
||||
default:
|
||||
return "text"
|
||||
}
|
||||
}
|
||||
|
||||
// shouldUseTextarea determines if a string field should use textarea
|
||||
func shouldUseTextarea(schema *jsonschema.Schema) bool {
|
||||
if schema.UI != nil {
|
||||
if element, ok := schema.UI["element"].(string); ok {
|
||||
return element == "textarea"
|
||||
}
|
||||
if rows, ok := schema.UI["rows"]; ok && rows != nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Use textarea for long strings
|
||||
if schema.MaxLength != nil && *schema.MaxLength > 255 {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// shouldUseSelect determines if a field should use select element
|
||||
func shouldUseSelect(schema *jsonschema.Schema) bool {
|
||||
if schema.UI != nil {
|
||||
if element, ok := schema.UI["element"].(string); ok {
|
||||
return element == "select"
|
||||
}
|
||||
}
|
||||
|
||||
// Use select for enum values
|
||||
return len(schema.Enum) > 0
|
||||
}
|
Reference in New Issue
Block a user