From a8cba9c5dda78ce77f4a146ace09adc3e1a8a18c Mon Sep 17 00:00:00 2001 From: alida Date: Thu, 29 Feb 2024 19:40:01 +0100 Subject: [PATCH 1/2] Fix KeyError @ fields to JSON:API schema rendering As the field called id according to rules guided by JSON:API is required it's processing must be be excluded in iteration's control structure used to apply requirment state to fields being either child of property called attributes or relationships. --- starlette_jsonapi/openapi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/starlette_jsonapi/openapi.py b/starlette_jsonapi/openapi.py index 217f4a9..9eabf49 100644 --- a/starlette_jsonapi/openapi.py +++ b/starlette_jsonapi/openapi.py @@ -106,7 +106,7 @@ def fields2jsonschema(self, fields, *, ordered=False, partial=None): properties['attributes']['properties'][observed_field_name] = prop # TODO: support meta fields - if field_obj.required: + if field_obj.required and field_name != 'id': if not partial or ( is_collection(partial) and field_name not in partial ): From e657182501bbb828107769ba3255dc0ac16514c4 Mon Sep 17 00:00:00 2001 From: alida Date: Fri, 1 Mar 2024 14:46:57 +0100 Subject: [PATCH 2/2] Unit tests: test random order of fields conversion --- tests/test_openapi.py | 200 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 199 insertions(+), 1 deletion(-) diff --git a/tests/test_openapi.py b/tests/test_openapi.py index 0886c54..54dffed 100644 --- a/tests/test_openapi.py +++ b/tests/test_openapi.py @@ -15,11 +15,209 @@ from starlette_jsonapi.schema import JSONAPISchema from starlette_jsonapi.meta import registered_resources from starlette_jsonapi.openapi import ( - JSONAPISchemaGenerator, JSONAPIMarshmallowPlugin, + JSONAPISchemaConverter, JSONAPISchemaGenerator, JSONAPIMarshmallowPlugin, with_openapi_info, response_for_relationship, request_for_relationship, ) +JSONAPI_SCHEMA_CONVERTER = JSONAPISchemaConverter( + openapi_version='3.0.0', + schema_name_resolver=None, + spec=APISpec( + title='Test API', + version='1.0', + openapi_version='3.0.0', + info={'description': 'Test OpenAPI resource'}, + plugins=[JSONAPIMarshmallowPlugin()], + ) +) + + +class FakeSchema(JSONAPISchema): + class Meta: + type_ = "fakes" + self_route = f"{type_}:get" + self_route_kwargs = {"id": ""} + self_route_many = f"{type_}:get_many" + + id = fields.UUID( + metadata={ + 'unique': True, + 'description': 'identifier of the instance', + }, + required=True, + ) + name = fields.Str( + required=True, + metadata={ + 'description': 'name of the instance' + } + ) + version = fields.Str( + metadata={ + 'description': 'version of the instance' + } + ) + deleted = fields.Boolean( + required=True, + metadata={ + 'description': 'if the instance is deleted' + } + ) + + @classmethod + def get_fields_with_parent_assigned(cls): + fields_parent_assigned = FakeSchema.get_fields() + + for item in fields_parent_assigned: + fields_parent_assigned[item].parent = FakeSchema + + return fields_parent_assigned + + +def test_fields2jsonschema_conversion_if_id_first_item_iterated(): + error = None + fields_to_convert = FakeSchema.get_fields_with_parent_assigned() + + try: + jsonschema = JSONAPI_SCHEMA_CONVERTER.fields2jsonschema( + fields={ + 'id': fields_to_convert['id'], + 'name': fields_to_convert['name'], + 'deleted': fields_to_convert['deleted'], + }, + ) + except Exception as e: + error = e + + assert not error + assert jsonschema == { + 'type': 'object', + 'properties': { + 'id': { + 'type': 'string', + 'format': 'uuid', + 'description': 'identifier of the instance', + }, + 'type': { + 'type': 'string', + 'enum': ['fakes'], + }, + 'attributes': { + 'type': 'object', + 'properties': { + 'name': { + 'type': 'string', + 'description': 'name of the instance', + }, + 'deleted': { + 'type': 'boolean', + 'description': 'if the instance is deleted', + } + }, + 'required': ['deleted', 'name'], + } + }, + 'required': ['type', 'attributes'], + } + + +def test_fields2jsonschema_conversion_if_id_last_item_iterated(): + error = None + fields_to_convert = FakeSchema.get_fields_with_parent_assigned() + + try: + jsonschema = JSONAPI_SCHEMA_CONVERTER.fields2jsonschema( + fields={ + 'name': fields_to_convert['name'], + 'deleted': fields_to_convert['deleted'], + 'id': fields_to_convert['id'], + }, + ) + except Exception as e: + error = e + + assert not error + assert jsonschema == { + 'type': 'object', + 'properties': { + 'id': { + 'type': 'string', + 'format': 'uuid', + 'description': 'identifier of the instance', + }, + 'type': { + 'type': 'string', + 'enum': ['fakes'], + }, + 'attributes': { + 'type': 'object', + 'properties': { + 'name': { + 'type': 'string', + 'description': 'name of the instance', + }, + 'deleted': { + 'type': 'boolean', + 'description': 'if the instance is deleted', + } + }, + 'required': ['deleted', 'name'], + } + }, + 'required': ['type', 'attributes'], + } + + +def test_fields2jsonschema_conversion_if_id_in_between_item_iterated(): + error = None + fields_to_convert = FakeSchema.get_fields_with_parent_assigned() + + try: + jsonschema = JSONAPI_SCHEMA_CONVERTER.fields2jsonschema( + fields={ + 'name': fields_to_convert['name'], + 'version': fields_to_convert['version'], + 'id': fields_to_convert['id'], + 'deleted': fields_to_convert['deleted'], + }, + ) + except Exception as e: + error = e + + assert not error + assert jsonschema == { + 'type': 'object', + 'properties': { + 'id': { + 'type': 'string', + 'format': 'uuid', + 'description': 'identifier of the instance', + }, + 'type': { + 'type': 'string', + 'enum': ['fakes'], + }, + 'attributes': { + 'type': 'object', + 'properties': { + 'name': { + 'type': 'string', + 'description': 'name of the instance', + }, + 'deleted': { + 'type': 'boolean', + 'description': 'if the instance is deleted', + }, + 'version': {'description': 'version of the instance', 'type': 'string'}, + }, + 'required': ['deleted', 'name'], + } + }, + 'required': ['type', 'attributes'], + } + + @pytest.fixture def openapi_schema_as_dict(): def make_schema_for_app(starlette_app: Starlette) -> dict: