Skip to content
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
12 changes: 12 additions & 0 deletions natsapi/_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
12 changes: 0 additions & 12 deletions shell.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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 [
Expand Down
48 changes: 47 additions & 1 deletion tests/asyncapi/test_generation.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import Any, Union
import sys
from typing import Any, Optional, Union
from uuid import uuid4

import pytest
Expand Down Expand Up @@ -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")
Expand Down