From fa435994f260799cdd660dc6fdb41ae14d5d5c76 Mon Sep 17 00:00:00 2001 From: 0x6d696b7566616e Date: Sun, 29 Jun 2025 00:01:00 +0700 Subject: [PATCH] crud testi --- Cargo.lock | 1 + imphnen-cms/Cargo.toml | 1 + imphnen-cms/src/v1/landing/mod.rs | 2 + .../src/v1/landing/testimonials/mod.rs | 44 ++++++ .../testimonials/testimonials_controller.rs | 125 ++++++++++++++++++ .../landing/testimonials/testimonials_dto.rs | 78 +++++++++++ .../testimonials/testimonials_repository.rs | 120 +++++++++++++++++ .../testimonials/testimonials_schema.rs | 84 ++++++++++++ .../testimonials/testimonials_service.rs | 108 +++++++++++++++ imphnen-gateway/src/docs.rs | 13 +- imphnen-gateway/src/lib.rs | 7 +- imphnen-libs/src/surrealdb/resource.rs | 2 + 12 files changed, 581 insertions(+), 4 deletions(-) create mode 100644 imphnen-cms/src/v1/landing/testimonials/mod.rs create mode 100644 imphnen-cms/src/v1/landing/testimonials/testimonials_controller.rs create mode 100644 imphnen-cms/src/v1/landing/testimonials/testimonials_dto.rs create mode 100644 imphnen-cms/src/v1/landing/testimonials/testimonials_repository.rs create mode 100644 imphnen-cms/src/v1/landing/testimonials/testimonials_schema.rs create mode 100644 imphnen-cms/src/v1/landing/testimonials/testimonials_service.rs diff --git a/Cargo.lock b/Cargo.lock index c7748fc..a1478ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1955,6 +1955,7 @@ dependencies = [ "axum-test", "chrono", "imphnen-entities", + "imphnen-iam", "imphnen-libs", "imphnen-utils", "lazy_static", diff --git a/imphnen-cms/Cargo.toml b/imphnen-cms/Cargo.toml index 6ef7021..21d91c9 100644 --- a/imphnen-cms/Cargo.toml +++ b/imphnen-cms/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] +imphnen-iam = { version = "0.1.0", path = "../imphnen-iam" } imphnen-libs = { version = "0.1.0", path = "../imphnen-libs" } imphnen-utils = { version = "0.1.0", path = "../imphnen-utils" } imphnen-entities = { version = "0.1.0", path = "../imphnen-entities" } diff --git a/imphnen-cms/src/v1/landing/mod.rs b/imphnen-cms/src/v1/landing/mod.rs index c60c659..1c0a16a 100644 --- a/imphnen-cms/src/v1/landing/mod.rs +++ b/imphnen-cms/src/v1/landing/mod.rs @@ -1,3 +1,5 @@ pub mod events; +pub mod testimonials; pub use events::*; +pub use testimonials::*; diff --git a/imphnen-cms/src/v1/landing/testimonials/mod.rs b/imphnen-cms/src/v1/landing/testimonials/mod.rs new file mode 100644 index 0000000..3ee60e2 --- /dev/null +++ b/imphnen-cms/src/v1/landing/testimonials/mod.rs @@ -0,0 +1,44 @@ +use axum::{ + Router, + routing::{delete, get, patch, post}, +}; + +pub mod testimonials_controller; +pub mod testimonials_dto; +pub mod testimonials_repository; +pub mod testimonials_schema; +pub mod testimonials_service; + +pub use testimonials_controller::*; +pub use testimonials_dto::*; +pub use testimonials_repository::*; +pub use testimonials_schema::*; +pub use testimonials_service::*; + +pub fn testimonials_public_routes() -> Router { + Router::new() + .route( + "/cms/landing/testimonials", + get(testimonials_controller::get_testimonial_list), + ) + .route( + "/cms/landing/testimonials/detail/{id}", + get(testimonials_controller::get_testimonial_by_id), + ) +} + +pub fn testimonials_protected_routes() -> Router { + Router::new() + .route( + "/cms/landing/testimonials/create", + post(testimonials_controller::post_create_testimonial), + ) + .route( + "/cms/landing/testimonials/update/{id}", + patch(testimonials_controller::patch_update_testimonial), + ) + .route( + "/cms/landing/testimonials/delete/{id}", + delete(testimonials_controller::delete_testimonial), + ) +} diff --git a/imphnen-cms/src/v1/landing/testimonials/testimonials_controller.rs b/imphnen-cms/src/v1/landing/testimonials/testimonials_controller.rs new file mode 100644 index 0000000..0429b7d --- /dev/null +++ b/imphnen-cms/src/v1/landing/testimonials/testimonials_controller.rs @@ -0,0 +1,125 @@ +use super::{ + testimonials_dto::{ + TestimonialsCreateRequestDto, TestimonialsDetailItemDto, + TestimonialsListItemDto, TestimonialsUpdateRequestDto, + }, + testimonials_service::TestimonialsService, +}; +use axum::extract::{Path, Query}; +use axum::response::IntoResponse; +use axum::{Extension, Json}; +use imphnen_iam::UsersDetailQueryDto; +use imphnen_libs::{ + AppState, MessageResponseDto, MetaRequestDto, ResponseListSuccessDto, + ResponseSuccessDto, +}; + +#[utoipa::path( + get, + path = "/v1/cms/landing/testimonials", + params( + ("page" = Option, Query, description = "Page number"), + ("per_page" = Option, Query, description = "Items per page"), + ("search" = Option, Query, description = "Search keyword"), + ("sort_by" = Option, Query, description = "Sort by field"), + ("order" = Option, Query, description = "Order ASC or DESC"), + ("filter" = Option, Query, description = "Filter value"), + ("filter_by" = Option, Query, description = "Field to filter by"), + ), + responses( + (status = 200, description = "Get testimonial list", body = ResponseListSuccessDto>) + ), + tag = "Testimonials" +)] +pub async fn get_testimonial_list( + Extension(state): Extension, + Query(meta): Query, +) -> impl IntoResponse { + TestimonialsService::get_testimonial_list(&state, meta).await +} + +#[utoipa::path( + get, + path = "/v1/cms/landing/testimonials/detail/{id}", + params( + ("id" = String, Path, description = "Testimonial ID") + ), + responses( + (status = 200, description = "Get testimonial by ID", body = ResponseSuccessDto) + ), + tag = "Testimonials" +)] +pub async fn get_testimonial_by_id( + Extension(state): Extension, + Path(id): Path, +) -> impl IntoResponse { + TestimonialsService::get_testimonial_by_id(&state, id).await +} + +#[utoipa::path( + post, + security( + ("Bearer" = []) + ), + path = "/v1/cms/landing/testimonials/create", + request_body = TestimonialsCreateRequestDto, + responses( + (status = 201, description = "Create new testimonial", body = MessageResponseDto) + ), + tag = "Testimonials" +)] +pub async fn post_create_testimonial( + Extension(state): Extension, + Extension(authenticated_user): Extension, + Json(payload): Json, +) -> impl IntoResponse { + println!("Authenticated User Now: {:?}", authenticated_user); + TestimonialsService::create_testimonial(&state, payload, &authenticated_user).await +} + +#[utoipa::path( + patch, + security( + ("Bearer" = []) + ), + path = "/v1/cms/landing/testimonials/update/{id}", + params( + ("id" = String, Path, description = "Testimonial ID") + ), + request_body = TestimonialsUpdateRequestDto, + responses( + (status = 200, description = "Update testimonial", body = MessageResponseDto) + ), + tag = "Testimonials" +)] +pub async fn patch_update_testimonial( + Path(id): Path, + Extension(state): Extension, + Extension(authenticated_user): Extension, + Json(payload): Json, +) -> impl IntoResponse { + TestimonialsService::update_testimonial(&state, id, payload, &authenticated_user) + .await +} + +#[utoipa::path( + delete, + security( + ("Bearer" = []) + ), + path = "/v1/cms/landing/testimonials/delete/{id}", + params( + ("id" = String, Path, description = "Testimonial ID") + ), + responses( + (status = 200, description = "Soft delete testimonial", body = MessageResponseDto) + ), + tag = "Testimonials" +)] +pub async fn delete_testimonial( + Extension(state): Extension, + Extension(authenticated_user): Extension, + Path(id): Path, +) -> impl IntoResponse { + TestimonialsService::delete_testimonial(&state, id, &authenticated_user).await +} diff --git a/imphnen-cms/src/v1/landing/testimonials/testimonials_dto.rs b/imphnen-cms/src/v1/landing/testimonials/testimonials_dto.rs new file mode 100644 index 0000000..393139e --- /dev/null +++ b/imphnen-cms/src/v1/landing/testimonials/testimonials_dto.rs @@ -0,0 +1,78 @@ +use imphnen_iam::users::UsersSchema; +use serde::{Deserialize, Serialize}; +use surrealdb::sql::Thing; +use utoipa::ToSchema; +use validator::Validate; + +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)] +pub struct TestimonialsCreateRequestDto { + #[validate(length(min = 1, message = "Role is required"))] + pub role: String, + + #[validate(length( + min = 1, + max = 500, + message = "Content must be between 1 and 500 characters" + ))] + pub content: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)] +pub struct TestimonialsUpdateRequestDto { + #[validate(length(min = 1, message = "Role is required"))] + pub role: String, + + #[validate(length( + min = 1, + max = 500, + message = "Content must be between 1 and 500 characters" + ))] + pub content: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] +pub struct TestimonialsListItemDto { + pub id: String, + pub user_id: String, + pub user_fullname: String, // Assuming we'll fetch user's full name + pub role: String, + pub content: String, + pub created_at: String, + pub is_deleted: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] +pub struct TestimonialsDetailItemDto { + pub id: String, + pub user_id: String, + pub user_fullname: String, // Assuming we'll fetch user's full name + pub role: String, + pub content: String, + pub created_at: String, + pub updated_at: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct TestimonialsQueryDto { + pub id: Thing, + pub user: UsersSchema, // Change from Thing to UsersSchema + pub role: String, + pub content: String, + pub is_deleted: bool, + pub created_at: String, + pub updated_at: String, +} + +impl TestimonialsQueryDto { + pub fn from(self) -> TestimonialsListItemDto { + TestimonialsListItemDto { + id: self.id.id.to_raw(), + user_id: self.user.id.id.to_raw(), + user_fullname: self.user.fullname, // Extract fullname from UsersSchema + role: self.role, + content: self.content, + created_at: self.created_at, + is_deleted: self.is_deleted, + } + } +} diff --git a/imphnen-cms/src/v1/landing/testimonials/testimonials_repository.rs b/imphnen-cms/src/v1/landing/testimonials/testimonials_repository.rs new file mode 100644 index 0000000..ae4c41c --- /dev/null +++ b/imphnen-cms/src/v1/landing/testimonials/testimonials_repository.rs @@ -0,0 +1,120 @@ +use super::{ + testimonials_dto::TestimonialsQueryDto, testimonials_schema::TestimonialsSchema, +}; +use anyhow::{Result, bail}; +use imphnen_libs::{AppState, MetaRequestDto, ResourceEnum, ResponseListSuccessDto}; +use imphnen_utils::{DetailQueryBuilder, ListQueryBuilder, get_id, get_iso_date}; + +pub struct TestimonialsRepository<'a> { + state: &'a AppState, +} + +impl<'a> TestimonialsRepository<'a> { + pub fn new(state: &'a AppState) -> Self { + Self { state } + } + + pub async fn query_testimonial_list( + &self, + meta: MetaRequestDto, + ) -> Result>> { + let query = ListQueryBuilder::new(&ResourceEnum::Testimonials.to_string()) + .with_select_fields(vec!["*", "user.* as user"]) // Select user details + .with_pagination(meta.page, Some(10)) + .with_sorting(meta.sort_by.as_deref(), meta.order.as_deref()) + .build(); + let res: Vec = + self.state.surrealdb_ws.query(query).await?.take(0)?; + let data = ResponseListSuccessDto { + data: res, + meta: None, + }; + Ok(data) + } + + pub async fn query_testimonial_by_id( + &self, + id: String, + ) -> Result { + let db = &self.state.surrealdb_ws; + let builder = DetailQueryBuilder::new(ResourceEnum::Testimonials.to_string()) + .with_id(&id) + .with_select_fields(vec!["*", "user.* as user"]); // Select user details + let sql = builder.build(); + let result: Option = + builder.apply_bindings(db.query(sql)).await?.take(0)?; + + match result { + Some(testimonial) => { + if testimonial.is_deleted { + bail!("Testimonial not found"); + } + Ok(testimonial) + } + None => bail!("Testimonial not found"), + } + } + + pub async fn query_create_testimonial( + &self, + data: TestimonialsSchema, + ) -> Result { + let db = &self.state.surrealdb_ws; + let record: Option = db + .create(ResourceEnum::Testimonials.to_string()) + .content(data) + .await?; + + match record { + Some(_) => Ok("Success create testimonial".into()), + None => bail!("Failed to create testimonial"), + } + } + + pub async fn query_update_testimonial( + &self, + data: TestimonialsSchema, + ) -> Result { + let db = &self.state.surrealdb_ws; + + let existing = self.query_testimonial_by_id(data.id.id.to_raw()).await?; + if existing.is_deleted { + bail!("Testimonial already deleted"); + } + + let merged = TestimonialsSchema { + created_at: existing.created_at, + updated_at: get_iso_date(), + user: existing.user.id, // Preserve user ID + ..data + }; + + let record_key = get_id(&merged.id)?; + let record: Option = + db.update(record_key).merge(merged).await?; + + match record { + Some(_) => Ok("Success update testimonial".into()), + None => bail!("Failed to update testimonial"), + } + } + + pub async fn query_delete_testimonial(&self, id: String) -> Result { + let db = &self.state.surrealdb_ws; + let testimonial = self.query_testimonial_by_id(id).await?; + if testimonial.is_deleted { + bail!("Testimonial not found"); + } + + let record_key = get_id(&testimonial.id)?; + let record: Option = db + .update(record_key) + .merge(serde_json::json!({ "is_deleted": true })) + .await?; + + match record { + Some(_) => Ok("Success delete testimonial".into()), + None => bail!("Failed to delete testimonial"), + } + } +} diff --git a/imphnen-cms/src/v1/landing/testimonials/testimonials_schema.rs b/imphnen-cms/src/v1/landing/testimonials/testimonials_schema.rs new file mode 100644 index 0000000..6b8a4a1 --- /dev/null +++ b/imphnen-cms/src/v1/landing/testimonials/testimonials_schema.rs @@ -0,0 +1,84 @@ +use imphnen_libs::ResourceEnum; +use imphnen_utils::{get_iso_date, make_thing}; +use serde::{Deserialize, Serialize}; +use surrealdb::Uuid; +use surrealdb::sql::Thing; + +use super::testimonials_dto::{ + TestimonialsCreateRequestDto, TestimonialsQueryDto, TestimonialsUpdateRequestDto, +}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct TestimonialsSchema { + pub id: Thing, + pub user: Thing, // Link to app_users table + pub role: String, + pub content: String, + pub is_deleted: bool, + pub created_at: String, + pub updated_at: String, +} + +impl Default for TestimonialsSchema { + fn default() -> Self { + Self { + id: make_thing( + &ResourceEnum::Testimonials.to_string(), + &Uuid::new_v4().to_string(), + ), + user: make_thing( + &ResourceEnum::Users.to_string(), + &Uuid::new_v4().to_string(), // Placeholder, will be replaced by actual user ID + ), + role: String::new(), + content: String::new(), + is_deleted: false, + created_at: get_iso_date(), + updated_at: get_iso_date(), + } + } +} + +impl TestimonialsSchema { + pub fn from(dto: TestimonialsQueryDto) -> Self { + Self { + id: dto.id, + user: dto.user.id, + role: dto.role, + content: dto.content, + is_deleted: dto.is_deleted, + created_at: dto.created_at, + updated_at: dto.updated_at, + } + } + + pub fn create(payload: TestimonialsCreateRequestDto, user_id: &Thing) -> Self { + Self { + id: make_thing( + &ResourceEnum::Testimonials.to_string(), + &Uuid::new_v4().to_string(), + ), + user: user_id.clone(), + role: payload.role, + content: payload.content, + is_deleted: false, + created_at: get_iso_date(), + updated_at: get_iso_date(), + } + } + + pub fn update( + payload: TestimonialsUpdateRequestDto, + id: String, + user_id: &Thing, + ) -> Self { + Self { + id: make_thing(&ResourceEnum::Testimonials.to_string(), &id), + role: payload.role, + content: payload.content, + updated_at: get_iso_date(), + user: user_id.clone(), + ..Default::default() + } + } +} diff --git a/imphnen-cms/src/v1/landing/testimonials/testimonials_service.rs b/imphnen-cms/src/v1/landing/testimonials/testimonials_service.rs new file mode 100644 index 0000000..87de012 --- /dev/null +++ b/imphnen-cms/src/v1/landing/testimonials/testimonials_service.rs @@ -0,0 +1,108 @@ +use super::{ + testimonials_dto::{ + TestimonialsCreateRequestDto, TestimonialsDetailItemDto, + TestimonialsListItemDto, TestimonialsUpdateRequestDto, + }, + testimonials_repository::TestimonialsRepository, + testimonials_schema::TestimonialsSchema, +}; +use axum::{http::StatusCode, response::Response}; +use imphnen_libs::{ + AppState, MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto, +}; +use imphnen_utils::{ + common_response, success_list_response, success_response, validate_request, +}; + +pub struct TestimonialsService; + +impl TestimonialsService { + pub async fn get_testimonial_list( + state: &AppState, + meta: MetaRequestDto, + ) -> Response { + let repo = TestimonialsRepository::new(state); + match repo.query_testimonial_list(meta).await { + Ok(data) => { + let items: Vec = data + .data + .into_iter() + .filter(|e| !e.is_deleted) + .map(|e| e.from()) + .collect(); + let response = ResponseListSuccessDto { + data: items, + meta: data.meta, + }; + success_list_response(response) + } + Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()), + } + } + + pub async fn get_testimonial_by_id(state: &AppState, id: String) -> Response { + let repo = TestimonialsRepository::new(state); + match repo.query_testimonial_by_id(id).await { + Ok(testimonial) if !testimonial.is_deleted => { + success_response(ResponseSuccessDto { + data: TestimonialsDetailItemDto { + id: testimonial.id.id.to_raw(), + user_id: testimonial.user.id.id.to_raw(), + user_fullname: testimonial.user.fullname, + role: testimonial.role, + content: testimonial.content, + created_at: testimonial.created_at, + updated_at: testimonial.updated_at, + }, + }) + } + Ok(_) => common_response(StatusCode::NOT_FOUND, "Testimonial not found"), + Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()), + } + } + + pub async fn create_testimonial( + state: &AppState, + payload: TestimonialsCreateRequestDto, + authenticated_user: &imphnen_iam::UsersDetailQueryDto, + ) -> Response { + if let Err((status, message)) = validate_request(&payload) { + return common_response(status, &message); + } + let repo = TestimonialsRepository::new(state); + let schema = TestimonialsSchema::create(payload, &authenticated_user.id); + match repo.query_create_testimonial(schema).await { + Ok(msg) => common_response(StatusCode::CREATED, &msg), + Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()), + } + } + + pub async fn update_testimonial( + state: &AppState, + id: String, + payload: TestimonialsUpdateRequestDto, + authenticated_user: &imphnen_iam::UsersDetailQueryDto, + ) -> Response { + if let Err((status, message)) = validate_request(&payload) { + return common_response(status, &message); + } + let repo = TestimonialsRepository::new(state); + let schema = TestimonialsSchema::update(payload, id, &authenticated_user.id); + match repo.query_update_testimonial(schema).await { + Ok(msg) => common_response(StatusCode::OK, &msg), + Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()), + } + } + + pub async fn delete_testimonial( + state: &AppState, + id: String, + _authenticated_user: &imphnen_iam::UsersDetailQueryDto, + ) -> Response { + let repo = TestimonialsRepository::new(state); + match repo.query_delete_testimonial(id).await { + Ok(msg) => common_response(StatusCode::OK, &msg), + Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()), + } + } +} diff --git a/imphnen-gateway/src/docs.rs b/imphnen-gateway/src/docs.rs index e6d31ec..49362ec 100644 --- a/imphnen-gateway/src/docs.rs +++ b/imphnen-gateway/src/docs.rs @@ -6,7 +6,7 @@ use utoipa::{ Modify, OpenApi, }; use imphnen_gacha::{gacha_claims, gacha_items, gacha_rolls, GachaClaimItemDto, GachaClaimRequestDto, GachaItemDto, GachaItemRequestDto, GachaRollItemDto, GachaRollRequestDto}; -use imphnen_cms::{events_controller, events_dto::{EventsDetailItemDto, EventsListItemDto}}; +use imphnen_cms::{events_controller, events_dto::{EventsDetailItemDto, EventsListItemDto}, testimonials_controller, testimonials_dto::{TestimonialsCreateRequestDto, TestimonialsDetailItemDto, TestimonialsListItemDto, TestimonialsUpdateRequestDto}}; #[derive(OpenApi)] @@ -52,6 +52,11 @@ use imphnen_cms::{events_controller, events_dto::{EventsDetailItemDto, EventsLis events_controller::post_create_event, events_controller::patch_update_event, events_controller::delete_event, + testimonials_controller::get_testimonial_list, + testimonials_controller::get_testimonial_by_id, + testimonials_controller::post_create_testimonial, + testimonials_controller::patch_update_testimonial, + testimonials_controller::delete_testimonial, ), components( schemas( @@ -93,8 +98,10 @@ use imphnen_cms::{events_controller, events_dto::{EventsDetailItemDto, EventsLis ResponseSuccessDto, ResponseListSuccessDto>, ResponseSuccessDto, - MessageResponseDto, - MessageResponseDto, + ResponseListSuccessDto>, + ResponseSuccessDto, + TestimonialsCreateRequestDto, + TestimonialsUpdateRequestDto, MessageResponseDto, ) ), diff --git a/imphnen-gateway/src/lib.rs b/imphnen-gateway/src/lib.rs index c2bcdad..32dd671 100644 --- a/imphnen-gateway/src/lib.rs +++ b/imphnen-gateway/src/lib.rs @@ -1,7 +1,10 @@ use axum::{ Extension, Router, middleware::from_fn, response::Redirect, routing::get, }; -use imphnen_cms::{events_protected_routes, events_public_routes}; +use imphnen_cms::{ + events_protected_routes, events_public_routes, testimonials_protected_routes, + testimonials_public_routes, +}; use imphnen_entities::{AppState, SurrealMemClient, SurrealWsClient}; use imphnen_gacha::gacha_router; use imphnen_iam::{iam_protected_routes, iam_public_routes}; @@ -22,11 +25,13 @@ pub async fn gateway_service( let public_routes = Router::new() .merge(iam_public_routes()) + .merge(testimonials_public_routes()) .merge(events_public_routes()); let protected_routes = Router::new() .merge(iam_protected_routes()) .merge(events_protected_routes()) + .merge(testimonials_protected_routes()) .merge(gacha_router()) .layer(from_fn(auth_middleware)); diff --git a/imphnen-libs/src/surrealdb/resource.rs b/imphnen-libs/src/surrealdb/resource.rs index 870840f..9167ba9 100644 --- a/imphnen-libs/src/surrealdb/resource.rs +++ b/imphnen-libs/src/surrealdb/resource.rs @@ -13,6 +13,7 @@ pub enum ResourceEnum { Permissions, RolesPermissions, Events, + Testimonials, } impl fmt::Display for ResourceEnum { @@ -29,6 +30,7 @@ impl fmt::Display for ResourceEnum { ResourceEnum::GachaRolls => "app_gacha_rolls", ResourceEnum::GachaCredits => "app_gacha_credits", ResourceEnum::Events => "app_events", + ResourceEnum::Testimonials => "app_testimonials", }; write!(f, "{}", str) }