Skip to content

Commit 5c58af7

Browse files
committed
editoast: add train_schedule object to search endpoint
Signed-off-by: Ethan Perruzza <[email protected]>
1 parent eb992f6 commit 5c58af7

File tree

4 files changed

+240
-4
lines changed

4 files changed

+240
-4
lines changed

editoast/editoast_schemas/src/train_schedule/train_schedule_options.rs

-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ editoast_common::schemas! {
99

1010
#[derive(Debug, Derivative, Clone, Serialize, Deserialize, ToSchema, Hash)]
1111
#[serde(deny_unknown_fields)]
12-
#[schema(as = TrainScheduleOptionsV2)] // Avoiding conflict with v1. TODO: remove after migration to v2
1312
#[derivative(Default)]
1413
pub struct TrainScheduleOptions {
1514
#[derivative(Default(value = "true"))]

editoast/openapi.yaml

+68-1
Original file line numberDiff line numberDiff line change
@@ -8770,6 +8770,7 @@ components:
87708770
- $ref: '#/components/schemas/SearchResultItemProject'
87718771
- $ref: '#/components/schemas/SearchResultItemStudy'
87728772
- $ref: '#/components/schemas/SearchResultItemScenario'
8773+
- $ref: '#/components/schemas/SearchResultItemTrainSchedule'
87738774
description: A search result item that depends on the query's `object`
87748775
SearchResultItemOperationalPoint:
87758776
type: object
@@ -9003,6 +9004,72 @@ components:
90039004
format: int64
90049005
line_name:
90059006
type: string
9007+
SearchResultItemTrainSchedule:
9008+
type: object
9009+
description: A search result item for a query with `object = "trainschedule"`
9010+
required:
9011+
- id
9012+
- train_name
9013+
- labels
9014+
- rolling_stock_name
9015+
- timetable_id
9016+
- start_time
9017+
- schedule
9018+
- margins
9019+
- initial_speed
9020+
- comfort
9021+
- path
9022+
- constraint_distribution
9023+
- power_restrictions
9024+
- options
9025+
properties:
9026+
comfort:
9027+
type: integer
9028+
format: int64
9029+
constraint_distribution:
9030+
type: integer
9031+
format: int64
9032+
id:
9033+
type: integer
9034+
format: int64
9035+
minimum: 0
9036+
initial_speed:
9037+
type: number
9038+
format: double
9039+
labels:
9040+
type: array
9041+
items:
9042+
type: string
9043+
nullable: true
9044+
margins:
9045+
$ref: '#/components/schemas/Margins'
9046+
options:
9047+
$ref: '#/components/schemas/TrainScheduleOptions'
9048+
path:
9049+
type: array
9050+
items:
9051+
$ref: '#/components/schemas/PathItem'
9052+
power_restrictions:
9053+
type: array
9054+
items:
9055+
$ref: '#/components/schemas/PowerRestrictionItem'
9056+
rolling_stock_name:
9057+
type: string
9058+
schedule:
9059+
type: array
9060+
items:
9061+
$ref: '#/components/schemas/ScheduleItem'
9062+
speed_limit_tag:
9063+
type: string
9064+
nullable: true
9065+
start_time:
9066+
type: string
9067+
format: date-time
9068+
timetable_id:
9069+
type: integer
9070+
format: int64
9071+
train_name:
9072+
type: string
90069073
Side:
90079074
type: string
90089075
enum:
@@ -10206,7 +10273,7 @@ components:
1020610273
format: int64
1020710274
description: Timetable attached to the train schedule
1020810275
nullable: true
10209-
TrainScheduleOptionsV2:
10276+
TrainScheduleOptions:
1021010277
type: object
1021110278
properties:
1021210279
use_electrical_profiles:

editoast/src/views/search.rs

+117
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,9 @@ use axum::extract::Json;
203203
use axum::extract::Query;
204204
use axum::extract::State;
205205
use axum::Extension;
206+
use chrono::DateTime;
206207
use chrono::NaiveDateTime;
208+
use chrono::Utc;
207209
use diesel::pg::Pg;
208210
use diesel::sql_query;
209211
use diesel::sql_types::Jsonb;
@@ -215,6 +217,11 @@ use editoast_common::geometry::GeoJsonPoint;
215217
use editoast_derive::EditoastError;
216218
use editoast_derive::Search;
217219
use editoast_derive::SearchConfigStore;
220+
use editoast_schemas::train_schedule::Margins;
221+
use editoast_schemas::train_schedule::PathItem;
222+
use editoast_schemas::train_schedule::PowerRestrictionItem;
223+
use editoast_schemas::train_schedule::ScheduleItem;
224+
use editoast_schemas::train_schedule::TrainScheduleOptions;
218225
use editoast_search::query_into_sql;
219226
use editoast_search::SearchConfigStore as _;
220227
use editoast_search::SearchError;
@@ -345,6 +352,7 @@ async fn search(
345352
) -> Result<Json<serde_json::Value>> {
346353
let roles: HashSet<BuiltinRole> = match object.as_str() {
347354
"track" | "operationalpoint" | "signal" => HashSet::from([BuiltinRole::InfraRead]),
355+
"trainschedule" => HashSet::from([BuiltinRole::TimetableRead]),
348356
"project" | "study" | "scenario" => HashSet::from([BuiltinRole::OpsRead]),
349357
_ => {
350358
return Err(SearchApiError::ObjectType {
@@ -683,6 +691,48 @@ pub(super) struct SearchResultItemScenario {
683691
tags: Vec<String>,
684692
}
685693

694+
#[derive(Search, Serialize, ToSchema)]
695+
#[cfg_attr(test, derive(serde::Deserialize))]
696+
#[search(
697+
table = "train_schedule",
698+
column(name = "timetable_id", data_type = "integer"),
699+
column(name = "train_name", data_type = "string")
700+
)]
701+
#[allow(unused)]
702+
/// A search result item for a query with `object = "trainschedule"`
703+
pub(super) struct SearchResultItemTrainSchedule {
704+
#[search(sql = "train_schedule.id")]
705+
id: u64,
706+
#[search(sql = "train_schedule.train_name")]
707+
train_name: String,
708+
#[search(sql = "train_schedule.labels")]
709+
labels: Vec<Option<String>>,
710+
#[search(sql = "train_schedule.rolling_stock_name")]
711+
rolling_stock_name: String,
712+
#[search(sql = "train_schedule.timetable_id")]
713+
timetable_id: i64,
714+
#[search(sql = "train_schedule.start_time")]
715+
start_time: DateTime<Utc>,
716+
#[search(sql = "train_schedule.schedule")]
717+
schedule: Vec<ScheduleItem>,
718+
#[search(sql = "train_schedule.margins")]
719+
margins: Margins,
720+
#[search(sql = "train_schedule.initial_speed")]
721+
initial_speed: f64,
722+
#[search(sql = "train_schedule.comfort")]
723+
comfort: i64,
724+
#[search(sql = "train_schedule.path")]
725+
path: Vec<PathItem>,
726+
#[search(sql = "train_schedule.constraint_distribution")]
727+
constraint_distribution: i64,
728+
#[search(sql = "train_schedule.speed_limit_tag")]
729+
speed_limit_tag: Option<String>,
730+
#[search(sql = "train_schedule.power_restrictions")]
731+
power_restrictions: Vec<PowerRestrictionItem>,
732+
#[search(sql = "train_schedule.options")]
733+
options: TrainScheduleOptions,
734+
}
735+
686736
/// See [editoast_search::SearchConfigStore::find]
687737
#[derive(SearchConfigStore)]
688738
#[search_config_store(
@@ -692,5 +742,72 @@ pub(super) struct SearchResultItemScenario {
692742
object(name = "project", config = SearchResultItemProject),
693743
object(name = "study", config = SearchResultItemStudy),
694744
object(name = "scenario", config = SearchResultItemScenario),
745+
object(name = "trainschedule", config = SearchResultItemTrainSchedule),
695746
)]
696747
pub struct SearchConfigFinder;
748+
749+
#[cfg(test)]
750+
pub mod test {
751+
752+
use axum::http::StatusCode;
753+
use pretty_assertions::assert_eq;
754+
use rstest::rstest;
755+
use serde_json::json;
756+
757+
use super::*;
758+
use crate::models::fixtures::{create_simple_train_schedule, create_timetable};
759+
use crate::views::test_app::TestAppBuilder;
760+
761+
#[rstest]
762+
async fn search_trainschedule_post_found() {
763+
let app = TestAppBuilder::default_app();
764+
let pool = app.db_pool();
765+
766+
// Create the timetable in the database
767+
let timetable = create_timetable(&mut pool.get_ok()).await;
768+
let timetable_id = timetable.id;
769+
770+
// Add a train_schedule in the database
771+
let train = create_simple_train_schedule(&mut pool.get_ok(), timetable_id).await;
772+
773+
// The body
774+
let request = app.post("/search").json(&json!({
775+
"object": "trainschedule",
776+
"query": ["and", ["=", ["train_name"], train.train_name],
777+
["=", ["timetable_id"], timetable_id]],
778+
}));
779+
780+
let response: Vec<SearchResultItemTrainSchedule> =
781+
app.fetch(request).assert_status(StatusCode::OK).json_into();
782+
783+
assert_eq!(response.len(), 1);
784+
assert_eq!(response[0].train_name, train.train_name);
785+
}
786+
787+
#[rstest]
788+
async fn search_trainschedule_post_not_found() {
789+
let app = TestAppBuilder::default_app();
790+
let pool = app.db_pool();
791+
792+
// Create the timetable in the database
793+
let timetable = create_timetable(&mut pool.get_ok()).await;
794+
let timetable_id = timetable.id;
795+
796+
// Add a train_schedule in the database
797+
create_simple_train_schedule(&mut pool.get_ok(), timetable_id).await;
798+
799+
let train_name = "NonExistingTrain";
800+
801+
// The body
802+
let request = app.post("/search").json(&json!({
803+
"object": "trainschedule",
804+
"query": ["and", ["=", ["train_name"], train_name],
805+
["=", ["timetable_id"], timetable_id]],
806+
}));
807+
808+
let response: Vec<SearchResultItemTrainSchedule> =
809+
app.fetch(request).assert_status(StatusCode::OK).json_into();
810+
811+
assert_eq!(response.len(), 0);
812+
}
813+
}

front/src/common/api/generatedEditoastApi.ts

+55-2
Original file line numberDiff line numberDiff line change
@@ -2848,13 +2848,67 @@ export type SearchResultItemScenario = {
28482848
tags: string[];
28492849
trains_count: number;
28502850
};
2851+
export type Margins = {
2852+
boundaries: string[];
2853+
/** The values of the margins. Must contains one more element than the boundaries
2854+
Can be a percentage `X%` or a time in minutes per 100 kilometer `Xmin/100km` */
2855+
values: string[];
2856+
};
2857+
export type TrainScheduleOptions = {
2858+
use_electrical_profiles?: boolean;
2859+
};
2860+
export type PathItem = PathItemLocation & {
2861+
/** Metadata given to mark a point as wishing to be deleted by the user.
2862+
It's useful for soft deleting the point (waiting to fix / remove all references)
2863+
If true, the train schedule is consider as invalid and must be edited */
2864+
deleted?: boolean;
2865+
id: string;
2866+
};
2867+
export type PowerRestrictionItem = {
2868+
from: string;
2869+
to: string;
2870+
value: string;
2871+
};
2872+
export type ReceptionSignal = 'OPEN' | 'STOP' | 'SHORT_SLIP_STOP';
2873+
export type ScheduleItem = {
2874+
/** The expected arrival time at the stop.
2875+
This will be used to compute the final simulation time. */
2876+
arrival?: string | null;
2877+
at: string;
2878+
/** Whether the schedule item is locked (only for display purposes) */
2879+
locked?: boolean;
2880+
reception_signal?: ReceptionSignal;
2881+
/** Duration of the stop.
2882+
Can be `None` if the train does not stop.
2883+
If `None`, `reception_signal` must be `Open`.
2884+
`Some("PT0S")` means the train stops for 0 seconds. */
2885+
stop_for?: string | null;
2886+
};
2887+
export type SearchResultItemTrainSchedule = {
2888+
comfort: number;
2889+
constraint_distribution: number;
2890+
id: number;
2891+
initial_speed: number;
2892+
labels: (string | null)[];
2893+
margins: Margins;
2894+
options: TrainScheduleOptions;
2895+
path: PathItem[];
2896+
power_restrictions: PowerRestrictionItem[];
2897+
rolling_stock_name: string;
2898+
schedule: ScheduleItem[];
2899+
speed_limit_tag?: string | null;
2900+
start_time: string;
2901+
timetable_id: number;
2902+
train_name: string;
2903+
};
28512904
export type SearchResultItem =
28522905
| SearchResultItemTrack
28532906
| SearchResultItemOperationalPoint
28542907
| SearchResultItemSignal
28552908
| SearchResultItemProject
28562909
| SearchResultItemStudy
2857-
| SearchResultItemScenario;
2910+
| SearchResultItemScenario
2911+
| SearchResultItemTrainSchedule;
28582912
export type SearchQuery = boolean | number | number | string | (SearchQuery | null)[];
28592913
export type SearchPayload = {
28602914
/** Whether to return the SQL query instead of executing it
@@ -3032,7 +3086,6 @@ export type PathfindingItem = {
30323086
timing_data?: StepTimingData | null;
30333087
};
30343088
export type Distribution = 'STANDARD' | 'MARECO';
3035-
export type ReceptionSignal = 'OPEN' | 'STOP' | 'SHORT_SLIP_STOP';
30363089
export type TrainScheduleBase = {
30373090
comfort?: Comfort;
30383091
constraint_distribution: Distribution;

0 commit comments

Comments
 (0)