loader.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. """JSON Schema Loading and Validation.
  2. This module provides functionality to load, cache, and validate JSON schemas
  3. for boilerplate modules. Schemas are stored in cli/core/schema/<module>/v*.json files.
  4. """
  5. import json
  6. from pathlib import Path
  7. from typing import Any
  8. from cli.core.exceptions import SchemaError
  9. class SchemaLoader:
  10. """Loads and validates JSON schemas for modules."""
  11. def __init__(self, schema_dir: Path | None = None):
  12. """Initialize schema loader.
  13. Args:
  14. schema_dir: Directory containing schema files. If None, uses cli/core/schema/
  15. """
  16. if schema_dir is None:
  17. # Use path relative to this file (in cli/core/schema/)
  18. # __file__ is cli/core/schema/loader.py, parent is cli/core/schema/
  19. self.schema_dir = Path(__file__).parent
  20. else:
  21. self.schema_dir = schema_dir
  22. def load_schema(self, module: str, version: str) -> list[dict[str, Any]]:
  23. """Load a JSON schema from file.
  24. Args:
  25. module: Module name (e.g., 'compose', 'ansible')
  26. version: Schema version (e.g., '1.0', '1.2')
  27. Returns:
  28. Schema as list of section specifications
  29. Raises:
  30. SchemaError: If schema file not found or invalid JSON
  31. """
  32. schema_file = self.schema_dir / module / f"v{version}.json"
  33. if not schema_file.exists():
  34. raise SchemaError(
  35. f"Schema file not found: {schema_file}",
  36. details=f"Module: {module}, Version: {version}",
  37. )
  38. try:
  39. with schema_file.open(encoding="utf-8") as f:
  40. schema = json.load(f)
  41. except json.JSONDecodeError as e:
  42. raise SchemaError(
  43. f"Invalid JSON in schema file: {schema_file}",
  44. details=f"Error: {e}",
  45. ) from e
  46. except Exception as e:
  47. raise SchemaError(
  48. f"Failed to read schema file: {schema_file}",
  49. details=f"Error: {e}",
  50. ) from e
  51. # Validate schema structure
  52. self._validate_schema_structure(schema, module, version)
  53. return schema
  54. def _validate_schema_structure(self, schema: Any, module: str, version: str) -> None:
  55. """Validate that schema has correct structure.
  56. Args:
  57. schema: Schema to validate
  58. module: Module name for error messages
  59. version: Version for error messages
  60. Raises:
  61. SchemaError: If schema structure is invalid
  62. """
  63. if not isinstance(schema, list):
  64. raise SchemaError(
  65. f"Schema must be a list, got {type(schema).__name__}",
  66. details=f"Module: {module}, Version: {version}",
  67. )
  68. for idx, section in enumerate(schema):
  69. if not isinstance(section, dict):
  70. raise SchemaError(
  71. f"Section {idx} must be a dict, got {type(section).__name__}",
  72. details=f"Module: {module}, Version: {version}",
  73. )
  74. # Check required fields
  75. if "key" not in section:
  76. raise SchemaError(
  77. f"Section {idx} missing required field 'key'",
  78. details=f"Module: {module}, Version: {version}",
  79. )
  80. if "vars" not in section:
  81. raise SchemaError(
  82. f"Section '{section.get('key')}' missing required field 'vars'",
  83. details=f"Module: {module}, Version: {version}",
  84. )
  85. if not isinstance(section["vars"], list):
  86. raise SchemaError(
  87. f"Section '{section['key']}' vars must be a list",
  88. details=f"Module: {module}, Version: {version}",
  89. )
  90. # Validate variables
  91. for var_idx, var in enumerate(section["vars"]):
  92. if not isinstance(var, dict):
  93. raise SchemaError(
  94. f"Variable {var_idx} in section '{section['key']}' must be a dict",
  95. details=f"Module: {module}, Version: {version}",
  96. )
  97. if "name" not in var:
  98. raise SchemaError(
  99. f"Variable {var_idx} in section '{section['key']}' missing 'name'",
  100. details=f"Module: {module}, Version: {version}",
  101. )
  102. if "type" not in var:
  103. raise SchemaError(
  104. f"Variable '{var.get('name')}' in section '{section['key']}' missing 'type'",
  105. details=f"Module: {module}, Version: {version}",
  106. )
  107. def list_versions(self, module: str) -> list[str]:
  108. """List available schema versions for a module.
  109. Args:
  110. module: Module name
  111. Returns:
  112. List of version strings (e.g., ['1.0', '1.1', '1.2'])
  113. """
  114. module_dir = self.schema_dir / module
  115. if not module_dir.exists():
  116. return []
  117. versions = []
  118. for file in module_dir.glob("v*.json"):
  119. # Extract version from filename (v1.0.json -> 1.0)
  120. version = file.stem[1:] # Remove 'v' prefix
  121. versions.append(version)
  122. return sorted(versions)
  123. def has_schema(self, module: str, version: str) -> bool:
  124. """Check if a schema exists.
  125. Args:
  126. module: Module name
  127. version: Schema version
  128. Returns:
  129. True if schema exists
  130. """
  131. schema_file = self.schema_dir / module / f"v{version}.json"
  132. return schema_file.exists()
  133. # Global schema loader instance
  134. _loader: SchemaLoader | None = None
  135. def get_loader() -> SchemaLoader:
  136. """Get global schema loader instance.
  137. Returns:
  138. SchemaLoader instance
  139. """
  140. global _loader # noqa: PLW0603
  141. if _loader is None:
  142. _loader = SchemaLoader()
  143. return _loader
  144. def load_schema(module: str, version: str) -> list[dict[str, Any]]:
  145. """Load a schema using the global loader.
  146. Args:
  147. module: Module name
  148. version: Schema version
  149. Returns:
  150. Schema as list of section specifications
  151. """
  152. return get_loader().load_schema(module, version)
  153. def list_versions(module: str) -> list[str]:
  154. """List available versions for a module.
  155. Args:
  156. module: Module name
  157. Returns:
  158. List of version strings
  159. """
  160. return get_loader().list_versions(module)
  161. def has_schema(module: str, version: str) -> bool:
  162. """Check if a schema exists.
  163. Args:
  164. module: Module name
  165. version: Schema version
  166. Returns:
  167. True if schema exists
  168. """
  169. return get_loader().has_schema(module, version)