diff --git a/docs/en/docs/routing.md b/docs/en/docs/routing.md index 58dca3be..4688d1be 100644 --- a/docs/en/docs/routing.md +++ b/docs/en/docs/routing.md @@ -28,6 +28,20 @@ the [SQLAlchemyCRUDRouter](backends/sqlalchemy.md) will use the model's table na You are also able to set custom prefixes with the `prefix` kwarg when creating your CRUDRouter. This can be done like so: `router = CRUDRouter(model=mymodel, prefix='carrot')` +## Custom item_id + +If you don't want the default `item_id` as your path parameter name for the item id you can use set the `item_id_parameter_name` key when creating a new CRUDRouter. This will change the parameter name in the OpenAPI specification for you. + +```python +SQLAlchemyCRUDRouter( + schema=Potato, + db_model=PotatoModel, + db=session, + prefix="potato", + item_id_parameter_name="potato_id", +) +``` + ## Disabling Routes Routes can be disabled from generating with a key word argument (kwarg) when creating your CRUDRouter. The valid kwargs are shown below. diff --git a/fastapi_crudrouter/core/_base.py b/fastapi_crudrouter/core/_base.py index e45d33fe..c827e35a 100644 --- a/fastapi_crudrouter/core/_base.py +++ b/fastapi_crudrouter/core/_base.py @@ -30,6 +30,7 @@ def __init__( update_route: Union[bool, DEPENDENCIES] = True, delete_one_route: Union[bool, DEPENDENCIES] = True, delete_all_route: Union[bool, DEPENDENCIES] = True, + item_id_parameter_name: Optional[str] = "item_id", **kwargs: Any, ) -> None: @@ -46,6 +47,7 @@ def __init__( if update_schema else schema_factory(self.schema, pk_field_name=self._pk, name="Update") ) + item_id_path = f"/{{{item_id_parameter_name}}}" prefix = str(prefix if prefix else self.schema.__name__).lower() prefix = self._base_path + prefix.strip("/") @@ -85,7 +87,7 @@ def __init__( if get_one_route: self._add_api_route( - "/{item_id}", + item_id_path, self._get_one(), methods=["GET"], response_model=self.schema, @@ -96,7 +98,7 @@ def __init__( if update_route: self._add_api_route( - "/{item_id}", + item_id_path, self._update(), methods=["PUT"], response_model=self.schema, @@ -107,7 +109,7 @@ def __init__( if delete_one_route: self._add_api_route( - "/{item_id}", + item_id_path, self._delete_one(), methods=["DELETE"], response_model=self.schema, diff --git a/fastapi_crudrouter/core/databases.py b/fastapi_crudrouter/core/databases.py index 7ea3c711..e4ffc99f 100644 --- a/fastapi_crudrouter/core/databases.py +++ b/fastapi_crudrouter/core/databases.py @@ -56,6 +56,7 @@ def __init__( update_route: Union[bool, DEPENDENCIES] = True, delete_one_route: Union[bool, DEPENDENCIES] = True, delete_all_route: Union[bool, DEPENDENCIES] = True, + item_id_parameter_name: Optional[str] = "item_id", **kwargs: Any ) -> None: assert ( @@ -81,6 +82,7 @@ def __init__( update_route=update_route, delete_one_route=delete_one_route, delete_all_route=delete_all_route, + item_id_parameter_name=item_id_parameter_name, **kwargs ) diff --git a/fastapi_crudrouter/core/gino_starlette.py b/fastapi_crudrouter/core/gino_starlette.py index d07d893e..c1b10d17 100644 --- a/fastapi_crudrouter/core/gino_starlette.py +++ b/fastapi_crudrouter/core/gino_starlette.py @@ -41,6 +41,7 @@ def __init__( update_route: Union[bool, DEPENDENCIES] = True, delete_one_route: Union[bool, DEPENDENCIES] = True, delete_all_route: Union[bool, DEPENDENCIES] = True, + item_id_parameter_name: Optional[str] = "item_id", **kwargs: Any ) -> None: assert gino_installed, "Gino must be installed to use the GinoCRUDRouter." @@ -63,6 +64,7 @@ def __init__( update_route=update_route, delete_one_route=delete_one_route, delete_all_route=delete_all_route, + item_id_parameter_name=item_id_parameter_name, **kwargs ) diff --git a/fastapi_crudrouter/core/mem.py b/fastapi_crudrouter/core/mem.py index d4e13c11..bbcbed13 100644 --- a/fastapi_crudrouter/core/mem.py +++ b/fastapi_crudrouter/core/mem.py @@ -22,6 +22,7 @@ def __init__( update_route: Union[bool, DEPENDENCIES] = True, delete_one_route: Union[bool, DEPENDENCIES] = True, delete_all_route: Union[bool, DEPENDENCIES] = True, + item_id_parameter_name: Optional[str] = "item_id", **kwargs: Any ) -> None: super().__init__( @@ -37,6 +38,7 @@ def __init__( update_route=update_route, delete_one_route=delete_one_route, delete_all_route=delete_all_route, + item_id_parameter_name=item_id_parameter_name, **kwargs ) diff --git a/fastapi_crudrouter/core/ormar.py b/fastapi_crudrouter/core/ormar.py index 99952600..148b5603 100644 --- a/fastapi_crudrouter/core/ormar.py +++ b/fastapi_crudrouter/core/ormar.py @@ -42,6 +42,7 @@ def __init__( update_route: Union[bool, DEPENDENCIES] = True, delete_one_route: Union[bool, DEPENDENCIES] = True, delete_all_route: Union[bool, DEPENDENCIES] = True, + item_id_parameter_name: Optional[str] = "item_id", **kwargs: Any ) -> None: assert ormar_installed, "Ormar must be installed to use the OrmarCRUDRouter." @@ -62,6 +63,7 @@ def __init__( update_route=update_route, delete_one_route=delete_one_route, delete_all_route=delete_all_route, + item_id_parameter_name=item_id_parameter_name, **kwargs ) diff --git a/fastapi_crudrouter/core/sqlalchemy.py b/fastapi_crudrouter/core/sqlalchemy.py index 58270f34..dc278970 100644 --- a/fastapi_crudrouter/core/sqlalchemy.py +++ b/fastapi_crudrouter/core/sqlalchemy.py @@ -39,6 +39,7 @@ def __init__( update_route: Union[bool, DEPENDENCIES] = True, delete_one_route: Union[bool, DEPENDENCIES] = True, delete_all_route: Union[bool, DEPENDENCIES] = True, + item_id_parameter_name: Optional[str] = "item_id", **kwargs: Any ) -> None: assert ( @@ -63,6 +64,7 @@ def __init__( update_route=update_route, delete_one_route=delete_one_route, delete_all_route=delete_all_route, + item_id_parameter_name=item_id_parameter_name, **kwargs ) diff --git a/fastapi_crudrouter/core/tortoise.py b/fastapi_crudrouter/core/tortoise.py index 52972a48..b3ff5b5b 100644 --- a/fastapi_crudrouter/core/tortoise.py +++ b/fastapi_crudrouter/core/tortoise.py @@ -32,6 +32,7 @@ def __init__( update_route: Union[bool, DEPENDENCIES] = True, delete_one_route: Union[bool, DEPENDENCIES] = True, delete_all_route: Union[bool, DEPENDENCIES] = True, + item_id_parameter_name: Optional[str] = "item_id", **kwargs: Any ) -> None: assert ( @@ -54,6 +55,7 @@ def __init__( update_route=update_route, delete_one_route=delete_one_route, delete_all_route=delete_all_route, + item_id_parameter_name=item_id_parameter_name, **kwargs ) diff --git a/tests/conftest.py b/tests/conftest.py index 74ac91a2..daacc235 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -68,3 +68,13 @@ def string_pk_client(request): ) def integrity_errors_client(request): yield from yield_test_client(request.param(), request.param) + + +@pytest.fixture( + params=[ + sqlalchemy_implementation_custom_item_id, + ], + scope="function", +) +def custom_item_id_client(request): + yield from yield_test_client(request.param(), request.param) \ No newline at end of file diff --git a/tests/implementations/__init__.py b/tests/implementations/__init__.py index 839f22a4..b2e1a734 100644 --- a/tests/implementations/__init__.py +++ b/tests/implementations/__init__.py @@ -23,6 +23,7 @@ sqlalchemy_implementation_custom_ids, sqlalchemy_implementation_integrity_errors, sqlalchemy_implementation_string_pk, + sqlalchemy_implementation_custom_item_id, DSN_LIST, ) from .tortoise_ import tortoise_implementation diff --git a/tests/implementations/sqlalchemy_.py b/tests/implementations/sqlalchemy_.py index e2295ab8..e76d1c7c 100644 --- a/tests/implementations/sqlalchemy_.py +++ b/tests/implementations/sqlalchemy_.py @@ -168,3 +168,43 @@ class CarrotModel(Base): ) return app + + +def sqlalchemy_implementation_custom_item_id(): + app, engine, Base, session = _setup_base_app() + + class PotatoModel(Base): + __tablename__ = "potatoes" + id = Column(Integer, primary_key=True, index=True) + thickness = Column(Float) + mass = Column(Float) + color = Column(String, unique=True) + type = Column(String) + + class CarrotModel(Base): + __tablename__ = "carrots" + id = Column(Integer, primary_key=True, index=True) + length = Column(Float) + color = Column(String) + + Base.metadata.create_all(bind=engine) + app.include_router( + SQLAlchemyCRUDRouter( + schema=Potato, + db_model=PotatoModel, + db=session, + prefix="potato", + item_id_parameter_name="potato_id", + ) + ) + app.include_router( + SQLAlchemyCRUDRouter( + schema=Carrot, + db_model=CarrotModel, + db=session, + prefix="carrot", + item_id_parameter_name="carrot_id" + ) + ) + + return app diff --git a/tests/test_custom_item_id.py b/tests/test_custom_item_id.py new file mode 100644 index 00000000..81a5137a --- /dev/null +++ b/tests/test_custom_item_id.py @@ -0,0 +1,33 @@ +from . import test_router +from pytest import mark + + +potato_type = dict(name="russet", origin="Canada") + +PATHS = ["/potato", "/carrot"] + +class TestOpenAPISpec: + def test_schema_exists(self, custom_item_id_client): + res = custom_item_id_client.get("/openapi.json") + assert res.status_code == 200 + + return res + + @mark.parametrize("path", PATHS) + def test_response_types(self, custom_item_id_client, path): + schema = self.test_schema_exists(custom_item_id_client).json() + paths = schema["paths"] + + for method in ["get", "post", "delete"]: + assert "200" in paths[path][method]["responses"] + + assert "422" in paths[path]["post"]["responses"] + + if path == "/potato": + item_path = path + "/{potato_id}" + else: + item_path = path + "/{carrot_id}" + for method in ["get", "put", "delete"]: + assert "200" in paths[item_path][method]["responses"] + assert "404" in paths[item_path][method]["responses"] + assert "422" in paths[item_path][method]["responses"]