diff --git a/src/common/macros/router.rs b/src/common/macros/router.rs index 0994905..257626c 100644 --- a/src/common/macros/router.rs +++ b/src/common/macros/router.rs @@ -4,11 +4,13 @@ macro_rules! generate_router { resource_name: $resource_name:expr, db_entity: $db_entity:ty, db_model: $db_model:ty, + active_model: $active_model:ty, db_columns: $db_columns:ty, get_one_response_model: $get_one_response_model:ty, get_all_response_model: $get_all_response_model:ty, + create_one_request_model: $create_one_request_model:ty, order_column_logic: $order_column_logic:expr, - searchable_columns: $searchable_columns:expr // New argument for searchable fields + searchable_columns: $searchable_columns:expr ) => { use crate::common::models::FilterOptions; use crate::common::sort::generic_sort; @@ -23,13 +25,13 @@ macro_rules! generate_router { }; use sea_orm::query::*; use sea_orm::ColumnTrait; - use sea_orm::{DatabaseConnection, EntityTrait}; + use sea_orm::{DatabaseConnection, EntityTrait,ActiveModelTrait}; use std::iter::Iterator; use uuid::Uuid; pub fn router(db: DatabaseConnection) -> Router { Router::new() - .route("/", routing::get(get_all)) + .route("/", routing::get(get_all).post(create_one)) .route("/:id", routing::get(get_one)) .with_state(db) } @@ -102,5 +104,16 @@ macro_rules! generate_router { } + #[utoipa::path(post, path = format!("/v1/{}", $resource_name), responses((status = 201, body = $get_one_response_model)))] + pub async fn create_one( + State(db): State, + Json(payload): Json<$create_one_request_model>, + ) -> impl IntoResponse { + let db_obj: $active_model = <$active_model>::from(payload); + let response = db_obj.insert(&db).await.unwrap(); + let response_obj: $get_one_response_model = response.into(); + + (StatusCode::CREATED, Json(response_obj)) + } }; } diff --git a/src/soil/profiles/models.rs b/src/soil/profiles/models.rs index 9934950..ffb218f 100644 --- a/src/soil/profiles/models.rs +++ b/src/soil/profiles/models.rs @@ -1,10 +1,10 @@ use chrono::NaiveDateTime; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use serde_json::Value; use utoipa::ToSchema; use uuid::Uuid; -#[derive(ToSchema, Serialize)] +#[derive(ToSchema, Serialize, Deserialize)] pub struct SoilProfile { pub id: Uuid, pub name: String, @@ -25,7 +25,7 @@ pub struct SoilProfile { pub parent_material: Option, } -#[derive(ToSchema, Serialize)] +#[derive(ToSchema, Serialize, Deserialize)] pub struct SoilProfileBasic { pub id: Uuid, pub last_updated: chrono::NaiveDateTime, @@ -67,3 +67,50 @@ impl From for SoilProfile { } } } + +#[derive(ToSchema, Serialize, Deserialize)] +pub struct SoilProfileCreate { + pub name: String, + pub profile_iterator: i32, + pub gradient: String, + pub description_horizon: Option, + pub weather: Option, + pub topography: Option, + pub vegetation_type: Option, + pub aspect: Option, + pub lythology_surficial_deposit: Option, + pub soil_type_id: Uuid, + pub area_id: Uuid, + pub soil_diagram: Option, + pub photo: Option, + pub parent_material: Option, +} + +impl From for crate::soil::profiles::db::ActiveModel { + fn from(soil_profile: SoilProfileCreate) -> Self { + let now = chrono::Utc::now().naive_utc(); + + crate::soil::profiles::db::ActiveModel { + id: sea_orm::ActiveValue::Set(Uuid::new_v4()), + last_updated: sea_orm::ActiveValue::Set(chrono::Utc::now().naive_utc()), + name: sea_orm::ActiveValue::Set(soil_profile.name), + profile_iterator: sea_orm::ActiveValue::Set(soil_profile.profile_iterator), + gradient: sea_orm::ActiveValue::Set(soil_profile.gradient), + description_horizon: sea_orm::ActiveValue::Set(soil_profile.description_horizon), + weather: sea_orm::ActiveValue::Set(soil_profile.weather), + topography: sea_orm::ActiveValue::Set(soil_profile.topography), + vegetation_type: sea_orm::ActiveValue::Set(soil_profile.vegetation_type), + aspect: sea_orm::ActiveValue::Set(soil_profile.aspect), + lythology_surficial_deposit: sea_orm::ActiveValue::Set( + soil_profile.lythology_surficial_deposit, + ), + created_on: sea_orm::ActiveValue::Set(Some(now)), + soil_type_id: sea_orm::ActiveValue::Set(soil_profile.soil_type_id), + area_id: sea_orm::ActiveValue::Set(soil_profile.area_id), + soil_diagram: sea_orm::ActiveValue::Set(soil_profile.soil_diagram), + photo: sea_orm::ActiveValue::Set(soil_profile.photo), + parent_material: sea_orm::ActiveValue::Set(soil_profile.parent_material), + iterator: sea_orm::ActiveValue::NotSet, + } + } +} diff --git a/src/soil/profiles/views.rs b/src/soil/profiles/views.rs index 1ee75b9..065067c 100644 --- a/src/soil/profiles/views.rs +++ b/src/soil/profiles/views.rs @@ -4,9 +4,11 @@ generate_router!( resource_name: "soil_profiles", db_entity: crate::soil::profiles::db::Entity, db_model: crate::soil::profiles::db::Model, + active_model: crate::soil::profiles::db::ActiveModel, db_columns: crate::soil::profiles::db::Column, get_one_response_model: crate::soil::profiles::models::SoilProfile, get_all_response_model: crate::soil::profiles::models::SoilProfileBasic, + create_one_request_model: crate::soil::profiles::models::SoilProfileCreate, order_column_logic: [ ("id", crate::soil::profiles::db::Column::Id), ("name", crate::soil::profiles::db::Column::Name), diff --git a/src/soil/types/models.rs b/src/soil/types/models.rs index 1f09677..79b13e4 100644 --- a/src/soil/types/models.rs +++ b/src/soil/types/models.rs @@ -1,8 +1,8 @@ -use serde::Serialize; +use serde::{Deserialize, Serialize}; use utoipa::ToSchema; use uuid::Uuid; -#[derive(ToSchema, Serialize)] +#[derive(ToSchema, Serialize, Deserialize)] pub struct SoilType { pub id: Uuid, pub last_updated: chrono::NaiveDateTime, @@ -11,7 +11,7 @@ pub struct SoilType { pub image: Option, } -#[derive(ToSchema, Serialize)] +#[derive(ToSchema, Serialize, Deserialize)] pub struct SoilTypeBasic { pub id: Uuid, pub last_updated: chrono::NaiveDateTime, @@ -41,3 +41,30 @@ impl From for SoilType { } } } + +#[derive(ToSchema, Serialize, Deserialize)] +pub struct SoilTypeCreate { + pub name: String, + pub description: String, + pub image: Option, +} + +impl From for crate::soil::types::db::ActiveModel { + fn from(soil_type: SoilTypeCreate) -> Self { + crate::soil::types::db::ActiveModel { + id: sea_orm::ActiveValue::Set(Uuid::new_v4()), + last_updated: sea_orm::ActiveValue::Set(chrono::Utc::now().naive_utc()), + name: sea_orm::ActiveValue::Set(soil_type.name), + description: sea_orm::ActiveValue::Set(soil_type.description), + image: sea_orm::ActiveValue::Set(soil_type.image), + iterator: sea_orm::ActiveValue::NotSet, + } + } +} + +#[derive(ToSchema, Serialize, Deserialize)] +pub struct SoilTypeUpdate { + pub name: Option, + pub description: Option, + pub image: Option, +} diff --git a/src/soil/types/views.rs b/src/soil/types/views.rs index 9a12976..91c3478 100644 --- a/src/soil/types/views.rs +++ b/src/soil/types/views.rs @@ -4,9 +4,11 @@ generate_router!( resource_name: "soil_types", db_entity: crate::soil::types::db::Entity, db_model: crate::soil::types::db::Model, + active_model: crate::soil::types::db::ActiveModel, db_columns: crate::soil::types::db::Column, get_one_response_model: crate::soil::types::models::SoilType, get_all_response_model: crate::soil::types::models::SoilTypeBasic, + create_one_request_model: crate::soil::types::models::SoilTypeCreate, order_column_logic: [ ("id", crate::soil::types::db::Column::Id), ("name", crate::soil::types::db::Column::Name), diff --git a/tests/mock_fixtures.rs b/tests/mock_fixtures.rs index 23ce372..05f984f 100644 --- a/tests/mock_fixtures.rs +++ b/tests/mock_fixtures.rs @@ -2,10 +2,9 @@ use axum::routing::Router; use rstest::fixture; use sea_orm::sea_query::TableCreateStatement; use sea_orm::Database; -use sea_orm::{ActiveModelTrait, ConnectionTrait, DatabaseConnection, Schema}; +use sea_orm::{ConnectionTrait, DatabaseConnection, Schema}; use soil_api_rust::soil::types::db::Entity; use soil_api_rust::soil::types::views::router; -use uuid::Uuid; pub async fn setup_database() -> DatabaseConnection { // Use an in-memory SQLite database for testing. @@ -21,34 +20,9 @@ pub async fn setup_database() -> DatabaseConnection { db } -pub async fn insert_mock_data(db: &DatabaseConnection) { - let now = chrono::Utc::now().naive_utc(); - - let soil_type_1 = soil_api_rust::soil::types::db::ActiveModel { - id: sea_orm::ActiveValue::Set(Uuid::new_v4()), - iterator: sea_orm::ActiveValue::Set(1), - last_updated: sea_orm::ActiveValue::Set(now), - name: sea_orm::ActiveValue::Set("Clay".to_string()), - description: sea_orm::ActiveValue::Set("Clay soil type".to_string()), - image: sea_orm::ActiveValue::Set(Some("clay.png".to_string())), - }; - soil_type_1.insert(db).await.unwrap(); - - let soil_type_2 = soil_api_rust::soil::types::db::ActiveModel { - id: sea_orm::ActiveValue::Set(Uuid::new_v4()), - iterator: sea_orm::ActiveValue::Set(2), - last_updated: sea_orm::ActiveValue::Set(now), - name: sea_orm::ActiveValue::Set("Sand".to_string()), - description: sea_orm::ActiveValue::Set("Sandy soil type".to_string()), - image: sea_orm::ActiveValue::Set(None), - }; - soil_type_2.insert(db).await.unwrap(); -} - #[fixture] pub async fn mock_api() -> Router { // Use a unique in-memory SQLite database per test run let db = setup_database().await; - insert_mock_data(&db).await; router(db) } diff --git a/tests/soil_type_test.rs b/tests/soil_type_test.rs index 08321db..af0bfcd 100644 --- a/tests/soil_type_test.rs +++ b/tests/soil_type_test.rs @@ -7,33 +7,36 @@ use tower::ServiceExt; mod mock_fixtures; use mock_fixtures::mock_api; use serde_json::{from_slice, Value}; -use uuid::Uuid; #[rstest] #[tokio::test] async fn test_get_all_soil_types(#[future(awt)] mock_api: Router) { - let now = chrono::Utc::now().naive_utc(); - - let soil_type_1 = serde_json::json!({ - "id": Uuid::new_v4(), - "iterator": 1, - "last_updated": now, + let soil_type = serde_json::json!({ "name": "Clay", "description": "Clay soil type", "image": "clay.png" }); - let soil_type_2 = serde_json::json!({ - "id": Uuid::new_v4(), - "iterator": 2, - "last_updated": now, - "name": "Sand", - "description": "Sandy soil type", - "image": null - }); + // Insert mock data into the API with the POST method. - // Add objects to the POST endpoint let response = mock_api + .clone() + .oneshot( + Request::builder() + .method("POST") + .uri("/") + .header("Content-Type", "application/json") + .body(Body::from(soil_type.clone().to_string())) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::CREATED); + + // Get all from API + let response = mock_api + .clone() .oneshot(Request::builder().uri("/").body(Body::empty()).unwrap()) .await .unwrap(); @@ -45,38 +48,29 @@ async fn test_get_all_soil_types(#[future(awt)] mock_api: Router) { // Assert that the body is an array and contains expected mock data. assert!(body_json.is_array(), "Response body is not an array"); - let soil_types = body_json.as_array().unwrap(); - - // Assert that at least two soil types are present in the response. - assert!(soil_types.len() >= 2, "Expected at least 2 soil types"); - - // Check each soil type in the array, by looping through them and checking their fields. - for soil_type in soil_types { - assert!(soil_type.is_object()); - let soil_type = soil_type.as_object().unwrap(); + let soil_type = body_json.as_array().unwrap(); + println!("{:?}", soil_type); - // Check that all necessary fields exist in the soil type. - assert!(soil_type.get("id").is_some()); - // assert!(soil_type.get("iterator").is_some()); - assert!(soil_type.get("last_updated").is_some()); - assert!(soil_type.get("name").is_some()); - assert!(soil_type.get("description").is_some()); - // assert!(soil_type.get("image").is_some()); - } + // // Assert that at least two soil types are present in the response. + assert!( + soil_type.len() == 1, + "Expected only 1 soil type, found {}", + soil_type.len() + ); - let clay_soil = &soil_types[0]; - let sand_soil = &soil_types[1]; + assert!(soil_type[0].is_object()); - assert_eq!(clay_soil["name"], "Clay"); - assert_eq!(clay_soil["description"], "Clay soil type"); - assert_eq!(sand_soil["name"], "Sand"); - assert_eq!(sand_soil["description"], "Sandy soil type"); + // Check the soil type matches the expected mock data from above. + let soil_type = soil_type[0].as_object().unwrap(); + assert_eq!( + soil_type.get("name").unwrap(), + &serde_json::Value::String("Clay".to_string()) + ); + assert_eq!( + soil_type.get("description").unwrap(), + &serde_json::Value::String("Clay soil type".to_string()) + ); - // Check that all necessary fields exist in the first soil type. - assert!(clay_soil.get("id").is_some()); - // assert!(clay_soil.get("iterator").is_some()); - assert!(clay_soil.get("last_updated").is_some()); - assert!(clay_soil.get("name").is_some()); - assert!(clay_soil.get("description").is_some()); - // assert!(clay_soil.get("image").is_some()); + // Should not be in response on getall); + assert_eq!(soil_type.get("image"), None); }