amati 0.1.0__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.
- amati/__init__.py +14 -0
- amati/_resolve_forward_references.py +185 -0
- amati/amati.py +143 -0
- amati/data/http-status-codes.json +1 -0
- amati/data/iso9110.json +1 -0
- amati/data/media-types.json +1 -0
- amati/data/schemes.json +1 -0
- amati/data/spdx-licences.json +1 -0
- amati/data/tlds.json +1 -0
- amati/exceptions.py +26 -0
- amati/fields/__init__.py +15 -0
- amati/fields/_custom_types.py +71 -0
- amati/fields/commonmark.py +9 -0
- amati/fields/email.py +27 -0
- amati/fields/http_status_codes.py +95 -0
- amati/fields/iso9110.py +61 -0
- amati/fields/json.py +13 -0
- amati/fields/media.py +100 -0
- amati/fields/oas.py +79 -0
- amati/fields/spdx_licences.py +92 -0
- amati/fields/uri.py +342 -0
- amati/file_handler.py +155 -0
- amati/grammars/oas.py +45 -0
- amati/grammars/rfc6901.py +26 -0
- amati/grammars/rfc7159.py +65 -0
- amati/logging.py +57 -0
- amati/model_validators.py +438 -0
- amati/references.py +33 -0
- amati/validators/__init__.py +0 -0
- amati/validators/generic.py +133 -0
- amati/validators/oas304.py +1031 -0
- amati/validators/oas311.py +615 -0
- amati-0.1.0.dist-info/METADATA +89 -0
- amati-0.1.0.dist-info/RECORD +37 -0
- amati-0.1.0.dist-info/WHEEL +4 -0
- amati-0.1.0.dist-info/entry_points.txt +2 -0
- amati-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,1031 @@
|
|
1
|
+
"""
|
2
|
+
Validates the OpenAPI Specification version 3.1.1
|
3
|
+
|
4
|
+
Note that per https://spec.openapis.org/oas/v3.0.4.html#relative-references-in-api-description-uris # pylint: disable=line-too-long
|
5
|
+
|
6
|
+
> URIs used as references within an OpenAPI Description, or to external documentation
|
7
|
+
> or other supplementary information such as a license, are resolved as identifiers,
|
8
|
+
> and described by this specification as URIs.
|
9
|
+
|
10
|
+
> Note that some URI fields are named url for historical reasons, but the descriptive
|
11
|
+
> text for those fields uses the correct “URI” terminology.
|
12
|
+
|
13
|
+
"""
|
14
|
+
|
15
|
+
import re
|
16
|
+
from typing import Any, ClassVar, Optional
|
17
|
+
from typing_extensions import Self
|
18
|
+
|
19
|
+
from jsonschema.exceptions import ValidationError as JSONVSchemeValidationError
|
20
|
+
from jsonschema.protocols import Validator as JSONSchemaValidator
|
21
|
+
from jsonschema.validators import validator_for # type: ignore
|
22
|
+
from pydantic import (
|
23
|
+
ConfigDict,
|
24
|
+
Field,
|
25
|
+
RootModel,
|
26
|
+
ValidationError,
|
27
|
+
field_validator,
|
28
|
+
model_validator,
|
29
|
+
)
|
30
|
+
|
31
|
+
from amati import AmatiValueError, Reference
|
32
|
+
from amati import model_validators as mv
|
33
|
+
from amati.fields import (
|
34
|
+
URI,
|
35
|
+
Email,
|
36
|
+
HTTPAuthenticationScheme,
|
37
|
+
HTTPStatusCode,
|
38
|
+
MediaType,
|
39
|
+
URIType,
|
40
|
+
URIWithVariables,
|
41
|
+
)
|
42
|
+
from amati.fields.commonmark import CommonMark
|
43
|
+
from amati.fields.json import JSON
|
44
|
+
from amati.fields.oas import OpenAPI, RuntimeExpression
|
45
|
+
from amati.logging import Log, LogMixin
|
46
|
+
from amati.validators.generic import GenericObject, allow_extra_fields
|
47
|
+
|
48
|
+
type JSONPrimitive = str | int | float | bool | None
|
49
|
+
type JSONArray = list["JSONValue"]
|
50
|
+
type JSONObject = dict[str, "JSONValue"]
|
51
|
+
type JSONValue = JSONPrimitive | JSONArray | JSONObject
|
52
|
+
|
53
|
+
|
54
|
+
TITLE = "OpenAPI Specification v3.0.4"
|
55
|
+
|
56
|
+
# Convenience naming to ensure that it's clear what's happening.
|
57
|
+
# https://spec.openapis.org/oas/v3.0.4.html#specification-extensions
|
58
|
+
specification_extensions = allow_extra_fields
|
59
|
+
|
60
|
+
|
61
|
+
@specification_extensions("x-")
|
62
|
+
class ContactObject(GenericObject):
|
63
|
+
"""
|
64
|
+
Validates the OpenAPI Specification contact object - §4.8.3
|
65
|
+
"""
|
66
|
+
|
67
|
+
name: Optional[str] = None
|
68
|
+
url: Optional[URI] = None
|
69
|
+
email: Optional[Email] = None
|
70
|
+
_reference: ClassVar[Reference] = Reference(
|
71
|
+
title=TITLE,
|
72
|
+
url="https://spec.openapis.org/oas/3.0.4.html#contact-object",
|
73
|
+
section="Contact Object",
|
74
|
+
)
|
75
|
+
|
76
|
+
|
77
|
+
@specification_extensions("x-")
|
78
|
+
class LicenceObject(GenericObject):
|
79
|
+
"""
|
80
|
+
A model representing the OpenAPI Specification licence object §4.8.4
|
81
|
+
"""
|
82
|
+
|
83
|
+
name: str = Field(min_length=1)
|
84
|
+
url: Optional[URI] = None
|
85
|
+
_reference: ClassVar[Reference] = Reference(
|
86
|
+
title=TITLE,
|
87
|
+
url="https://spec.openapis.org/oas/v3.0.4.html#license-object",
|
88
|
+
section="License Object",
|
89
|
+
)
|
90
|
+
|
91
|
+
|
92
|
+
class ReferenceObject(GenericObject):
|
93
|
+
"""
|
94
|
+
Validates the OpenAPI Specification reference object - §4.8.23
|
95
|
+
|
96
|
+
Note, "URIs" can be prefixed with a hash; this is because if the
|
97
|
+
representation of the referenced document is JSON or YAML, then
|
98
|
+
the fragment identifier SHOULD be interpreted as a JSON-Pointer
|
99
|
+
as per RFC6901.
|
100
|
+
"""
|
101
|
+
|
102
|
+
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
103
|
+
|
104
|
+
ref: URI = Field(alias="$ref")
|
105
|
+
_reference: ClassVar[Reference] = Reference(
|
106
|
+
title=TITLE,
|
107
|
+
url="https://spec.openapis.org/oas/v3.0.4.html#reference-object",
|
108
|
+
section="Reference Object",
|
109
|
+
)
|
110
|
+
|
111
|
+
|
112
|
+
@specification_extensions("x-")
|
113
|
+
class InfoObject(GenericObject):
|
114
|
+
"""
|
115
|
+
Validates the OpenAPI Specification info object - §4.8.2:
|
116
|
+
"""
|
117
|
+
|
118
|
+
title: str
|
119
|
+
description: Optional[str | CommonMark] = None
|
120
|
+
termsOfService: Optional[str] = None # pylint: disable=invalid-name
|
121
|
+
contact: Optional[ContactObject] = None
|
122
|
+
license: Optional[LicenceObject] = None
|
123
|
+
version: str
|
124
|
+
_reference: ClassVar[Reference] = Reference(
|
125
|
+
title=TITLE,
|
126
|
+
url="https://spec.openapis.org/oas/3.0.4.html#info-object",
|
127
|
+
section="Info Object",
|
128
|
+
)
|
129
|
+
|
130
|
+
|
131
|
+
class DiscriminatorObject(GenericObject):
|
132
|
+
"""
|
133
|
+
Validates the OpenAPI Specification object - §4.8.25
|
134
|
+
"""
|
135
|
+
|
136
|
+
# FIXME: Need post processing to determine whether the property actually exists
|
137
|
+
# FIXME: The component and schema objects need to check that this is being used
|
138
|
+
# properly.
|
139
|
+
propertyName: str
|
140
|
+
mapping: Optional[dict[str, str | URI]] = None
|
141
|
+
_reference: ClassVar[Reference] = Reference(
|
142
|
+
title=TITLE,
|
143
|
+
url="https://spec.openapis.org/oas/v3.0.4.html#discriminator-object",
|
144
|
+
section="Security Scheme Object",
|
145
|
+
)
|
146
|
+
|
147
|
+
|
148
|
+
@specification_extensions("x-")
|
149
|
+
class ExampleObject(GenericObject):
|
150
|
+
"""
|
151
|
+
Validates the OpenAPI Specification example object - §4.8.19
|
152
|
+
"""
|
153
|
+
|
154
|
+
summary: Optional[str] = None
|
155
|
+
description: Optional[str | CommonMark] = None
|
156
|
+
value: Optional[JSONValue] = None
|
157
|
+
externalValue: Optional[URI] = None
|
158
|
+
_reference: ClassVar[Reference] = Reference(
|
159
|
+
title=TITLE,
|
160
|
+
url="https://spec.openapis.org/oas/v3.0.4.html#example-object",
|
161
|
+
section="Example Object",
|
162
|
+
)
|
163
|
+
|
164
|
+
_not_value_and_external_value = mv.only_one_of(["value", "externalValue"])
|
165
|
+
|
166
|
+
|
167
|
+
@specification_extensions("x-")
|
168
|
+
class ServerVariableObject(GenericObject):
|
169
|
+
"""
|
170
|
+
Validates the OpenAPI Specification server variable object - §4.8.6
|
171
|
+
"""
|
172
|
+
|
173
|
+
enum: Optional[list[str]] = Field(None, min_length=1)
|
174
|
+
default: str = Field(min_length=1)
|
175
|
+
description: Optional[str | CommonMark] = None
|
176
|
+
_reference: ClassVar[Reference] = Reference(
|
177
|
+
title=TITLE,
|
178
|
+
url="https://spec.openapis.org/oas/v3.0.4.html#server-variable-object",
|
179
|
+
section="Server Variable Object",
|
180
|
+
)
|
181
|
+
|
182
|
+
@model_validator(mode="after")
|
183
|
+
def check_enum_default(self: Self) -> Self:
|
184
|
+
"""
|
185
|
+
Validate that the default value is in the enum list.
|
186
|
+
|
187
|
+
Returns:
|
188
|
+
The validated server variable object
|
189
|
+
"""
|
190
|
+
if self.enum is None:
|
191
|
+
return self
|
192
|
+
|
193
|
+
if self.default not in self.enum:
|
194
|
+
LogMixin.log(
|
195
|
+
Log(
|
196
|
+
message=f"The default value {self.default} is not in the enum list {self.enum}", # pylint: disable=line-too-long
|
197
|
+
type=Warning,
|
198
|
+
reference=self._reference,
|
199
|
+
)
|
200
|
+
)
|
201
|
+
|
202
|
+
return self
|
203
|
+
|
204
|
+
|
205
|
+
@specification_extensions("x-")
|
206
|
+
class ServerObject(GenericObject):
|
207
|
+
"""
|
208
|
+
Validates the OpenAPI Specification server object - §4.8.5
|
209
|
+
"""
|
210
|
+
|
211
|
+
url: URIWithVariables | URI
|
212
|
+
description: Optional[str | CommonMark] = None
|
213
|
+
variables: Optional[dict[str, ServerVariableObject]] = None
|
214
|
+
_reference: ClassVar[Reference] = Reference(
|
215
|
+
title=TITLE,
|
216
|
+
url="https://spec.openapis.org/oas/v3.0.4.html#server-object",
|
217
|
+
section="Server Object",
|
218
|
+
)
|
219
|
+
|
220
|
+
|
221
|
+
@specification_extensions("x-")
|
222
|
+
class ExternalDocumentationObject(GenericObject):
|
223
|
+
"""
|
224
|
+
Validates the OpenAPI Specification external documentation object - §4.8.22
|
225
|
+
"""
|
226
|
+
|
227
|
+
description: Optional[str | CommonMark] = None
|
228
|
+
url: URI
|
229
|
+
_reference: ClassVar[Reference] = Reference(
|
230
|
+
title=TITLE,
|
231
|
+
url="https://spec.openapis.org/oas/v3.0.4.html#external-documentation-object",
|
232
|
+
section="External Documentation Object",
|
233
|
+
)
|
234
|
+
|
235
|
+
|
236
|
+
# FIXME: Specification extensions should be "^x-", but the implementation
|
237
|
+
# doesn't play well with ConfigDict(extra="allow"). This is the only case
|
238
|
+
# so less important to change as the eventual logic is still correct.
|
239
|
+
@specification_extensions(".*")
|
240
|
+
class PathsObject(GenericObject):
|
241
|
+
"""Validates the OpenAPI Specification paths object - §4.8.8"""
|
242
|
+
|
243
|
+
model_config = ConfigDict(extra="allow")
|
244
|
+
|
245
|
+
@model_validator(mode="before")
|
246
|
+
@classmethod
|
247
|
+
def paths_are_uris(cls, data: Any) -> Any:
|
248
|
+
"""
|
249
|
+
Validates that paths are valid URIs, it's allowed that they
|
250
|
+
have variables, e.g. /pets or /pets/{petID}
|
251
|
+
|
252
|
+
Special-case specification extensions, which are also allowed.
|
253
|
+
"""
|
254
|
+
|
255
|
+
for field in data.keys():
|
256
|
+
|
257
|
+
# Specification extensions
|
258
|
+
if field.startswith("x-"):
|
259
|
+
continue
|
260
|
+
|
261
|
+
URIWithVariables(field)
|
262
|
+
|
263
|
+
return data
|
264
|
+
|
265
|
+
|
266
|
+
@specification_extensions("x-")
|
267
|
+
class OperationObject(GenericObject):
|
268
|
+
"""Validates the OpenAPI Specification operation object - §4.8.10"""
|
269
|
+
|
270
|
+
tags: Optional[list[str]] = None
|
271
|
+
summary: Optional[str] = None
|
272
|
+
description: Optional[str | CommonMark] = None
|
273
|
+
externalDocs: Optional[ExternalDocumentationObject] = None
|
274
|
+
operationId: Optional[str] = None
|
275
|
+
parameters: Optional[list["ParameterObject | ReferenceObject"]] = None
|
276
|
+
requestBody: Optional["RequestBodyObject | ReferenceObject"] = None
|
277
|
+
responses: "ResponsesObject"
|
278
|
+
callbacks: Optional[dict[str, "CallbackObject | ReferenceObject"]] = None
|
279
|
+
deprecated: Optional[bool] = False
|
280
|
+
security: Optional[list["SecurityRequirementObject"]] = None
|
281
|
+
servers: Optional[list[ServerObject]] = None
|
282
|
+
|
283
|
+
_reference: ClassVar[Reference] = Reference(
|
284
|
+
title=TITLE,
|
285
|
+
url="https://spec.openapis.org/oas/v3.0.4.html#operation-object",
|
286
|
+
section="Operation Object",
|
287
|
+
)
|
288
|
+
|
289
|
+
|
290
|
+
PARAMETER_STYLES: set[str] = {
|
291
|
+
"matrix",
|
292
|
+
"label",
|
293
|
+
"simple",
|
294
|
+
"form",
|
295
|
+
"spaceDelimited",
|
296
|
+
"pipeDelimited",
|
297
|
+
"deepObject",
|
298
|
+
}
|
299
|
+
|
300
|
+
|
301
|
+
class ParameterObject(GenericObject):
|
302
|
+
"""Validates the OpenAPI Specification parameter object - §4.8.11"""
|
303
|
+
|
304
|
+
name: str
|
305
|
+
in_: str = Field(alias="in")
|
306
|
+
description: Optional[str | CommonMark] = None
|
307
|
+
required: Optional[bool] = None
|
308
|
+
deprecated: Optional[bool] = None
|
309
|
+
allowEmptyValue: Optional[bool] = None
|
310
|
+
style: Optional[str] = None
|
311
|
+
explode: Optional[bool] = None
|
312
|
+
allowReserved: Optional[bool] = None
|
313
|
+
schema_: Optional["SchemaObject | ReferenceObject"] = Field(alias="schema")
|
314
|
+
example: Optional[Any] = None
|
315
|
+
examples: Optional[dict[str, "ExampleObject | ReferenceObject"]] = None
|
316
|
+
content: Optional[dict[str, "MediaTypeObject"]] = None
|
317
|
+
|
318
|
+
_in_valid = mv.if_then(
|
319
|
+
conditions={"in_": mv.UNKNOWN},
|
320
|
+
consequences={"in_": ["query", "header", "path", "cookie"]},
|
321
|
+
)
|
322
|
+
_path_location_is_required = mv.if_then(
|
323
|
+
conditions={"in_": "path"}, consequences={"required": True}
|
324
|
+
)
|
325
|
+
_empty_value_only_with_query = mv.if_then(
|
326
|
+
conditions={"allowEmptyValue": mv.UNKNOWN},
|
327
|
+
consequences={"in_": PARAMETER_STYLES ^ {"query"}},
|
328
|
+
)
|
329
|
+
_style_is_valid = mv.if_then(
|
330
|
+
conditions={"style": mv.UNKNOWN}, consequences={"style": list(PARAMETER_STYLES)}
|
331
|
+
)
|
332
|
+
_reserved_only_with_query = mv.if_then(
|
333
|
+
conditions={"allowReserved": mv.UNKNOWN},
|
334
|
+
consequences={"in_": PARAMETER_STYLES ^ {"query"}},
|
335
|
+
)
|
336
|
+
_disallowed_if_schema = mv.if_then(
|
337
|
+
conditions={"schema_": mv.UNKNOWN}, consequences={"content": None}
|
338
|
+
)
|
339
|
+
_disallowed_if_content = mv.if_then(
|
340
|
+
conditions={"content": mv.UNKNOWN},
|
341
|
+
consequences={
|
342
|
+
"content": None,
|
343
|
+
"style": None,
|
344
|
+
"explode": None,
|
345
|
+
"allowReserved": None,
|
346
|
+
"schema_": None,
|
347
|
+
},
|
348
|
+
)
|
349
|
+
|
350
|
+
|
351
|
+
@specification_extensions("x-")
|
352
|
+
class RequestBodyObject(GenericObject):
|
353
|
+
"""
|
354
|
+
Validates the OpenAPI Specification request body object - §4.8.13
|
355
|
+
"""
|
356
|
+
|
357
|
+
description: Optional[CommonMark | str] = None
|
358
|
+
content: dict[str, "MediaTypeObject"]
|
359
|
+
required: Optional[bool] = False
|
360
|
+
_reference: ClassVar[Reference] = Reference(
|
361
|
+
title=TITLE,
|
362
|
+
url="https://spec.openapis.org/oas/v3.0.4.html#request-body-object",
|
363
|
+
section="Request Body Object",
|
364
|
+
)
|
365
|
+
|
366
|
+
|
367
|
+
@specification_extensions("x-")
|
368
|
+
class MediaTypeObject(GenericObject):
|
369
|
+
"""
|
370
|
+
Validates the OpenAPI Specification media type object - §4.8.14
|
371
|
+
"""
|
372
|
+
|
373
|
+
schema_: Optional["SchemaObject | ReferenceObject"] = Field(
|
374
|
+
alias="schema", default=None
|
375
|
+
)
|
376
|
+
# FIXME: Define example
|
377
|
+
example: Optional[Any] = None
|
378
|
+
examples: Optional[dict[str, ExampleObject | ReferenceObject]] = None
|
379
|
+
encoding: Optional["EncodingObject"] = None
|
380
|
+
_reference: ClassVar[Reference] = Reference(
|
381
|
+
title=TITLE,
|
382
|
+
url="https://spec.openapis.org/oas/v3.0.4.html#media-type-object",
|
383
|
+
section="Tag Object",
|
384
|
+
)
|
385
|
+
|
386
|
+
|
387
|
+
@specification_extensions("x-")
|
388
|
+
class EncodingObject(GenericObject):
|
389
|
+
"""
|
390
|
+
Validates the OpenAPI Specification media type object - §4.8.15
|
391
|
+
"""
|
392
|
+
|
393
|
+
contentType: Optional[str] = None
|
394
|
+
headers: Optional[dict[str, "HeaderObject | ReferenceObject"]] = None
|
395
|
+
_reference: ClassVar[Reference] = Reference(
|
396
|
+
title=TITLE,
|
397
|
+
url="https://spec.openapis.org/oas/v3.0.4.html#encoding object-object",
|
398
|
+
section="Encoding Object",
|
399
|
+
)
|
400
|
+
|
401
|
+
@field_validator("contentType", mode="after")
|
402
|
+
@classmethod
|
403
|
+
def check_content_type(cls, value: str) -> str:
|
404
|
+
"""
|
405
|
+
contentType is a comma-separated list of media types.
|
406
|
+
Check that they are all valid
|
407
|
+
|
408
|
+
raises: ValueError
|
409
|
+
"""
|
410
|
+
|
411
|
+
for media_type in value.split(","):
|
412
|
+
MediaType(media_type.strip())
|
413
|
+
|
414
|
+
return value
|
415
|
+
|
416
|
+
|
417
|
+
type _ResponsesObjectReturnType = dict[str, "ReferenceObject | ResponseObject"]
|
418
|
+
|
419
|
+
|
420
|
+
@specification_extensions("x-")
|
421
|
+
class ResponsesObject(GenericObject):
|
422
|
+
"""
|
423
|
+
Validates the OpenAPI Specification responses object - §4.8.16
|
424
|
+
"""
|
425
|
+
|
426
|
+
model_config = ConfigDict(
|
427
|
+
extra="allow",
|
428
|
+
)
|
429
|
+
|
430
|
+
default: Optional["ResponseObject | ReferenceObject"] = None
|
431
|
+
_reference: ClassVar[Reference] = Reference(
|
432
|
+
title=TITLE,
|
433
|
+
url="https://spec.openapis.org/oas/v3.0.4.html#responses-object",
|
434
|
+
section="Responses Object",
|
435
|
+
)
|
436
|
+
|
437
|
+
@classmethod
|
438
|
+
def _choose_model(
|
439
|
+
cls, value: Any, field_name: str
|
440
|
+
) -> "ReferenceObject | ResponseObject":
|
441
|
+
"""
|
442
|
+
Choose the model to use for validation based on the type of value.
|
443
|
+
|
444
|
+
Args:
|
445
|
+
value: The value to validate.
|
446
|
+
|
447
|
+
Returns:
|
448
|
+
The model class to use for validation.
|
449
|
+
"""
|
450
|
+
|
451
|
+
message = f"{field_name} must be a ResponseObject or ReferenceObject, got {type(value)}" # pylint: disable=line-too-long
|
452
|
+
|
453
|
+
try:
|
454
|
+
return ResponseObject.model_validate(value)
|
455
|
+
except ValidationError:
|
456
|
+
try:
|
457
|
+
return ReferenceObject.model_validate(value)
|
458
|
+
except ValidationError as e:
|
459
|
+
raise ValueError(message, ResponsesObject._reference) from e
|
460
|
+
|
461
|
+
@model_validator(mode="before")
|
462
|
+
@classmethod
|
463
|
+
def validate_all_fields(cls, data: dict[str, Any]) -> _ResponsesObjectReturnType:
|
464
|
+
"""
|
465
|
+
Validates the responses object.
|
466
|
+
"""
|
467
|
+
|
468
|
+
validated_data: _ResponsesObjectReturnType = {}
|
469
|
+
|
470
|
+
for field_name, value in data.items():
|
471
|
+
|
472
|
+
# If the value is a specification extension, allow it
|
473
|
+
if field_name.startswith("x-"):
|
474
|
+
validated_data[field_name] = value
|
475
|
+
continue
|
476
|
+
|
477
|
+
# If the value is the fixed field, "default", allow it
|
478
|
+
if field_name == "default":
|
479
|
+
if isinstance(value, dict):
|
480
|
+
validated_data[field_name] = ResponsesObject._choose_model(
|
481
|
+
value, field_name
|
482
|
+
)
|
483
|
+
continue
|
484
|
+
|
485
|
+
# Otherwise, if the field appears like a valid HTTP status code or a range
|
486
|
+
if re.match(r"^[1-5]([0-9]{2}|XX)+$", str(field_name)):
|
487
|
+
|
488
|
+
# Double check and raise a value error if not
|
489
|
+
HTTPStatusCode(field_name)
|
490
|
+
|
491
|
+
# and validate as a ResponseObject or ReferenceObject
|
492
|
+
validated_data[field_name] = ResponsesObject._choose_model(
|
493
|
+
value, field_name
|
494
|
+
)
|
495
|
+
|
496
|
+
continue
|
497
|
+
|
498
|
+
# If the field is not a valid HTTP status code or "default"
|
499
|
+
raise ValueError(f"Invalid type for numeric field '{field_name}'")
|
500
|
+
|
501
|
+
return validated_data
|
502
|
+
|
503
|
+
|
504
|
+
@specification_extensions("x-")
|
505
|
+
class ResponseObject(GenericObject):
|
506
|
+
"""
|
507
|
+
Validates the OpenAPI Specification response object - §4.8.17
|
508
|
+
"""
|
509
|
+
|
510
|
+
description: str | CommonMark
|
511
|
+
headers: Optional[dict[str, "HeaderObject | ReferenceObject"]] = None
|
512
|
+
content: Optional[dict[str, MediaTypeObject]] = None
|
513
|
+
links: Optional[dict[str, "LinkObject | ReferenceObject"]] = None
|
514
|
+
_reference: ClassVar[Reference] = Reference(
|
515
|
+
title=TITLE,
|
516
|
+
url="https://spec.openapis.org/oas/v3.0.4.html#response-object",
|
517
|
+
section="Response Object",
|
518
|
+
)
|
519
|
+
|
520
|
+
|
521
|
+
@specification_extensions("x-")
|
522
|
+
class CallbackObject(GenericObject):
|
523
|
+
"""
|
524
|
+
Validates the OpenAPI Specification callback object - §4.8.18
|
525
|
+
"""
|
526
|
+
|
527
|
+
model_config = ConfigDict(extra="allow")
|
528
|
+
|
529
|
+
# The keys are runtime expressions that resolve to a URL
|
530
|
+
# The values are Response Objects or Reference Objects
|
531
|
+
_reference: ClassVar[Reference] = Reference(
|
532
|
+
title=TITLE,
|
533
|
+
url="https://spec.openapis.org/oas/v3.0.4.html#callback-object",
|
534
|
+
section="Callback Object",
|
535
|
+
)
|
536
|
+
|
537
|
+
@model_validator(mode="before")
|
538
|
+
@classmethod
|
539
|
+
def validate_all_fields(cls, data: dict[str, Any]) -> dict[str, "PathItemObject"]:
|
540
|
+
"""
|
541
|
+
Validates the callback object.
|
542
|
+
"""
|
543
|
+
|
544
|
+
validated_data: dict[str, PathItemObject] = {}
|
545
|
+
|
546
|
+
# Everything after a { but before a } should be runtime expression
|
547
|
+
pattern: str = r"\{([^}]+)\}"
|
548
|
+
|
549
|
+
for field_name, value in data.items():
|
550
|
+
|
551
|
+
# If the value is a specification extension, allow it
|
552
|
+
if field_name.startswith("x-"):
|
553
|
+
validated_data[field_name] = PathItemObject.model_validate(value)
|
554
|
+
continue
|
555
|
+
|
556
|
+
# Either the field name is a runtime expression, so test this:
|
557
|
+
try:
|
558
|
+
RuntimeExpression(field_name)
|
559
|
+
validated_data[field_name] = PathItemObject.model_validate(value)
|
560
|
+
continue
|
561
|
+
except AmatiValueError:
|
562
|
+
pass
|
563
|
+
|
564
|
+
# Or, the field name is a runtime expression embedded in a string
|
565
|
+
# value per https://spec.openapis.org/oas/latest.html#examples-0
|
566
|
+
matches = re.findall(pattern, field_name)
|
567
|
+
|
568
|
+
for match in matches:
|
569
|
+
try:
|
570
|
+
RuntimeExpression(match)
|
571
|
+
except AmatiValueError as e:
|
572
|
+
raise AmatiValueError(
|
573
|
+
f"Invalid runtime expression '{match}' in field '{field_name}'",
|
574
|
+
CallbackObject._reference,
|
575
|
+
) from e
|
576
|
+
|
577
|
+
if matches:
|
578
|
+
validated_data[field_name] = PathItemObject.model_validate(value)
|
579
|
+
else:
|
580
|
+
# If the field does not contain a valid runtime expression
|
581
|
+
raise ValueError(f"Invalid type for numeric field '{field_name}'")
|
582
|
+
|
583
|
+
return validated_data
|
584
|
+
|
585
|
+
|
586
|
+
@specification_extensions("x-")
|
587
|
+
class TagObject(GenericObject):
|
588
|
+
"""
|
589
|
+
Validates the OpenAPI Specification tag object - §4.8.22
|
590
|
+
"""
|
591
|
+
|
592
|
+
name: str
|
593
|
+
description: Optional[str | CommonMark] = None
|
594
|
+
externalDocs: Optional[ExternalDocumentationObject] = None
|
595
|
+
_reference: ClassVar[Reference] = Reference(
|
596
|
+
title=TITLE,
|
597
|
+
url="https://spec.openapis.org/oas/v3.0.4.html#tag-object",
|
598
|
+
section="Tag Object",
|
599
|
+
)
|
600
|
+
|
601
|
+
|
602
|
+
@specification_extensions("x-")
|
603
|
+
class LinkObject(GenericObject):
|
604
|
+
"""
|
605
|
+
Validates the OpenAPI Specification link object - §4.8.20
|
606
|
+
"""
|
607
|
+
|
608
|
+
operationRef: Optional[URI] = None
|
609
|
+
operationId: Optional[str] = None
|
610
|
+
parameters: Optional[dict[str, RuntimeExpression | JSONValue]] = None
|
611
|
+
requestBody: Optional[JSONValue | RuntimeExpression] = None
|
612
|
+
description: Optional[str | CommonMark] = None
|
613
|
+
server: Optional[ServerObject] = None
|
614
|
+
_reference: ClassVar[Reference] = Reference(
|
615
|
+
title=TITLE,
|
616
|
+
url="https://spec.openapis.org/oas/v3.0.4.html#link-object",
|
617
|
+
section="Link Object",
|
618
|
+
)
|
619
|
+
|
620
|
+
_not_operationref_and_operationid = mv.only_one_of(
|
621
|
+
fields=["operationRef", "operationId"]
|
622
|
+
)
|
623
|
+
|
624
|
+
|
625
|
+
@specification_extensions("x-")
|
626
|
+
class HeaderObject(GenericObject):
|
627
|
+
"""
|
628
|
+
Validates the OpenAPI Specification link object - §4.8.20
|
629
|
+
"""
|
630
|
+
|
631
|
+
# Common schema/content fields
|
632
|
+
description: Optional[str | CommonMark] = None
|
633
|
+
required: Optional[bool] = Field(default=False)
|
634
|
+
deprecated: Optional[bool] = Field(default=False)
|
635
|
+
|
636
|
+
# Schema fields
|
637
|
+
style: Optional[str] = Field(default="simple")
|
638
|
+
explode: Optional[bool] = Field(default=False)
|
639
|
+
schema_: Optional["SchemaObject | ReferenceObject"] = Field(
|
640
|
+
alias="schema", default=None
|
641
|
+
)
|
642
|
+
example: Optional[JSONValue] = None
|
643
|
+
examples: Optional[dict[str, ExampleObject | ReferenceObject]] = None
|
644
|
+
|
645
|
+
# Content fields
|
646
|
+
content: Optional[dict[str, MediaTypeObject]] = None
|
647
|
+
|
648
|
+
_reference: ClassVar[Reference] = Reference(
|
649
|
+
title=TITLE,
|
650
|
+
url="https://spec.openapis.org/oas/v3.0.4.html#link-object",
|
651
|
+
section="Link Object",
|
652
|
+
)
|
653
|
+
|
654
|
+
_not_schema_and_content = mv.only_one_of(["schema_", "content"])
|
655
|
+
|
656
|
+
|
657
|
+
@specification_extensions("x-")
|
658
|
+
class XMLObject(GenericObject):
|
659
|
+
"""
|
660
|
+
Validates the OpenAPI Specification object - §4.8.26
|
661
|
+
"""
|
662
|
+
|
663
|
+
name: Optional[str] = None
|
664
|
+
namespace: Optional[URI] = None
|
665
|
+
prefix: Optional[str] = None
|
666
|
+
attribute: Optional[bool] = Field(default=False)
|
667
|
+
wrapped: Optional[bool] = None
|
668
|
+
_reference: ClassVar[Reference] = Reference(
|
669
|
+
title=TITLE,
|
670
|
+
url="https://spec.openapis.org/oas/v3.0.4.html#xml-object",
|
671
|
+
section="Security Scheme Object",
|
672
|
+
)
|
673
|
+
|
674
|
+
@field_validator("namespace", mode="after")
|
675
|
+
@classmethod
|
676
|
+
def _validate_namespace(cls, value: URI) -> URI:
|
677
|
+
if value.type == URIType.RELATIVE:
|
678
|
+
message = "XML namespace {value} cannot be a relative URI"
|
679
|
+
LogMixin.log(
|
680
|
+
Log(message=message, type=ValueError, reference=cls._reference)
|
681
|
+
)
|
682
|
+
|
683
|
+
return value
|
684
|
+
|
685
|
+
|
686
|
+
class SchemaObject(GenericObject):
|
687
|
+
"""
|
688
|
+
Schema Object as per OAS 3.1.1 specification (section 4.8.24)
|
689
|
+
|
690
|
+
This model defines only the OpenAPI-specific fields explicitly.
|
691
|
+
Standard JSON Schema fields are allowed via the 'extra' config
|
692
|
+
and validated through jsonschema.
|
693
|
+
"""
|
694
|
+
|
695
|
+
model_config = ConfigDict(
|
696
|
+
populate_by_name=True,
|
697
|
+
extra="allow", # Allow all standard JSON Schema fields
|
698
|
+
)
|
699
|
+
|
700
|
+
# OpenAPI-specific fields not in standard JSON Schema
|
701
|
+
nullable: Optional[bool] = None # OAS 3.0 style nullable flag
|
702
|
+
discriminator: Optional[DiscriminatorObject] = None # Polymorphism support
|
703
|
+
readOnly: Optional[bool] = None # Declares property as read-only for requests
|
704
|
+
writeOnly: Optional[bool] = None # Declares property as write-only for responses
|
705
|
+
xml: Optional[XMLObject] = None # XML metadata
|
706
|
+
externalDocs: Optional[ExternalDocumentationObject] = None # External documentation
|
707
|
+
example: Optional[Any] = None # Example of schema
|
708
|
+
examples: Optional[list[Any]] = None # Examples of schema (OAS 3.1)
|
709
|
+
deprecated: Optional[bool] = None # Specifies schema is deprecated
|
710
|
+
|
711
|
+
# JSON Schema fields that need special handling in OAS context
|
712
|
+
ref: Optional[str] = Field(
|
713
|
+
default=None, alias="$ref"
|
714
|
+
) # Reference to another schema
|
715
|
+
|
716
|
+
_reference: ClassVar[Reference] = Reference(
|
717
|
+
title=TITLE,
|
718
|
+
url="https://spec.openapis.org/oas/v3.0.4.html#schema-object",
|
719
|
+
section="Link Object",
|
720
|
+
)
|
721
|
+
|
722
|
+
@model_validator(mode="after")
|
723
|
+
def validate_schema(self):
|
724
|
+
"""
|
725
|
+
Use jsonschema to validate the model as a valid JSON Schema
|
726
|
+
"""
|
727
|
+
schema_dict = self.model_dump(exclude_none=True, by_alias=True)
|
728
|
+
|
729
|
+
# Handle OAS 3.1 specific validations
|
730
|
+
|
731
|
+
# 1. Convert nullable to type array with null if needed
|
732
|
+
if schema_dict.get("nullable") is True and "type" in schema_dict:
|
733
|
+
type_val = schema_dict["type"]
|
734
|
+
if isinstance(type_val, str) and type_val != "null":
|
735
|
+
schema_dict["type"] = [type_val, "null"]
|
736
|
+
elif isinstance(type_val, list) and "null" not in type_val:
|
737
|
+
schema_dict["type"] = type_val + ["null"]
|
738
|
+
|
739
|
+
# 2. Validate the schema structure using jsonschema's meta-schema
|
740
|
+
# Get the right validator based on the declared $schema or default
|
741
|
+
# to Draft 2020-12
|
742
|
+
schema_version = schema_dict.get(
|
743
|
+
"$schema", "https://json-schema.org/draft/2020-12/schema"
|
744
|
+
)
|
745
|
+
try:
|
746
|
+
validator_cls: JSONSchemaValidator = validator_for( # type: ignore
|
747
|
+
{"$schema": schema_version}
|
748
|
+
)
|
749
|
+
meta_schema: JSON = validator_cls.META_SCHEMA # type: ignore
|
750
|
+
|
751
|
+
# This will validate the structure conforms to JSON Schema
|
752
|
+
validator_cls(meta_schema).validate(schema_dict) # type: ignore
|
753
|
+
except JSONVSchemeValidationError as e:
|
754
|
+
LogMixin.log(
|
755
|
+
Log(
|
756
|
+
message=f"Invalid JSON Schema: {e.message}",
|
757
|
+
type=ValueError,
|
758
|
+
reference=self._reference,
|
759
|
+
)
|
760
|
+
)
|
761
|
+
|
762
|
+
return self
|
763
|
+
|
764
|
+
|
765
|
+
OAUTH_FLOW_TYPES: set[str] = {
|
766
|
+
"implicit",
|
767
|
+
"authorizationCode",
|
768
|
+
"clientCredentials",
|
769
|
+
"password",
|
770
|
+
}
|
771
|
+
|
772
|
+
|
773
|
+
@specification_extensions("x-")
|
774
|
+
class OAuthFlowObject(GenericObject):
|
775
|
+
"""
|
776
|
+
Validates the OpenAPI OAuth Flow object - §4.8.29
|
777
|
+
"""
|
778
|
+
|
779
|
+
type: Optional[str] = None
|
780
|
+
authorizationUrl: Optional[URI] = None
|
781
|
+
tokenUrl: Optional[URI] = None
|
782
|
+
refreshUrl: Optional[URI] = None
|
783
|
+
scopes: dict[str, str] = {}
|
784
|
+
_reference: ClassVar[Reference] = Reference(
|
785
|
+
title=TITLE,
|
786
|
+
url="https://spec.openapis.org/oas/v3.0.4.html#oauth-flow-object",
|
787
|
+
section="OAuth Flow Object",
|
788
|
+
)
|
789
|
+
|
790
|
+
_implicit_has_authorization_url = mv.if_then(
|
791
|
+
conditions={"type": "implicit"},
|
792
|
+
consequences={"authorizationUrl": mv.UNKNOWN},
|
793
|
+
)
|
794
|
+
|
795
|
+
_token_url_not_implicit = mv.if_then(
|
796
|
+
conditions={"tokenUrl": mv.UNKNOWN},
|
797
|
+
consequences={"type": OAUTH_FLOW_TYPES ^ {"implicit"}},
|
798
|
+
)
|
799
|
+
|
800
|
+
_authorization_code_has_urls = mv.if_then(
|
801
|
+
conditions={"type": "authorizationCode"},
|
802
|
+
consequences={"authorizationUrl": mv.UNKNOWN, "tokenUrl": mv.UNKNOWN},
|
803
|
+
)
|
804
|
+
|
805
|
+
_authorization_url_not_credentials_password = mv.if_then(
|
806
|
+
conditions={"authorizationUrl": mv.UNKNOWN},
|
807
|
+
consequences={"type": OAUTH_FLOW_TYPES ^ {"clientCredentials", "password"}},
|
808
|
+
)
|
809
|
+
|
810
|
+
_client_credentials_has_token = mv.if_then(
|
811
|
+
conditions={"type": "clientCredentials"},
|
812
|
+
consequences={"tokenUrl": mv.UNKNOWN},
|
813
|
+
)
|
814
|
+
_password_has_token = mv.if_then(
|
815
|
+
conditions={"type": "password"}, consequences={"tokenUrl": mv.UNKNOWN}
|
816
|
+
)
|
817
|
+
|
818
|
+
|
819
|
+
@specification_extensions("-x")
|
820
|
+
class OAuthFlowsObject(GenericObject):
|
821
|
+
"""
|
822
|
+
Validates the OpenAPI OAuth Flows object - §4.8.28
|
823
|
+
|
824
|
+
SPECFIX: Not all of these should be optional as an OAuth2 workflow
|
825
|
+
without any credentials will not do anything.
|
826
|
+
"""
|
827
|
+
|
828
|
+
implicit: Optional[OAuthFlowObject] = None
|
829
|
+
password: Optional[OAuthFlowObject] = None
|
830
|
+
clientCredentials: Optional[OAuthFlowObject] = None
|
831
|
+
authorizationCode: Optional[OAuthFlowObject] = None
|
832
|
+
_reference: ClassVar[Reference] = Reference(
|
833
|
+
title=TITLE,
|
834
|
+
url="https://spec.openapis.org/oas/v3.0.4.html#oauth-flow-object",
|
835
|
+
section="OAuth Flows Object",
|
836
|
+
)
|
837
|
+
|
838
|
+
@model_validator(mode="before")
|
839
|
+
@classmethod
|
840
|
+
def _push_down_type(cls, data: Any) -> Any:
|
841
|
+
"""
|
842
|
+
Adds the type of OAuth2 flow, e.g. implicit, password to the child
|
843
|
+
OAuthFlowObject so that additional validation can be done on this object.
|
844
|
+
"""
|
845
|
+
|
846
|
+
for k, v in data.items():
|
847
|
+
|
848
|
+
if isinstance(v, OAuthFlowObject):
|
849
|
+
raise NotImplementedError("Must pass a dict")
|
850
|
+
|
851
|
+
if v:
|
852
|
+
data[k]["type"] = k
|
853
|
+
|
854
|
+
return data
|
855
|
+
|
856
|
+
|
857
|
+
class SecuritySchemeObject(GenericObject):
|
858
|
+
"""
|
859
|
+
Validates the OpenAPI Security Scheme object - §4.8.27
|
860
|
+
"""
|
861
|
+
|
862
|
+
type: str
|
863
|
+
description: Optional[str | CommonMark] = None
|
864
|
+
name: Optional[str] = None
|
865
|
+
in_: Optional[str] = Field(default=None, alias="in")
|
866
|
+
scheme: Optional[HTTPAuthenticationScheme] = None
|
867
|
+
bearerFormat: Optional[str] = None
|
868
|
+
flows: Optional[OAuthFlowsObject] = None
|
869
|
+
openIdConnectUrl: Optional[URI] = None
|
870
|
+
|
871
|
+
_SECURITY_SCHEME_TYPES: ClassVar[set[str]] = {
|
872
|
+
"apiKey",
|
873
|
+
"http",
|
874
|
+
"oauth2",
|
875
|
+
"openIdConnect",
|
876
|
+
}
|
877
|
+
|
878
|
+
_reference: ClassVar[Reference] = Reference(
|
879
|
+
title=TITLE,
|
880
|
+
url="https://spec.openapis.org/oas/v3.0.4.html#security-scheme-object-0",
|
881
|
+
section="Security Scheme Object",
|
882
|
+
)
|
883
|
+
|
884
|
+
_type_in_enum = mv.if_then(
|
885
|
+
conditions={"type": mv.UNKNOWN}, consequences={"type": _SECURITY_SCHEME_TYPES}
|
886
|
+
)
|
887
|
+
|
888
|
+
_apikey_has_name_and_in = mv.if_then(
|
889
|
+
conditions={"type": "apiKey"},
|
890
|
+
consequences={"name": mv.UNKNOWN, "in_": ("query", "header", "cookie")},
|
891
|
+
)
|
892
|
+
|
893
|
+
_http_has_scheme = mv.if_then(
|
894
|
+
conditions={"type": "http"}, consequences={"scheme": mv.UNKNOWN}
|
895
|
+
)
|
896
|
+
|
897
|
+
_oauth2_has_flows = mv.if_then(
|
898
|
+
conditions={"type": "oauth2"}, consequences={"flows": mv.UNKNOWN}
|
899
|
+
)
|
900
|
+
|
901
|
+
_open_id_connect_has_url = mv.if_then(
|
902
|
+
conditions={"type": "openIdConnect"},
|
903
|
+
consequences={"openIdConnectUrl": mv.UNKNOWN},
|
904
|
+
)
|
905
|
+
|
906
|
+
_flows_not_oauth2 = mv.if_then(
|
907
|
+
conditions={"flows": None},
|
908
|
+
consequences={"type": _SECURITY_SCHEME_TYPES ^ {"oauth2"}},
|
909
|
+
)
|
910
|
+
|
911
|
+
|
912
|
+
type _Requirement = dict[str, list[str]]
|
913
|
+
|
914
|
+
|
915
|
+
# NB This is implemented as a RootModel as there are no pre-defined field names.
|
916
|
+
class SecurityRequirementObject(RootModel[list[_Requirement] | _Requirement]):
|
917
|
+
"""
|
918
|
+
Validates the OpenAPI Specification security requirement object - §4.8.30:
|
919
|
+
"""
|
920
|
+
|
921
|
+
# FIXME: The name must be a valid Security Scheme - need to use post-processing
|
922
|
+
# FIXME If the security scheme is of type "oauth2" or "openIdConnect", then the
|
923
|
+
# value must be a list For other security scheme types, the array MAY contain a
|
924
|
+
# list of role names which are required for the execution
|
925
|
+
_reference: ClassVar[Reference] = Reference(
|
926
|
+
title=TITLE,
|
927
|
+
url="https://spec.openapis.org/oas/3.0.4.html#security-requirement-object",
|
928
|
+
section="Security Requirement Object",
|
929
|
+
)
|
930
|
+
|
931
|
+
|
932
|
+
@specification_extensions("x-")
|
933
|
+
class PathItemObject(GenericObject):
|
934
|
+
"""Validates the OpenAPI Specification path item object - §4.8.9"""
|
935
|
+
|
936
|
+
ref_: Optional[URI] = Field(alias="$ref", default=None)
|
937
|
+
summary: Optional[str] = None
|
938
|
+
description: Optional[str | CommonMark] = None
|
939
|
+
get: Optional[OperationObject] = None
|
940
|
+
put: Optional[OperationObject] = None
|
941
|
+
post: Optional[OperationObject] = None
|
942
|
+
delete: Optional[OperationObject] = None
|
943
|
+
options: Optional[OperationObject] = None
|
944
|
+
head: Optional[OperationObject] = None
|
945
|
+
patch: Optional[OperationObject] = None
|
946
|
+
trace: Optional[OperationObject] = None
|
947
|
+
servers: Optional[list[ServerObject]] = None
|
948
|
+
parameters: Optional[list[ParameterObject | ReferenceObject]] = None
|
949
|
+
_reference: ClassVar[Reference] = Reference(
|
950
|
+
title=TITLE,
|
951
|
+
url="https://spec.openapis.org/oas/v3.0.4.html#path-item-object",
|
952
|
+
section="Path Item Object",
|
953
|
+
)
|
954
|
+
|
955
|
+
|
956
|
+
@specification_extensions("x-")
|
957
|
+
class ComponentsObject(GenericObject):
|
958
|
+
"""
|
959
|
+
Validates the OpenAPI Specification components object - §4.8.7
|
960
|
+
"""
|
961
|
+
|
962
|
+
schemas: Optional[dict[str, SchemaObject]] = None
|
963
|
+
responses: Optional[dict[str, ResponseObject | ReferenceObject]] = None
|
964
|
+
paremeters: Optional[dict[str, ParameterObject | ReferenceObject]] = None
|
965
|
+
examples: Optional[dict[str, ExampleObject | ReferenceObject]] = None
|
966
|
+
requestBodies: Optional[dict[str, RequestBodyObject | ReferenceObject]] = None
|
967
|
+
headers: Optional[dict[str, HeaderObject | ReferenceObject]] = None
|
968
|
+
securitySchemes: Optional[dict[str, SecuritySchemeObject | ReferenceObject]] = None
|
969
|
+
links: Optional[dict[str, LinkObject | ReferenceObject]] = None
|
970
|
+
callbacks: Optional[dict[str, CallbackObject | ReferenceObject]] = None
|
971
|
+
_reference: ClassVar[Reference] = Reference(
|
972
|
+
title=TITLE,
|
973
|
+
url="https://spec.openapis.org/oas/v3.0.4.html#components-object",
|
974
|
+
section="Components Object",
|
975
|
+
)
|
976
|
+
|
977
|
+
@model_validator(mode="before")
|
978
|
+
@classmethod
|
979
|
+
def validate_all_fields(
|
980
|
+
cls, data: dict[str, dict[str, Any]]
|
981
|
+
) -> dict[str, dict[str, Any]]:
|
982
|
+
"""
|
983
|
+
Validates the components object.
|
984
|
+
|
985
|
+
Args:
|
986
|
+
data: The data to validate.
|
987
|
+
|
988
|
+
Returns:
|
989
|
+
The validated components object.
|
990
|
+
"""
|
991
|
+
|
992
|
+
pattern: str = r"^[a-zA-Z0-9\.\-_]+$"
|
993
|
+
|
994
|
+
# Validate each field in the components object
|
995
|
+
for field_name, value in data.items():
|
996
|
+
if field_name.startswith("x-"):
|
997
|
+
continue
|
998
|
+
|
999
|
+
if not isinstance(value, dict): # type: ignore
|
1000
|
+
raise ValueError(
|
1001
|
+
f"Invalid type for '{field_name}': expected dict, got {type(value)}"
|
1002
|
+
)
|
1003
|
+
|
1004
|
+
for key in value.keys():
|
1005
|
+
if not re.match(pattern, key):
|
1006
|
+
raise ValueError(
|
1007
|
+
f"Invalid key '{key}' in '{field_name}': must match pattern {pattern}" # pylint: disable=line-too-long
|
1008
|
+
)
|
1009
|
+
|
1010
|
+
return data
|
1011
|
+
|
1012
|
+
|
1013
|
+
@specification_extensions("x-")
|
1014
|
+
class OpenAPIObject(GenericObject):
|
1015
|
+
"""
|
1016
|
+
Validates the OpenAPI Specification object - §4.1
|
1017
|
+
"""
|
1018
|
+
|
1019
|
+
openapi: OpenAPI
|
1020
|
+
info: InfoObject
|
1021
|
+
servers: Optional[list[ServerObject]] = Field(default=[ServerObject(url=URI("/"))])
|
1022
|
+
paths: PathsObject
|
1023
|
+
components: Optional[ComponentsObject] = None
|
1024
|
+
security: Optional[list[SecurityRequirementObject]] = None
|
1025
|
+
tags: Optional[list[TagObject]] = None
|
1026
|
+
externalDocs: Optional[ExternalDocumentationObject] = None
|
1027
|
+
_reference: ClassVar[Reference] = Reference(
|
1028
|
+
title=TITLE,
|
1029
|
+
url="https://spec.openapis.org/oas/3.0.4.html#openapi-object",
|
1030
|
+
section="OpenAPI Object",
|
1031
|
+
)
|