jentic-openapi-datamodels 1.0.0a18__py3-none-any.whl → 1.0.0a20__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 (73) hide show
  1. jentic/apitools/openapi/datamodels/low/extractors.py +3 -3
  2. jentic/apitools/openapi/datamodels/low/v30/__init__.py +76 -0
  3. jentic/apitools/openapi/datamodels/low/v30/builders/__init__.py +312 -0
  4. jentic/apitools/openapi/datamodels/low/v30/callback.py +131 -0
  5. jentic/apitools/openapi/datamodels/low/v30/components.py +236 -0
  6. jentic/apitools/openapi/datamodels/low/v30/contact.py +4 -10
  7. jentic/apitools/openapi/datamodels/low/v30/discriminator.py +4 -9
  8. jentic/apitools/openapi/datamodels/low/v30/encoding.py +81 -0
  9. jentic/apitools/openapi/datamodels/low/v30/example.py +91 -0
  10. jentic/apitools/openapi/datamodels/low/v30/external_documentation.py +4 -10
  11. jentic/apitools/openapi/datamodels/low/v30/header.py +120 -0
  12. jentic/apitools/openapi/datamodels/low/v30/info.py +14 -23
  13. jentic/apitools/openapi/datamodels/low/v30/license.py +4 -10
  14. jentic/apitools/openapi/datamodels/low/v30/link.py +141 -0
  15. jentic/apitools/openapi/datamodels/low/v30/media_type.py +110 -0
  16. jentic/apitools/openapi/datamodels/low/v30/oauth_flow.py +4 -10
  17. jentic/apitools/openapi/datamodels/low/v30/oauth_flows.py +7 -15
  18. jentic/apitools/openapi/datamodels/low/v30/openapi.py +149 -0
  19. jentic/apitools/openapi/datamodels/low/v30/operation.py +134 -0
  20. jentic/apitools/openapi/datamodels/low/v30/parameter.py +123 -0
  21. jentic/apitools/openapi/datamodels/low/v30/path_item.py +125 -0
  22. jentic/apitools/openapi/datamodels/low/v30/paths.py +108 -0
  23. jentic/apitools/openapi/datamodels/low/v30/reference.py +5 -9
  24. jentic/apitools/openapi/datamodels/low/v30/request_body.py +108 -0
  25. jentic/apitools/openapi/datamodels/low/v30/response.py +104 -0
  26. jentic/apitools/openapi/datamodels/low/v30/responses.py +109 -0
  27. jentic/apitools/openapi/datamodels/low/v30/schema.py +81 -97
  28. jentic/apitools/openapi/datamodels/low/v30/security_requirement.py +14 -9
  29. jentic/apitools/openapi/datamodels/low/v30/security_scheme.py +42 -22
  30. jentic/apitools/openapi/datamodels/low/v30/server.py +111 -0
  31. jentic/apitools/openapi/datamodels/low/v30/server_variable.py +4 -10
  32. jentic/apitools/openapi/datamodels/low/v30/tag.py +8 -46
  33. jentic/apitools/openapi/datamodels/low/v30/xml.py +4 -10
  34. jentic/apitools/openapi/datamodels/low/v31/__init__.py +77 -0
  35. jentic/apitools/openapi/datamodels/low/v31/builders/__init__.py +347 -0
  36. jentic/apitools/openapi/datamodels/low/v31/callback.py +131 -0
  37. jentic/apitools/openapi/datamodels/low/v31/components.py +240 -0
  38. jentic/apitools/openapi/datamodels/low/v31/contact.py +61 -0
  39. jentic/apitools/openapi/datamodels/low/v31/discriminator.py +62 -0
  40. jentic/apitools/openapi/datamodels/low/v31/encoding.py +81 -0
  41. jentic/apitools/openapi/datamodels/low/v31/example.py +91 -0
  42. jentic/apitools/openapi/datamodels/low/v31/external_documentation.py +59 -0
  43. jentic/apitools/openapi/datamodels/low/v31/header.py +120 -0
  44. jentic/apitools/openapi/datamodels/low/v31/info.py +116 -0
  45. jentic/apitools/openapi/datamodels/low/v31/license.py +61 -0
  46. jentic/apitools/openapi/datamodels/low/v31/link.py +141 -0
  47. jentic/apitools/openapi/datamodels/low/v31/media_type.py +110 -0
  48. jentic/apitools/openapi/datamodels/low/v31/oauth_flow.py +65 -0
  49. jentic/apitools/openapi/datamodels/low/v31/oauth_flows.py +108 -0
  50. jentic/apitools/openapi/datamodels/low/v31/openapi.py +168 -0
  51. jentic/apitools/openapi/datamodels/low/v31/operation.py +133 -0
  52. jentic/apitools/openapi/datamodels/low/v31/parameter.py +123 -0
  53. jentic/apitools/openapi/datamodels/low/v31/path_item.py +125 -0
  54. jentic/apitools/openapi/datamodels/low/v31/paths.py +108 -0
  55. jentic/apitools/openapi/datamodels/low/v31/reference.py +65 -0
  56. jentic/apitools/openapi/datamodels/low/v31/request_body.py +108 -0
  57. jentic/apitools/openapi/datamodels/low/v31/response.py +104 -0
  58. jentic/apitools/openapi/datamodels/low/v31/responses.py +109 -0
  59. jentic/apitools/openapi/datamodels/low/v31/schema.py +498 -0
  60. jentic/apitools/openapi/datamodels/low/v31/security_requirement.py +106 -0
  61. jentic/apitools/openapi/datamodels/low/v31/security_scheme.py +129 -0
  62. jentic/apitools/openapi/datamodels/low/v31/server.py +111 -0
  63. jentic/apitools/openapi/datamodels/low/v31/server_variable.py +70 -0
  64. jentic/apitools/openapi/datamodels/low/v31/tag.py +63 -0
  65. jentic/apitools/openapi/datamodels/low/v31/xml.py +54 -0
  66. jentic_openapi_datamodels-1.0.0a20.dist-info/METADATA +379 -0
  67. jentic_openapi_datamodels-1.0.0a20.dist-info/RECORD +75 -0
  68. jentic/apitools/openapi/datamodels/low/model_builder.py +0 -129
  69. jentic_openapi_datamodels-1.0.0a18.dist-info/METADATA +0 -211
  70. jentic_openapi_datamodels-1.0.0a18.dist-info/RECORD +0 -27
  71. {jentic_openapi_datamodels-1.0.0a18.dist-info → jentic_openapi_datamodels-1.0.0a20.dist-info}/WHEEL +0 -0
  72. {jentic_openapi_datamodels-1.0.0a18.dist-info → jentic_openapi_datamodels-1.0.0a20.dist-info}/licenses/LICENSE +0 -0
  73. {jentic_openapi_datamodels-1.0.0a18.dist-info → jentic_openapi_datamodels-1.0.0a20.dist-info}/licenses/NOTICE +0 -0
@@ -0,0 +1,109 @@
1
+ import re
2
+ from dataclasses import dataclass, field
3
+ from typing import cast
4
+
5
+ from ruamel import yaml
6
+
7
+ from ..context import Context
8
+ from ..extractors import extract_extension_fields
9
+ from ..fields import fixed_field
10
+ from ..sources import FieldSource, KeySource, ValueSource, YAMLInvalidValue, YAMLValue
11
+ from .reference import Reference
12
+ from .response import Response, build_response_or_reference
13
+
14
+
15
+ __all__ = ["Responses", "build"]
16
+
17
+
18
+ @dataclass(frozen=True, slots=True)
19
+ class Responses:
20
+ """
21
+ Responses Object representation for OpenAPI 3.0.
22
+
23
+ A container for the expected responses of an operation. The container maps a HTTP response code
24
+ to the expected response.
25
+
26
+ Attributes:
27
+ root_node: The top-level node representing the entire Responses object in the original source file
28
+ default: The documentation of responses other than the ones declared for specific HTTP response codes.
29
+ Use this field to cover undeclared responses.
30
+ responses: Maps HTTP status codes to their Response objects. The key is the HTTP status code
31
+ (e.g., "200", "404", "4XX") and the value is a Response object or Reference.
32
+ extensions: Specification extensions (x-* fields)
33
+ """
34
+
35
+ root_node: yaml.Node
36
+ default: FieldSource[Response | Reference] | None = fixed_field()
37
+ responses: dict[KeySource[str], Response | Reference] = field(default_factory=dict)
38
+ extensions: dict[KeySource[str], ValueSource[YAMLValue]] = field(default_factory=dict)
39
+
40
+
41
+ def build(
42
+ root: yaml.Node, context: Context | None = None
43
+ ) -> Responses | ValueSource[YAMLInvalidValue]:
44
+ """
45
+ Build a Responses object from a YAML node.
46
+
47
+ Preserves all source data as-is, regardless of type. This is a low-level/plumbing
48
+ model that provides complete source fidelity for inspection and validation.
49
+
50
+ Args:
51
+ root: The YAML node to parse (should be a MappingNode)
52
+ context: Optional parsing context. If None, a default context will be created.
53
+
54
+ Returns:
55
+ A Responses object if the node is valid, or a ValueSource containing
56
+ the invalid data if the root is not a MappingNode (preserving the invalid data
57
+ and its source location for validation).
58
+
59
+ Example:
60
+ from ruamel.yaml import YAML
61
+ yaml = YAML()
62
+ root = yaml.compose('''
63
+ '200':
64
+ description: successful operation
65
+ '404':
66
+ description: not found
67
+ default:
68
+ description: unexpected error
69
+ ''')
70
+ responses = build(root)
71
+ assert '200' in {k.value for k in responses.responses.keys()}
72
+ """
73
+ context = context or Context()
74
+
75
+ # Check if root is a MappingNode, if not return ValueSource with invalid data
76
+ if not isinstance(root, yaml.MappingNode):
77
+ value = context.yaml_constructor.construct_object(root, deep=True)
78
+ return ValueSource(value=value, value_node=root)
79
+
80
+ # Process each field to determine if it's default or a status code response
81
+ responses = {}
82
+ default: FieldSource[Response | Reference] | None = None
83
+
84
+ for key_node, value_node in root.value:
85
+ key = context.yaml_constructor.construct_yaml_str(key_node)
86
+
87
+ if key == "default":
88
+ # Handle default response - can be Response or Reference
89
+ response_or_reference = build_response_or_reference(value_node, context)
90
+ default = cast(
91
+ FieldSource[Response | Reference],
92
+ FieldSource(value=response_or_reference, key_node=key_node, value_node=value_node),
93
+ )
94
+ elif _HTTP_STATUS_CODE_PATTERN.match(key):
95
+ # Valid HTTP status code (100-599) or pattern (1XX-5XX)
96
+ response_or_reference = build_response_or_reference(value_node, context)
97
+ responses[KeySource(value=key, key_node=key_node)] = response_or_reference
98
+
99
+ # Create and return the Responses object with collected data
100
+ return Responses(
101
+ root_node=root,
102
+ default=default,
103
+ responses=responses,
104
+ extensions=extract_extension_fields(root, context),
105
+ )
106
+
107
+
108
+ # Pattern for valid HTTP status codes: 100-599 or wildcard patterns (1XX-5XX)
109
+ _HTTP_STATUS_CODE_PATTERN = re.compile(r"^([1-5]XX|[1-5][0-9]{2})$")
@@ -3,31 +3,22 @@ from typing import Any, TypeAlias, get_args
3
3
 
4
4
  from ruamel import yaml
5
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
- )
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
18
- from jentic.apitools.openapi.datamodels.low.v30.external_documentation import (
19
- ExternalDocumentation,
20
- )
21
- from jentic.apitools.openapi.datamodels.low.v30.external_documentation import (
22
- build as build_external_documentation,
23
- )
24
- from jentic.apitools.openapi.datamodels.low.v30.reference import Reference
25
- from jentic.apitools.openapi.datamodels.low.v30.reference import build as build_reference
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"]
6
+ from ..context import Context
7
+ from ..extractors import extract_extension_fields
8
+ from ..fields import fixed_field, fixed_fields
9
+ from ..sources import FieldSource, KeySource, ValueSource, YAMLInvalidValue, YAMLValue
10
+ from .builders import build_field_source
11
+ from .discriminator import Discriminator
12
+ from .discriminator import build as build_discriminator
13
+ from .external_documentation import ExternalDocumentation
14
+ from .external_documentation import build as build_external_documentation
15
+ from .reference import Reference
16
+ from .reference import build as build_reference
17
+ from .xml import XML
18
+ from .xml import build as build_xml
19
+
20
+
21
+ __all__ = ["Schema", "NestedSchema", "build", "build_schema_or_reference"]
31
22
 
32
23
 
33
24
  # Type alias for nested schema references
@@ -52,31 +43,31 @@ class Schema:
52
43
 
53
44
  # JSON Schema Core validation keywords
54
45
  title: A title for the schema
55
- multipleOf: A numeric instance is valid only if division by this value results in an integer
46
+ multiple_of: A numeric instance is valid only if division by this value results in an integer
56
47
  maximum: Upper limit for a numeric instance
57
- exclusiveMaximum: If true, the value must be strictly less than maximum
48
+ exclusive_maximum: If true, the value must be strictly less than maximum
58
49
  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
50
+ exclusive_minimum: If true, the value must be strictly greater than minimum
51
+ max_length: Maximum length of a string instance
52
+ min_length: Minimum length of a string instance
62
53
  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
54
+ max_items: Maximum number of items in an array instance
55
+ min_items: Minimum number of items in an array instance
56
+ unique_items: If true, array items must be unique
57
+ max_properties: Maximum number of properties in an object instance
58
+ min_properties: Minimum number of properties in an object instance
68
59
  required: List of required property names
69
60
  enum: Fixed set of allowed values
70
61
 
71
62
  # JSON Schema Type and Structure
72
63
  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
64
+ all_of: Must be valid against all of the subschemas
65
+ one_of: Must be valid against exactly one of the subschemas
66
+ any_of: Must be valid against any of the subschemas
76
67
  not_: Must not be valid against the given schema
77
68
  items: Schema for array items (or array of schemas for tuple validation)
78
69
  properties: Property name to schema mappings
79
- additionalProperties: Schema for properties not defined in properties, or boolean to allow/disallow
70
+ additional_properties: Schema for properties not defined in properties, or boolean to allow/disallow
80
71
 
81
72
  # JSON Schema Metadata
82
73
  description: A short description. CommonMark syntax MAY be used for rich text representation.
@@ -86,10 +77,10 @@ class Schema:
86
77
  # OpenAPI-specific extensions
87
78
  nullable: Allows sending a null value
88
79
  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
80
+ read_only: Relevant only for Schema "properties" definitions - sent in response but not in request
81
+ write_only: Relevant only for Schema "properties" definitions - sent in request but not in response
91
82
  xml: Additional metadata for XML representations
92
- externalDocs: Additional external documentation
83
+ external_docs: Additional external documentation
93
84
  example: Example of the media type
94
85
  deprecated: Specifies that the schema is deprecated
95
86
 
@@ -100,31 +91,37 @@ class Schema:
100
91
 
101
92
  # JSON Schema Core validation keywords
102
93
  title: FieldSource[str] | None = fixed_field()
103
- multipleOf: FieldSource[int | float] | None = fixed_field()
94
+ multiple_of: FieldSource[int | float] | None = fixed_field(metadata={"yaml_name": "multipleOf"})
104
95
  maximum: FieldSource[int | float] | None = fixed_field()
105
- exclusiveMaximum: FieldSource[bool] | None = fixed_field()
96
+ exclusive_maximum: FieldSource[bool] | None = fixed_field(
97
+ metadata={"yaml_name": "exclusiveMaximum"}
98
+ )
106
99
  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()
100
+ exclusive_minimum: FieldSource[bool] | None = fixed_field(
101
+ metadata={"yaml_name": "exclusiveMinimum"}
102
+ )
103
+ max_length: FieldSource[int] | None = fixed_field(metadata={"yaml_name": "maxLength"})
104
+ min_length: FieldSource[int] | None = fixed_field(metadata={"yaml_name": "minLength"})
110
105
  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()
106
+ max_items: FieldSource[int] | None = fixed_field(metadata={"yaml_name": "maxItems"})
107
+ min_items: FieldSource[int] | None = fixed_field(metadata={"yaml_name": "minItems"})
108
+ unique_items: FieldSource[bool] | None = fixed_field(metadata={"yaml_name": "uniqueItems"})
109
+ max_properties: FieldSource[int] | None = fixed_field(metadata={"yaml_name": "maxProperties"})
110
+ min_properties: FieldSource[int] | None = fixed_field(metadata={"yaml_name": "minProperties"})
116
111
  required: FieldSource[list[ValueSource[str]]] | None = fixed_field()
117
112
  enum: FieldSource[list[ValueSource[YAMLValue]]] | None = fixed_field()
118
113
 
119
114
  # JSON Schema Type and Structure (nested schemas)
120
115
  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()
116
+ all_of: FieldSource[list[NestedSchema]] | None = fixed_field(metadata={"yaml_name": "allOf"})
117
+ one_of: FieldSource[list[NestedSchema]] | None = fixed_field(metadata={"yaml_name": "oneOf"})
118
+ any_of: FieldSource[list[NestedSchema]] | None = fixed_field(metadata={"yaml_name": "anyOf"})
124
119
  not_: FieldSource[NestedSchema] | None = fixed_field(metadata={"yaml_name": "not"})
125
120
  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()
121
+ properties: FieldSource[dict[KeySource[str], NestedSchema]] | None = fixed_field()
122
+ additional_properties: FieldSource["bool | NestedSchema"] | None = fixed_field(
123
+ metadata={"yaml_name": "additionalProperties"}
124
+ )
128
125
 
129
126
  # JSON Schema Metadata
130
127
  description: FieldSource[str] | None = fixed_field()
@@ -134,10 +131,12 @@ class Schema:
134
131
  # OpenAPI-specific extensions
135
132
  nullable: FieldSource[bool] | None = fixed_field()
136
133
  discriminator: FieldSource[Discriminator] | None = fixed_field()
137
- readOnly: FieldSource[bool] | None = fixed_field()
138
- writeOnly: FieldSource[bool] | None = fixed_field()
134
+ read_only: FieldSource[bool] | None = fixed_field(metadata={"yaml_name": "readOnly"})
135
+ write_only: FieldSource[bool] | None = fixed_field(metadata={"yaml_name": "writeOnly"})
139
136
  xml: FieldSource[XML] | None = fixed_field()
140
- externalDocs: FieldSource[ExternalDocumentation] | None = fixed_field()
137
+ external_docs: FieldSource[ExternalDocumentation] | None = fixed_field(
138
+ metadata={"yaml_name": "externalDocs"}
139
+ )
141
140
  example: FieldSource[YAMLValue] | None = fixed_field()
142
141
  deprecated: FieldSource[bool] | None = fixed_field()
143
142
 
@@ -153,8 +152,8 @@ def build(
153
152
  Preserves all source data as-is, regardless of type. This is a low-level/plumbing
154
153
  model that provides complete source fidelity for inspection and validation.
155
154
 
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
155
+ Note: Schema is self-referential (can contain other Schema objects in all_of, one_of, any_of, not_,
156
+ items, properties, additional_properties). The builder handles nested Schema objects by preserving
158
157
  them as raw YAML values, letting validation layers interpret them.
159
158
 
160
159
  Args:
@@ -172,11 +171,9 @@ def build(
172
171
  root = yaml.compose("type: string\\nminLength: 1\\nmaxLength: 100")
173
172
  schema = build(root)
174
173
  assert schema.type.value == 'string'
175
- assert schema.minLength.value == 1
174
+ assert schema.min_length.value == 1
176
175
  """
177
- # Initialize context once at the beginning
178
- if context is None:
179
- context = Context()
176
+ context = context or Context()
180
177
 
181
178
  if not isinstance(root, yaml.MappingNode):
182
179
  # Preserve invalid root data instead of returning None
@@ -216,10 +213,7 @@ def build(
216
213
  FieldSource[int | float],
217
214
  FieldSource[YAMLValue],
218
215
  }:
219
- value = context.yaml_constructor.construct_object(value_node, deep=True)
220
- field_values[field_name] = FieldSource(
221
- value=value, key_node=key_node, value_node=value_node
222
- )
216
+ field_values[field_name] = build_field_source(key_node, value_node, context)
223
217
 
224
218
  # Handle list with ValueSource wrapping for each item (e.g., required, enum fields)
225
219
  elif field_type_args & {
@@ -236,32 +230,26 @@ def build(
236
230
  )
237
231
  else:
238
232
  # Not a sequence - preserve as-is for validation
239
- value = context.yaml_constructor.construct_object(value_node, deep=True)
240
- field_values[field_name] = FieldSource(
241
- value=value, key_node=key_node, value_node=value_node
242
- )
233
+ field_values[field_name] = build_field_source(key_node, value_node, context)
243
234
 
244
235
  # Recursive schema list fields (allOf, oneOf, anyOf)
245
236
  elif key in ("allOf", "oneOf", "anyOf"):
246
237
  if isinstance(value_node, yaml.SequenceNode):
247
238
  schemas = []
248
239
  for item_node in value_node.value:
249
- schema_or_ref = _build_schema_or_reference(item_node, context)
250
- schemas.append(schema_or_ref)
240
+ schema_or_reference = build_schema_or_reference(item_node, context)
241
+ schemas.append(schema_or_reference)
251
242
  field_values[field_name] = FieldSource(
252
243
  value=schemas, key_node=key_node, value_node=value_node
253
244
  )
254
245
  else:
255
246
  # Not a sequence - preserve as-is for validation
256
- value = context.yaml_constructor.construct_object(value_node, deep=True)
257
- field_values[field_name] = FieldSource(
258
- value=value, key_node=key_node, value_node=value_node
259
- )
247
+ field_values[field_name] = build_field_source(key_node, value_node, context)
260
248
  # Recursive schema single fields (not, items)
261
249
  elif key in ("not", "items"):
262
- schema_or_ref = _build_schema_or_reference(value_node, context)
250
+ schema_or_reference = build_schema_or_reference(value_node, context)
263
251
  field_values[field_name] = FieldSource(
264
- value=schema_or_ref, key_node=key_node, value_node=value_node
252
+ value=schema_or_reference, key_node=key_node, value_node=value_node
265
253
  )
266
254
  # additionalProperties (boolean | schema | reference)
267
255
  elif key == "additionalProperties":
@@ -270,36 +258,32 @@ def build(
270
258
  isinstance(value_node, yaml.ScalarNode)
271
259
  and value_node.tag == "tag:yaml.org,2002:bool"
272
260
  ):
273
- value = context.yaml_constructor.construct_object(value_node)
274
- field_values[field_name] = FieldSource(
275
- value=value, key_node=key_node, value_node=value_node
276
- )
261
+ field_values[field_name] = build_field_source(key_node, value_node, context)
277
262
  else:
278
263
  # It's a schema or reference
279
- schema_or_ref = _build_schema_or_reference(value_node, context)
264
+ schema_or_reference = build_schema_or_reference(value_node, context)
280
265
  field_values[field_name] = FieldSource(
281
- value=schema_or_ref, key_node=key_node, value_node=value_node
266
+ value=schema_or_reference, key_node=key_node, value_node=value_node
282
267
  )
283
- # properties (dict[KeySource[str], ValueSource[NestedSchema]])
268
+ # properties (dict[KeySource[str], NestedSchema])
284
269
  elif key == "properties":
285
270
  if isinstance(value_node, yaml.MappingNode):
286
- properties_dict: dict[KeySource[str], ValueSource[NestedSchema]] = {}
271
+ properties_dict: dict[KeySource[str], NestedSchema] = {}
287
272
  for prop_key_node, prop_value_node in value_node.value:
288
273
  prop_key = context.yaml_constructor.construct_yaml_str(prop_key_node)
289
274
  # Recursively build each property schema
290
- prop_schema_or_ref = _build_schema_or_reference(prop_value_node, context)
275
+ prop_schema_or_reference: NestedSchema = build_schema_or_reference(
276
+ prop_value_node, context
277
+ )
291
278
  properties_dict[KeySource(value=prop_key, key_node=prop_key_node)] = (
292
- ValueSource(value=prop_schema_or_ref, value_node=prop_value_node)
279
+ prop_schema_or_reference
293
280
  )
294
281
  field_values[field_name] = FieldSource(
295
282
  value=properties_dict, key_node=key_node, value_node=value_node
296
283
  )
297
284
  else:
298
285
  # Not a mapping - preserve as-is for validation
299
- value = context.yaml_constructor.construct_object(value_node, deep=True)
300
- field_values[field_name] = FieldSource(
301
- value=value, key_node=key_node, value_node=value_node
302
- )
286
+ field_values[field_name] = build_field_source(key_node, value_node, context)
303
287
  # Nested objects (discriminator, xml, externalDocs)
304
288
  elif key == "discriminator":
305
289
  field_values[field_name] = FieldSource(
@@ -328,7 +312,7 @@ def build(
328
312
  )
329
313
 
330
314
 
331
- def _build_schema_or_reference(node: yaml.Node, context: Context) -> NestedSchema:
315
+ def build_schema_or_reference(node: yaml.Node, context: Context) -> NestedSchema:
332
316
  """
333
317
  Build either a Schema or Reference from a YAML node.
334
318
 
@@ -3,9 +3,9 @@ from dataclasses import dataclass
3
3
  from ruamel import yaml
4
4
  from ruamel.yaml import MappingNode, SequenceNode
5
5
 
6
- from jentic.apitools.openapi.datamodels.low.context import Context
7
- from jentic.apitools.openapi.datamodels.low.fields import patterned_field
8
- from jentic.apitools.openapi.datamodels.low.sources import KeySource, ValueSource
6
+ from ..context import Context
7
+ from ..fields import patterned_field
8
+ from ..sources import KeySource, ValueSource, YAMLInvalidValue
9
9
 
10
10
 
11
11
  __all__ = ["SecurityRequirement", "build"]
@@ -38,7 +38,9 @@ class SecurityRequirement:
38
38
  )
39
39
 
40
40
 
41
- def build(root: yaml.Node, context: Context | None = None) -> SecurityRequirement | None:
41
+ def build(
42
+ root: yaml.Node, context: Context | None = None
43
+ ) -> SecurityRequirement | ValueSource[YAMLInvalidValue]:
42
44
  """
43
45
  Build a SecurityRequirement object from a YAML node.
44
46
 
@@ -50,7 +52,9 @@ def build(root: yaml.Node, context: Context | None = None) -> SecurityRequiremen
50
52
  context: Optional parsing context. If None, a default context will be created.
51
53
 
52
54
  Returns:
53
- A SecurityRequirement object if the node is valid, None otherwise
55
+ A SecurityRequirement object if the node is valid, or a ValueSource containing
56
+ the invalid data if the root is not a MappingNode (preserving the invalid data
57
+ and its source location for validation).
54
58
 
55
59
  Example:
56
60
  from ruamel.yaml import YAML
@@ -59,11 +63,12 @@ def build(root: yaml.Node, context: Context | None = None) -> SecurityRequiremen
59
63
  security_req = build(root)
60
64
  assert security_req.requirements is not None
61
65
  """
62
- if not isinstance(root, MappingNode):
63
- return None
66
+ context = context or Context()
64
67
 
65
- if context is None:
66
- context = Context()
68
+ if not isinstance(root, MappingNode):
69
+ # Preserve invalid root data instead of returning None
70
+ value = context.yaml_constructor.construct_object(root, deep=True)
71
+ return ValueSource(value=value, value_node=root)
67
72
 
68
73
  requirements_dict: dict[KeySource[str], ValueSource[list[ValueSource[str]]]] = {}
69
74
 
@@ -2,21 +2,17 @@ from dataclasses import dataclass, field, replace
2
2
 
3
3
  from ruamel import yaml
4
4
 
5
- from jentic.apitools.openapi.datamodels.low.context import Context
6
- from jentic.apitools.openapi.datamodels.low.fields import fixed_field
7
- from jentic.apitools.openapi.datamodels.low.model_builder import build_model
8
- from jentic.apitools.openapi.datamodels.low.sources import (
9
- FieldSource,
10
- KeySource,
11
- ValueSource,
12
- YAMLInvalidValue,
13
- YAMLValue,
14
- )
15
- from jentic.apitools.openapi.datamodels.low.v30.oauth_flows import OAuthFlows
16
- from jentic.apitools.openapi.datamodels.low.v30.oauth_flows import build as build_oauth_flows
5
+ from ..context import Context
6
+ from ..fields import fixed_field
7
+ from ..sources import FieldSource, KeySource, ValueSource, YAMLInvalidValue, YAMLValue
8
+ from .builders import build_model
9
+ from .oauth_flows import OAuthFlows
10
+ from .oauth_flows import build as build_oauth_flows
11
+ from .reference import Reference
12
+ from .reference import build as build_reference
17
13
 
18
14
 
19
- __all__ = ["SecurityScheme", "build"]
15
+ __all__ = ["SecurityScheme", "build", "build_security_scheme_or_reference"]
20
16
 
21
17
 
22
18
  @dataclass(frozen=True, slots=True)
@@ -79,18 +75,15 @@ def build(
79
75
  security_scheme = build(root)
80
76
  assert security_scheme.type.value == 'apiKey'
81
77
  """
82
- # Initialize context once at the beginning
83
- if context is None:
84
- context = Context()
78
+ context = context or Context()
85
79
 
86
- if not isinstance(root, yaml.MappingNode):
87
- # Preserve invalid root data instead of returning None
88
- value = context.yaml_constructor.construct_object(root, deep=True)
89
- return ValueSource(value=value, value_node=root)
90
-
91
- # Use build_model to handle most fields
80
+ # Use build_model for initial construction
92
81
  security_scheme = build_model(root, SecurityScheme, context=context)
93
82
 
83
+ # If build_model returned ValueSource (invalid node), return it immediately
84
+ if not isinstance(security_scheme, SecurityScheme):
85
+ return security_scheme
86
+
94
87
  # Manually handle special fields that build_model can't process (nested objects)
95
88
  for key_node, value_node in root.value:
96
89
  key = context.yaml_constructor.construct_yaml_str(key_node)
@@ -107,3 +100,30 @@ def build(
107
100
  break
108
101
 
109
102
  return security_scheme
103
+
104
+
105
+ def build_security_scheme_or_reference(
106
+ node: yaml.Node, context: Context
107
+ ) -> SecurityScheme | Reference | ValueSource[YAMLInvalidValue]:
108
+ """
109
+ Build either a SecurityScheme or Reference from a YAML node.
110
+
111
+ This helper handles the polymorphic nature of OpenAPI where many fields
112
+ can contain either a SecurityScheme object or a Reference object ($ref).
113
+
114
+ Args:
115
+ node: The YAML node to parse
116
+ context: Parsing context
117
+
118
+ Returns:
119
+ A SecurityScheme, Reference, or ValueSource if the node is invalid
120
+ """
121
+ # Check if it's a reference (has $ref key)
122
+ if isinstance(node, yaml.MappingNode):
123
+ for key_node, _ in node.value:
124
+ key = context.yaml_constructor.construct_yaml_str(key_node)
125
+ if key == "$ref":
126
+ return build_reference(node, context)
127
+
128
+ # Otherwise, try to build as SecurityScheme
129
+ return build(node, context)
@@ -0,0 +1,111 @@
1
+ from dataclasses import dataclass, field, replace
2
+
3
+ from ruamel import yaml
4
+
5
+ from ..context import Context
6
+ from ..fields import fixed_field
7
+ from ..sources import FieldSource, KeySource, ValueSource, YAMLInvalidValue, YAMLValue
8
+ from .builders import build_field_source, build_model
9
+ from .server_variable import ServerVariable
10
+ from .server_variable import build as build_server_variable
11
+
12
+
13
+ __all__ = ["Server", "build"]
14
+
15
+
16
+ @dataclass(frozen=True, slots=True)
17
+ class Server:
18
+ """
19
+ Server Object representation for OpenAPI 3.0.
20
+
21
+ An object representing a Server.
22
+
23
+ Attributes:
24
+ root_node: The top-level node representing the entire Server object in the original source file
25
+ url: REQUIRED. A URL to the target host. This URL supports Server Variables and MAY be relative, to indicate that the host location is relative to the location where the OpenAPI document is being served. Variable substitutions will be made when a variable is named in {brackets}.
26
+ description: An optional string describing the host designated by the URL. CommonMark syntax MAY be used for rich text representation.
27
+ variables: A map between a variable name and its value. The value is used for substitution in the server's URL template.
28
+ extensions: Specification extensions (x-* fields)
29
+ """
30
+
31
+ root_node: yaml.Node
32
+ url: FieldSource[str] | None = fixed_field()
33
+ description: FieldSource[str] | None = fixed_field()
34
+ variables: FieldSource[dict[KeySource[str], ServerVariable]] | None = fixed_field()
35
+ extensions: dict[KeySource[str], ValueSource[YAMLValue]] = field(default_factory=dict)
36
+
37
+
38
+ def build(
39
+ root: yaml.Node, context: Context | None = None
40
+ ) -> Server | ValueSource[YAMLInvalidValue]:
41
+ """
42
+ Build a Server object from a YAML node.
43
+
44
+ Preserves all source data as-is, regardless of type. This is a low-level/plumbing
45
+ model that provides complete source fidelity for inspection and validation.
46
+
47
+ Args:
48
+ root: The YAML node to parse (should be a MappingNode)
49
+ context: Optional parsing context. If None, a default context will be created.
50
+
51
+ Returns:
52
+ A Server object if the node is valid, or a ValueSource containing
53
+ the invalid data if the root is not a MappingNode (preserving the invalid data
54
+ and its source location for validation).
55
+
56
+ Example:
57
+ from ruamel.yaml import YAML
58
+ yaml = YAML()
59
+ root = yaml.compose('''
60
+ url: https://{environment}.example.com/api/v1
61
+ description: Production API server
62
+ variables:
63
+ environment:
64
+ default: production
65
+ enum:
66
+ - production
67
+ - staging
68
+ - development
69
+ description: The deployment environment
70
+ ''')
71
+ server = build(root)
72
+ assert server.url.value == 'https://{environment}.example.com/api/v1'
73
+ assert server.description.value == 'Production API server'
74
+ assert 'environment' in {k.value for k in server.variables.value.keys()}
75
+ """
76
+ context = context or Context()
77
+
78
+ # Use build_model for initial construction
79
+ server = build_model(root, Server, context=context)
80
+
81
+ # If build_model returned ValueSource (invalid node), return it immediately
82
+ if not isinstance(server, Server):
83
+ return server
84
+
85
+ # Manually handle nested variables field
86
+ for key_node, value_node in root.value:
87
+ key = context.yaml_constructor.construct_yaml_str(key_node)
88
+
89
+ if key == "variables":
90
+ # Handle variables field - map of ServerVariable objects
91
+ if isinstance(value_node, yaml.MappingNode):
92
+ variables_dict: dict[
93
+ KeySource[str], ServerVariable | ValueSource[YAMLInvalidValue]
94
+ ] = {}
95
+ for var_key_node, var_value_node in value_node.value:
96
+ var_key = context.yaml_constructor.construct_yaml_str(var_key_node)
97
+ # Build ServerVariable for each variable - child builder handles invalid nodes
98
+ variables_dict[KeySource(value=var_key, key_node=var_key_node)] = (
99
+ build_server_variable(var_value_node, context=context)
100
+ )
101
+ variables = FieldSource(
102
+ value=variables_dict, key_node=key_node, value_node=value_node
103
+ )
104
+ server = replace(server, variables=variables)
105
+ else:
106
+ # Not a mapping - preserve as-is for validation
107
+ variables = build_field_source(key_node, value_node, context)
108
+ server = replace(server, variables=variables)
109
+ break
110
+
111
+ return server