plansolve 0.25.0

Official Rust client library for the PlanSolve optimization API.
Documentation
use std::time::Duration;

use serde_json::Value;

use super::models::*;
use crate::error::{Error, Result};
use crate::solver_status::SolverStatusResponse;
use crate::transport::Transport;

const ROUTE: &str = "/api/v1/solvers/professionalservices/solve";

/// Client for the professional services optimization API.
#[derive(Clone)]
pub struct ProfessionalServicesClient {
    transport: Transport,
}

impl ProfessionalServicesClient {
    pub(crate) fn new(transport: Transport) -> Self {
        Self { transport }
    }

    /// Starts a new professional services optimization job.
    pub async fn start(
        &self,
        request: &ProfessionalServicesRequest,
    ) -> Result<ProfessionalServicesStartResponse> {
        self.transport.post_json(ROUTE, request).await
    }

    /// Gets the status of a professional services optimization job.
    pub async fn get_status(&self, job_id: &str) -> Result<SolverStatusResponse> {
        self.transport
            .get_json(&format!("{ROUTE}/{job_id}/status"))
            .await
    }

    /// Gets the result of a completed professional services optimization job.
    pub async fn get_result(&self, job_id: &str) -> Result<ProfessionalServicesResultResponse> {
        let mut result: ProfessionalServicesResultResponse = self
            .transport
            .get_json(&format!("{ROUTE}/{job_id}"))
            .await?;
        result.job_id = Some(job_id.to_string());
        Ok(result)
    }

    /// Gets analysis for a completed professional services optimization job.
    pub async fn analyze(&self, job_id: &str) -> Result<Value> {
        self.transport
            .get_json(&format!("{ROUTE}/{job_id}/analyze"))
            .await
    }

    /// Starts a job and polls until it completes.
    pub async fn start_and_wait_for_completion(
        &self,
        request: &ProfessionalServicesRequest,
        poll_interval_ms: u64,
        max_attempts: u32,
    ) -> Result<ProfessionalServicesResultResponse> {
        let start = self.start(request).await?;
        self.wait_for_completion(&start.job_id, poll_interval_ms, max_attempts)
            .await
    }

    /// Polls for job status until the solver finishes or `max_attempts` is reached.
    pub async fn wait_for_completion(
        &self,
        job_id: &str,
        poll_interval_ms: u64,
        max_attempts: u32,
    ) -> Result<ProfessionalServicesResultResponse> {
        if job_id.is_empty() {
            return Err(Error::MissingJobId);
        }
        let poll_interval_ms = if poll_interval_ms == 0 {
            5000
        } else {
            poll_interval_ms
        };
        let max_attempts = if max_attempts == 0 { 10 } else { max_attempts };

        let mut attempts = 0u32;
        loop {
            tokio::time::sleep(Duration::from_millis(poll_interval_ms)).await;
            let status = self.get_status(job_id).await?;
            attempts += 1;

            let still_solving = status.is_solving();
            if !still_solving || attempts >= max_attempts {
                if still_solving {
                    return Err(Error::SolverTimeout);
                }
                return self.get_result(job_id).await;
            }
        }
    }
}