""" Profile schema definition and validation for MarkiTect user profiles. This module defines the JSON schema structure for user profiles and provides validation functionality to ensure profile data integrity. """ import json from datetime import datetime from typing import Dict, Any, Optional, List from dataclasses import dataclass, asdict, field from jsonschema import validate, ValidationError @dataclass class ContactInfo: """Contact information structure.""" email: Optional[str] = None phone: Optional[str] = None website: Optional[str] = None linkedin: Optional[str] = None github: Optional[str] = None twitter: Optional[str] = None @dataclass class Address: """Address structure.""" street: Optional[str] = None city: Optional[str] = None state: Optional[str] = None postal_code: Optional[str] = None country: Optional[str] = None @dataclass class Organization: """Organization/company information.""" name: Optional[str] = None position: Optional[str] = None department: Optional[str] = None website: Optional[str] = None address: Optional[Address] = field(default_factory=Address) @dataclass class ProfileData: """ Complete user profile data structure. This dataclass defines all the fields that can be stored in a user profile for template auto-filling purposes. """ # Basic personal information first_name: Optional[str] = None last_name: Optional[str] = None full_name: Optional[str] = None preferred_name: Optional[str] = None title: Optional[str] = None # Mr., Ms., Dr., etc. # Contact information contact: ContactInfo = field(default_factory=ContactInfo) # Address information address: Address = field(default_factory=Address) # Professional information organization: Organization = field(default_factory=Organization) profession: Optional[str] = None bio: Optional[str] = None # Additional fields for template filling custom_fields: Dict[str, Any] = field(default_factory=dict) # Metadata created_at: Optional[str] = None updated_at: Optional[str] = None def to_dict(self) -> Dict[str, Any]: """Convert profile data to dictionary.""" return asdict(self) @classmethod def from_dict(cls, data: Dict[str, Any]) -> 'ProfileData': """Create ProfileData from dictionary.""" # Handle nested structures if 'contact' in data and isinstance(data['contact'], dict): data['contact'] = ContactInfo(**data['contact']) if 'address' in data and isinstance(data['address'], dict): data['address'] = Address(**data['address']) if 'organization' in data and isinstance(data['organization'], dict): org_data = data['organization'].copy() if 'address' in org_data and isinstance(org_data['address'], dict): org_data['address'] = Address(**org_data['address']) data['organization'] = Organization(**org_data) return cls(**data) class ProfileSchema: """ JSON Schema validation for user profiles. Provides schema definition and validation methods to ensure profile data integrity and consistency. """ # JSON Schema definition for profile validation SCHEMA = { "$schema": "http://json-schema.org/draft-07/schema#", "title": "MarkiTect User Profile", "type": "object", "properties": { "first_name": { "type": ["string", "null"], "maxLength": 100, "description": "User's first name" }, "last_name": { "type": ["string", "null"], "maxLength": 100, "description": "User's last name" }, "full_name": { "type": ["string", "null"], "maxLength": 200, "description": "User's full name" }, "preferred_name": { "type": ["string", "null"], "maxLength": 100, "description": "User's preferred name" }, "title": { "type": ["string", "null"], "maxLength": 20, "description": "Title (Mr., Ms., Dr., etc.)" }, "contact": { "type": "object", "properties": { "email": { "type": ["string", "null"], "format": "email", "description": "Email address" }, "phone": { "type": ["string", "null"], "maxLength": 50, "description": "Phone number" }, "website": { "type": ["string", "null"], "format": "uri", "description": "Personal website URL" }, "linkedin": { "type": ["string", "null"], "description": "LinkedIn profile URL" }, "github": { "type": ["string", "null"], "description": "GitHub profile URL" }, "twitter": { "type": ["string", "null"], "description": "Twitter handle or URL" } }, "additionalProperties": False }, "address": { "type": "object", "properties": { "street": { "type": ["string", "null"], "maxLength": 200, "description": "Street address" }, "city": { "type": ["string", "null"], "maxLength": 100, "description": "City" }, "state": { "type": ["string", "null"], "maxLength": 100, "description": "State or province" }, "postal_code": { "type": ["string", "null"], "maxLength": 20, "description": "Postal or ZIP code" }, "country": { "type": ["string", "null"], "maxLength": 100, "description": "Country" } }, "additionalProperties": False }, "organization": { "type": "object", "properties": { "name": { "type": ["string", "null"], "maxLength": 200, "description": "Organization name" }, "position": { "type": ["string", "null"], "maxLength": 100, "description": "Job position/title" }, "department": { "type": ["string", "null"], "maxLength": 100, "description": "Department" }, "website": { "type": ["string", "null"], "format": "uri", "description": "Organization website" }, "address": { "$ref": "#/properties/address" } }, "additionalProperties": False }, "profession": { "type": ["string", "null"], "maxLength": 100, "description": "Professional role or occupation" }, "bio": { "type": ["string", "null"], "maxLength": 2000, "description": "Professional biography" }, "custom_fields": { "type": "object", "description": "Additional custom fields for template filling", "additionalProperties": True }, "created_at": { "type": ["string", "null"], "format": "date-time", "description": "Profile creation timestamp" }, "updated_at": { "type": ["string", "null"], "format": "date-time", "description": "Profile last update timestamp" } }, "additionalProperties": False } @classmethod def validate(cls, data: Dict[str, Any]) -> None: """ Validate profile data against the schema. Args: data: Profile data dictionary to validate Raises: ValidationError: If data doesn't match schema """ validate(instance=data, schema=cls.SCHEMA) @classmethod def is_valid(cls, data: Dict[str, Any]) -> bool: """ Check if profile data is valid. Args: data: Profile data dictionary to check Returns: True if valid, False otherwise """ try: cls.validate(data) return True except ValidationError: return False @classmethod def get_field_description(cls, field_path: str) -> Optional[str]: """ Get description for a specific field. Args: field_path: Dot-separated field path (e.g., 'contact.email') Returns: Field description or None if not found """ parts = field_path.split('.') current = cls.SCHEMA['properties'] try: for part in parts: if part in current: current = current[part] if 'properties' in current: current = current['properties'] else: return current.get('description') return current.get('description') except (KeyError, TypeError): return None @classmethod def get_all_fields(cls) -> List[str]: """ Get list of all available field paths. Returns: List of dot-separated field paths """ fields = [] def extract_fields(schema_dict: Dict[str, Any], prefix: str = '') -> None: if 'properties' not in schema_dict: return for field_name, field_def in schema_dict['properties'].items(): field_path = f"{prefix}.{field_name}" if prefix else field_name fields.append(field_path) if field_def.get('type') == 'object' and 'properties' in field_def: extract_fields(field_def, field_path) extract_fields(cls.SCHEMA) return sorted(fields) @classmethod def create_empty_profile(self) -> ProfileData: """ Create an empty profile with default structure. Returns: Empty ProfileData instance """ now = datetime.now().isoformat() return ProfileData( created_at=now, updated_at=now )