Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v3x - boolean schemas #287

Merged
merged 1 commit into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 49 additions & 19 deletions aiopenapi3/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,9 +177,9 @@
self.root = v
elif _type == "object":
if (
schema.additionalProperties
and isinstance(schema.additionalProperties, (SchemaBase, ReferenceBase))
and not schema.properties
not schema.properties
and schema.additionalProperties is not None
and not Model.booleanFalse(schema.additionalProperties)
):
"""
https://swagger.io/docs/specification/data-models/dictionaries/
Expand Down Expand Up @@ -412,7 +412,7 @@

classinfo.properties["aio3_patternProperties"].default = property(mkx())

if not schema.additionalProperties:
if Model.booleanFalse(schema.additionalProperties):

def mkx():
def validate_patternProperties(self_):
Expand Down Expand Up @@ -473,21 +473,16 @@
arbitrary_types_allowed_ = False
extra_ = "allow"

if schema.additionalProperties is not None:
if isinstance(schema.additionalProperties, bool):
if not schema.additionalProperties:
extra_ = "forbid"
else:
arbitrary_types_allowed_ = True
elif isinstance(schema.additionalProperties, (SchemaBase, ReferenceBase)):
"""
we allow arbitrary types if additionalProperties has no properties
"""
assert schema.additionalProperties.properties is not None
if len(schema.additionalProperties.properties) == 0:
arbitrary_types_allowed_ = True
else:
raise TypeError(schema.additionalProperties)
if Model.booleanFalse(schema.additionalProperties):
extra_ = "forbid"
elif Model.booleanTrue(schema.additionalProperties) or (
isinstance(schema.additionalProperties, (SchemaBase, ReferenceBase))
and len(schema.additionalProperties.properties) == 0
):
"""
we allow arbitrary types if additionalProperties has no properties
"""
arbitrary_types_allowed_ = True

if getattr(schema, "patternProperties", None):
extra_ = "allow"
Expand Down Expand Up @@ -671,6 +666,41 @@
def is_type_any(schema: "SchemaType"):
return schema.type is None

@staticmethod
def booleanTrue(schema: Optional[Union["SchemaType", bool]]) -> bool:
"""
ACCEPT all?
:param schema:
:return: True if Schema is {} or True or None
"""
if schema is None:
return True
if isinstance(schema, bool):
return schema is True
elif isinstance(schema, (SchemaBase, ReferenceBase)):
"""matches Any - {}"""
return len(schema.model_fields_set) == 0
else:
raise ValueError(schema)

Check warning on line 684 in aiopenapi3/model.py

View check run for this annotation

Codecov / codecov/patch

aiopenapi3/model.py#L684

Added line #L684 was not covered by tests

@staticmethod
def booleanFalse(schema: Optional[Union["SchemaType", bool]]) -> bool:
"""
REJECT all?
:param schema:
:return: True if Schema is {'not':{}} or False
"""

if schema is None:
return False
if isinstance(schema, bool):
return schema is False
elif isinstance(schema, (SchemaBase, ReferenceBase)):
"""match {'not':{}}"""
return (v := getattr(schema, "not_", False)) and Model.booleanTrue(v)
else:
raise ValueError(schema)

Check warning on line 702 in aiopenapi3/model.py

View check run for this annotation

Codecov / codecov/patch

aiopenapi3/model.py#L702

Added line #L702 was not covered by tests

@staticmethod
def createField(schema: "SchemaType", _type=None, args=None):
if args is None:
Expand Down
8 changes: 5 additions & 3 deletions aiopenapi3/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
from .base import RootBase, ReferenceBase, SchemaBase, OperationBase, DiscriminatorBase
from .request import RequestBase
from .v30.paths import Operation
from .model import is_basemodel
from .model import is_basemodel, Model


if typing.TYPE_CHECKING:
Expand Down Expand Up @@ -428,6 +428,7 @@ def _init_operationindex(self, use_operation_tags: bool) -> bool:
@staticmethod
def _get_combined_attributes(schema):
"""Combine attributes from the schema."""
is_array = Model.is_type_any(schema) or Model.is_type(schema, "array")
return (
getattr(schema, "oneOf", []) # Swagger compat
+ (
Expand All @@ -438,8 +439,9 @@ def _get_combined_attributes(schema):
+ getattr(schema, "anyOf", []) # Swagger compat
+ schema.allOf
+ list(schema.properties.values())
+ ([schema.items] if schema.type == "array" and schema.items and not isinstance(schema, list) else [])
+ (schema.items if schema.type == "array" and schema.items and isinstance(schema, list) else [])
+ ([schema.items] if is_array and schema.items is not None and not isinstance(schema, list) else [])
+ (schema.items if is_array and schema.items is not None and isinstance(schema, list) else [])
+ (getattr(schema, "prefixItems", []) or [] if is_array else [])
)

@classmethod
Expand Down
12 changes: 11 additions & 1 deletion aiopenapi3/v30/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class Schema(ObjectExtended, SchemaBase):
not_: Optional[Union["Schema", Reference]] = Field(default=None, alias="not")
items: Optional[Union["Schema", Reference]] = Field(default=None)
properties: dict[str, Union["Schema", Reference]] = Field(default_factory=dict)
additionalProperties: Optional[Union[bool, "Schema", Reference]] = Field(default=None)
additionalProperties: Optional[Union["Schema", Reference]] = Field(default=None)
description: Optional[str] = Field(default=None)
format: Optional[str] = Field(default=None)
default: Optional[Any] = Field(default=None)
Expand All @@ -63,6 +63,16 @@ class Schema(ObjectExtended, SchemaBase):

model_config = ConfigDict(extra="forbid")

@model_validator(mode="before")
@classmethod
def is_boolean_schema(cls, data: Any) -> Any:
if not isinstance(data, bool):
return data
if data:
return {}
else:
return {"not": {}}

@model_validator(mode="after")
@classmethod
def validate_Schema_number_type(cls, s: "Schema"):
Expand Down
2 changes: 1 addition & 1 deletion aiopenapi3/v31/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class Components(ObjectExtended):
.. _Components Object: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#components-object
"""

schemas: dict[str, Union[Schema, bool]] = Field(default_factory=dict)
schemas: dict[str, Union[Schema]] = Field(default_factory=dict)
responses: dict[str, Union[Response, Reference]] = Field(default_factory=dict)
parameters: dict[str, Union[Parameter, Reference]] = Field(default_factory=dict)
examples: dict[str, Union[Example, Reference]] = Field(default_factory=dict)
Expand Down
3 changes: 2 additions & 1 deletion aiopenapi3/v31/root.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ class Root(ObjectExtended, RootBase):
externalDocs: dict[Any, Any] = Field(default_factory=dict)

@model_validator(mode="after")
def validate_Root(cls, r: "Root"):
@classmethod
def validate_Root(cls, r: "Root") -> "Self":
assert r.paths or r.components or r.webhooks
return r

Expand Down
13 changes: 12 additions & 1 deletion aiopenapi3/v31/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ class Schema(ObjectExtended, SchemaBase):
"""
properties: dict[str, "Schema"] = Field(default_factory=dict)
patternProperties: dict[str, "Schema"] = Field(default_factory=dict)
additionalProperties: Optional[Union[bool, "Schema"]] = Field(default=None)
additionalProperties: Optional["Schema"] = Field(default=None)
propertyNames: Optional["Schema"] = Field(default=None)

"""
Expand Down Expand Up @@ -162,7 +162,18 @@ class Schema(ObjectExtended, SchemaBase):
externalDocs: Optional[dict] = Field(default=None) # 'ExternalDocs'
example: Optional[Any] = Field(default=None)

@model_validator(mode="before")
@classmethod
def is_boolean_schema(cls, data: Any) -> Any:
if not isinstance(data, bool):
return data
if data:
return {}
else:
return {"not": {}}

@model_validator(mode="after")
@classmethod
def validate_Schema_number_type(cls, s: "Schema"):
if s.type == "integer":
for i in ["minimum", "maximum"]:
Expand Down
5 changes: 5 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,11 @@ def with_schema_additionalProperties_and_named_properties():
yield _get_parsed_yaml("schema-additionalProperties-and-named-properties" ".yaml")


@pytest.fixture
def with_schema_boolean(openapi_version):
yield _get_parsed_yaml("schema-boolean.yaml", openapi_version)


@pytest.fixture
def with_schema_empty(openapi_version):
yield _get_parsed_yaml("schema-empty.yaml", openapi_version)
Expand Down
66 changes: 66 additions & 0 deletions tests/fixtures/schema-boolean.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
openapi: 3.0.0
info:
version: 1.0.0
title: Example
license:
name: MIT
description: |
https://github.com/swagger-api/swagger-parser/issues/1770
servers:
- url: http://api.example.xyz/v1
paths:
/person/display/{personId}:
get:
parameters:
- name: personId
in: path
required: true
description: The id of the person to retrieve
schema:
type: string
operationId: list
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/BooleanTrue"
components:
schemas:
BooleanTrue: true
ArrayWithTrueItems:
type: array
items: true
ObjectWithTrueProperty:
properties:
someProp: true
ObjectWithTrueAdditionalProperties:
additionalProperties: true
AllOfWithTrue:
allOf:
- true
AnyOfWithTrue:
anyOf:
- true
OneOfWithTrue:
oneOf:
- true
NotWithTrue:
not: true
UnevaluatedItemsTrue:
unevaluatedItems: true
UnevaluatedPropertiesTrue:
unevaluatedProperties: true
PrefixitemsWithNoAdditionalItemsAllowed:
$schema: https://json-schema.org/draft/2020-12/schema
prefixItems:
- {}
- {}
- {}
items: false
PrefixitemsWithBooleanSchemas:
$schema: https://json-schema.org/draft/2020-12/schema
prefixItems:
- true
- false
13 changes: 13 additions & 0 deletions tests/schema_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -741,3 +741,16 @@ def test_schema_allof_oneof_combined(with_schema_allof_oneof_combined):
t.model_validate({"token": "1", "cmd": "invalid", "data": {"delay": 0}})
with pytest.raises(ValidationError):
t.model_validate({"token": "1", "cmd": "shutdown", "data": {"delay": "invalid"}})


def test_schema_boolean(with_schema_boolean):
v = copy.deepcopy(with_schema_boolean)
if v["openapi"] == "3.0.3":
for i in [
"PrefixitemsWithNoAdditionalItemsAllowed",
"PrefixitemsWithBooleanSchemas",
"UnevaluatedItemsTrue",
"UnevaluatedPropertiesTrue",
]:
del v["components"]["schemas"][i]
OpenAPI("/", v)
Loading