From 538ee4b6519be6cc9aca75d77777b4c437235265 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20K=C3=B6tter?= Date: Thu, 15 Feb 2024 20:14:23 +0100 Subject: [PATCH] =?UTF-8?q?v3x/parameter=20-=20encoding=20=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit & tests for boolean, integer, number --- aiopenapi3/v30/parameter.py | 42 ++- tests/api/v2/main.py | 11 +- tests/apiv2_test.py | 11 +- tests/fixtures/paths-parameter-format.yaml | 302 ++++++++++++++++++--- tests/path_test.py | 18 +- 5 files changed, 320 insertions(+), 64 deletions(-) diff --git a/aiopenapi3/v30/parameter.py b/aiopenapi3/v30/parameter.py index 752cda5c..31c0e3e7 100644 --- a/aiopenapi3/v30/parameter.py +++ b/aiopenapi3/v30/parameter.py @@ -3,6 +3,7 @@ import decimal import typing import uuid +import json from typing import Union, Optional, Dict, Any from collections.abc import MutableMapping @@ -54,12 +55,19 @@ def _encode(self, name: str, value): value = schema.model(value) if isinstance(value, BaseModel): type_ = "object" - elif (t := type(value)) == bool: - type_ = "boolean" - elif t in (bytes, datetime.datetime, datetime.date, datetime.time, datetime.timedelta, uuid.UUID): + elif (t := type(value)) in ( + bytes, + datetime.datetime, + datetime.date, + datetime.time, + datetime.timedelta, + uuid.UUID, + ): type_ = "string" + elif t in TYPES_SCHEMA_MAP: + type_ = TYPES_SCHEMA_MAP[t] else: - type_ = TYPES_SCHEMA_MAP[type(value)] + raise TypeError(f"Unsupported type {t}") return self._encode_value(name, type_, value, schema, explode, style) @@ -74,9 +82,11 @@ def _encode__matrix(self, name: str, type_: str, value, schema: "v3xSchemaType", https://www.rfc-editor.org/rfc/rfc6570#section-3.2.8 :param type_: """ - if type_ in frozenset(["string", "number", "boolean", "integer"]): + if type_ in frozenset(["string", "number", "integer"]): # ;color=blue value = f";{name}={value}" + elif type_ == "boolean": + value = f";{name}={json.dumps(value)}" elif type_ == "null": value = f";{name}" elif type_ == "array": @@ -101,11 +111,15 @@ def _encode__label(self, name: str, type_: str, value, schema: "v3xSchemaType", """ 3.2.5. Label Expansion with Dot-Prefix: {.var} - https://www.rfc-editor.org/rfc/rfc6570#section-3.2.8 + https://www.rfc-editor.org/rfc/rfc6570#section-3.2.5 """ - if type_ in frozenset(["string", "number", "boolean", "integer"]): + if type_ == "string": # .blue value = f".{value}" + elif type_ in frozenset(["number", "integer"]): + value = f".{str(value)}" + elif type_ == "boolean": + value = f".{json.dumps(value)}" elif type_ == "null": value = "." elif type_ == "array": @@ -134,11 +148,15 @@ def _encode__form(self, name: str, type_: str, value, schema: "v3xSchemaType", e https://www.rfc-editor.org/rfc/rfc6570#section-3.2.8 """ - if type_ in frozenset(["string", "number", "boolean", "integer"]): + if type_ == "string": # color=blue return {name: value} + elif type_ in frozenset(["number", "integer"]): + return {name: str(value)} + elif type_ == "boolean": + return {name: json.dumps(value)} elif type_ == "null": - return {name: None} + return {name: ""} elif type_ == "array": assert isinstance(value, (list, tuple)) if explode is False: @@ -168,8 +186,12 @@ def _encode__simple(self, name: str, type_: str, value, schema: "v3xSchemaType", https://www.rfc-editor.org/rfc/rfc6570#section-3.2.2 """ - if type_ in frozenset(["string", "number", "boolean", "integer"]): + if type_ == "string": return {name: value} + elif type_ in frozenset(["number", "integer"]): + return {name: str(value)} + elif type_ == "boolean": + return {name: json.dumps(value)} elif type_ == "null": return dict() elif type_ == "array": diff --git a/tests/api/v2/main.py b/tests/api/v2/main.py index ce3d5ac3..f2d3c1e2 100644 --- a/tests/api/v2/main.py +++ b/tests/api/v2/main.py @@ -1,9 +1,10 @@ import errno import uuid -from typing import Optional +from typing import Optional, Union +from typing_extensions import Annotated import starlette.status -from fastapi import Body, Response, APIRouter, Path +from fastapi import Body, Response, Header, APIRouter, Path from fastapi.responses import JSONResponse from fastapi_versioning import version @@ -70,7 +71,11 @@ def getPet(pet_id: str = Path(..., alias="petId")) -> schema.Pets: @router.delete( "/pets/{petId}", operation_id="deletePet", responses={204: {"model": None}, 404: {"model": schema.Error}} ) -def deletePet(response: Response, pet_id: uuid.UUID = Path(..., alias="petId")) -> None: +def deletePet( + response: Response, + x_raise_nonexist: Annotated[Union[bool, None], Header()], + pet_id: uuid.UUID = Path(..., alias="petId"), +) -> None: for k, v in ZOO.items(): if pet_id == v.identifier: del ZOO[k] diff --git a/tests/apiv2_test.py b/tests/apiv2_test.py index ade3e895..0f29bcbc 100644 --- a/tests/apiv2_test.py +++ b/tests/apiv2_test.py @@ -191,9 +191,12 @@ async def test_createPet(event_loop, server, client): @pytest.mark.asyncio async def test_listPet(event_loop, server, client): r = await client._.createPet(data=randomPet(client, str(uuid.uuid4()))) - l = await client._.listPet() + l = await client._.listPet(parameters={"limit": 1}) assert len(l) > 0 + l = await client._.listPet(parameters={"limit": None}) + assert isinstance(l, client.components.schemas["HTTPValidationError"].get_type()) + @pytest.mark.asyncio async def test_getPet(event_loop, server, client): @@ -210,15 +213,15 @@ async def test_getPet(event_loop, server, client): @pytest.mark.asyncio async def test_deletePet(event_loop, server, client): - r = await client._.deletePet(parameters={"petId": uuid.uuid4()}) + r = await client._.deletePet(parameters={"petId": uuid.uuid4(), "x-raise-nonexist": False}) assert type(r).model_json_schema() == client.components.schemas["Error"].get_type().model_json_schema() await client._.createPet(data=randomPet(client, str(uuid.uuid4()))) - zoo = await client._.listPet() + zoo = await client._.listPet(parameters={"limit": 1}) for pet in zoo: while hasattr(pet, "root"): pet = pet.root - await client._.deletePet(parameters={"petId": pet.identifier}) + await client._.deletePet(parameters={"petId": pet.identifier, "x-raise-nonexist": None}) @pytest.mark.asyncio diff --git a/tests/fixtures/paths-parameter-format.yaml b/tests/fixtures/paths-parameter-format.yaml index dbc52536..de03bace 100644 --- a/tests/fixtures/paths-parameter-format.yaml +++ b/tests/fixtures/paths-parameter-format.yaml @@ -17,6 +17,9 @@ paths: - $ref: "#/components/parameters/StringFormQuery" - $ref: "#/components/parameters/ArrayFormQuery" - $ref: "#/components/parameters/ObjectFormQuery" + - $ref: "#/components/parameters/BooleanFormQuery" + - $ref: "#/components/parameters/IntegerFormQuery" + - $ref: "#/components/parameters/NumberFormQuery" responses: &resp '200': @@ -36,10 +39,13 @@ paths: - $ref: "#/components/parameters/StringFormExplodeQuery" - $ref: "#/components/parameters/ArrayFormExplodeQuery" - $ref: "#/components/parameters/ObjectFormExplodeQuery" + - $ref: "#/components/parameters/BooleanFormExplodeQuery" + - $ref: "#/components/parameters/IntegerFormExplodeQuery" + - $ref: "#/components/parameters/NumberFormExplodeQuery" responses: *resp - /label/query/{string}/{array}/{object}/{empty}: + /label/query/{string}/{array}/{object}/{empty}/{boolean}/{integer}/{number}: get: operationId: LabelPath parameters: @@ -47,6 +53,9 @@ paths: - $ref: "#/components/parameters/StringLabelPath" - $ref: "#/components/parameters/ArrayLabelPath" - $ref: "#/components/parameters/ObjectLabelPath" + - $ref: "#/components/parameters/BooleanLabelPath" + - $ref: "#/components/parameters/IntegerLabelPath" + - $ref: "#/components/parameters/NumberLabelPath" responses: *resp @@ -57,6 +66,9 @@ paths: - $ref: "#/components/parameters/StringLabelExplodePath" - $ref: "#/components/parameters/ArrayLabelExplodePath" - $ref: "#/components/parameters/ObjectLabelExplodePath" + - $ref: "#/components/parameters/BooleanLabelExplodePath" + - $ref: "#/components/parameters/IntegerLabelExplodePath" + - $ref: "#/components/parameters/NumberLabelExplodePath" responses: *resp @@ -87,12 +99,15 @@ paths: - $ref: "#/components/parameters/ArraypipeDelimitedQuery" responses: *resp - /matrix/path/{string}/{array}/{object}/{empty}: + /matrix/path/{string}/{array}/{object}/{empty}/{boolean}/{integer}/{number}: parameters: - $ref: "#/components/parameters/EmptyMatrixPath" - $ref: "#/components/parameters/StringMatrixPath" - $ref: "#/components/parameters/ArrayMatrixPath" - $ref: "#/components/parameters/ObjectMatrixPath" + - $ref: "#/components/parameters/BooleanMatrixPath" + - $ref: "#/components/parameters/IntegerMatrixPath" + - $ref: "#/components/parameters/NumberMatrixPath" get: operationId: matrixPath @@ -107,15 +122,20 @@ paths: - $ref: "#/components/parameters/StringSimpleHeader" - $ref: "#/components/parameters/ArraySimpleHeader" - $ref: "#/components/parameters/ObjectSimpleHeader" + - $ref: "#/components/parameters/BooleanSimpleHeader" + - $ref: "#/components/parameters/IntegerSimpleHeader" + - $ref: "#/components/parameters/NumberSimpleHeader" responses: *resp - /simple/explode/path/{string}/{array}/{object}: + /simple/explode/path/{string}/{array}/{object}/{boolean}/{integer}/{number}: parameters: - $ref: "#/components/parameters/StringSimpleExplodePath" - $ref: "#/components/parameters/ArraySimpleExplodePath" - $ref: "#/components/parameters/ObjectSimpleExplodePath" - + - $ref: "#/components/parameters/BooleanSimpleExplodePath" + - $ref: "#/components/parameters/IntegerSimpleExplodePath" + - $ref: "#/components/parameters/NumberSimpleExplodePath" get: operationId: simpleExplodePath @@ -139,6 +159,28 @@ components: B: type: integer + Array: + type: array + items: + type: string + + String: + type: string + + Empty: + oneOf: + - type: string + - enum: [null] + + Boolean: + type: boolean + + Integer: + type: integer + + Number: + type: number + Matrjoschka: type: object properties: @@ -167,8 +209,7 @@ components: explode: false required: true schema: - type: number - nullable: true + $ref: "#/components/schemas/Empty" StringFormQuery: in: query @@ -177,7 +218,7 @@ components: explode: false required: true schema: - type: string + $ref: "#/components/schemas/String" ArrayFormQuery: in: query @@ -186,9 +227,7 @@ components: explode: false required: true schema: - type: array - items: - type: string + $ref: "#/components/schemas/Array" ObjectFormQuery: in: query @@ -199,6 +238,33 @@ components: schema: $ref: "#/components/schemas/RGB" + BooleanFormQuery: + in: query + name: boolean + style: form + explode: false + required: true + schema: + $ref: "#/components/schemas/Boolean" + + IntegerFormQuery: + in: query + name: integer + style: form + explode: false + required: true + schema: + $ref: "#/components/schemas/Integer" + + NumberFormQuery: + in: query + name: number + style: form + explode: false + required: true + schema: + $ref: "#/components/schemas/Number" + StringFormExplodeQuery: in: query name: string @@ -206,7 +272,7 @@ components: explode: true required: true schema: - type: string + $ref: "#/components/schemas/String" ArrayFormExplodeQuery: in: query @@ -215,9 +281,7 @@ components: explode: true required: true schema: - type: array - items: - type: string + $ref: "#/components/schemas/Array" ObjectFormExplodeQuery: in: query @@ -228,6 +292,34 @@ components: schema: $ref: "#/components/schemas/RGB" + BooleanFormExplodeQuery: + in: query + name: boolean + style: form + explode: true + required: true + schema: + $ref: "#/components/schemas/Boolean" + + IntegerFormExplodeQuery: + in: query + name: integer + style: form + explode: true + required: true + schema: + $ref: "#/components/schemas/Integer" + + NumberFormExplodeQuery: + in: query + name: number + style: form + explode: true + required: true + schema: + $ref: "#/components/schemas/Number" + + EmptyLabelPath: in: path name: empty @@ -235,8 +327,7 @@ components: explode: false required: true schema: - type: string - nullable: true + $ref: "#/components/schemas/Empty" StringLabelPath: in: path @@ -245,7 +336,7 @@ components: explode: false required: true schema: - type: string + $ref: "#/components/schemas/String" ArrayLabelPath: in: path @@ -254,9 +345,7 @@ components: explode: false required: true schema: - type: array - items: - type: string + $ref: "#/components/schemas/Array" ObjectLabelPath: in: path @@ -267,6 +356,34 @@ components: schema: $ref: "#/components/schemas/RGB" + BooleanLabelPath: + in: path + name: boolean + style: label + explode: false + required: true + schema: + $ref: "#/components/schemas/Boolean" + + IntegerLabelPath: + in: path + name: integer + style: label + explode: false + required: true + schema: + $ref: "#/components/schemas/Integer" + + NumberLabelPath: + in: path + name: number + style: label + explode: false + required: true + schema: + $ref: "#/components/schemas/Number" + + StringLabelExplodePath: in: path name: string @@ -274,7 +391,7 @@ components: explode: true required: true schema: - type: string + $ref: "#/components/schemas/String" ArrayLabelExplodePath: in: path @@ -283,9 +400,7 @@ components: explode: true required: true schema: - type: array - items: - type: string + $ref: "#/components/schemas/Array" ObjectLabelExplodePath: in: path @@ -296,6 +411,32 @@ components: schema: $ref: "#/components/schemas/RGB" + BooleanLabelExplodePath: + in: path + name: boolean + style: label + explode: true + required: true + schema: + $ref: "#/components/schemas/Boolean" + + IntegerLabelExplodePath: + in: path + name: integer + style: label + explode: true + required: true + schema: + $ref: "#/components/schemas/Integer" + + NumberLabelExplodePath: + in: path + name: number + style: label + explode: true + required: true + schema: + $ref: "#/components/schemas/Number" ObjectdeepObjectExplodeQuery: in: query @@ -322,9 +463,7 @@ components: explode: false required: true schema: - type: array - items: - type: string + $ref: "#/components/schemas/Array" ObjectspaceDelimitedQuery: in: query @@ -342,9 +481,7 @@ components: explode: false required: true schema: - type: array - items: - type: string + $ref: "#/components/schemas/Array" EmptyMatrixPath: in: path @@ -353,8 +490,7 @@ components: explode: false required: true schema: - type: string - nullable: true + $ref: "#/components/schemas/Empty" StringMatrixPath: in: path @@ -363,7 +499,7 @@ components: explode: false required: true schema: - type: string + $ref: "#/components/schemas/String" ArrayMatrixPath: in: path @@ -372,9 +508,7 @@ components: explode: false required: true schema: - type: array - items: - type: string + $ref: "#/components/schemas/Array" ObjectMatrixPath: in: path @@ -385,6 +519,33 @@ components: schema: $ref: "#/components/schemas/RGB" + BooleanMatrixPath: + in: path + name: boolean + style: matrix + explode: false + required: true + schema: + $ref: "#/components/schemas/Boolean" + + IntegerMatrixPath: + in: path + name: integer + style: matrix + explode: false + required: true + schema: + $ref: "#/components/schemas/Integer" + + NumberMatrixPath: + in: path + name: number + style: matrix + explode: false + required: true + schema: + $ref: "#/components/schemas/Number" + EmptySimpleHeader: in: header name: empty @@ -392,7 +553,7 @@ components: explode: false required: true schema: - type: string + $ref: "#/components/schemas/String" StringSimpleHeader: in: header @@ -401,7 +562,7 @@ components: explode: false required: true schema: - type: string + $ref: "#/components/schemas/String" ArraySimpleHeader: in: header @@ -410,9 +571,7 @@ components: explode: false required: true schema: - type: array - items: - type: string + $ref: "#/components/schemas/Array" ObjectSimpleHeader: in: header @@ -423,6 +582,34 @@ components: schema: $ref: "#/components/schemas/RGB" + BooleanSimpleHeader: + in: header + name: boolean + style: simple + explode: false + required: true + schema: + $ref: "#/components/schemas/Boolean" + + IntegerSimpleHeader: + in: header + name: integer + style: simple + explode: false + required: true + schema: + $ref: "#/components/schemas/Integer" + + NumberSimpleHeader: + in: header + name: number + style: simple + explode: false + required: true + schema: + $ref: "#/components/schemas/Number" + + StringSimpleExplodePath: in: path name: string @@ -430,7 +617,7 @@ components: explode: true required: true schema: - type: string + $ref: "#/components/schemas/String" ArraySimpleExplodePath: in: path @@ -439,9 +626,7 @@ components: explode: true required: true schema: - type: array - items: - type: string + $ref: "#/components/schemas/Array" ObjectSimpleExplodePath: in: path @@ -451,3 +636,30 @@ components: required: true schema: $ref: "#/components/schemas/RGB" + + BooleanSimpleExplodePath: + in: path + name: boolean + style: simple + explode: true + required: true + schema: + $ref: "#/components/schemas/Boolean" + + IntegerSimpleExplodePath: + in: path + name: integer + style: simple + explode: true + required: true + schema: + $ref: "#/components/schemas/Integer" + + NumberSimpleExplodePath: + in: path + name: number + style: simple + explode: true + required: true + schema: + $ref: "#/components/schemas/Number" diff --git a/tests/path_test.py b/tests/path_test.py index 0e5b2737..0660fad3 100644 --- a/tests/path_test.py +++ b/tests/path_test.py @@ -278,6 +278,9 @@ def test_paths_parameter_format(httpx_mock, with_paths_parameter_format): "string": "blue", "empty": None, "object": {"R": 100, "G": 200, "B": 150}, + "boolean": False, + "integer": 100, + "number": 2**78 * 1.1, } ne = parameters.copy() @@ -300,7 +303,7 @@ def test_paths_parameter_format(httpx_mock, with_paths_parameter_format): # r = api._.LabelPath(parameters=parameters) r = api.createRequest("LabelPath") r._prepare(None, parameters) - assert r.req.url == "/label/query/.blue/.blue.black.brown/.R.100.G.200.B.150/." + assert r.req.url == "/label/query/.blue/.blue.black.brown/.R.100.G.200.B.150/./.false/.100/.3.3245460039402305e+23" v = r.request(parameters=parameters) request = httpx_mock.get_requests()[-1] u = yarl.URL(str(request.url)) @@ -315,7 +318,9 @@ def test_paths_parameter_format(httpx_mock, with_paths_parameter_format): assert u.parts[4] == ".blue" assert u.parts[5] == ".blue.black.brown" assert u.parts[6] == ".R=100.G=200.B=150" - # assert u.parts[7] == "" + assert u.parts[7] == ".false" + assert u.parts[8] == ".100" + assert u.parts[9] == ".3.3245460039402305e+23" r = api._.deepObjectExplodeQuery(parameters={"object": parameters["object"]}) request = httpx_mock.get_requests()[-1] @@ -355,6 +360,9 @@ def test_paths_parameter_format(httpx_mock, with_paths_parameter_format): assert u.parts[5] == ";array=blue,black,brown" assert u.parts[6] == ";object=R,100,G,200,B,150" assert u.parts[7] == ";empty" + assert u.parts[8] == ";boolean=false" + assert u.parts[9] == ";integer=100" + assert u.parts[10] == ";number=3.3245460039402305e+23" r = api._.simpleHeader(parameters=ne) request = httpx_mock.get_requests()[-1] @@ -363,6 +371,9 @@ def test_paths_parameter_format(httpx_mock, with_paths_parameter_format): assert request.headers.get("string") == "blue" assert request.headers.get("array") == "blue,black,brown" assert request.headers.get("object") == "R,100,G,200,B,150" + assert request.headers.get("boolean") == "false" + assert request.headers.get("integer") == "100" + assert request.headers.get("number") == "3.3245460039402305e+23" r = api._.simpleExplodePath(parameters=ne) request = httpx_mock.get_requests()[-1] @@ -371,6 +382,9 @@ def test_paths_parameter_format(httpx_mock, with_paths_parameter_format): assert u.parts[5] == "blue" assert u.parts[6] == "blue,black,brown" assert u.parts[7] == "R=100,G=200,B=150" + assert u.parts[8] == "false" + assert u.parts[9] == "100" + assert u.parts[10] == "3.3245460039402305e+23" return