diff --git a/natsapi/_compat.py b/natsapi/_compat.py index e8f1d82..0395b13 100644 --- a/natsapi/_compat.py +++ b/natsapi/_compat.py @@ -235,3 +235,15 @@ def sort(self, value: JsonSchemaValue, *args) -> JsonSchemaValue: https://docs.pydantic.dev/latest/concepts/json_schema/#json-schema-sorting """ return value + + def nullable_schema(self, schema: JsonSchemaValue) -> JsonSchemaValue: + """ + Override nullable schema generation to flatten optional types. + Instead of generating anyOf with [type, null], just return the type. + """ + if PYDANTIC_V2: + # Extract the inner schema and generate it without the null variant + inner_schema = schema.get("schema") + if inner_schema: + return self.generate_inner(inner_schema) + return super().nullable_schema(schema) diff --git a/shell.nix b/shell.nix index 0703d57..309bf2e 100644 --- a/shell.nix +++ b/shell.nix @@ -9,21 +9,9 @@ in pkgs.mkShell { packages = with pkgs; [ python311 - ruff rustc cargo - - (poetry.override { python3 = python311; }) - - (python311.withPackages (p: with p; [ - pip - python-lsp-server - pynvim - pyls-isort - python-lsp-black - ])) - ]; env.LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ diff --git a/tests/asyncapi/test_generation.py b/tests/asyncapi/test_generation.py index 9d68cba..395ba74 100644 --- a/tests/asyncapi/test_generation.py +++ b/tests/asyncapi/test_generation.py @@ -1,4 +1,5 @@ -from typing import Any, Union +import sys +from typing import Any, Optional, Union from uuid import uuid4 import pytest @@ -126,6 +127,51 @@ def test_generate_schema_w_external_docs_should_generate(): assert schema["externalDocs"] == external_docs.dict() +@pytest.mark.skipif(sys.version_info < (3, 10), reason="Requires Python 3.10+ for union syntax") +async def test_optional_types_are_generated_correctly(app: NatsAPI): + class User(BaseModel): + name: str + mandatory_property_1: str + optional_property_1: str | None + optional_property_2: Optional[str] + optional_property_3: Optional[str] = None + optional_property_4: Optional[str] | None = None + optional_property_5: str | None = None + optional_property_6: str = None + optional_property_7: str | int + + user_router = SubjectRouter(prefix="v1", tags=["users"], deprecated=True) + + @user_router.request( + "users.CREATE", + result=User, + description="Creates user that can be used throughout the app", + tags=["auth"], + suggested_timeout=0.5, + ) + def create_base_user(app, user: User): + return {"id": uuid4(), "name": user.name} + + app.include_router(user_router) + app.generate_asyncapi() + schema = app.asyncapi_schema + + assert schema["components"]["schemas"]["User"]["properties"]["mandatory_property_1"]["type"] == "string" + assert schema["components"]["schemas"]["User"]["properties"]["optional_property_1"]["type"] == "string" + assert schema["components"]["schemas"]["User"]["properties"]["optional_property_2"]["type"] == "string" + assert schema["components"]["schemas"]["User"]["properties"]["optional_property_3"]["type"] == "string" + assert schema["components"]["schemas"]["User"]["properties"]["optional_property_4"]["type"] == "string" + assert schema["components"]["schemas"]["User"]["properties"]["optional_property_5"]["type"] == "string" + assert schema["components"]["schemas"]["User"]["properties"]["optional_property_6"]["type"] == "string" + assert schema["components"]["schemas"]["User"]["properties"]["optional_property_7"]["anyOf"] == [ + {"type": "string"}, + {"type": "integer"}, + ] + + schema_from_request = (await app.nc.request("natsapi.development.schema.RETRIEVE", {})).result + assert schema_from_request == schema + + async def test_generate_shema_w_requests_should_generate(app: NatsAPI): class BaseUser(BaseModel): email: str = Field(..., description="Unique email of user", example="foo@bar.com")