from datetime import datetime import re from marshmallow import Schema, fields, ValidationError from typing import Dict, Any, Optional, Type, Union from config import item_attributes from definitions.attributes import * def create_text_field(attrib: textAttribute) -> fields.String: """Create a Marshmallow String field for textAttribute.""" def validate_allowed_chars(value: str, attrib=attrib) -> None: if attrib.allowed_chars and not all(char in attrib.allowed_chars for char in value.strip()): raise ValidationError(f"Invalid characters in {attrib.display_name}. Allowed characters are: {attrib.allowed_chars}") return fields.String( required=attrib.required, validate=[ lambda x, attrib=attrib: len(x.strip()) <= attrib.max_length if attrib.max_length else True, lambda x, attrib=attrib: len(x.strip()) >= attrib.min_length if attrib.min_length else True, lambda x, attrib=attrib: re.match(attrib.regex, x) if attrib.regex else True, validate_allowed_chars, ], error_messages={ "required": f"{attrib.display_name} is required.", "validator_failed": f"Invalid value for {attrib.display_name}.", }, ) def create_numeric_field(attrib: Union[intAttribute, floatAttribute]) -> Union[fields.Integer, fields.Float]: """Create a Marshmallow Integer or Float field for intAttribute or floatAttribute.""" field_type = fields.Integer if isinstance(attrib, intAttribute) else fields.Float return field_type( required=attrib.required, validate=[ lambda x, attrib=attrib: x >= attrib.min_val if attrib.min_val is not None else True, lambda x, attrib=attrib: x <= attrib.max_val if attrib.max_val is not None else True, ], error_messages={ "required": f"{attrib.display_name} is required.", "validator_failed": f"Invalid value for {attrib.display_name}.", }, ) def create_date_field(attrib: dateAttribute) -> fields.Date: """Create a Marshmallow Date field for dateAttribute.""" return fields.Date( required=attrib.required, format="%Y-%m-%d", validate=[ lambda x, attrib=attrib: x >= datetime.strptime(attrib.min_val, "%Y-%m-%d").date() if attrib.min_val else True, lambda x, attrib=attrib: x <= datetime.strptime(attrib.max_val, "%Y-%m-%d").date() if attrib.max_val else True, ], error_messages={ "required": f"{attrib.display_name} is required.", "validator_failed": f"Invalid value for {attrib.display_name}.", }, ) def create_select_field(attrib: selectAttribute) -> fields.String: """Create a Marshmallow String field for selectAttribute.""" return fields.String( required=attrib.required, validate=[lambda x, attrib=attrib: x in attrib.options], error_messages={ "required": f"{attrib.display_name} is required.", "validator_failed": f"Invalid value for {attrib.display_name}. Must be one of: {', '.join(attrib.options)}.", }, ) def create_dynamic_schema() -> Type[Schema]: """ Dynamically creates a Marshmallow Schema based on the configuration in item_attributes. """ schema_fields = {} # Dictionary to store dynamically created fields for attrib in item_attributes: if isinstance(attrib, textAttribute): schema_fields[attrib.attrib_name] = create_text_field(attrib) elif isinstance(attrib, (intAttribute, floatAttribute)): schema_fields[attrib.attrib_name] = create_numeric_field(attrib) elif isinstance(attrib, dateAttribute): schema_fields[attrib.attrib_name] = create_date_field(attrib) elif isinstance(attrib, selectAttribute): schema_fields[attrib.attrib_name] = create_select_field(attrib) # Dynamically create the schema class DynamicSchema = type("DynamicSchema", (Schema,), schema_fields) return DynamicSchema def validate_values(form_data: Dict[str, Any]) -> Optional[str]: """ Validate form data against the configuration in item_attributes using Marshmallow. Returns an error message if invalid, otherwise None. """ DynamicSchema = create_dynamic_schema() schema = DynamicSchema() try: schema.load(form_data) # Validate the data return None # No errors except ValidationError as e: # Format the error message for display error_messages = [] for field, errors in e.messages.items(): for error in errors: error_messages.append(f"{field}: {error}") return "; ".join(error_messages)