use std::sync::Arc;
use business::domain::{{ snake }}::use_cases::{
create_{{ snake }}::{Create{{ pascal }}Params, Create{{ pascal }}UseCaseTrait},
delete_{{ snake }}::{Delete{{ pascal }}Params, Delete{{ pascal }}UseCaseTrait},
get_{{ snake }}::{Get{{ pascal }}Params, Get{{ pascal }}UseCaseTrait},
list_{{ plural }}::{List{{ plural_pascal }}Params, List{{ plural_pascal }}UseCaseTrait},
update_{{ snake }}::{Update{{ pascal }}Params, Update{{ pascal }}UseCaseTrait},
};
use poem_openapi::{OpenApi, param::Path, payload::Json};
use uuid::Uuid;
use crate::api::error::IntoErrorResponse;
use crate::api::tags::ApiTags;
use crate::api::{{ snake }}::dto::{Create{{ pascal }}Request, Update{{ pascal }}Request, {{ pascal }}Dto};
use crate::api::{{ snake }}::responses::{
Create{{ pascal }}Response, Delete{{ pascal }}Response, Get{{ pascal }}Response, List{{ plural_pascal }}Response,
Update{{ pascal }}Response,
};
/// Holds the use case ports; `generated/bootstrap.rs` plugs in the implementations.
pub struct {{ pascal }}Api {
pub create_{{ snake }}: Arc<dyn Create{{ pascal }}UseCaseTrait>,
pub get_{{ snake }}: Arc<dyn Get{{ pascal }}UseCaseTrait>,
pub list_{{ plural }}: Arc<dyn List{{ plural_pascal }}UseCaseTrait>,
pub update_{{ snake }}: Arc<dyn Update{{ pascal }}UseCaseTrait>,
pub delete_{{ snake }}: Arc<dyn Delete{{ pascal }}UseCaseTrait>,
}
#[OpenApi]
impl {{ pascal }}Api {
/// Create a new {{ pascal }}
#[oai(path = "/{{ plural }}", method = "post", tag = "ApiTags::{{ pascal }}")]
async fn create(&self, body: Json<Create{{ pascal }}Request>) -> Create{{ pascal }}Response {
match self
.create_{{ snake }}
.execute(Create{{ pascal }}Params {
{% for f in fields %} {{ f.name }}: body.{{ f.name }}{% if f.needs_clone %}.clone(){% endif %},
{% endfor %} })
.await
{
Ok(entity) => Create{{ pascal }}Response::Created(Json({{ pascal }}Dto::from_domain(&entity))),
Err(err) => {
let (status, error) = err.into_error_response();
tracing::warn!(code = %error.0.name, "create_{{ snake }} failed");
Create{{ pascal }}Response::from_status(status, error)
}
}
}
/// Get a {{ pascal }} by ID
#[oai(path = "/{{ plural }}/:id", method = "get", tag = "ApiTags::{{ pascal }}")]
async fn get_by_id(&self, id: Path<Uuid>) -> Get{{ pascal }}Response {
match self
.get_{{ snake }}
.execute(Get{{ pascal }}Params { id: id.0 })
.await
{
Ok(entity) => Get{{ pascal }}Response::Ok(Json({{ pascal }}Dto::from_domain(&entity))),
Err(err) => {
let (status, error) = err.into_error_response();
tracing::warn!(code = %error.0.name, "get_{{ snake }} failed");
Get{{ pascal }}Response::from_status(status, error)
}
}
}
/// List all {{ plural_pascal }}
#[oai(path = "/{{ plural }}", method = "get", tag = "ApiTags::{{ pascal }}")]
async fn list(&self) -> List{{ plural_pascal }}Response {
match self.list_{{ plural }}.execute(List{{ plural_pascal }}Params).await {
Ok(entities) => {
List{{ plural_pascal }}Response::Ok(Json(entities.iter().map({{ pascal }}Dto::from_domain).collect()))
}
Err(err) => {
let (status, error) = err.into_error_response();
tracing::error!(code = %error.0.name, "list_{{ plural }} failed");
List{{ plural_pascal }}Response::from_status(status, error)
}
}
}
/// Update a {{ pascal }}
#[oai(path = "/{{ plural }}/:id", method = "put", tag = "ApiTags::{{ pascal }}")]
async fn update(&self, id: Path<Uuid>, body: Json<Update{{ pascal }}Request>) -> Update{{ pascal }}Response {
match self
.update_{{ snake }}
.execute(Update{{ pascal }}Params {
id: id.0,
{% for f in fields %} {{ f.name }}: body.{{ f.name }}{% if f.needs_clone %}.clone(){% endif %},
{% endfor %} })
.await
{
Ok(entity) => Update{{ pascal }}Response::Ok(Json({{ pascal }}Dto::from_domain(&entity))),
Err(err) => {
let (status, error) = err.into_error_response();
tracing::warn!(code = %error.0.name, "update_{{ snake }} failed");
Update{{ pascal }}Response::from_status(status, error)
}
}
}
/// Delete a {{ pascal }}
#[oai(path = "/{{ plural }}/:id", method = "delete", tag = "ApiTags::{{ pascal }}")]
async fn delete(&self, id: Path<Uuid>) -> Delete{{ pascal }}Response {
match self
.delete_{{ snake }}
.execute(Delete{{ pascal }}Params { id: id.0 })
.await
{
Ok(()) => Delete{{ pascal }}Response::NoContent,
Err(err) => {
let (status, error) = err.into_error_response();
tracing::warn!(code = %error.0.name, "delete_{{ snake }} failed");
Delete{{ pascal }}Response::from_status(status, error)
}
}
}
}
#[cfg(test)]
mod tests {
use business::domain::{{ snake }}::errors::{{ pascal }}Error;
use business::domain::{{ snake }}::use_cases::{
create_{{ snake }}::mocks::MockCreate{{ pascal }}UseCase, delete_{{ snake }}::mocks::MockDelete{{ pascal }}UseCase,
get_{{ snake }}::mocks::MockGet{{ pascal }}UseCase, list_{{ plural }}::mocks::MockList{{ plural_pascal }}UseCase,
update_{{ snake }}::mocks::MockUpdate{{ pascal }}UseCase,
};
use business::tests::mothers::{{ snake }}_mother::{{ pascal }}Mother;
use poem::{Route, http::StatusCode, test::TestClient};
use poem_openapi::{OpenApiService, types::ToJSON};{% for import in test_imports %}
use {{ import }};{% endfor %}
use super::*;
/// One mock per use case. A test sets expectations only on the one its route should call: a
/// route that reaches for another use case panics instead of passing.
#[derive(Default)]
struct Mocks {
create: MockCreate{{ pascal }}UseCase,
get: MockGet{{ pascal }}UseCase,
list: MockList{{ plural_pascal }}UseCase,
update: MockUpdate{{ pascal }}UseCase,
delete: MockDelete{{ pascal }}UseCase,
}
impl Mocks {
fn client(self) -> TestClient<Route> {
let api = {{ pascal }}Api {
create_{{ snake }}: Arc::new(self.create),
get_{{ snake }}: Arc::new(self.get),
list_{{ plural }}: Arc::new(self.list),
update_{{ snake }}: Arc::new(self.update),
delete_{{ snake }}: Arc::new(self.delete),
};
TestClient::new(Route::new().nest("/", OpenApiService::new(api, "test", "1.0")))
}
}
fn create_request() -> Create{{ pascal }}Request {
Create{{ pascal }}Request {
{% for f in fields %} {{ f.name }}: {{ f.default_literal }},
{% endfor %} }
}
fn update_request() -> Update{{ pascal }}Request {
Update{{ pascal }}Request {
{% for f in fields %} {{ f.name }}: {{ f.default_literal }},
{% endfor %} }
}
fn item_path() -> String {
format!("/{{ plural }}/{}", Uuid::new_v4())
}
#[tokio::test]
async fn should_respond_201_when_{{ snake }}_is_created() {
// Arrange
let mut mocks = Mocks::default();
mocks.create.expect_execute().returning(|_| Ok({{ pascal }}Mother::random()));
// Act
let response = mocks
.client()
.post("/{{ plural }}")
.body_json(&create_request().to_json())
.send()
.await;
// Assert
response.assert_status(StatusCode::CREATED);
}
#[tokio::test]
async fn should_respond_422_when_{{ snake }}_is_invalid() {
// Arrange
let mut mocks = Mocks::default();
mocks
.create
.expect_execute()
.returning(|_| Err({{ pascal }}Error::ValidationError("invalid".to_string())));
// Act
let response = mocks
.client()
.post("/{{ plural }}")
.body_json(&create_request().to_json())
.send()
.await;
// Assert
response.assert_status(StatusCode::UNPROCESSABLE_ENTITY);
}
#[tokio::test]
async fn should_respond_200_when_{{ snake }}_is_found() {
// Arrange
let mut mocks = Mocks::default();
mocks.get.expect_execute().returning(|_| Ok({{ pascal }}Mother::random()));
// Act
let response = mocks.client().get(item_path()).send().await;
// Assert
response.assert_status(StatusCode::OK);
}
#[tokio::test]
async fn should_respond_404_when_{{ snake }}_does_not_exist() {
// Arrange
let mut mocks = Mocks::default();
mocks.get.expect_execute().returning(|_| Err({{ pascal }}Error::NotFound));
// Act
let response = mocks.client().get(item_path()).send().await;
// Assert
response.assert_status(StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn should_respond_200_with_every_{{ snake }}_when_listing() {
// Arrange
let mut mocks = Mocks::default();
mocks.list.expect_execute().returning(|_| Ok({{ pascal }}Mother::random_vec(2)));
// Act
let response = mocks.client().get("/{{ plural }}").send().await;
// Assert
response.assert_status(StatusCode::OK);
response.json().await.value().array().assert_len(2);
}
#[tokio::test]
async fn should_respond_500_when_listing_fails_in_the_repository() {
// Arrange
let mut mocks = Mocks::default();
mocks.list.expect_execute().returning(|_| Err({{ pascal }}Error::RepositoryError));
// Act
let response = mocks.client().get("/{{ plural }}").send().await;
// Assert
response.assert_status(StatusCode::INTERNAL_SERVER_ERROR);
}
#[tokio::test]
async fn should_respond_200_when_{{ snake }}_is_updated() {
// Arrange
let mut mocks = Mocks::default();
mocks.update.expect_execute().returning(|_| Ok({{ pascal }}Mother::random()));
// Act
let response = mocks
.client()
.put(item_path())
.body_json(&update_request().to_json())
.send()
.await;
// Assert
response.assert_status(StatusCode::OK);
}
#[tokio::test]
async fn should_respond_404_when_updating_a_missing_{{ snake }}() {
// Arrange
let mut mocks = Mocks::default();
mocks.update.expect_execute().returning(|_| Err({{ pascal }}Error::NotFound));
// Act
let response = mocks
.client()
.put(item_path())
.body_json(&update_request().to_json())
.send()
.await;
// Assert
response.assert_status(StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn should_respond_204_when_{{ snake }}_is_deleted() {
// Arrange
let mut mocks = Mocks::default();
mocks.delete.expect_execute().returning(|_| Ok(()));
// Act
let response = mocks.client().delete(item_path()).send().await;
// Assert
response.assert_status(StatusCode::NO_CONTENT);
}
#[tokio::test]
async fn should_respond_404_when_deleting_a_missing_{{ snake }}() {
// Arrange
let mut mocks = Mocks::default();
mocks.delete.expect_execute().returning(|_| Err({{ pascal }}Error::NotFound));
// Act
let response = mocks.client().delete(item_path()).send().await;
// Assert
response.assert_status(StatusCode::NOT_FOUND);
}
}