Files
mq/renderer/validation.go
2025-09-18 18:26:35 +05:45

664 lines
19 KiB
Go

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 []any
MultipleOf *jsonschema.Rat
ExclusiveMin *jsonschema.Rat
ExclusiveMax *jsonschema.Rat
MinItems *float64
MaxItems *float64
UniqueItems bool
MinProperties *float64
MaxProperties *float64
Const any
// 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 any
Examples []any
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, 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
}