Files
lmiranda 9e4b3d5a91 feat(viz-platform): implement DMC 2.x component registry (#171)
- Add component_registry.py with version-locked registry loading
- Create dmc_2_5.json with 39 components for DMC 2.5.1/Mantine 7
- Add generate-dmc-registry.py script for future registry generation
- Update dependencies to DMC >=2.0.0 (installs 2.5.1)
- Includes prop validation with typo detection

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 11:41:42 -05:00

302 lines
9.0 KiB
Python

"""
DMC Component Registry for viz-platform.
Provides version-locked component definitions to prevent Claude from
hallucinating invalid props. Uses static JSON registries pre-generated
from DMC source.
"""
import json
import logging
from pathlib import Path
from typing import Dict, List, Optional, Any
logger = logging.getLogger(__name__)
class ComponentRegistry:
"""
Version-locked registry of Dash Mantine Components.
Loads component definitions from static JSON files and provides
lookup methods for validation tools.
"""
def __init__(self, dmc_version: Optional[str] = None):
"""
Initialize the component registry.
Args:
dmc_version: Installed DMC version (e.g., "0.14.7").
If None, will try to detect or use fallback.
"""
self.dmc_version = dmc_version
self.registry_dir = Path(__file__).parent.parent / 'registry'
self.components: Dict[str, Dict[str, Any]] = {}
self.categories: Dict[str, List[str]] = {}
self.loaded_version: Optional[str] = None
def load(self) -> bool:
"""
Load the component registry for the configured DMC version.
Returns:
True if registry loaded successfully, False otherwise
"""
registry_file = self._find_registry_file()
if not registry_file:
logger.warning(
f"No registry found for DMC {self.dmc_version}. "
"Component validation will be limited."
)
return False
try:
with open(registry_file, 'r') as f:
data = json.load(f)
self.loaded_version = data.get('version')
self.components = data.get('components', {})
self.categories = data.get('categories', {})
logger.info(
f"Loaded component registry v{self.loaded_version} "
f"with {len(self.components)} components"
)
return True
except Exception as e:
logger.error(f"Failed to load registry: {e}")
return False
def _find_registry_file(self) -> Optional[Path]:
"""
Find the best matching registry file for the DMC version.
Strategy:
1. Exact major.minor match (e.g., dmc_0_14.json for 0.14.7)
2. Fallback to latest available registry
Returns:
Path to registry file, or None if not found
"""
if not self.registry_dir.exists():
logger.warning(f"Registry directory not found: {self.registry_dir}")
return None
# Try exact major.minor match
if self.dmc_version:
parts = self.dmc_version.split('.')
if len(parts) >= 2:
major_minor = f"{parts[0]}_{parts[1]}"
exact_match = self.registry_dir / f"dmc_{major_minor}.json"
if exact_match.exists():
return exact_match
# Fallback: find latest registry
registry_files = list(self.registry_dir.glob("dmc_*.json"))
if registry_files:
# Sort by version and return latest
registry_files.sort(reverse=True)
fallback = registry_files[0]
if self.dmc_version:
logger.warning(
f"No exact match for DMC {self.dmc_version}, "
f"using fallback: {fallback.name}"
)
return fallback
return None
def get_component(self, name: str) -> Optional[Dict[str, Any]]:
"""
Get component definition by name.
Args:
name: Component name (e.g., "Button", "TextInput")
Returns:
Component definition dict, or None if not found
"""
return self.components.get(name)
def get_component_props(self, name: str) -> Optional[Dict[str, Any]]:
"""
Get props schema for a component.
Args:
name: Component name
Returns:
Props dict with type info, or None if component not found
"""
component = self.get_component(name)
if component:
return component.get('props', {})
return None
def list_components(self, category: Optional[str] = None) -> Dict[str, List[str]]:
"""
List available components, optionally filtered by category.
Args:
category: Optional category filter (e.g., "inputs", "buttons")
Returns:
Dict of category -> component names
"""
if category:
if category in self.categories:
return {category: self.categories[category]}
return {}
return self.categories
def get_categories(self) -> List[str]:
"""
Get list of available component categories.
Returns:
List of category names
"""
return list(self.categories.keys())
def validate_prop(
self,
component: str,
prop_name: str,
prop_value: Any
) -> Dict[str, Any]:
"""
Validate a single prop value against the registry.
Args:
component: Component name
prop_name: Prop name
prop_value: Value to validate
Returns:
Dict with valid: bool, error: Optional[str]
"""
props = self.get_component_props(component)
if props is None:
return {
'valid': False,
'error': f"Unknown component: {component}"
}
if prop_name not in props:
# Check for similar prop names (typo detection)
similar = self._find_similar_props(prop_name, props.keys())
if similar:
return {
'valid': False,
'error': f"Unknown prop '{prop_name}' for {component}. Did you mean '{similar}'?"
}
return {
'valid': False,
'error': f"Unknown prop '{prop_name}' for {component}"
}
prop_schema = props[prop_name]
return self._validate_value(prop_value, prop_schema, prop_name)
def _validate_value(
self,
value: Any,
schema: Dict[str, Any],
prop_name: str
) -> Dict[str, Any]:
"""
Validate a value against a prop schema.
Args:
value: Value to validate
schema: Prop schema from registry
prop_name: Prop name (for error messages)
Returns:
Dict with valid: bool, error: Optional[str]
"""
prop_type = schema.get('type', 'any')
# Any type always valid
if prop_type == 'any':
return {'valid': True}
# Check enum values
if 'enum' in schema:
if value not in schema['enum']:
return {
'valid': False,
'error': f"Prop '{prop_name}' expects one of {schema['enum']}, got '{value}'"
}
return {'valid': True}
# Type checking
type_checks = {
'string': lambda v: isinstance(v, str),
'number': lambda v: isinstance(v, (int, float)),
'integer': lambda v: isinstance(v, int),
'boolean': lambda v: isinstance(v, bool),
'array': lambda v: isinstance(v, list),
'object': lambda v: isinstance(v, dict),
}
checker = type_checks.get(prop_type)
if checker and not checker(value):
return {
'valid': False,
'error': f"Prop '{prop_name}' expects type '{prop_type}', got '{type(value).__name__}'"
}
return {'valid': True}
def _find_similar_props(
self,
prop_name: str,
available_props: List[str]
) -> Optional[str]:
"""
Find a similar prop name for typo suggestions.
Uses simple edit distance heuristic.
Args:
prop_name: The (possibly misspelled) prop name
available_props: List of valid prop names
Returns:
Most similar prop name, or None if no close match
"""
prop_lower = prop_name.lower()
for prop in available_props:
# Exact match after lowercase
if prop.lower() == prop_lower:
return prop
# Common typos: extra/missing letter
if abs(len(prop) - len(prop_name)) == 1:
if prop_lower.startswith(prop.lower()[:3]):
return prop
return None
def is_loaded(self) -> bool:
"""Check if registry is loaded."""
return len(self.components) > 0
def load_registry(dmc_version: Optional[str] = None) -> ComponentRegistry:
"""
Convenience function to load and return a component registry.
Args:
dmc_version: Optional DMC version string
Returns:
Loaded ComponentRegistry instance
"""
registry = ComponentRegistry(dmc_version)
registry.load()
return registry