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
@@ -2,16 +2,10 @@ from dataclasses import dataclass, field
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
- )
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
15
9
 
16
10
 
17
11
  __all__ = ["ServerVariable", "build"]
@@ -1,23 +1,12 @@
1
- from dataclasses import dataclass, field, replace
1
+ from dataclasses import dataclass, field
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.external_documentation import (
16
- ExternalDocumentation,
17
- )
18
- from jentic.apitools.openapi.datamodels.low.v30.external_documentation import (
19
- build as build_external_documentation,
20
- )
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 .external_documentation import ExternalDocumentation
21
10
 
22
11
 
23
12
  __all__ = ["Tag", "build"]
@@ -42,7 +31,7 @@ class Tag:
42
31
  root_node: yaml.Node
43
32
  name: FieldSource[str] | None = fixed_field()
44
33
  description: FieldSource[str] | None = fixed_field()
45
- external_docs: FieldSource[ExternalDocumentation] | None = fixed_field(
34
+ external_docs: FieldSource["ExternalDocumentation"] | None = fixed_field(
46
35
  metadata={"yaml_name": "externalDocs"}
47
36
  )
48
37
  extensions: dict[KeySource[str], ValueSource[YAMLValue]] = field(default_factory=dict)
@@ -71,31 +60,4 @@ def build(root: yaml.Node, context: Context | None = None) -> Tag | ValueSource[
71
60
  tag = build(root)
72
61
  assert tag.name.value == 'pet'
73
62
  """
74
- # Initialize context once at the beginning
75
- if context is None:
76
- context = Context()
77
-
78
- if not isinstance(root, yaml.MappingNode):
79
- # Preserve invalid root data instead of returning None
80
- value = context.yaml_constructor.construct_object(root, deep=True)
81
- return ValueSource(value=value, value_node=root)
82
-
83
- # Use build_model to handle most fields
84
- tag = build_model(root, Tag, context=context)
85
-
86
- # Manually handle special fields that build_model can't process (nested objects)
87
- for key_node, value_node in root.value:
88
- key = context.yaml_constructor.construct_yaml_str(key_node)
89
-
90
- if key == "externalDocs":
91
- # Handle nested ExternalDocumentation object - child builder handles invalid nodes
92
- # FieldSource will auto-unwrap ValueSource if child returns it for invalid data
93
- external_docs = FieldSource(
94
- value=build_external_documentation(value_node, context=context),
95
- key_node=key_node,
96
- value_node=value_node,
97
- )
98
- tag = replace(tag, external_docs=external_docs)
99
- break
100
-
101
- return tag
63
+ return build_model(root, Tag, context=context)
@@ -2,16 +2,10 @@ from dataclasses import dataclass, field
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
- )
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
15
9
 
16
10
 
17
11
  __all__ = ["XML", "build"]
@@ -0,0 +1,77 @@
1
+ """OpenAPI 3.1.x Low-Level Data Models.
2
+
3
+ This module provides low-level/plumbing data models for OpenAPI 3.1 specification objects.
4
+ These models preserve complete source fidelity for inspection and validation.
5
+ """
6
+
7
+ # Main entry point for parsing OpenAPI documents
8
+ # All dataclasses for type hints and isinstance checks
9
+ from .callback import Callback
10
+ from .components import Components
11
+ from .contact import Contact
12
+ from .discriminator import Discriminator
13
+ from .encoding import Encoding
14
+ from .example import Example
15
+ from .external_documentation import ExternalDocumentation
16
+ from .header import Header
17
+ from .info import Info
18
+ from .license import License
19
+ from .link import Link
20
+ from .media_type import MediaType
21
+ from .oauth_flow import OAuthFlow
22
+ from .oauth_flows import OAuthFlows
23
+ from .openapi import OpenAPI31, build
24
+ from .operation import Operation
25
+ from .parameter import Parameter
26
+ from .path_item import PathItem
27
+ from .paths import Paths
28
+ from .reference import Reference
29
+ from .request_body import RequestBody
30
+ from .response import Response
31
+ from .responses import Responses
32
+ from .schema import BooleanJSONSchema, Schema
33
+ from .security_requirement import SecurityRequirement
34
+ from .security_scheme import SecurityScheme
35
+ from .server import Server
36
+ from .server_variable import ServerVariable
37
+ from .tag import Tag
38
+ from .xml import XML
39
+
40
+
41
+ __all__ = [
42
+ # Main entry point
43
+ "build",
44
+ # Root object
45
+ "OpenAPI31",
46
+ # All dataclasses (alphabetically sorted)
47
+ "Callback",
48
+ "Components",
49
+ "Contact",
50
+ "Discriminator",
51
+ "Encoding",
52
+ "Example",
53
+ "ExternalDocumentation",
54
+ "Header",
55
+ "Info",
56
+ "License",
57
+ "Link",
58
+ "MediaType",
59
+ "OAuthFlow",
60
+ "OAuthFlows",
61
+ "Operation",
62
+ "Parameter",
63
+ "PathItem",
64
+ "Paths",
65
+ "Reference",
66
+ "RequestBody",
67
+ "Response",
68
+ "Responses",
69
+ "Schema",
70
+ "BooleanJSONSchema",
71
+ "SecurityRequirement",
72
+ "SecurityScheme",
73
+ "Server",
74
+ "ServerVariable",
75
+ "Tag",
76
+ "XML",
77
+ ]
@@ -0,0 +1,347 @@
1
+ """Builders for OpenAPI 3.1.x specification objects."""
2
+
3
+ from dataclasses import fields
4
+ from typing import TYPE_CHECKING, Any, TypeVar, cast, get_args
5
+
6
+ from ruamel import yaml
7
+
8
+ from ...context import Context
9
+ from ...extractors import extract_extension_fields
10
+ from ...fields import fixed_fields
11
+ from ...sources import FieldSource, KeySource, ValueSource, YAMLInvalidValue, YAMLValue
12
+
13
+
14
+ if TYPE_CHECKING:
15
+ from ..callback import Callback # noqa: F401
16
+ from ..example import Example # noqa: F401
17
+ from ..external_documentation import ExternalDocumentation # noqa: F401
18
+ from ..header import Header # noqa: F401
19
+ from ..link import Link # noqa: F401
20
+ from ..media_type import MediaType # noqa: F401
21
+ from ..parameter import Parameter # noqa: F401
22
+ from ..path_item import PathItem # noqa: F401
23
+ from ..reference import Reference # noqa: F401
24
+ from ..schema import BooleanJSONSchema, Schema # noqa: F401
25
+ from ..security_requirement import SecurityRequirement # noqa: F401
26
+ from ..server import Server # noqa: F401
27
+
28
+
29
+ __all__ = ["build_model", "build_field_source"]
30
+
31
+
32
+ T = TypeVar("T")
33
+
34
+
35
+ def build_model(
36
+ root: yaml.Node, dataclass_type: type[T], *, context: Context | None = None
37
+ ) -> T | ValueSource[YAMLInvalidValue]:
38
+ """
39
+ Generic builder for OpenAPI 3.1 low model.
40
+
41
+ Builds any dataclass that follows the pattern:
42
+ - Has a required `root_node: yaml.Node` field
43
+ - Has an optional `extensions: dict[...]` field
44
+ - Has spec fields marked with `fixed_field()`
45
+
46
+ Args:
47
+ root: The YAML node to parse (should be a MappingNode)
48
+ dataclass_type: The dataclass type to build
49
+ context: Optional parsing context. If None, a default context will be created.
50
+
51
+ Returns:
52
+ An instance of dataclass_type 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
+ xml = build_model(root_node, XML, context=context)
58
+ """
59
+ # Initialize context once at the beginning
60
+ if context is None:
61
+ context = Context()
62
+
63
+ if not isinstance(root, yaml.MappingNode):
64
+ # Preserve invalid root data instead of returning None
65
+ value = context.yaml_constructor.construct_object(root, deep=True)
66
+ return ValueSource(value=value, value_node=root)
67
+
68
+ # Get fixed specification fields for this dataclass type
69
+ _fixed_fields = fixed_fields(dataclass_type)
70
+
71
+ # Build YAML name to Python field name mapping
72
+ yaml_to_field = {
73
+ field.metadata.get("yaml_name", fname): fname for fname, field in _fixed_fields.items()
74
+ }
75
+
76
+ # Extract field values in a single pass (non-recursive, single layer only)
77
+ field_values: dict[str, FieldSource[Any]] = {}
78
+ for key_node, value_node in root.value:
79
+ key = context.yaml_constructor.construct_yaml_str(key_node)
80
+
81
+ # Map YAML key to Python field name
82
+ field_name = yaml_to_field.get(key)
83
+ if field_name:
84
+ field = _fixed_fields[field_name]
85
+ field_type_args = set(get_args(field.type))
86
+
87
+ if field_type_args & {
88
+ FieldSource[str],
89
+ FieldSource[bool],
90
+ FieldSource[int],
91
+ FieldSource[YAMLValue],
92
+ }:
93
+ field_values[field_name] = build_field_source(key_node, value_node, context)
94
+ elif field_type_args & {FieldSource[dict[KeySource[str], ValueSource[str]]]}:
95
+ # Handle dict with KeySource/ValueSource wrapping
96
+ if isinstance(value_node, yaml.MappingNode):
97
+ mapping_dict: dict[KeySource[str], ValueSource[str]] = {}
98
+ for map_key_node, map_value_node in value_node.value:
99
+ map_key = context.yaml_constructor.construct_yaml_str(map_key_node)
100
+ map_value = context.yaml_constructor.construct_object(
101
+ map_value_node, deep=True
102
+ )
103
+ mapping_dict[KeySource(value=map_key, key_node=map_key_node)] = ValueSource(
104
+ value=map_value, value_node=map_value_node
105
+ )
106
+ field_values[field_name] = FieldSource(
107
+ value=mapping_dict, key_node=key_node, value_node=value_node
108
+ )
109
+ else:
110
+ # Not a mapping - preserve as-is for validation
111
+ field_values[field_name] = build_field_source(key_node, value_node, context)
112
+ elif field_type_args & {FieldSource[list[ValueSource[str]]]}:
113
+ # Handle list with ValueSource wrapping for each item
114
+ if isinstance(value_node, yaml.SequenceNode):
115
+ value_list: list[ValueSource[str]] = []
116
+ for item_node in value_node.value:
117
+ item_value = context.yaml_constructor.construct_object(item_node, deep=True)
118
+ value_list.append(ValueSource(value=item_value, value_node=item_node))
119
+ field_values[field_name] = FieldSource(
120
+ value=value_list, key_node=key_node, value_node=value_node
121
+ )
122
+ else:
123
+ # Not a sequence - preserve as-is for validation
124
+ field_values[field_name] = build_field_source(key_node, value_node, context)
125
+ elif field_type_args & {FieldSource[list["Server"]]}:
126
+ # Handle list[Server] with lazy import
127
+ from ..server import build as build_server
128
+
129
+ if isinstance(value_node, yaml.SequenceNode):
130
+ servers_list = []
131
+ for item_node in value_node.value:
132
+ server_obj = build_server(item_node, context)
133
+ servers_list.append(server_obj)
134
+ field_values[field_name] = FieldSource(
135
+ value=servers_list, key_node=key_node, value_node=value_node
136
+ )
137
+ else:
138
+ # Not a sequence - preserve as-is for validation
139
+ field_values[field_name] = build_field_source(key_node, value_node, context)
140
+ elif field_type_args & {FieldSource[list["SecurityRequirement"]]}:
141
+ # Handle list[SecurityRequirement] with lazy import
142
+ from ..security_requirement import build as build_security_requirement
143
+
144
+ if isinstance(value_node, yaml.SequenceNode):
145
+ security_list = []
146
+ for item_node in value_node.value:
147
+ security_req = build_security_requirement(item_node, context)
148
+ security_list.append(security_req)
149
+ field_values[field_name] = FieldSource(
150
+ value=security_list, key_node=key_node, value_node=value_node
151
+ )
152
+ else:
153
+ # Not a sequence - preserve as-is for validation
154
+ field_values[field_name] = build_field_source(key_node, value_node, context)
155
+ elif field_type_args & {FieldSource[list["Parameter | Reference"]]}:
156
+ # Handle list[Parameter | Reference] with lazy import
157
+ from ..parameter import build_parameter_or_reference
158
+
159
+ if isinstance(value_node, yaml.SequenceNode):
160
+ parameters_list = []
161
+ for item_node in value_node.value:
162
+ parameter_or_reference = build_parameter_or_reference(item_node, context)
163
+ parameters_list.append(parameter_or_reference)
164
+ field_values[field_name] = FieldSource(
165
+ value=parameters_list, key_node=key_node, value_node=value_node
166
+ )
167
+ else:
168
+ # Not a sequence - preserve as-is for validation
169
+ field_values[field_name] = build_field_source(key_node, value_node, context)
170
+ elif field_type_args & {FieldSource[dict[KeySource[str], "Example | Reference"]]}:
171
+ # Handle dict[KeySource[str], Example | Reference] with lazy import
172
+ from ..example import build_example_or_reference
173
+
174
+ if isinstance(value_node, yaml.MappingNode):
175
+ examples_dict = {}
176
+ for map_key_node, map_value_node in value_node.value:
177
+ map_key = context.yaml_constructor.construct_yaml_str(map_key_node)
178
+ example_or_reference = build_example_or_reference(map_value_node, context)
179
+ examples_dict[KeySource(value=map_key, key_node=map_key_node)] = (
180
+ example_or_reference
181
+ )
182
+ field_values[field_name] = FieldSource(
183
+ value=examples_dict, key_node=key_node, value_node=value_node
184
+ )
185
+ else:
186
+ # Not a mapping - preserve as-is for validation
187
+ field_values[field_name] = build_field_source(key_node, value_node, context)
188
+ elif field_type_args & {FieldSource[dict[KeySource[str], "MediaType"]]}:
189
+ # Handle dict[KeySource[str], MediaType] with lazy import
190
+ from ..media_type import build as build_media_type
191
+
192
+ if isinstance(value_node, yaml.MappingNode):
193
+ content_dict = {}
194
+ for map_key_node, map_value_node in value_node.value:
195
+ map_key = context.yaml_constructor.construct_yaml_str(map_key_node)
196
+ media_type_obj = build_media_type(map_value_node, context)
197
+ content_dict[KeySource(value=map_key, key_node=map_key_node)] = (
198
+ media_type_obj
199
+ )
200
+ field_values[field_name] = FieldSource(
201
+ value=content_dict, key_node=key_node, value_node=value_node
202
+ )
203
+ else:
204
+ # Not a mapping - preserve as-is for validation
205
+ field_values[field_name] = build_field_source(key_node, value_node, context)
206
+ elif field_type_args & {FieldSource[dict[KeySource[str], "Header | Reference"]]}:
207
+ # Handle dict[KeySource[str], Header | Reference] with lazy import
208
+ from ..header import build_header_or_reference
209
+
210
+ if isinstance(value_node, yaml.MappingNode):
211
+ headers_dict = {}
212
+ for map_key_node, map_value_node in value_node.value:
213
+ map_key = context.yaml_constructor.construct_yaml_str(map_key_node)
214
+ header_or_reference = build_header_or_reference(map_value_node, context)
215
+ headers_dict[KeySource(value=map_key, key_node=map_key_node)] = (
216
+ header_or_reference
217
+ )
218
+ field_values[field_name] = FieldSource(
219
+ value=headers_dict, key_node=key_node, value_node=value_node
220
+ )
221
+ else:
222
+ # Not a mapping - preserve as-is for validation
223
+ field_values[field_name] = build_field_source(key_node, value_node, context)
224
+ elif field_type_args & {FieldSource["ExternalDocumentation"]}:
225
+ # Handle ExternalDocumentation with lazy import
226
+ from ..external_documentation import build as build_external_docs
227
+
228
+ field_values[field_name] = FieldSource(
229
+ value=build_external_docs(value_node, context),
230
+ key_node=key_node,
231
+ value_node=value_node,
232
+ )
233
+ elif field_type_args & {FieldSource["Schema | BooleanJSONSchema"]}:
234
+ # Handle Schema | Reference union with lazy import
235
+ from ..schema import build as build_schema
236
+
237
+ field_values[field_name] = FieldSource(
238
+ value=build_schema(value_node, context),
239
+ key_node=key_node,
240
+ value_node=value_node,
241
+ )
242
+ elif field_type_args & {FieldSource[dict[KeySource[str], "PathItem"]]}:
243
+ # Handle dict[KeySource[str], PathItem] with lazy import
244
+ from ..path_item import build as build_path_item
245
+
246
+ if isinstance(value_node, yaml.MappingNode):
247
+ path_items_dict = {}
248
+ for map_key_node, map_value_node in value_node.value:
249
+ map_key = context.yaml_constructor.construct_yaml_str(map_key_node)
250
+ path_item = build_path_item(map_value_node, context)
251
+ path_items_dict[KeySource(value=map_key, key_node=map_key_node)] = path_item
252
+ field_values[field_name] = FieldSource(
253
+ value=path_items_dict, key_node=key_node, value_node=value_node
254
+ )
255
+ else:
256
+ # Not a mapping - preserve as-is for validation
257
+ field_values[field_name] = build_field_source(key_node, value_node, context)
258
+ elif field_type_args & {FieldSource[dict[KeySource[str], "Callback | Reference"]]}:
259
+ # Handle dict[KeySource[str], Callback | Reference] with lazy import
260
+ from ..callback import build_callback_or_reference
261
+
262
+ if isinstance(value_node, yaml.MappingNode):
263
+ callbacks_dict = {}
264
+ for map_key_node, map_value_node in value_node.value:
265
+ map_key = context.yaml_constructor.construct_yaml_str(map_key_node)
266
+ callback_or_reference = build_callback_or_reference(map_value_node, context)
267
+ callbacks_dict[KeySource(value=map_key, key_node=map_key_node)] = (
268
+ callback_or_reference
269
+ )
270
+ field_values[field_name] = FieldSource(
271
+ value=callbacks_dict, key_node=key_node, value_node=value_node
272
+ )
273
+ else:
274
+ # Not a mapping - preserve as-is for validation
275
+ field_values[field_name] = build_field_source(key_node, value_node, context)
276
+ elif field_type_args & {FieldSource[dict[KeySource[str], "Header | Reference"]]}:
277
+ # Handle dict[KeySource[str], Header | Reference] with lazy import
278
+ from ..header import build_header_or_reference
279
+
280
+ if isinstance(value_node, yaml.MappingNode):
281
+ headers_dict = {}
282
+ for map_key_node, map_value_node in value_node.value:
283
+ map_key = context.yaml_constructor.construct_yaml_str(map_key_node)
284
+ header_or_reference = build_header_or_reference(map_value_node, context)
285
+ headers_dict[KeySource(value=map_key, key_node=map_key_node)] = (
286
+ header_or_reference
287
+ )
288
+ field_values[field_name] = FieldSource(
289
+ value=headers_dict, key_node=key_node, value_node=value_node
290
+ )
291
+ else:
292
+ # Not a mapping - preserve as-is for validation
293
+ field_values[field_name] = build_field_source(key_node, value_node, context)
294
+ elif field_type_args & {FieldSource[dict[KeySource[str], "Link | Reference"]]}:
295
+ # Handle dict[KeySource[str], Link | Reference] with lazy import
296
+ from ..link import build_link_or_reference
297
+
298
+ if isinstance(value_node, yaml.MappingNode):
299
+ links_dict = {}
300
+ for map_key_node, map_value_node in value_node.value:
301
+ map_key = context.yaml_constructor.construct_yaml_str(map_key_node)
302
+ link_or_reference = build_link_or_reference(map_value_node, context)
303
+ links_dict[KeySource(value=map_key, key_node=map_key_node)] = (
304
+ link_or_reference
305
+ )
306
+ field_values[field_name] = FieldSource(
307
+ value=links_dict, key_node=key_node, value_node=value_node
308
+ )
309
+ else:
310
+ # Not a mapping - preserve as-is for validation
311
+ field_values[field_name] = build_field_source(key_node, value_node, context)
312
+
313
+ # Build and return the dataclass instance
314
+ # Conditionally include extensions field if dataclass supports it
315
+ # Cast to Any to work around generic type constraints
316
+ has_extensions = any(f.name == "extensions" for f in fields(cast(Any, dataclass_type)))
317
+ return cast(
318
+ T,
319
+ dataclass_type(
320
+ root_node=root, # type: ignore[call-arg]
321
+ **field_values,
322
+ **({"extensions": extract_extension_fields(root, context)} if has_extensions else {}),
323
+ ),
324
+ )
325
+
326
+
327
+ def build_field_source(
328
+ key_node: yaml.Node,
329
+ value_node: yaml.Node,
330
+ context: Context,
331
+ ) -> FieldSource[Any]:
332
+ """
333
+ Build a FieldSource from a YAML node.
334
+
335
+ Constructs the Python value from the YAML node and wraps it in a FieldSource
336
+ that preserves the original node locations for error reporting.
337
+
338
+ Args:
339
+ key_node: The YAML node for the field key
340
+ value_node: The YAML node for the field value
341
+ context: Parsing context
342
+
343
+ Returns:
344
+ A FieldSource containing the constructed value and node references
345
+ """
346
+ value = context.yaml_constructor.construct_object(value_node, deep=True)
347
+ return FieldSource(value=value, key_node=key_node, value_node=value_node)
@@ -0,0 +1,131 @@
1
+ from dataclasses import dataclass, field
2
+
3
+ from ruamel import yaml
4
+
5
+ from ..context import Context
6
+ from ..extractors import extract_extension_fields
7
+ from ..sources import KeySource, ValueSource, YAMLInvalidValue, YAMLValue
8
+ from .path_item import PathItem
9
+ from .path_item import build as build_path_item
10
+ from .reference import Reference
11
+ from .reference import build as build_reference
12
+
13
+
14
+ __all__ = ["Callback", "build", "build_callback_or_reference"]
15
+
16
+
17
+ @dataclass(frozen=True, slots=True)
18
+ class Callback:
19
+ """
20
+ Callback Object representation for OpenAPI 3.1.
21
+
22
+ A map of possible out-of-band callbacks related to the parent operation. Each value in the map
23
+ is a Path Item Object that describes a set of requests that may be initiated by the API provider
24
+ and the expected responses.
25
+
26
+ The key is a runtime expression that identifies a URL to use for the callback operation.
27
+
28
+ Attributes:
29
+ root_node: The top-level node representing the entire Callback object in the original source file
30
+ path_items: Map of expression keys to Path Item Objects. Each key is a runtime expression
31
+ that will be evaluated to determine the callback URL.
32
+ extensions: Specification extensions (x-* fields)
33
+ """
34
+
35
+ root_node: yaml.Node
36
+ path_items: dict[KeySource[str], PathItem] = field(default_factory=dict)
37
+ extensions: dict[KeySource[str], ValueSource[YAMLValue]] = field(default_factory=dict)
38
+
39
+
40
+ def build(
41
+ root: yaml.Node, context: Context | None = None
42
+ ) -> Callback | ValueSource[YAMLInvalidValue]:
43
+ """
44
+ Build a Callback object from a YAML node.
45
+
46
+ Preserves all source data as-is, regardless of type. This is a low-level/plumbing
47
+ model that provides complete source fidelity for inspection and validation.
48
+
49
+ Args:
50
+ root: The YAML node to parse (should be a MappingNode)
51
+ context: Optional parsing context. If None, a default context will be created.
52
+
53
+ Returns:
54
+ A Callback object if the node is valid, or a ValueSource containing
55
+ the invalid data if the root is not a MappingNode (preserving the invalid data
56
+ and its source location for validation).
57
+
58
+ Example:
59
+ from ruamel.yaml import YAML
60
+ yaml = YAML()
61
+ root = yaml.compose('''
62
+ '{$request.body#/callbackUrl}':
63
+ post:
64
+ requestBody:
65
+ required: true
66
+ content:
67
+ application/json:
68
+ schema:
69
+ type: object
70
+ responses:
71
+ '200':
72
+ description: callback successfully processed
73
+ ''')
74
+ callback = build(root)
75
+ assert len(callback.path_items) > 0
76
+ """
77
+ context = context or Context()
78
+
79
+ # Check if root is a MappingNode, if not return ValueSource with invalid data
80
+ if not isinstance(root, yaml.MappingNode):
81
+ value = context.yaml_constructor.construct_object(root, deep=True)
82
+ return ValueSource(value=value, value_node=root)
83
+
84
+ # Extract extensions first
85
+ extensions = extract_extension_fields(root, context)
86
+ extension_properties = {k.value for k in extensions.keys()}
87
+
88
+ # Process each field to determine if it's an expression (not an extension)
89
+ path_items: dict[KeySource[str], PathItem | ValueSource[YAMLInvalidValue]] = {}
90
+
91
+ for key_node, value_node in root.value:
92
+ expression = context.yaml_constructor.construct_yaml_str(key_node)
93
+
94
+ if expression not in extension_properties:
95
+ # Expression field (any key that's not an extension) - build as Path Item
96
+ path_item_obj = build_path_item(value_node, context)
97
+ path_items[KeySource(value=expression, key_node=key_node)] = path_item_obj
98
+
99
+ # Create and return the Callback object with collected data
100
+ return Callback(
101
+ root_node=root,
102
+ path_items=path_items, # type: ignore[arg-type]
103
+ extensions=extensions,
104
+ )
105
+
106
+
107
+ def build_callback_or_reference(
108
+ node: yaml.Node, context: Context
109
+ ) -> Callback | Reference | ValueSource[YAMLInvalidValue]:
110
+ """
111
+ Build either a Callback or Reference from a YAML node.
112
+
113
+ This helper handles the polymorphic nature of OpenAPI where many fields
114
+ can contain either a Callback object or a Reference object ($ref).
115
+
116
+ Args:
117
+ node: The YAML node to parse
118
+ context: Parsing context
119
+
120
+ Returns:
121
+ A Callback, Reference, or ValueSource if the node is invalid
122
+ """
123
+ # Check if it's a reference (has $ref key)
124
+ if isinstance(node, yaml.MappingNode):
125
+ for key_node, _ in node.value:
126
+ key = context.yaml_constructor.construct_yaml_str(key_node)
127
+ if key == "$ref":
128
+ return build_reference(node, context)
129
+
130
+ # Otherwise, try to build as Callback
131
+ return build(node, context)