plansolve 0.25.5

Official Rust client library for the PlanSolve optimization API.
Documentation
use serde::{Deserialize, Serialize};

/// Request model for starting a professional services optimization.
#[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>,
}

/// An employee available for task assignment.
#[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>,
    pub shifts: Vec<Shift>,
    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>,
}

/// A time window during which an employee is available.
#[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,
}

/// A task to be scheduled.
#[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,
    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>,
}

/// An employee contract with working-hour constraints.
#[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>,
}

/// A span of employee availability.
#[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>,
}

/// Response from starting a professional services optimization.
#[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>,
}

/// Response from getting professional services optimization results.
#[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>,
    pub employees: Vec<ScheduledEmployee>,
    pub tasks: Vec<ScheduledTask>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub score: Option<String>,
    #[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
    pub weights: std::collections::HashMap<String, String>,
}

/// An employee in the optimization result.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ScheduledEmployee {
    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>,
    pub shifts: Vec<Shift>,
    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>,
    pub tasks: Vec<String>,
}

/// A task in the optimization result.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ScheduledTask {
    pub id: String,
    pub name: String,
    pub start_time: String,
    pub end_time: String,
    #[serde(default)]
    pub duration: f64,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub deadline: Option<String>,
    pub priority: String,
    pub required_skills: 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\""));
        // start_date is optional and unset => omitted.
        assert!(!json.contains("\"startDate\""));
    }

    #[test]
    fn availability_span_uses_reserved_type_key() {
        // The `kind` field maps to the reserved word "type" in the contract.
        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\""));

        // ...and round-trips back.
        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#"{
            "employees": [
                {"id": "e1", "shifts": [], "skills": ["dev"], "tasks": ["t1"]}
            ],
            "tasks": [
                {"id": "t1", "name": "Build", "startTime": "2024-01-15T09:00:00",
                 "endTime": "2024-01-15T11:00:00", "duration": 2.0, "priority": "HIGH",
                 "requiredSkills": ["dev"]}
            ],
            "score": "0hard/0medium/-5soft"
        }"#;

        let result: ProfessionalServicesResultResponse = serde_json::from_str(json).unwrap();
        assert_eq!(result.employees.len(), 1);
        assert_eq!(result.employees[0].tasks, vec!["t1".to_string()]);
        assert_eq!(result.tasks[0].start_time, "2024-01-15T09:00:00");
        assert_eq!(result.tasks[0].duration, 2.0);
        assert_eq!(result.score.as_deref(), Some("0hard/0medium/-5soft"));
    }
}