use serde::{Deserialize, Serialize};
pub type Location = [f64; 2];
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct FieldServiceRequest {
pub vehicles: Vec<Vehicle>,
pub visits: Vec<Visit>,
#[serde(skip_serializing_if = "Option::is_none")]
pub weights: Option<Weights>,
#[serde(skip_serializing_if = "Option::is_none")]
pub options: Option<SolverOptions>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Vehicle {
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
pub location: Location,
pub shifts: Vec<Shift>,
pub skills: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub departure_time: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub visits: Vec<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Visit {
pub id: String,
pub name: String,
pub location: Location,
pub time_windows: Vec<TimeWindow>,
pub service_duration: String,
pub priority: String,
pub required_skills: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pinned: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub vehicle: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub arrival_time: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub departure_time: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub start_service_time: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub driving_time_seconds_from_previous_standstill: Option<i64>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Shift {
pub id: String,
pub min_start_time: String,
pub max_end_time: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TimeWindow {
pub min_start_time: String,
pub max_end_time: String,
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
pub struct ConstraintWeight {
pub hard: i64,
pub medium: i64,
pub soft: i64,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Weights {
#[serde(skip_serializing_if = "Option::is_none")]
pub pinned_visit_vehicle_assignment: Option<ConstraintWeight>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pinned_visit_service_time: Option<ConstraintWeight>,
#[serde(skip_serializing_if = "Option::is_none")]
pub no_missing_skills: Option<ConstraintWeight>,
#[serde(skip_serializing_if = "Option::is_none")]
pub service_time_missing: Option<ConstraintWeight>,
#[serde(skip_serializing_if = "Option::is_none")]
pub vehicle_unassigned: Option<ConstraintWeight>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prefer_high_priority: Option<ConstraintWeight>,
#[serde(skip_serializing_if = "Option::is_none")]
pub minimize_travel_time: Option<ConstraintWeight>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prefer_using_idle_vehicles: Option<ConstraintWeight>,
#[serde(skip_serializing_if = "Option::is_none")]
pub minimize_early_arrival_wait: Option<ConstraintWeight>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prefer_initial_vehicle_assignment: Option<ConstraintWeight>,
#[serde(
rename = "PreferEarlierVisitDates",
skip_serializing_if = "Option::is_none"
)]
pub prefer_earlier_visit_dates: Option<ConstraintWeight>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SolverOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub spent_limit: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub unimproved_spent_limit: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FieldServiceStartResponse {
pub job_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
#[serde(default)]
pub status_code: i32,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FieldServiceResultResponse {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub job_id: Option<String>,
pub vehicles: Vec<ScheduledVehicle>,
pub visits: Vec<ScheduledVisit>,
#[serde(skip_serializing_if = "Option::is_none")]
pub score: Option<String>,
#[serde(default)]
pub total_driving_time_seconds: i64,
#[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
pub weights: std::collections::HashMap<String, String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ScheduledVehicle {
pub id: String,
pub location: Location,
pub shifts: Vec<Shift>,
pub skills: Vec<String>,
pub visits: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub daily_return_times: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub total_driving_time_seconds: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub arrival_time: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub departure_time: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ScheduledVisit {
pub id: String,
pub name: String,
pub location: Location,
pub time_windows: Vec<TimeWindow>,
pub service_duration: String,
pub priority: String,
pub required_skills: Vec<String>,
pub vehicle: String,
#[serde(default)]
pub previous_visit: Option<String>,
pub arrival_time: String,
pub departure_time: String,
pub start_service_time: String,
#[serde(default)]
pub driving_time_seconds_from_previous_standstill: i64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn can_construct_request() {
let request = FieldServiceRequest {
vehicles: vec![Vehicle {
id: "1".into(),
location: [51.52, -0.1],
shifts: vec![Shift {
id: "full-day".into(),
min_start_time: "2024-01-15T08:00:00".into(),
max_end_time: "2024-01-15T18:00:00".into(),
}],
skills: vec!["English".into()],
..Default::default()
}],
visits: vec![Visit {
id: "1".into(),
name: "Task 1".into(),
location: [51.51, -0.12],
service_duration: "PT30M".into(),
priority: "HIGH".into(),
required_skills: vec!["English".into()],
..Default::default()
}],
..Default::default()
};
assert_eq!(request.vehicles.len(), 1);
assert_eq!(request.visits.len(), 1);
}
#[test]
fn serializes_expected_keys() {
let json = serde_json::to_string(&FieldServiceRequest::default()).unwrap();
assert!(json.contains("\"vehicles\""));
assert!(json.contains("\"visits\""));
assert!(!json.contains("\"weights\""));
}
#[test]
fn weights_preserve_the_exact_contract_keys() {
let weights = Weights {
minimize_travel_time: Some(ConstraintWeight {
soft: 1,
..Default::default()
}),
prefer_earlier_visit_dates: Some(ConstraintWeight {
soft: 2,
..Default::default()
}),
..Default::default()
};
let json = serde_json::to_string(&weights).unwrap();
assert!(json.contains("\"minimizeTravelTime\""));
assert!(json.contains("\"PreferEarlierVisitDates\""));
assert!(!json.contains("\"preferEarlierVisitDates\""));
}
#[test]
fn deserializes_result_response() {
let json = r#"{
"vehicles": [
{"id": "1", "location": [51.52, -0.1], "shifts": [], "skills": ["English"], "visits": ["1"]}
],
"visits": [
{
"id": "1", "name": "Task 1", "location": [51.51, -0.12],
"timeWindows": [], "serviceDuration": "PT30M", "priority": "HIGH",
"requiredSkills": ["English"], "vehicle": "1", "previousVisit": null,
"arrivalTime": "2024-01-15T10:15:33", "departureTime": "2024-01-15T10:45:33",
"startServiceTime": "2024-01-15T10:15:33",
"drivingTimeSecondsFromPreviousStandstill": 933
}
],
"totalDrivingTimeSeconds": 933
}"#;
let result: FieldServiceResultResponse = serde_json::from_str(json).unwrap();
assert_eq!(result.vehicles.len(), 1);
assert_eq!(result.visits.len(), 1);
let v = &result.visits[0];
assert_eq!(v.vehicle, "1");
assert_eq!(v.arrival_time, "2024-01-15T10:15:33");
assert_eq!(v.start_service_time, "2024-01-15T10:15:33");
assert_eq!(v.driving_time_seconds_from_previous_standstill, 933);
assert_eq!(result.total_driving_time_seconds, 933);
}
}