jentic-openapi-datamodels 1.0.0a11__py3-none-any.whl → 1.0.0a13__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. jentic/apitools/openapi/datamodels/low/context.py +21 -0
  2. jentic/apitools/openapi/datamodels/low/extractors.py +126 -0
  3. jentic/apitools/openapi/datamodels/low/fields.py +45 -0
  4. jentic/apitools/openapi/datamodels/low/model_builder.py +113 -0
  5. jentic/apitools/openapi/datamodels/low/py.typed +0 -0
  6. jentic/apitools/openapi/datamodels/low/sources.py +89 -0
  7. jentic/apitools/openapi/datamodels/low/v30/__init__.py +0 -28
  8. jentic/apitools/openapi/datamodels/low/v30/discriminator.py +53 -78
  9. jentic/apitools/openapi/datamodels/low/v30/external_documentation.py +47 -61
  10. jentic/apitools/openapi/datamodels/low/v30/oauth_flow.py +54 -123
  11. jentic/apitools/openapi/datamodels/low/v30/oauth_flows.py +102 -151
  12. jentic/apitools/openapi/datamodels/low/v30/reference.py +43 -44
  13. jentic/apitools/openapi/datamodels/low/v30/schema.py +316 -607
  14. jentic/apitools/openapi/datamodels/low/v30/security_requirement.py +82 -72
  15. jentic/apitools/openapi/datamodels/low/v30/security_scheme.py +94 -286
  16. jentic/apitools/openapi/datamodels/low/v30/tag.py +88 -119
  17. jentic/apitools/openapi/datamodels/low/v30/xml.py +46 -120
  18. jentic_openapi_datamodels-1.0.0a13.dist-info/METADATA +211 -0
  19. jentic_openapi_datamodels-1.0.0a13.dist-info/RECORD +23 -0
  20. jentic/apitools/openapi/datamodels/low/v30/specification_object.py +0 -217
  21. jentic_openapi_datamodels-1.0.0a11.dist-info/METADATA +0 -52
  22. jentic_openapi_datamodels-1.0.0a11.dist-info/RECORD +0 -18
  23. /jentic/apitools/openapi/datamodels/low/{v30/py.typed → __init__.py} +0 -0
  24. {jentic_openapi_datamodels-1.0.0a11.dist-info → jentic_openapi_datamodels-1.0.0a13.dist-info}/WHEEL +0 -0
  25. {jentic_openapi_datamodels-1.0.0a11.dist-info → jentic_openapi_datamodels-1.0.0a13.dist-info}/licenses/LICENSE +0 -0
  26. {jentic_openapi_datamodels-1.0.0a11.dist-info → jentic_openapi_datamodels-1.0.0a13.dist-info}/licenses/NOTICE +0 -0
@@ -1,626 +1,335 @@
1
- """
2
- OpenAPI 3.0.4 Schema Object model.
3
-
4
- The Schema Object allows the definition of input and output data types.
5
- These types can be objects, but also primitives and arrays.
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- from collections.abc import Mapping, Sequence
11
- from typing import Any
12
-
1
+ from dataclasses import dataclass, field
2
+ from typing import Any, TypeAlias, get_args
3
+
4
+ from ruamel import yaml
5
+
6
+ from jentic.apitools.openapi.datamodels.low.context import Context
7
+ from jentic.apitools.openapi.datamodels.low.extractors import extract_extension_fields
8
+ from jentic.apitools.openapi.datamodels.low.fields import fixed_field, fixed_fields
9
+ from jentic.apitools.openapi.datamodels.low.sources import (
10
+ FieldSource,
11
+ KeySource,
12
+ ValueSource,
13
+ YAMLInvalidValue,
14
+ YAMLValue,
15
+ )
13
16
  from jentic.apitools.openapi.datamodels.low.v30.discriminator import Discriminator
17
+ from jentic.apitools.openapi.datamodels.low.v30.discriminator import build as build_discriminator
14
18
  from jentic.apitools.openapi.datamodels.low.v30.external_documentation import (
15
19
  ExternalDocumentation,
16
20
  )
21
+ from jentic.apitools.openapi.datamodels.low.v30.external_documentation import (
22
+ build as build_external_documentation,
23
+ )
17
24
  from jentic.apitools.openapi.datamodels.low.v30.reference import Reference
18
- from jentic.apitools.openapi.datamodels.low.v30.specification_object import SpecificationObject
25
+ from jentic.apitools.openapi.datamodels.low.v30.reference import build as build_reference
19
26
  from jentic.apitools.openapi.datamodels.low.v30.xml import XML
27
+ from jentic.apitools.openapi.datamodels.low.v30.xml import build as build_xml
28
+
29
+
30
+ __all__ = ["Schema", "NestedSchema", "build"]
20
31
 
21
32
 
22
- __all__ = ["Schema"]
33
+ # Type alias for nested schema references
34
+ # A schema node that can be nested within another schema, representing:
35
+ # - Schema: A valid schema object
36
+ # - Reference: A $ref reference to another schema
37
+ # - ValueSource[YAMLInvalidValue]: Invalid/malformed data preserved for validation
38
+ NestedSchema: TypeAlias = "Schema | Reference | ValueSource[YAMLInvalidValue]"
23
39
 
24
40
 
25
- class Schema(SpecificationObject):
41
+ @dataclass(frozen=True, slots=True)
42
+ class Schema:
43
+ """
44
+ Schema Object representation for OpenAPI 3.0.
45
+
46
+ The Schema Object allows the definition of input and output data types. These types can be
47
+ objects, but also primitives and arrays. This object is an extended subset of the JSON Schema
48
+ Specification Wright Draft 00.
49
+
50
+ Attributes:
51
+ root_node: The top-level node representing the entire Schema object in the original source file
52
+
53
+ # JSON Schema Core validation keywords
54
+ title: A title for the schema
55
+ multipleOf: A numeric instance is valid only if division by this value results in an integer
56
+ maximum: Upper limit for a numeric instance
57
+ exclusiveMaximum: If true, the value must be strictly less than maximum
58
+ minimum: Lower limit for a numeric instance
59
+ exclusiveMinimum: If true, the value must be strictly greater than minimum
60
+ maxLength: Maximum length of a string instance
61
+ minLength: Minimum length of a string instance
62
+ pattern: A string instance is valid if the regular expression matches the instance successfully
63
+ maxItems: Maximum number of items in an array instance
64
+ minItems: Minimum number of items in an array instance
65
+ uniqueItems: If true, array items must be unique
66
+ maxProperties: Maximum number of properties in an object instance
67
+ minProperties: Minimum number of properties in an object instance
68
+ required: List of required property names
69
+ enum: Fixed set of allowed values
70
+
71
+ # JSON Schema Type and Structure
72
+ type: Value type (string, number, integer, boolean, array, object)
73
+ allOf: Must be valid against all of the subschemas
74
+ oneOf: Must be valid against exactly one of the subschemas
75
+ anyOf: Must be valid against any of the subschemas
76
+ not_: Must not be valid against the given schema
77
+ items: Schema for array items (or array of schemas for tuple validation)
78
+ properties: Property name to schema mappings
79
+ additionalProperties: Schema for properties not defined in properties, or boolean to allow/disallow
80
+
81
+ # JSON Schema Metadata
82
+ description: A short description. CommonMark syntax MAY be used for rich text representation.
83
+ format: Additional format hint for the type (e.g., "email", "uuid", "uri", "date-time")
84
+ default: Default value
85
+
86
+ # OpenAPI-specific extensions
87
+ nullable: Allows sending a null value
88
+ discriminator: Adds support for polymorphism
89
+ readOnly: Relevant only for Schema "properties" definitions - sent in response but not in request
90
+ writeOnly: Relevant only for Schema "properties" definitions - sent in request but not in response
91
+ xml: Additional metadata for XML representations
92
+ externalDocs: Additional external documentation
93
+ example: Example of the media type
94
+ deprecated: Specifies that the schema is deprecated
95
+
96
+ extensions: Specification extensions (x-* fields)
26
97
  """
27
- Represents a Schema Object from OpenAPI 3.0.4.
28
98
 
29
- The Schema Object allows the definition of input and output data types.
30
- Based on a subset of JSON Schema Draft 4/5 with OpenAPI-specific extensions.
99
+ root_node: yaml.Node
100
+
101
+ # JSON Schema Core validation keywords
102
+ title: FieldSource[str] | None = fixed_field()
103
+ multipleOf: FieldSource[int | float] | None = fixed_field()
104
+ maximum: FieldSource[int | float] | None = fixed_field()
105
+ exclusiveMaximum: FieldSource[bool] | None = fixed_field()
106
+ minimum: FieldSource[int | float] | None = fixed_field()
107
+ exclusiveMinimum: FieldSource[bool] | None = fixed_field()
108
+ maxLength: FieldSource[int] | None = fixed_field()
109
+ minLength: FieldSource[int] | None = fixed_field()
110
+ pattern: FieldSource[str] | None = fixed_field()
111
+ maxItems: FieldSource[int] | None = fixed_field()
112
+ minItems: FieldSource[int] | None = fixed_field()
113
+ uniqueItems: FieldSource[bool] | None = fixed_field()
114
+ maxProperties: FieldSource[int] | None = fixed_field()
115
+ minProperties: FieldSource[int] | None = fixed_field()
116
+ required: FieldSource[list[str]] | None = fixed_field()
117
+ enum: FieldSource[list[YAMLValue]] | None = fixed_field()
118
+
119
+ # JSON Schema Type and Structure (nested schemas)
120
+ type: FieldSource[str] | None = fixed_field()
121
+ allOf: FieldSource[list[NestedSchema]] | None = fixed_field()
122
+ oneOf: FieldSource[list[NestedSchema]] | None = fixed_field()
123
+ anyOf: FieldSource[list[NestedSchema]] | None = fixed_field()
124
+ not_: FieldSource[NestedSchema] | None = fixed_field(metadata={"yaml_name": "not"})
125
+ items: FieldSource[NestedSchema] | None = fixed_field()
126
+ properties: FieldSource[dict[KeySource[str], ValueSource[NestedSchema]]] | None = fixed_field()
127
+ additionalProperties: FieldSource["bool | NestedSchema"] | None = fixed_field()
31
128
 
32
- Supports specification extensions (x-* fields).
129
+ # JSON Schema Metadata
130
+ description: FieldSource[str] | None = fixed_field()
131
+ format: FieldSource[str] | None = fixed_field()
132
+ default: FieldSource[YAMLValue] | None = fixed_field()
133
+
134
+ # OpenAPI-specific extensions
135
+ nullable: FieldSource[bool] | None = fixed_field()
136
+ discriminator: FieldSource[Discriminator] | None = fixed_field()
137
+ readOnly: FieldSource[bool] | None = fixed_field()
138
+ writeOnly: FieldSource[bool] | None = fixed_field()
139
+ xml: FieldSource[XML] | None = fixed_field()
140
+ externalDocs: FieldSource[ExternalDocumentation] | None = fixed_field()
141
+ example: FieldSource[YAMLValue] | None = fixed_field()
142
+ deprecated: FieldSource[bool] | None = fixed_field()
143
+
144
+ extensions: dict[KeySource[str], ValueSource[YAMLValue]] = field(default_factory=dict)
145
+
146
+
147
+ def build(
148
+ root: yaml.Node, context: Context | None = None
149
+ ) -> "Schema | ValueSource[YAMLInvalidValue]":
150
+ """
151
+ Build a Schema object from a YAML node.
152
+
153
+ Preserves all source data as-is, regardless of type. This is a low-level/plumbing
154
+ model that provides complete source fidelity for inspection and validation.
155
+
156
+ Note: Schema is self-referential (can contain other Schema objects in allOf, oneOf, anyOf, not,
157
+ items, properties, additionalProperties). The builder handles nested Schema objects by preserving
158
+ them as raw YAML values, letting validation layers interpret them.
159
+
160
+ Args:
161
+ root: The YAML node to parse (should be a MappingNode)
162
+ context: Optional parsing context. If None, a default context will be created.
163
+
164
+ Returns:
165
+ A Schema object if the node is valid, or a ValueSource containing
166
+ the invalid data if the root is not a MappingNode (preserving the invalid data
167
+ and its source location for validation).
33
168
 
34
169
  Example:
35
- >>> # String schema with constraints
36
- >>> schema = Schema({
37
- ... "type": "string",
38
- ... "minLength": 1,
39
- ... "maxLength": 100
40
- ... })
41
- >>> schema.type
42
- 'string'
43
-
44
- >>> # Object schema with properties
45
- >>> schema = Schema({
46
- ... "type": "object",
47
- ... "required": ["name"],
48
- ... "properties": {
49
- ... "name": {"type": "string"},
50
- ... "age": {"type": "integer"}
51
- ... }
52
- ... })
53
- >>> schema.required
54
- ['name']
55
-
56
- >>> # Schema with discriminator
57
- >>> schema = Schema({
58
- ... "discriminator": {
59
- ... "propertyName": "petType"
60
- ... }
61
- ... })
62
- >>> schema.discriminator.property_name
63
- 'petType'
170
+ from ruamel.yaml import YAML
171
+ yaml = YAML()
172
+ root = yaml.compose("type: string\\nminLength: 1\\nmaxLength: 100")
173
+ schema = build(root)
174
+ assert schema.type.value == 'string'
175
+ assert schema.minLength.value == 1
64
176
  """
65
-
66
- _supports_extensions: bool = True
67
- _fixed_fields: frozenset[str] = frozenset(
68
- {
69
- # JSON Schema Core
70
- "title",
71
- "multipleOf",
72
- "maximum",
73
- "exclusiveMaximum",
74
- "minimum",
75
- "exclusiveMinimum",
76
- "maxLength",
77
- "minLength",
78
- "pattern",
79
- "maxItems",
80
- "minItems",
81
- "uniqueItems",
82
- "maxProperties",
83
- "minProperties",
84
- "required",
85
- "enum",
86
- # JSON Schema Type
87
- "type",
88
- "allOf",
89
- "oneOf",
90
- "anyOf",
91
- "not",
92
- "items",
93
- "properties",
94
- "additionalProperties",
95
- # JSON Schema Metadata
96
- "description",
97
- "format",
98
- "default",
99
- # OpenAPI Extensions
100
- "nullable",
101
- "discriminator",
102
- "readOnly",
103
- "writeOnly",
104
- "xml",
105
- "externalDocs",
106
- "example",
107
- "deprecated",
108
- }
177
+ # Initialize context once at the beginning
178
+ if context is None:
179
+ context = Context()
180
+
181
+ if not isinstance(root, yaml.MappingNode):
182
+ # Preserve invalid root data instead of returning None
183
+ value = context.yaml_constructor.construct_object(root, deep=True)
184
+ return ValueSource(value=value, value_node=root)
185
+
186
+ # Build YAML name to Python field name mapping
187
+ _fixed_fields = fixed_fields(Schema)
188
+ yaml_to_field = {
189
+ f.metadata.get("yaml_name", fname): fname for fname, f in _fixed_fields.items()
190
+ }
191
+
192
+ # Accumulate all field values in a single pass
193
+ field_values: dict[str, Any] = {}
194
+
195
+ for key_node, value_node in root.value:
196
+ key = context.yaml_constructor.construct_yaml_str(key_node)
197
+
198
+ # Skip extension fields - handled separately at the end
199
+ if key.startswith("x-"):
200
+ continue
201
+
202
+ # Map YAML key to Python field name
203
+ field_name = yaml_to_field.get(key)
204
+ if not field_name:
205
+ continue
206
+
207
+ # Get field metadata
208
+ field_info = _fixed_fields[field_name]
209
+ field_type_args = set(get_args(field_info.type))
210
+
211
+ # Simple scalar fields (handled like build_model does)
212
+ if field_type_args & {
213
+ FieldSource[str],
214
+ FieldSource[bool],
215
+ FieldSource[int],
216
+ FieldSource[int | float],
217
+ FieldSource[YAMLValue],
218
+ FieldSource[list[str]],
219
+ FieldSource[list[YAMLValue]],
220
+ }:
221
+ value = context.yaml_constructor.construct_object(value_node, deep=True)
222
+ field_values[field_name] = FieldSource(
223
+ value=value, key_node=key_node, value_node=value_node
224
+ )
225
+
226
+ # Recursive schema list fields (allOf, oneOf, anyOf)
227
+ elif key in ("allOf", "oneOf", "anyOf"):
228
+ if isinstance(value_node, yaml.SequenceNode):
229
+ schemas = []
230
+ for item_node in value_node.value:
231
+ schema_or_ref = _build_schema_or_reference(item_node, context)
232
+ schemas.append(schema_or_ref)
233
+ field_values[field_name] = FieldSource(
234
+ value=schemas, key_node=key_node, value_node=value_node
235
+ )
236
+ else:
237
+ # Not a sequence - preserve as-is for validation
238
+ value = context.yaml_constructor.construct_object(value_node, deep=True)
239
+ field_values[field_name] = FieldSource(
240
+ value=value, key_node=key_node, value_node=value_node
241
+ )
242
+ # Recursive schema single fields (not, items)
243
+ elif key in ("not", "items"):
244
+ schema_or_ref = _build_schema_or_reference(value_node, context)
245
+ field_values[field_name] = FieldSource(
246
+ value=schema_or_ref, key_node=key_node, value_node=value_node
247
+ )
248
+ # additionalProperties (boolean | schema | reference)
249
+ elif key == "additionalProperties":
250
+ # Check if it's a boolean or a schema/reference
251
+ if (
252
+ isinstance(value_node, yaml.ScalarNode)
253
+ and value_node.tag == "tag:yaml.org,2002:bool"
254
+ ):
255
+ value = context.yaml_constructor.construct_object(value_node)
256
+ field_values[field_name] = FieldSource(
257
+ value=value, key_node=key_node, value_node=value_node
258
+ )
259
+ else:
260
+ # It's a schema or reference
261
+ schema_or_ref = _build_schema_or_reference(value_node, context)
262
+ field_values[field_name] = FieldSource(
263
+ value=schema_or_ref, key_node=key_node, value_node=value_node
264
+ )
265
+ # properties (dict[KeySource[str], ValueSource[NestedSchema]])
266
+ elif key == "properties":
267
+ if isinstance(value_node, yaml.MappingNode):
268
+ properties_dict: dict[KeySource[str], ValueSource[NestedSchema]] = {}
269
+ for prop_key_node, prop_value_node in value_node.value:
270
+ prop_key = context.yaml_constructor.construct_yaml_str(prop_key_node)
271
+ # Recursively build each property schema
272
+ prop_schema_or_ref = _build_schema_or_reference(prop_value_node, context)
273
+ properties_dict[KeySource(value=prop_key, key_node=prop_key_node)] = (
274
+ ValueSource(value=prop_schema_or_ref, value_node=prop_value_node)
275
+ )
276
+ field_values[field_name] = FieldSource(
277
+ value=properties_dict, key_node=key_node, value_node=value_node
278
+ )
279
+ else:
280
+ # Not a mapping - preserve as-is for validation
281
+ value = context.yaml_constructor.construct_object(value_node, deep=True)
282
+ field_values[field_name] = FieldSource(
283
+ value=value, key_node=key_node, value_node=value_node
284
+ )
285
+ # Nested objects (discriminator, xml, externalDocs)
286
+ elif key == "discriminator":
287
+ field_values[field_name] = FieldSource(
288
+ value=build_discriminator(value_node, context=context),
289
+ key_node=key_node,
290
+ value_node=value_node,
291
+ )
292
+ elif key == "xml":
293
+ field_values[field_name] = FieldSource(
294
+ value=build_xml(value_node, context=context),
295
+ key_node=key_node,
296
+ value_node=value_node,
297
+ )
298
+ elif key == "externalDocs":
299
+ field_values[field_name] = FieldSource(
300
+ value=build_external_documentation(value_node, context=context),
301
+ key_node=key_node,
302
+ value_node=value_node,
303
+ )
304
+
305
+ # Build and return the Schema instance (single constructor call)
306
+ return Schema(
307
+ root_node=root,
308
+ **field_values,
309
+ extensions=extract_extension_fields(root, context),
109
310
  )
110
311
 
111
- def __init__(self, data: Mapping[str, Any] | None = None):
112
- """
113
- Initialize a Schema object.
114
-
115
- Automatically marshals nested objects (discriminator, xml, externalDocs, and nested schemas).
116
-
117
- Args:
118
- data: Optional mapping to initialize the object with
119
- """
120
- super().__init__()
121
- if data:
122
- for key, value in data.items():
123
- # Marshal specific nested objects
124
- if (
125
- key == "discriminator"
126
- and isinstance(value, Mapping)
127
- and not isinstance(value, Discriminator)
128
- ):
129
- self[key] = Discriminator(value)
130
- elif key == "xml" and isinstance(value, Mapping) and not isinstance(value, XML):
131
- self[key] = XML(value)
132
- elif (
133
- key == "externalDocs"
134
- and isinstance(value, Mapping)
135
- and not isinstance(value, ExternalDocumentation)
136
- ):
137
- self[key] = ExternalDocumentation(value)
138
- # Unmarshal schema composition lists (allOf, oneOf, anyOf)
139
- elif (
140
- key in ("allOf", "oneOf", "anyOf")
141
- and isinstance(value, Sequence)
142
- and not isinstance(value, str)
143
- ):
144
- self[key] = [self._unmarshal_schema_or_reference(item) for item in value]
145
- # Unmarshal single schema fields (not, items)
146
- elif key in ("not", "items") and isinstance(value, Mapping):
147
- self[key] = self._unmarshal_schema_or_reference(value)
148
- # Unmarshal properties dict
149
- elif key == "properties" and isinstance(value, Mapping):
150
- self[key] = {
151
- k: self._unmarshal_schema_or_reference(v) for k, v in value.items()
152
- }
153
- # Unmarshal additionalProperties (can be bool or schema)
154
- elif key == "additionalProperties":
155
- if isinstance(value, bool):
156
- self[key] = value
157
- elif isinstance(value, Mapping):
158
- self[key] = self._unmarshal_schema_or_reference(value)
159
- else:
160
- self[key] = self._copy_value(value)
161
- else:
162
- # Store as-is (with defensive copy)
163
- self[key] = self._copy_value(value)
164
-
165
- # JSON Schema Core - Metadata
166
- @property
167
- def title(self) -> str | None:
168
- """A title for the schema."""
169
- return self.get("title")
170
-
171
- @title.setter
172
- def title(self, value: str | None) -> None:
173
- if value is None:
174
- self.pop("title", None)
175
- else:
176
- self["title"] = value
177
-
178
- # JSON Schema Core - Numeric validation
179
- @property
180
- def multiple_of(self) -> float | int | None:
181
- """Value must be multiple of this number."""
182
- return self.get("multipleOf")
183
-
184
- @multiple_of.setter
185
- def multiple_of(self, value: float | int | None) -> None:
186
- if value is None:
187
- self.pop("multipleOf", None)
188
- else:
189
- self["multipleOf"] = value
190
-
191
- @property
192
- def maximum(self) -> float | int | None:
193
- """Maximum value (inclusive)."""
194
- return self.get("maximum")
195
-
196
- @maximum.setter
197
- def maximum(self, value: float | int | None) -> None:
198
- if value is None:
199
- self.pop("maximum", None)
200
- else:
201
- self["maximum"] = value
202
-
203
- @property
204
- def exclusive_maximum(self) -> float | int | None:
205
- """Maximum value (exclusive)."""
206
- return self.get("exclusiveMaximum")
207
-
208
- @exclusive_maximum.setter
209
- def exclusive_maximum(self, value: float | int | None) -> None:
210
- if value is None:
211
- self.pop("exclusiveMaximum", None)
212
- else:
213
- self["exclusiveMaximum"] = value
214
-
215
- @property
216
- def minimum(self) -> float | int | None:
217
- """Minimum value (inclusive)."""
218
- return self.get("minimum")
219
-
220
- @minimum.setter
221
- def minimum(self, value: float | int | None) -> None:
222
- if value is None:
223
- self.pop("minimum", None)
224
- else:
225
- self["minimum"] = value
226
-
227
- @property
228
- def exclusive_minimum(self) -> float | int | None:
229
- """Minimum value (exclusive)."""
230
- return self.get("exclusiveMinimum")
231
-
232
- @exclusive_minimum.setter
233
- def exclusive_minimum(self, value: float | int | None) -> None:
234
- if value is None:
235
- self.pop("exclusiveMinimum", None)
236
- else:
237
- self["exclusiveMinimum"] = value
238
-
239
- # String validation properties
240
- @property
241
- def max_length(self) -> int | None:
242
- """Maximum string length."""
243
- return self.get("maxLength")
244
-
245
- @max_length.setter
246
- def max_length(self, value: int | None) -> None:
247
- if value is None:
248
- self.pop("maxLength", None)
249
- else:
250
- self["maxLength"] = value
251
-
252
- @property
253
- def min_length(self) -> int | None:
254
- """Minimum string length."""
255
- return self.get("minLength")
256
-
257
- @min_length.setter
258
- def min_length(self, value: int | None) -> None:
259
- if value is None:
260
- self.pop("minLength", None)
261
- else:
262
- self["minLength"] = value
263
-
264
- @property
265
- def pattern(self) -> str | None:
266
- """Regular expression pattern for string validation."""
267
- return self.get("pattern")
268
-
269
- @pattern.setter
270
- def pattern(self, value: str | None) -> None:
271
- if value is None:
272
- self.pop("pattern", None)
273
- else:
274
- self["pattern"] = value
275
-
276
- # Array validation properties
277
- @property
278
- def max_items(self) -> int | None:
279
- """Maximum number of array items."""
280
- return self.get("maxItems")
281
-
282
- @max_items.setter
283
- def max_items(self, value: int | None) -> None:
284
- if value is None:
285
- self.pop("maxItems", None)
286
- else:
287
- self["maxItems"] = value
288
-
289
- @property
290
- def min_items(self) -> int | None:
291
- """Minimum number of array items."""
292
- return self.get("minItems")
293
-
294
- @min_items.setter
295
- def min_items(self, value: int | None) -> None:
296
- if value is None:
297
- self.pop("minItems", None)
298
- else:
299
- self["minItems"] = value
300
-
301
- @property
302
- def unique_items(self) -> bool | None:
303
- """Whether array items must be unique."""
304
- return self.get("uniqueItems")
305
-
306
- @unique_items.setter
307
- def unique_items(self, value: bool | None) -> None:
308
- if value is None:
309
- self.pop("uniqueItems", None)
310
- else:
311
- self["uniqueItems"] = value
312
-
313
- # JSON Schema Core - Object validation
314
- @property
315
- def max_properties(self) -> int | None:
316
- """Maximum number of object properties."""
317
- return self.get("maxProperties")
318
-
319
- @max_properties.setter
320
- def max_properties(self, value: int | None) -> None:
321
- if value is None:
322
- self.pop("maxProperties", None)
323
- else:
324
- self["maxProperties"] = value
325
-
326
- @property
327
- def min_properties(self) -> int | None:
328
- """Minimum number of object properties."""
329
- return self.get("minProperties")
330
-
331
- @min_properties.setter
332
- def min_properties(self, value: int | None) -> None:
333
- if value is None:
334
- self.pop("minProperties", None)
335
- else:
336
- self["minProperties"] = value
337
-
338
- @property
339
- def required(self) -> list[str] | None:
340
- """List of required property names (for object types)."""
341
- return self.get("required")
342
-
343
- @required.setter
344
- def required(self, value: list[str] | None) -> None:
345
- if value is None:
346
- self.pop("required", None)
347
- else:
348
- self["required"] = value
349
-
350
- @property
351
- def enum(self) -> list[Any] | None:
352
- """Allowed values."""
353
- return self.get("enum")
354
-
355
- @enum.setter
356
- def enum(self, value: list[Any] | None) -> None:
357
- if value is None:
358
- self.pop("enum", None)
359
- else:
360
- self["enum"] = value
361
-
362
- # JSON Schema Type
363
- @property
364
- def type(self) -> str | None:
365
- """Type of the schema (string, number, integer, boolean, array, object, null)."""
366
- return self.get("type")
367
-
368
- @type.setter
369
- def type(self, value: str | None) -> None:
370
- if value is None:
371
- self.pop("type", None)
372
- else:
373
- self["type"] = value
374
-
375
- @property
376
- def all_of(self) -> list[Schema | Reference] | None:
377
- """Schemas that must all be valid (list of Schema or Reference objects)."""
378
- return self.get("allOf")
379
-
380
- @all_of.setter
381
- def all_of(self, value: list[Schema | Reference] | None) -> None:
382
- if value is None:
383
- self.pop("allOf", None)
384
- else:
385
- self["allOf"] = value
386
-
387
- @property
388
- def one_of(self) -> list[Schema | Reference] | None:
389
- """Schemas where exactly one must be valid (list of Schema or Reference objects)."""
390
- return self.get("oneOf")
391
-
392
- @one_of.setter
393
- def one_of(self, value: list[Schema | Reference] | None) -> None:
394
- if value is None:
395
- self.pop("oneOf", None)
396
- else:
397
- self["oneOf"] = value
398
-
399
- @property
400
- def any_of(self) -> list[Schema | Reference] | None:
401
- """Schemas where at least one must be valid (list of Schema or Reference objects)."""
402
- return self.get("anyOf")
403
-
404
- @any_of.setter
405
- def any_of(self, value: list[Schema | Reference] | None) -> None:
406
- if value is None:
407
- self.pop("anyOf", None)
408
- else:
409
- self["anyOf"] = value
410
-
411
- @property
412
- def not_(self) -> Schema | Reference | None:
413
- """Schema that must NOT be valid."""
414
- return self.get("not")
415
-
416
- @not_.setter
417
- def not_(self, value: Schema | Reference | None) -> None:
418
- if value is None:
419
- self.pop("not", None)
420
- else:
421
- self["not"] = value
422
-
423
- @property
424
- def items_(self) -> Schema | Reference | None:
425
- """
426
- Schema for array items (Schema or Reference object).
427
-
428
- Note: Property named 'items_' (with underscore) to avoid conflict with
429
- MutableMapping.items() method. Dict access still uses the standard field name:
430
- schema["items"].
431
- """
432
- return self.get("items")
433
-
434
- @items_.setter
435
- def items_(self, value: Schema | Reference | None) -> None:
436
- if value is None:
437
- self.pop("items", None)
438
- else:
439
- self["items"] = value
440
-
441
- @property
442
- def properties(self) -> dict[str, Schema | Reference] | None:
443
- """Object properties (map of property name to Schema or Reference)."""
444
- return self.get("properties")
445
-
446
- @properties.setter
447
- def properties(self, value: Mapping[str, Schema | Reference] | None) -> None:
448
- if value is None:
449
- self.pop("properties", None)
450
- else:
451
- self["properties"] = dict(value) if isinstance(value, Mapping) else value
452
-
453
- @property
454
- def additional_properties(self) -> bool | Schema | Reference | None:
455
- """Schema for additional properties (bool or Schema or Reference object)."""
456
- return self.get("additionalProperties")
457
-
458
- @additional_properties.setter
459
- def additional_properties(self, value: bool | Schema | Reference | None) -> None:
460
- if value is None:
461
- self.pop("additionalProperties", None)
462
- else:
463
- self["additionalProperties"] = value
464
312
 
465
- # JSON Schema Metadata
466
- @property
467
- def description(self) -> str | None:
468
- """A description of the schema."""
469
- return self.get("description")
470
-
471
- @description.setter
472
- def description(self, value: str | None) -> None:
473
- if value is None:
474
- self.pop("description", None)
475
- else:
476
- self["description"] = value
477
-
478
- @property
479
- def format(self) -> str | None:
480
- """Format hint for the type (e.g., date-time, email, uuid)."""
481
- return self.get("format")
482
-
483
- @format.setter
484
- def format(self, value: str | None) -> None:
485
- if value is None:
486
- self.pop("format", None)
487
- else:
488
- self["format"] = value
489
-
490
- @property
491
- def default(self) -> Any:
492
- """Default value."""
493
- return self.get("default")
494
-
495
- @default.setter
496
- def default(self, value: Any) -> None:
497
- if value is None:
498
- self.pop("default", None)
499
- else:
500
- self["default"] = value
501
-
502
- # OpenAPI Extensions
503
- @property
504
- def nullable(self) -> bool | None:
505
- """Whether the value can be null (OpenAPI extension)."""
506
- return self.get("nullable")
507
-
508
- @nullable.setter
509
- def nullable(self, value: bool | None) -> None:
510
- if value is None:
511
- self.pop("nullable", None)
512
- else:
513
- self["nullable"] = value
514
-
515
- @property
516
- def discriminator(self) -> Discriminator | None:
517
- """Discriminator for polymorphism (OpenAPI extension)."""
518
- return self.get("discriminator")
519
-
520
- @discriminator.setter
521
- def discriminator(self, value: Discriminator | None) -> None:
522
- if value is None:
523
- self.pop("discriminator", None)
524
- else:
525
- self["discriminator"] = value
526
-
527
- @property
528
- def read_only(self) -> bool | None:
529
- """Whether the property is read-only (OpenAPI extension)."""
530
- return self.get("readOnly")
531
-
532
- @read_only.setter
533
- def read_only(self, value: bool | None) -> None:
534
- if value is None:
535
- self.pop("readOnly", None)
536
- else:
537
- self["readOnly"] = value
538
-
539
- @property
540
- def write_only(self) -> bool | None:
541
- """Whether the property is write-only (OpenAPI extension)."""
542
- return self.get("writeOnly")
543
-
544
- @write_only.setter
545
- def write_only(self, value: bool | None) -> None:
546
- if value is None:
547
- self.pop("writeOnly", None)
548
- else:
549
- self["writeOnly"] = value
550
-
551
- @property
552
- def xml(self) -> XML | None:
553
- """XML representation metadata (OpenAPI extension)."""
554
- return self.get("xml")
555
-
556
- @xml.setter
557
- def xml(self, value: XML | None) -> None:
558
- if value is None:
559
- self.pop("xml", None)
560
- else:
561
- self["xml"] = value
562
-
563
- @property
564
- def external_docs(self) -> ExternalDocumentation | None:
565
- """External documentation (OpenAPI extension)."""
566
- return self.get("externalDocs")
567
-
568
- @external_docs.setter
569
- def external_docs(self, value: ExternalDocumentation | None) -> None:
570
- if value is None:
571
- self.pop("externalDocs", None)
572
- else:
573
- self["externalDocs"] = value
574
-
575
- @property
576
- def example(self) -> Any:
577
- """Example value (OpenAPI extension)."""
578
- return self.get("example")
579
-
580
- @example.setter
581
- def example(self, value: Any) -> None:
582
- if value is None:
583
- self.pop("example", None)
584
- else:
585
- self["example"] = value
586
-
587
- @property
588
- def deprecated(self) -> bool | None:
589
- """Whether the schema is deprecated (OpenAPI extension)."""
590
- return self.get("deprecated")
591
-
592
- @deprecated.setter
593
- def deprecated(self, value: bool | None) -> None:
594
- if value is None:
595
- self.pop("deprecated", None)
596
- else:
597
- self["deprecated"] = value
598
-
599
- # Helper methods
600
- @classmethod
601
- def _unmarshal_schema_or_reference(cls, value: Any) -> Schema | Reference | Any:
602
- """
603
- Unmarshal a value into Schema or Reference if it's a Mapping.
604
-
605
- Converts raw data (dict/Mapping) into Schema or Reference objects during construction.
606
- Uses the actual class (or subclass) for creating Schema instances.
607
-
608
- Args:
609
- value: Value to unmarshal
610
-
611
- Returns:
612
- Schema or Reference instance, or value as-is
613
- """
614
- if not isinstance(value, Mapping):
615
- return value
616
-
617
- # Already unmarshaled
618
- if isinstance(value, (Schema, Reference)):
619
- return value
620
-
621
- # Check if it's a reference (has $ref field)
622
- if "$ref" in value:
623
- return Reference(value)
624
- else:
625
- # Otherwise treat as Schema (or subclass)
626
- return cls(value)
313
+ def _build_schema_or_reference(node: yaml.Node, context: Context) -> NestedSchema:
314
+ """
315
+ Build either a Schema or Reference from a YAML node.
316
+
317
+ This helper handles the polymorphic nature of OpenAPI where many fields
318
+ can contain either a Schema object or a Reference object ($ref).
319
+
320
+ Args:
321
+ node: The YAML node to parse
322
+ context: Parsing context
323
+
324
+ Returns:
325
+ A Schema, Reference, or ValueSource if the node is invalid
326
+ """
327
+ # Check if it's a reference (has $ref key)
328
+ if isinstance(node, yaml.MappingNode):
329
+ for key_node, _ in node.value:
330
+ key = context.yaml_constructor.construct_yaml_str(key_node)
331
+ if key == "$ref":
332
+ return build_reference(node, context)
333
+
334
+ # Otherwise, try to build as Schema (which may be recursive)
335
+ return build(node, context)