Skip to content

Commit a209cb3

Browse files
mdellwegggainey
authored andcommitted
Adapt schema validation to openapi 3.1
This will allow to also consume openapi schema produced with version 3.1.1.
1 parent dc0ff02 commit a209cb3

File tree

5 files changed

+47
-5
lines changed

5 files changed

+47
-5
lines changed

.ci/run_container.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export PULP_CONTENT_ORIGIN
7272
"${CONTAINER_RUNTIME}" \
7373
run ${RM:+--rm} \
7474
--env S6_KEEP_ENV=1 \
75+
${OAS_VERSION:+--env PULP_SPECTACULAR_SETTINGS__OAS_VERSION="${OAS_VERSION}"} \
7576
${PULP_HTTPS:+--env PULP_HTTPS} \
7677
${PULP_OAUTH2:+--env PULP_OAUTH2} \
7778
${PULP_API_ROOT:+--env PULP_API_ROOT} \

.github/workflows/test.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ jobs:
5151
- image_tag: "latest"
5252
python: "3.11"
5353
upper_bounds: true
54+
oas_version: "3.1.1"
5455
- image_tag: "3.63"
5556
pulp_https: "true"
5657
pulp_oauth2: "true"
@@ -106,6 +107,7 @@ jobs:
106107
PULP_HTTPS: "${{ matrix.pulp_https }}"
107108
PULP_OAUTH2: "${{ matrix.pulp_oauth2 }}"
108109
PULP_API_ROOT: "${{ matrix.pulp_api_root }}"
110+
OAS_VERSION: "${{ matrix.oas_version }}"
109111
run: |
110112
.ci/run_container.sh make livetest
111113
...
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Improved validation to consume openapi 3.1 schemata also.

pulp-glue/pulp_glue/common/schema.py

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -99,16 +99,31 @@ def validate(schema: t.Any, name: str, value: t.Any, components: t.Dict[str, t.A
9999
return
100100

101101
if value is None:
102+
# This seems to be the openapi 3.0.3 way.
103+
# in 3.1.* they use `"type": ["string", "null"]` instead.
102104
if schema.get("nullable", False):
103105
return
104106

105107
if (schema_type := schema.get("type")) is not None:
106-
if (typed_transform := _TYPED_VALIDATORS.get(schema_type)) is not None:
107-
value = typed_transform(schema, name, value, components)
108+
if isinstance(schema_type, list):
109+
if len(schema_type) == 0:
110+
raise SchemaError(_("{name} specified an empty type array").format(name=name))
111+
errors = []
112+
for stype in schema_type:
113+
try:
114+
_validate_type(stype, schema, name, value, components)
115+
break
116+
except ValidationError as e:
117+
errors.append(f"{stype}: {e}")
118+
else:
119+
raise ValidationError(
120+
_("{name} did not match any of the types: {errors}").format(
121+
name=name, errors="\n".join(errors)
122+
)
123+
)
124+
108125
else:
109-
raise NotImplementedError(
110-
_("Type `{schema_type}` is not implemented yet.").format(schema_type=schema_type)
111-
)
126+
_validate_type(schema_type, schema, name, value, components)
112127

113128
# allOf etc allow for composition, but the spec isn't particularly clear about that.
114129
if (all_of := schema.get("allOf")) is not None:
@@ -133,6 +148,17 @@ def validate(schema: t.Any, name: str, value: t.Any, components: t.Dict[str, t.A
133148
)
134149

135150

151+
def _validate_type(
152+
schema_type: str, schema: t.Any, name: str, value: t.Any, components: t.Dict[str, t.Any]
153+
) -> None:
154+
if (typed_validator := _TYPED_VALIDATORS.get(schema_type)) is not None:
155+
typed_validator(schema, name, value, components)
156+
else:
157+
raise NotImplementedError(
158+
_("Type `{schema_type}` is not implemented yet.").format(schema_type=schema_type)
159+
)
160+
161+
136162
def _validate_ref(schema_ref: str, name: str, value: t.Any, components: t.Dict[str, t.Any]) -> None:
137163
if not schema_ref.startswith("#/components/schemas/"):
138164
raise SchemaError(_("'{name}' contains an invalid reference.").format(name=name))
@@ -185,6 +211,11 @@ def _validate_integer(
185211
)
186212

187213

214+
def _validate_null(schema: t.Any, name: str, value: t.Any, components: t.Dict[str, t.Any]) -> None:
215+
if value is not None:
216+
raise ValidationError(_("'{name}' is expected to be a null").format(name=name))
217+
218+
188219
def _validate_number(
189220
schema: t.Any, name: str, value: t.Any, components: t.Dict[str, t.Any]
190221
) -> None:
@@ -252,6 +283,7 @@ def _validate_string(
252283
"array": _validate_array,
253284
"boolean": _validate_boolean,
254285
"integer": _validate_integer,
286+
"null": _validate_null,
255287
"number": _validate_number,
256288
"object": _validate_object,
257289
"string": _validate_string,

pulp-glue/tests/test_schema.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,8 @@
153153
{"a": datetime.date(2000, 1, 1), "b": datetime.date(2000, 1, 2)},
154154
id="any_of_matches_one",
155155
),
156+
pytest.param({"type": ["string", "null"]}, "test_string", id="type_array_matches_string"),
157+
pytest.param({"type": ["string", "null"]}, None, id="type_array_matches_null"),
156158
],
157159
)
158160
def test_validates(schema: t.Any, value: t.Any) -> None:
@@ -309,6 +311,9 @@ def test_validates(schema: t.Any, value: t.Any) -> None:
309311
None,
310312
id="fails_validate_none_matched",
311313
),
314+
pytest.param(
315+
{"type": ["string", "null"]}, 1, "did not match any", id="type_array_matches_string"
316+
),
312317
],
313318
)
314319
def test_validation_failed(schema: t.Any, value: t.Any, match: t.Optional[str]) -> None:
@@ -322,6 +327,7 @@ def test_validation_failed(schema: t.Any, value: t.Any, match: t.Optional[str])
322327
pytest.param({"type": "blubb"}, 1, NotImplementedError, id="unknown_type"),
323328
pytest.param({"$ref": "blubb"}, 1, SchemaError, id="invalid_reference"),
324329
pytest.param({"$ref": "#/components/schemas/notHere"}, 1, KeyError, id="missing_reference"),
330+
pytest.param({"type": []}, 1, SchemaError, id="type_array_matches_string"),
325331
],
326332
)
327333
def test_invalid_schema_raises(schema: t.Any, value: t.Any, exc_type: t.Type[Exception]) -> None:

0 commit comments

Comments
 (0)