1717
1818from .generated .write import (
1919 AttributeSchemaWrite ,
20- GenericSchemaWrite ,
21- NodeSchemaWrite ,
20+ InfrahubSchemaWrite ,
2221 RelationshipSchemaWrite ,
2322)
2423
25- # Maps each collection in a schema-root payload to the write model its items must satisfy.
26- _WRITE_MODELS_BY_COLLECTION : dict [str , type [BaseModel ]] = {
27- "nodes" : NodeSchemaWrite ,
28- "generics" : GenericSchemaWrite ,
29- }
30-
3124
3225class SchemaValidationErrorDetail (BaseModel ):
3326 """A single field-level validation problem in a schema payload."""
@@ -59,34 +52,45 @@ def raise_for_status(self) -> None:
5952 raise ValueError ("; " .join (self .messages ))
6053
6154
62- def _format_error_location (prefix : str , loc : tuple [Any , ...]) -> str :
63- """Render a dotted field path from a base prefix and a pydantic error location .
55+ def _format_error_location (loc : tuple [Any , ...], prefix : str = "" ) -> str :
56+ """Render a dotted field path from a pydantic error location, optionally under a base prefix .
6457
6558 Integer elements index into the preceding segment (``attributes`` + ``1`` becomes
66- ``attributes[1]``); everything else is appended as a new dotted segment.
59+ ``attributes[1]``); everything else is appended as a new dotted segment. A ``prefix`` is used
60+ when the location is relative to an item validated on its own (e.g. an extension attribute).
6761 """
68- parts = [prefix ]
62+ parts = [prefix ] if prefix else []
6963 for element in loc :
7064 if isinstance (element , int ):
71- parts [- 1 ] = f"{ parts [- 1 ]} [{ element } ]"
65+ if parts :
66+ parts [- 1 ] = f"{ parts [- 1 ]} [{ element } ]"
67+ else :
68+ parts .append (f"[{ element } ]" )
7269 else :
7370 parts .append (str (element ))
7471 return "." .join (parts )
7572
7673
74+ def _collect_validation_errors (
75+ exc : PydanticValidationError , errors : list [SchemaValidationErrorDetail ], prefix : str = ""
76+ ) -> None :
77+ """Append a field-level detail for every problem in a pydantic validation error."""
78+ for error in exc .errors ():
79+ location = _format_error_location (loc = error ["loc" ], prefix = prefix )
80+ message = f"{ location } : { error ['msg' ]} "
81+ if error ["type" ] != "missing" and "input" in error :
82+ message += f" (received: { error ['input' ]!r} )"
83+ errors .append (SchemaValidationErrorDetail (field = location , message = message ))
84+
85+
7786def _validate_item (model : type [BaseModel ], item : Any , prefix : str , errors : list [SchemaValidationErrorDetail ]) -> None :
7887 """Validate a single mapping against a write model, appending field-level errors under ``prefix``."""
7988 if not isinstance (item , dict ):
8089 return
8190 try :
8291 model .model_validate (item )
8392 except PydanticValidationError as exc :
84- for error in exc .errors ():
85- location = _format_error_location (prefix = prefix , loc = error ["loc" ])
86- message = f"{ location } : { error ['msg' ]} "
87- if error ["type" ] != "missing" and "input" in error :
88- message += f" (received: { error ['input' ]!r} )"
89- errors .append (SchemaValidationErrorDetail (field = location , message = message ))
93+ _collect_validation_errors (exc = exc , errors = errors , prefix = prefix )
9094
9195
9296def _validate_extensions (extensions : Any , errors : list [SchemaValidationErrorDetail ]) -> None :
@@ -126,7 +130,7 @@ def _validate_extensions(extensions: Any, errors: list[SchemaValidationErrorDeta
126130
127131
128132def validate_schema (schema : dict [str , Any ], * , raise_on_error : bool = False ) -> SchemaValidationResult :
129- """Validate a single schema-root payload against the generated write models .
133+ """Validate a single schema-root payload against the generated write contract .
130134
131135 Args:
132136 schema: A schema-root mapping, e.g. ``{"version": "1.0", "nodes": [...], "generics": [...]}``.
@@ -135,20 +139,21 @@ def validate_schema(schema: dict[str, Any], *, raise_on_error: bool = False) ->
135139 Returns:
136140 A :class:`SchemaValidationResult` with a field-level message for every field that is not
137141 settable (read-level, internal, or unknown) and for every constrained field set outside
138- its allowed set. Nodes, generics, and the attributes/relationships nested under
139- ``extensions.nodes`` are all held to the write contract.
142+ its allowed set. The nodes and generics are validated together against the write
143+ document model; the attributes/relationships nested under ``extensions.nodes`` are held to
144+ the same write contract (extension nodes are ``kind``-only, so the document model cannot
145+ cover them).
140146
141147 Raises:
142148 ValueError: When ``raise_on_error`` is True and the payload is invalid.
143149
144150 """
145151 errors : list [SchemaValidationErrorDetail ] = []
146- for collection , model in _WRITE_MODELS_BY_COLLECTION .items ():
147- items = schema .get (collection )
148- if not isinstance (items , list ):
149- continue
150- for index , item in enumerate (items ):
151- _validate_item (model = model , item = item , prefix = f"{ collection } [{ index } ]" , errors = errors )
152+
153+ try :
154+ InfrahubSchemaWrite .model_validate (schema )
155+ except PydanticValidationError as exc :
156+ _collect_validation_errors (exc = exc , errors = errors )
152157
153158 _validate_extensions (extensions = schema .get ("extensions" ), errors = errors )
154159
0 commit comments