plansolve 0.27.0

Official Rust client library for the PlanSolve optimization API.
Documentation
use super::models::*;
use crate::error::Result;
use crate::transport::Transport;

const ROUTE: &str = "/api/v1/jobs";

/// Optional filters for listing jobs.
#[derive(Debug, Clone, Default)]
pub struct GetJobsParams {
    /// 1-based page index (default 1 when `None`).
    pub page: Option<i32>,
    /// Maximum number of items per page (default 10 when `None`).
    pub page_size: Option<i32>,
    /// Only return jobs billed against this subscription.
    pub subscription_id: Option<i32>,
    /// Only return jobs created at or after this timestamp (ISO-8601).
    pub start: Option<String>,
    /// Only return jobs created at or before this timestamp (ISO-8601).
    pub end: Option<String>,
}

/// Client for the public Jobs API.
#[derive(Clone)]
pub struct JobsClient {
    transport: Transport,
}

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

    /// Lists solver jobs for the authenticated tenant (`GET /api/v1/jobs`).
    ///
    /// `params` carries optional pagination (`page`, `page_size`) and filters
    /// (`subscription_id`, `start`, `end`); any field left as `None` falls back to
    /// the server default. Use [`GetJobsParams::default`] for the first page.
    ///
    /// Returns a [`PagedResultOfJobDto`] containing the current page of jobs plus
    /// pagination metadata, or an [`Error`](crate::Error) for a non-success status.
    pub async fn get_jobs(&self, params: &GetJobsParams) -> Result<PagedResultOfJobDto> {
        let mut query: Vec<String> = Vec::new();
        if let Some(page) = params.page {
            query.push(format!("page={page}"));
        }
        if let Some(page_size) = params.page_size {
            query.push(format!("pageSize={page_size}"));
        }
        if let Some(subscription_id) = params.subscription_id {
            query.push(format!("subscriptionId={subscription_id}"));
        }
        if let Some(start) = &params.start {
            query.push(format!("start={}", urlencode(start)));
        }
        if let Some(end) = &params.end {
            query.push(format!("end={}", urlencode(end)));
        }

        let path = if query.is_empty() {
            ROUTE.to_string()
        } else {
            format!("{ROUTE}?{}", query.join("&"))
        };
        self.transport.get_json(&path).await
    }

    /// Creates a new solver job (`POST /api/v1/jobs`).
    ///
    /// `request` specifies which solver to run (`solver_id`), the raw JSON problem
    /// definition (`request`), and an optional `subscription_id`. Returns the
    /// created [`JobDto`] (including its public `job_id`), or an
    /// [`Error`](crate::Error) for a non-success status.
    pub async fn create_job(&self, request: &CreateJobRequest) -> Result<JobDto> {
        self.transport.post_json(ROUTE, request).await
    }

    /// Gets a single job by its public job id (`GET /api/v1/jobs/{job_id}`).
    ///
    /// `job_id` is the public, opaque identifier (the `job_id` field, not the
    /// internal numeric `id`). Returns the matching [`JobDto`], or an
    /// [`Error`](crate::Error) for a non-success status (e.g. `404` when the job
    /// does not exist).
    pub async fn get_job(&self, job_id: &str) -> Result<JobDto> {
        self.transport.get_json(&format!("{ROUTE}/{job_id}")).await
    }
}

/// Minimal percent-encoding for query parameter values (encodes the characters
/// that commonly appear in ISO-8601 timestamps, e.g. `:` and `+`).
fn urlencode(value: &str) -> String {
    let mut encoded = String::with_capacity(value.len());
    for byte in value.bytes() {
        match byte {
            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
                encoded.push(byte as char);
            }
            _ => encoded.push_str(&format!("%{byte:02X}")),
        }
    }
    encoded
}