use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProfessionalServicesRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub start_date: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub end_date: Option<String>,
pub employees: Vec<Employee>,
pub tasks: Vec<Task>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub contracts: Vec<Contract>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Employee {
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub email: Option<String>,
#[serde(default)]
pub shifts: Vec<Shift>,
#[serde(default)]
pub skills: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hourly_rate: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub contract_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub time_zone_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dedicated_client_id: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub availability_time_spans: Vec<AvailabilityTimeSpan>,
}
#[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 Task {
pub id: String,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub deadline: Option<String>,
pub duration: String,
pub priority: String,
#[serde(default)]
pub required_skills: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub preferred_skills: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub client_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub project_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub time_zone_id: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub depends_on: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub preferred_employees: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub prohibited_employees: Vec<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Contract {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_hours_per_day: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_hours_per_week: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min_rest_between_shifts: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub target_utilization: Option<f64>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AvailabilityTimeSpan {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub start: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub end: Option<String>,
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub kind: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProfessionalServicesStartResponse {
pub job_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub solver_job_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProfessionalServicesResultResponse {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub job_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub start_date: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub end_date: Option<String>,
#[serde(default)]
pub employees: Vec<Employee>,
#[serde(default)]
pub tasks: Vec<Task>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub contracts: Vec<Contract>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub solver_status: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub feasible: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub score_string: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub score: Option<serde_json::Value>,
#[serde(default)]
pub unassigned_tasks: Vec<String>,
#[serde(default)]
pub assigned_tasks: Vec<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn serializes_expected_keys() {
let json = serde_json::to_string(&ProfessionalServicesRequest::default()).unwrap();
assert!(json.contains("\"employees\""));
assert!(json.contains("\"tasks\""));
assert!(!json.contains("\"startDate\""));
}
#[test]
fn availability_span_uses_reserved_type_key() {
let span = AvailabilityTimeSpan {
kind: Some("VACATION".into()),
..Default::default()
};
let json = serde_json::to_string(&span).unwrap();
assert!(json.contains("\"type\":\"VACATION\""));
assert!(!json.contains("\"kind\""));
let parsed: AvailabilityTimeSpan = serde_json::from_str(r#"{"type":"SICK"}"#).unwrap();
assert_eq!(parsed.kind.as_deref(), Some("SICK"));
}
#[test]
fn deserializes_result_response() {
let json = r#"{
"id": "plan-1",
"name": "Sprint 5",
"startDate": "2024-01-15",
"endDate": "2024-01-19",
"employees": [
{"id": "e1", "name": "Dev A", "skills": ["dev"]}
],
"tasks": [
{"id": "t1", "name": "Build", "duration": "PT2H", "priority": "HIGH",
"requiredSkills": ["dev"]}
],
"contracts": [{"id": "c1", "name": "full-time"}],
"solverStatus": "NOT_SOLVING",
"feasible": true,
"scoreString": "0hard/0medium/-5soft",
"score": {"hard": 0, "medium": 0, "soft": -5},
"assignedTasks": ["t1"],
"unassignedTasks": []
}"#;
let result: ProfessionalServicesResultResponse = serde_json::from_str(json).unwrap();
assert_eq!(result.id.as_deref(), Some("plan-1"));
assert_eq!(result.employees.len(), 1);
assert_eq!(result.employees[0].name.as_deref(), Some("Dev A"));
assert_eq!(result.tasks[0].name, "Build");
assert_eq!(result.contracts.len(), 1);
assert_eq!(result.solver_status.as_deref(), Some("NOT_SOLVING"));
assert_eq!(result.feasible, Some(true));
assert_eq!(result.score_string.as_deref(), Some("0hard/0medium/-5soft"));
assert_eq!(result.assigned_tasks, vec!["t1".to_string()]);
assert!(result.unassigned_tasks.is_empty());
}
}