diff --git a/editoast/Cargo.lock b/editoast/Cargo.lock index 53346642ba9..2d00a31236c 100644 --- a/editoast/Cargo.lock +++ b/editoast/Cargo.lock @@ -1409,6 +1409,7 @@ version = "0.1.0" dependencies = [ "diesel", "diesel-async", + "editoast_common", "editoast_derive", "editoast_models", "futures 0.3.31", @@ -1418,11 +1419,14 @@ dependencies = [ "postgis_diesel", "postgres-openssl", "regex", + "serde", + "strum", "thiserror 2.0.11", "tokio", "tokio-postgres", "tracing", "url", + "utoipa", ] [[package]] diff --git a/editoast/diesel.toml b/editoast/diesel.toml index d6b05bd31ef..8d52cd78f67 100644 --- a/editoast/diesel.toml +++ b/editoast/diesel.toml @@ -3,7 +3,6 @@ [print_schema] file = "editoast_models/src/tables.rs" -generate_missing_sql_type_definitions = false filter = { except_tables = ["spatial_ref_sys"] } import_types = ["diesel::sql_types::*", "postgis_diesel::sql_types::*"] diff --git a/editoast/editoast_models/Cargo.toml b/editoast/editoast_models/Cargo.toml index 63157406b03..4aa279f3ec4 100644 --- a/editoast/editoast_models/Cargo.toml +++ b/editoast/editoast_models/Cargo.toml @@ -10,6 +10,7 @@ testing = [] [dependencies] diesel.workspace = true diesel-async.workspace = true +editoast_common.workspace = true editoast_derive.workspace = true futures.workspace = true futures-util.workspace = true @@ -18,11 +19,14 @@ opentelemetry-semantic-conventions.workspace = true postgis_diesel.workspace = true postgres-openssl.workspace = true regex.workspace = true +serde.workspace = true +strum.workspace = true thiserror.workspace = true tokio.workspace = true tokio-postgres.workspace = true tracing.workspace = true url.workspace = true +utoipa.workspace = true [dev-dependencies] # The feature 'testing' is needed for all of the doc-tests diff --git a/editoast/editoast_models/src/lib.rs b/editoast/editoast_models/src/lib.rs index d68eaa75f50..57e3c27a68c 100644 --- a/editoast/editoast_models/src/lib.rs +++ b/editoast/editoast_models/src/lib.rs @@ -1,10 +1,15 @@ pub mod db_connection_pool; pub mod model; +pub mod rolling_stock; pub mod tables; pub use db_connection_pool::DbConnection; pub use db_connection_pool::DbConnectionPoolV2; +editoast_common::schemas! { + rolling_stock::schemas(), +} + /// Generic error type to forward errors from the database /// /// Useful for functions which only points of failure are the DB calls. diff --git a/editoast/editoast_models/src/rolling_stock.rs b/editoast/editoast_models/src/rolling_stock.rs new file mode 100644 index 00000000000..0bcbd2ad465 --- /dev/null +++ b/editoast/editoast_models/src/rolling_stock.rs @@ -0,0 +1,10 @@ +mod rolling_stock_category; +pub use rolling_stock_category::RollingStockCategory; + +mod rolling_stock_categories; +pub use rolling_stock_categories::RollingStockCategories; + +editoast_common::schemas! { + rolling_stock_category::schemas(), + rolling_stock_categories::schemas(), +} diff --git a/editoast/editoast_models/src/rolling_stock/rolling_stock_categories.rs b/editoast/editoast_models/src/rolling_stock/rolling_stock_categories.rs new file mode 100644 index 00000000000..8b07171b111 --- /dev/null +++ b/editoast/editoast_models/src/rolling_stock/rolling_stock_categories.rs @@ -0,0 +1,23 @@ +use serde::Deserialize; +use serde::Serialize; +use utoipa::ToSchema; + +use super::RollingStockCategory; + +editoast_common::schemas! { + RollingStockCategories, +} + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, ToSchema)] +pub struct RollingStockCategories(pub Vec); + +impl From>> for RollingStockCategories { + fn from(categories: Vec>) -> Self { + Self(categories.into_iter().flatten().collect()) + } +} +impl From for Vec> { + fn from(categories: RollingStockCategories) -> Self { + categories.0.into_iter().map(Some).collect() + } +} diff --git a/editoast/editoast_models/src/rolling_stock/rolling_stock_category.rs b/editoast/editoast_models/src/rolling_stock/rolling_stock_category.rs new file mode 100644 index 00000000000..e01e51fa95e --- /dev/null +++ b/editoast/editoast_models/src/rolling_stock/rolling_stock_category.rs @@ -0,0 +1,64 @@ +use std::io::Write; +use std::str::FromStr; + +use diesel::deserialize::FromSql; +use diesel::deserialize::FromSqlRow; +use diesel::expression::AsExpression; +use diesel::pg::Pg; +use diesel::pg::PgValue; +use diesel::serialize::Output; +use diesel::serialize::ToSql; +use serde::Deserialize; +use serde::Serialize; +use strum::Display; +use strum::EnumString; +use utoipa::ToSchema; + +editoast_common::schemas! { + RollingStockCategory, +} + +#[derive( + Debug, + Clone, + PartialEq, + Serialize, + Deserialize, + ToSchema, + FromSqlRow, + AsExpression, + EnumString, + Display, +)] +#[diesel(sql_type = crate::tables::sql_types::RollingStockCategory)] +#[strum(serialize_all = "SCREAMING_SNAKE_CASE")] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum RollingStockCategory { + Unknown, + HighSpeedTrain, + IntercityTrain, + RegionalTrainMU, + NightTrain, + CommuterTrain, + FreightTrain, + FastFreightTrain, + TramTrain, + TouristicTrain, + WorkTrain, +} + +impl FromSql for RollingStockCategory { + fn from_sql(value: PgValue) -> diesel::deserialize::Result { + let s = std::str::from_utf8(value.as_bytes()).map_err(|_| "Invalid UTF-8 data")?; + RollingStockCategory::from_str(s) + .map_err(|_| "Unrecognized enum variant for RollingStockCategory".into()) + } +} + +impl ToSql for RollingStockCategory { + fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Pg>) -> diesel::serialize::Result { + let variant = self.to_string(); + out.write_all(variant.as_bytes())?; + Ok(diesel::serialize::IsNull::No) + } +} diff --git a/editoast/editoast_models/src/tables.rs b/editoast/editoast_models/src/tables.rs index 2abec3f381f..c53de487a06 100644 --- a/editoast/editoast_models/src/tables.rs +++ b/editoast/editoast_models/src/tables.rs @@ -1,5 +1,15 @@ // @generated automatically by Diesel CLI. +pub mod sql_types { + #[derive(diesel::sql_types::SqlType)] + #[diesel(postgres_type(name = "geometry"))] + pub struct Geometry; + + #[derive(diesel::sql_types::SqlType)] + #[diesel(postgres_type(name = "rolling_stock_category"))] + pub struct RollingStockCategory; +} + diesel::table! { use diesel::sql_types::*; use postgis_diesel::sql_types::*; @@ -102,6 +112,7 @@ diesel::table! { diesel::table! { use diesel::sql_types::*; use postgis_diesel::sql_types::*; + use super::sql_types::Geometry; infra_layer_buffer_stop (id) { id -> Int8, @@ -115,6 +126,7 @@ diesel::table! { diesel::table! { use diesel::sql_types::*; use postgis_diesel::sql_types::*; + use super::sql_types::Geometry; infra_layer_detector (id) { id -> Int8, @@ -128,6 +140,7 @@ diesel::table! { diesel::table! { use diesel::sql_types::*; use postgis_diesel::sql_types::*; + use super::sql_types::Geometry; infra_layer_electrification (id) { id -> Int8, @@ -141,6 +154,7 @@ diesel::table! { diesel::table! { use diesel::sql_types::*; use postgis_diesel::sql_types::*; + use super::sql_types::Geometry; infra_layer_error (id) { id -> Int8, @@ -155,6 +169,7 @@ diesel::table! { diesel::table! { use diesel::sql_types::*; use postgis_diesel::sql_types::*; + use super::sql_types::Geometry; infra_layer_neutral_section (id) { id -> Int8, @@ -168,6 +183,7 @@ diesel::table! { diesel::table! { use diesel::sql_types::*; use postgis_diesel::sql_types::*; + use super::sql_types::Geometry; infra_layer_neutral_sign (id) { id -> Int8, @@ -183,6 +199,7 @@ diesel::table! { diesel::table! { use diesel::sql_types::*; use postgis_diesel::sql_types::*; + use super::sql_types::Geometry; infra_layer_operational_point (id) { id -> Int8, @@ -198,6 +215,7 @@ diesel::table! { diesel::table! { use diesel::sql_types::*; use postgis_diesel::sql_types::*; + use super::sql_types::Geometry; infra_layer_psl_sign (id) { id -> Int8, @@ -213,6 +231,7 @@ diesel::table! { diesel::table! { use diesel::sql_types::*; use postgis_diesel::sql_types::*; + use super::sql_types::Geometry; infra_layer_signal (id) { id -> Int8, @@ -231,6 +250,7 @@ diesel::table! { diesel::table! { use diesel::sql_types::*; use postgis_diesel::sql_types::*; + use super::sql_types::Geometry; infra_layer_speed_section (id) { id -> Int8, @@ -244,6 +264,7 @@ diesel::table! { diesel::table! { use diesel::sql_types::*; use postgis_diesel::sql_types::*; + use super::sql_types::Geometry; infra_layer_switch (id) { id -> Int8, @@ -257,6 +278,7 @@ diesel::table! { diesel::table! { use diesel::sql_types::*; use postgis_diesel::sql_types::*; + use super::sql_types::Geometry; infra_layer_track_section (id) { id -> Int8, @@ -455,6 +477,7 @@ diesel::table! { diesel::table! { use diesel::sql_types::*; use postgis_diesel::sql_types::*; + use super::sql_types::RollingStockCategory; rolling_stock (id) { id -> Int8, @@ -484,6 +507,8 @@ diesel::table! { version -> Int8, supported_signaling_systems -> Array>, etcs_brake_params -> Jsonb, + primary_category -> RollingStockCategory, + other_categories -> Array>, } } diff --git a/editoast/migrations/2025-02-03-094532_add_primary_and_other_categories_to_rolling_stock/down.sql b/editoast/migrations/2025-02-03-094532_add_primary_and_other_categories_to_rolling_stock/down.sql new file mode 100644 index 00000000000..b25db218885 --- /dev/null +++ b/editoast/migrations/2025-02-03-094532_add_primary_and_other_categories_to_rolling_stock/down.sql @@ -0,0 +1,5 @@ +ALTER TABLE rolling_stock +DROP COLUMN primary_category, +DROP COLUMN other_categories; + +DROP TYPE rolling_stock_category; diff --git a/editoast/migrations/2025-02-03-094532_add_primary_and_other_categories_to_rolling_stock/up.sql b/editoast/migrations/2025-02-03-094532_add_primary_and_other_categories_to_rolling_stock/up.sql new file mode 100644 index 00000000000..e62ee0fed37 --- /dev/null +++ b/editoast/migrations/2025-02-03-094532_add_primary_and_other_categories_to_rolling_stock/up.sql @@ -0,0 +1,17 @@ +CREATE TYPE rolling_stock_category AS ENUM ( + 'UNKNOWN', + 'HIGH_SPEED_TRAIN', + 'INTERCITY_TRAIN', + 'REGIONAL_TRAIN_MU', + 'NIGHT_TRAIN', + 'COMMUTER_TRAIN', + 'FREIGHT_TRAIN', + 'FAST_FREIGHT_TRAIN', + 'TRAM_TRAIN', + 'TOURISTIC_TRAIN', + 'WORK_TRAIN' +); + +ALTER TABLE rolling_stock +ADD COLUMN primary_category rolling_stock_category NOT NULL DEFAULT 'UNKNOWN', +ADD COLUMN other_categories rolling_stock_category[] NOT NULL DEFAULT '{}'; diff --git a/editoast/openapi.yaml b/editoast/openapi.yaml index c7b20a70325..985ca2c27cc 100644 --- a/editoast/openapi.yaml +++ b/editoast/openapi.yaml @@ -9410,6 +9410,8 @@ components: - raise_pantograph_time - version - supported_signaling_systems + - primary_category + - other_categories properties: base_power_class: type: string @@ -9466,10 +9468,14 @@ components: nullable: true name: type: string + other_categories: + $ref: '#/components/schemas/RollingStockCategories' power_restrictions: type: object additionalProperties: type: string + primary_category: + $ref: '#/components/schemas/RollingStockCategory' railjson_version: type: string raise_pantograph_time: @@ -9494,6 +9500,24 @@ components: version: type: integer format: int64 + RollingStockCategories: + type: array + items: + $ref: '#/components/schemas/RollingStockCategory' + RollingStockCategory: + type: string + enum: + - UNKNOWN + - HIGH_SPEED_TRAIN + - INTERCITY_TRAIN + - REGIONAL_TRAIN_M_U + - NIGHT_TRAIN + - COMMUTER_TRAIN + - FREIGHT_TRAIN + - FAST_FREIGHT_TRAIN + - TRAM_TRAIN + - TOURISTIC_TRAIN + - WORK_TRAIN RollingStockForm: type: object required: diff --git a/editoast/src/models/rolling_stock_model.rs b/editoast/src/models/rolling_stock_model.rs index a776316521f..84ffb13270d 100644 --- a/editoast/src/models/rolling_stock_model.rs +++ b/editoast/src/models/rolling_stock_model.rs @@ -8,6 +8,8 @@ use editoast_common::units::quantities::{ }; use editoast_derive::Model; use editoast_models::model; +use editoast_models::rolling_stock::RollingStockCategories; +use editoast_models::rolling_stock::RollingStockCategory; use editoast_schemas::rolling_stock::EffortCurves; use editoast_schemas::rolling_stock::EnergySource; use editoast_schemas::rolling_stock::EtcsBrakeParams; @@ -100,6 +102,9 @@ pub struct RollingStockModel { #[schema(value_type = Vec)] #[model(remote = "Vec>")] pub supported_signaling_systems: RollingStockSupportedSignalingSystems, + pub primary_category: RollingStockCategory, + #[model(remote = "Vec>")] + pub other_categories: RollingStockCategories, } #[derive(Debug, thiserror::Error)] @@ -241,13 +246,16 @@ impl From for RollingStockModelChangeset { #[cfg(test)] pub mod tests { - use rstest::*; + use editoast_models::rolling_stock::RollingStockCategories; + use editoast_models::rolling_stock::RollingStockCategory; + use rstest::rstest; use serde_json::to_value; use super::RollingStockModel; use crate::error::InternalError; use crate::models::fixtures::create_fast_rolling_stock; use crate::models::fixtures::create_rolling_stock_with_energy_sources; + use crate::models::fixtures::fast_rolling_stock_changeset; use crate::models::fixtures::rolling_stock_with_energy_sources_changeset; use crate::models::prelude::*; use crate::views::rolling_stock::map_diesel_error; @@ -317,4 +325,47 @@ pub mod tests { to_value(error).unwrap() ); } + + #[rstest] + async fn test_primary_category_is_unknown_with_other_categories_are_empty() { + let db_pool = DbConnectionPoolV2::for_tests(); + + let created_fast_rolling_stock = + create_fast_rolling_stock(&mut db_pool.get_ok(), "fast_rolling_stock_name").await; + + assert_eq!( + created_fast_rolling_stock.primary_category, + RollingStockCategory::Unknown + ); + assert_eq!( + created_fast_rolling_stock.other_categories, + RollingStockCategories(vec![]) + ); + } + + #[rstest] + async fn create_rolling_stock_with_categories() { + let db_pool = DbConnectionPoolV2::for_tests(); + + let rolling_stock = fast_rolling_stock_changeset("fost_rolling_stock") + .primary_category(RollingStockCategory::HighSpeedTrain) + .other_categories(RollingStockCategories(vec![ + RollingStockCategory::TramTrain, + RollingStockCategory::CommuterTrain, + ])) + .create(&mut db_pool.get_ok()) + .await + .expect("Failed to create rolling stock"); + assert_eq!( + rolling_stock.primary_category, + RollingStockCategory::HighSpeedTrain + ); + assert_eq!( + rolling_stock.other_categories, + RollingStockCategories(vec![ + RollingStockCategory::TramTrain, + RollingStockCategory::CommuterTrain, + ]) + ); + } } diff --git a/editoast/src/views/mod.rs b/editoast/src/views/mod.rs index 25f4f61b9e6..48ccf6a97c8 100644 --- a/editoast/src/views/mod.rs +++ b/editoast/src/views/mod.rs @@ -119,6 +119,7 @@ crate::routes! { editoast_common::schemas! { Version, + editoast_models::schemas(), editoast_common::schemas(), editoast_schemas::schemas(), models::schemas(), diff --git a/front/src/common/api/generatedEditoastApi.ts b/front/src/common/api/generatedEditoastApi.ts index d7a7ac57fa3..69d1eb0b9f7 100644 --- a/front/src/common/api/generatedEditoastApi.ts +++ b/front/src/common/api/generatedEditoastApi.ts @@ -3091,6 +3091,19 @@ export type EffortCurves = { [key: string]: ModeEffortCurves; }; }; +export type RollingStockCategory = + | 'UNKNOWN' + | 'HIGH_SPEED_TRAIN' + | 'INTERCITY_TRAIN' + | 'REGIONAL_TRAIN_M_U' + | 'NIGHT_TRAIN' + | 'COMMUTER_TRAIN' + | 'FREIGHT_TRAIN' + | 'FAST_FREIGHT_TRAIN' + | 'TRAM_TRAIN' + | 'TOURISTIC_TRAIN' + | 'WORK_TRAIN'; +export type RollingStockCategories = RollingStockCategory[]; export type RollingStock = { base_power_class: string | null; /** Acceleration in m·s⁻² */ @@ -3115,9 +3128,11 @@ export type RollingStock = { max_speed: number; metadata: RollingStockMetadata | null; name: string; + other_categories: RollingStockCategories; power_restrictions: { [key: string]: string; }; + primary_category: RollingStockCategory; railjson_version: string; /** Duration in s */ raise_pantograph_time: number | null; diff --git a/front/src/modules/rollingStock/components/RollingStockSelector/sampleData.ts b/front/src/modules/rollingStock/components/RollingStockSelector/sampleData.ts index 57adb55c22c..23356d42788 100644 --- a/front/src/modules/rollingStock/components/RollingStockSelector/sampleData.ts +++ b/front/src/modules/rollingStock/components/RollingStockSelector/sampleData.ts @@ -86,6 +86,8 @@ const ROLLING_STOCK_SAMPLE_DATA: RollingStockWithLiveries = { }, ], supported_signaling_systems: ['BAL', 'BAPR'], + primary_category: 'FREIGHT_TRAIN', + other_categories: [], }; export default ROLLING_STOCK_SAMPLE_DATA;