From 6152bb089331e85ea703eb38cf578548b7a611c0 Mon Sep 17 00:00:00 2001 From: Ethan Perruzza Date: Thu, 10 Oct 2024 16:12:57 +0200 Subject: [PATCH] editoast: add train_schedule object to search endpoint Signed-off-by: Ethan Perruzza --- .../train_schedule/train_schedule_options.rs | 1 - editoast/openapi.yaml | 69 ++++++++++- editoast/src/views/search.rs | 117 ++++++++++++++++++ front/src/common/api/generatedEditoastApi.ts | 57 ++++++++- 4 files changed, 240 insertions(+), 4 deletions(-) diff --git a/editoast/editoast_schemas/src/train_schedule/train_schedule_options.rs b/editoast/editoast_schemas/src/train_schedule/train_schedule_options.rs index 64b3795527d..693553e87b0 100644 --- a/editoast/editoast_schemas/src/train_schedule/train_schedule_options.rs +++ b/editoast/editoast_schemas/src/train_schedule/train_schedule_options.rs @@ -9,7 +9,6 @@ editoast_common::schemas! { #[derive(Debug, Derivative, Clone, Serialize, Deserialize, ToSchema, Hash)] #[serde(deny_unknown_fields)] -#[schema(as = TrainScheduleOptionsV2)] // Avoiding conflict with v1. TODO: remove after migration to v2 #[derivative(Default)] pub struct TrainScheduleOptions { #[derivative(Default(value = "true"))] diff --git a/editoast/openapi.yaml b/editoast/openapi.yaml index 46ca97afe7a..01c7c923655 100644 --- a/editoast/openapi.yaml +++ b/editoast/openapi.yaml @@ -8770,6 +8770,7 @@ components: - $ref: '#/components/schemas/SearchResultItemProject' - $ref: '#/components/schemas/SearchResultItemStudy' - $ref: '#/components/schemas/SearchResultItemScenario' + - $ref: '#/components/schemas/SearchResultItemTrainSchedule' description: A search result item that depends on the query's `object` SearchResultItemOperationalPoint: type: object @@ -9003,6 +9004,72 @@ components: format: int64 line_name: type: string + SearchResultItemTrainSchedule: + type: object + description: A search result item for a query with `object = "trainschedule"` + required: + - id + - train_name + - labels + - rolling_stock_name + - timetable_id + - start_time + - schedule + - margins + - initial_speed + - comfort + - path + - constraint_distribution + - power_restrictions + - options + properties: + comfort: + type: integer + format: int64 + constraint_distribution: + type: integer + format: int64 + id: + type: integer + format: int64 + minimum: 0 + initial_speed: + type: number + format: double + labels: + type: array + items: + type: string + nullable: true + margins: + $ref: '#/components/schemas/Margins' + options: + $ref: '#/components/schemas/TrainScheduleOptions' + path: + type: array + items: + $ref: '#/components/schemas/PathItem' + power_restrictions: + type: array + items: + $ref: '#/components/schemas/PowerRestrictionItem' + rolling_stock_name: + type: string + schedule: + type: array + items: + $ref: '#/components/schemas/ScheduleItem' + speed_limit_tag: + type: string + nullable: true + start_time: + type: string + format: date-time + timetable_id: + type: integer + format: int64 + train_name: + type: string Side: type: string enum: @@ -10206,7 +10273,7 @@ components: format: int64 description: Timetable attached to the train schedule nullable: true - TrainScheduleOptionsV2: + TrainScheduleOptions: type: object properties: use_electrical_profiles: diff --git a/editoast/src/views/search.rs b/editoast/src/views/search.rs index 0b4eaa2313f..7e4e640da97 100644 --- a/editoast/src/views/search.rs +++ b/editoast/src/views/search.rs @@ -203,7 +203,9 @@ use axum::extract::Json; use axum::extract::Query; use axum::extract::State; use axum::Extension; +use chrono::DateTime; use chrono::NaiveDateTime; +use chrono::Utc; use diesel::pg::Pg; use diesel::sql_query; use diesel::sql_types::Jsonb; @@ -215,6 +217,11 @@ use editoast_common::geometry::GeoJsonPoint; use editoast_derive::EditoastError; use editoast_derive::Search; use editoast_derive::SearchConfigStore; +use editoast_schemas::train_schedule::Margins; +use editoast_schemas::train_schedule::PathItem; +use editoast_schemas::train_schedule::PowerRestrictionItem; +use editoast_schemas::train_schedule::ScheduleItem; +use editoast_schemas::train_schedule::TrainScheduleOptions; use editoast_search::query_into_sql; use editoast_search::SearchConfigStore as _; use editoast_search::SearchError; @@ -345,6 +352,7 @@ async fn search( ) -> Result> { let roles: HashSet = match object.as_str() { "track" | "operationalpoint" | "signal" => HashSet::from([BuiltinRole::InfraRead]), + "trainschedule" => HashSet::from([BuiltinRole::TimetableRead]), "project" | "study" | "scenario" => HashSet::from([BuiltinRole::OpsRead]), _ => { return Err(SearchApiError::ObjectType { @@ -683,6 +691,48 @@ pub(super) struct SearchResultItemScenario { tags: Vec, } +#[derive(Search, Serialize, ToSchema)] +#[cfg_attr(test, derive(serde::Deserialize))] +#[search( + table = "train_schedule", + column(name = "timetable_id", data_type = "integer"), + column(name = "train_name", data_type = "string") +)] +#[allow(unused)] +/// A search result item for a query with `object = "trainschedule"` +pub(super) struct SearchResultItemTrainSchedule { + #[search(sql = "train_schedule.id")] + id: u64, + #[search(sql = "train_schedule.train_name")] + train_name: String, + #[search(sql = "train_schedule.labels")] + labels: Vec>, + #[search(sql = "train_schedule.rolling_stock_name")] + rolling_stock_name: String, + #[search(sql = "train_schedule.timetable_id")] + timetable_id: i64, + #[search(sql = "train_schedule.start_time")] + start_time: DateTime, + #[search(sql = "train_schedule.schedule")] + schedule: Vec, + #[search(sql = "train_schedule.margins")] + margins: Margins, + #[search(sql = "train_schedule.initial_speed")] + initial_speed: f64, + #[search(sql = "train_schedule.comfort")] + comfort: i64, + #[search(sql = "train_schedule.path")] + path: Vec, + #[search(sql = "train_schedule.constraint_distribution")] + constraint_distribution: i64, + #[search(sql = "train_schedule.speed_limit_tag")] + speed_limit_tag: Option, + #[search(sql = "train_schedule.power_restrictions")] + power_restrictions: Vec, + #[search(sql = "train_schedule.options")] + options: TrainScheduleOptions, +} + /// See [editoast_search::SearchConfigStore::find] #[derive(SearchConfigStore)] #[search_config_store( @@ -692,5 +742,72 @@ pub(super) struct SearchResultItemScenario { object(name = "project", config = SearchResultItemProject), object(name = "study", config = SearchResultItemStudy), object(name = "scenario", config = SearchResultItemScenario), + object(name = "trainschedule", config = SearchResultItemTrainSchedule), )] pub struct SearchConfigFinder; + +#[cfg(test)] +pub mod test { + + use axum::http::StatusCode; + use pretty_assertions::assert_eq; + use rstest::rstest; + use serde_json::json; + + use super::*; + use crate::models::fixtures::{create_simple_train_schedule, create_timetable}; + use crate::views::test_app::TestAppBuilder; + + #[rstest] + async fn search_trainschedule_post_found() { + let app = TestAppBuilder::default_app(); + let pool = app.db_pool(); + + // Create the timetable in the database + let timetable = create_timetable(&mut pool.get_ok()).await; + let timetable_id = timetable.id; + + // Add a train_schedule in the database + let train = create_simple_train_schedule(&mut pool.get_ok(), timetable_id).await; + + // The body + let request = app.post("/search").json(&json!({ + "object": "trainschedule", + "query": ["and", ["=", ["train_name"], train.train_name], + ["=", ["timetable_id"], timetable_id]], + })); + + let response: Vec = + app.fetch(request).assert_status(StatusCode::OK).json_into(); + + assert_eq!(response.len(), 1); + assert_eq!(response[0].train_name, train.train_name); + } + + #[rstest] + async fn search_trainschedule_post_not_found() { + let app = TestAppBuilder::default_app(); + let pool = app.db_pool(); + + // Create the timetable in the database + let timetable = create_timetable(&mut pool.get_ok()).await; + let timetable_id = timetable.id; + + // Add a train_schedule in the database + create_simple_train_schedule(&mut pool.get_ok(), timetable_id).await; + + let train_name = "NonExistingTrain"; + + // The body + let request = app.post("/search").json(&json!({ + "object": "trainschedule", + "query": ["and", ["=", ["train_name"], train_name], + ["=", ["timetable_id"], timetable_id]], + })); + + let response: Vec = + app.fetch(request).assert_status(StatusCode::OK).json_into(); + + assert_eq!(response.len(), 0); + } +} diff --git a/front/src/common/api/generatedEditoastApi.ts b/front/src/common/api/generatedEditoastApi.ts index 345807564de..a3912a97352 100644 --- a/front/src/common/api/generatedEditoastApi.ts +++ b/front/src/common/api/generatedEditoastApi.ts @@ -2848,13 +2848,67 @@ export type SearchResultItemScenario = { tags: string[]; trains_count: number; }; +export type Margins = { + boundaries: string[]; + /** The values of the margins. Must contains one more element than the boundaries + Can be a percentage `X%` or a time in minutes per 100 kilometer `Xmin/100km` */ + values: string[]; +}; +export type TrainScheduleOptions = { + use_electrical_profiles?: boolean; +}; +export type PathItem = PathItemLocation & { + /** Metadata given to mark a point as wishing to be deleted by the user. + It's useful for soft deleting the point (waiting to fix / remove all references) + If true, the train schedule is consider as invalid and must be edited */ + deleted?: boolean; + id: string; +}; +export type PowerRestrictionItem = { + from: string; + to: string; + value: string; +}; +export type ReceptionSignal = 'OPEN' | 'STOP' | 'SHORT_SLIP_STOP'; +export type ScheduleItem = { + /** The expected arrival time at the stop. + This will be used to compute the final simulation time. */ + arrival?: string | null; + at: string; + /** Whether the schedule item is locked (only for display purposes) */ + locked?: boolean; + reception_signal?: ReceptionSignal; + /** Duration of the stop. + Can be `None` if the train does not stop. + If `None`, `reception_signal` must be `Open`. + `Some("PT0S")` means the train stops for 0 seconds. */ + stop_for?: string | null; +}; +export type SearchResultItemTrainSchedule = { + comfort: number; + constraint_distribution: number; + id: number; + initial_speed: number; + labels: (string | null)[]; + margins: Margins; + options: TrainScheduleOptions; + path: PathItem[]; + power_restrictions: PowerRestrictionItem[]; + rolling_stock_name: string; + schedule: ScheduleItem[]; + speed_limit_tag?: string | null; + start_time: string; + timetable_id: number; + train_name: string; +}; export type SearchResultItem = | SearchResultItemTrack | SearchResultItemOperationalPoint | SearchResultItemSignal | SearchResultItemProject | SearchResultItemStudy - | SearchResultItemScenario; + | SearchResultItemScenario + | SearchResultItemTrainSchedule; export type SearchQuery = boolean | number | number | string | (SearchQuery | null)[]; export type SearchPayload = { /** Whether to return the SQL query instead of executing it @@ -3032,7 +3086,6 @@ export type PathfindingItem = { timing_data?: StepTimingData | null; }; export type Distribution = 'STANDARD' | 'MARECO'; -export type ReceptionSignal = 'OPEN' | 'STOP' | 'SHORT_SLIP_STOP'; export type TrainScheduleBase = { comfort?: Comfort; constraint_distribution: Distribution;