Skip to content

Using the API from Rust ​

This example uses reqwest (blocking client) for HTTP and serde with serde_json for JSON. It needs an API token and the COCOCO_URL and COCOCO_TOKEN environment variables described in Calling the CoCoCo API.

The operations ​

graphql
query ListJobs($first: Int, $after: String) {
  listJobs(first: $first, after: $after) {
    totalCount
    pageInfo { hasNextPage endCursor }
    edges { node { id name quantity status dueAt } }
  }
}
graphql
mutation CreateJob($input: CreateJobInput!) {
  createJob(input: $input) {
    job { id name status }
    errors { path message }
  }
}

The program ​

Create a project with cargo new jobs and add the dependencies to Cargo.toml:

toml
[dependencies]
reqwest = { version = "0.12", features = ["blocking", "json"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"

Replace src/main.rs with the program below. cargo run lists every job; cargo run -- create creates one.

rust
use serde::de::DeserializeOwned;
use serde::Deserialize;
use serde_json::{json, Value};
use std::{env, error::Error, time::Duration};

const LIST_JOBS: &str = r#"query ListJobs($first: Int, $after: String) {
  listJobs(first: $first, after: $after) {
    totalCount
    pageInfo { hasNextPage endCursor }
    edges { node { id name quantity status dueAt } }
  }
}"#;

const CREATE_JOB: &str = r#"mutation CreateJob($input: CreateJobInput!) {
  createJob(input: $input) {
    job { id name status }
    errors { path message }
  }
}"#;

type Result<T> = std::result::Result<T, Box<dyn Error>>;

#[derive(Deserialize)]
struct Response<T> {
    data: Option<T>,
    #[serde(default)]
    errors: Vec<GraphQLError>,
}

#[derive(Deserialize)]
struct GraphQLError {
    message: String,
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct Job {
    id: String, // a JobID such as "job_01M0SYJHB7FX6BKYVZZ7EAG4Y8"
    name: String,
    #[serde(default)]
    quantity: i64,
    status: String,
    due_at: Option<String>, // ISO 8601
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct ListJobsData {
    list_jobs: JobConnection,
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct JobConnection {
    page_info: PageInfo,
    edges: Vec<JobEdge>,
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct PageInfo {
    has_next_page: bool,
    end_cursor: Option<String>,
}

#[derive(Deserialize)]
struct JobEdge {
    node: Job,
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct CreateJobData {
    create_job: CreateJobPayload,
}

#[derive(Deserialize)]
struct CreateJobPayload {
    job: Option<Job>,
    errors: Vec<FieldError>,
}

#[derive(Deserialize)]
struct FieldError {
    path: String,
    message: String,
}

struct Client {
    http: reqwest::blocking::Client,
    endpoint: String,
    token: String,
}

impl Client {
    /// Posts one operation and returns its data.
    /// Fails on an HTTP error status and on any top-level GraphQL error.
    fn execute<T: DeserializeOwned>(&self, query: &str, variables: Value) -> Result<T> {
        let response: Response<T> = self
            .http
            .post(&self.endpoint)
            .bearer_auth(&self.token)
            .json(&json!({ "query": query, "variables": variables }))
            .send()?
            .error_for_status()?
            .json()?;
        // Authentication, permission and query errors arrive here with HTTP 200.
        if !response.errors.is_empty() {
            let messages: Vec<String> = response.errors.into_iter().map(|e| e.message).collect();
            return Err(messages.join("; ").into());
        }
        response.data.ok_or_else(|| "response has no data".into())
    }

    /// Returns every job, fetching 100 per request.
    fn list_jobs(&self) -> Result<Vec<Job>> {
        let mut jobs = Vec::new();
        let mut after: Option<String> = None;
        loop {
            let data: ListJobsData =
                self.execute(LIST_JOBS, json!({ "first": 100, "after": after }))?;
            let page = data.list_jobs;
            jobs.extend(page.edges.into_iter().map(|edge| edge.node));
            if !page.page_info.has_next_page {
                return Ok(jobs);
            }
            after = page.page_info.end_cursor;
        }
    }

    /// Creates a job. Validation problems come back in the payload.
    fn create_job(&self, name: &str, quantity: i64, external_id: &str) -> Result<Job> {
        let input = json!({ "name": name, "quantity": quantity, "externalId": external_id });
        let data: CreateJobData = self.execute(CREATE_JOB, json!({ "input": input }))?;
        let payload = data.create_job;
        if let Some(e) = payload.errors.first() {
            return Err(format!("{}: {}", e.path, e.message).into());
        }
        payload.job.ok_or_else(|| "no job returned".into())
    }
}

fn main() -> Result<()> {
    let client = Client {
        http: reqwest::blocking::Client::builder()
            .timeout(Duration::from_secs(30))
            .build()?,
        endpoint: format!("{}/graphql", env::var("COCOCO_URL")?.trim_end_matches('/')),
        token: env::var("COCOCO_TOKEN")?,
    };

    if env::args().nth(1).as_deref() == Some("create") {
        let job = client.create_job("Business cards", 500, "ERP-10042")?;
        println!("Created {} ({})", job.id, job.status);
    } else {
        for job in client.list_jobs()? {
            let due = job.due_at.as_deref().unwrap_or("-");
            println!(
                "{} {} {} {} {}",
                job.id, job.status, job.quantity, job.name, due
            );
        }
    }
    Ok(())
}

Notes ​

  • execute fails for both kinds of failure: an HTTP error status (error_for_status), and a response whose errors array is not empty.
  • list_jobs follows pageInfo.endCursor until hasNextPage is false. The API returns at most 100 items per page.
  • The create mutation selects only id, name and status, which is why quantity on Job has #[serde(default)]. due_at is an Option, so serde leaves it None when it is missing.
  • externalId must be unique in your tenant. Running create twice with the same value returns a validation error in createJob.errors.

Connect machines, automate workflows, and build custom apps for print production.