dimescheduler 0.1.0

Official Rust client library for the Dime.Scheduler API.
Documentation
//! Runs tests/contract/scenarios.json (shared with the .NET, JS and Python
//! runners) through the Rust client and asserts the captured request.

use serde::Deserialize;
use wiremock::matchers::any;
use wiremock::{Mock, MockServer, ResponseTemplate};

use crate::transport::Transport;
use crate::{entities, DimeSchedulerClient};

#[derive(Deserialize)]
struct Contract {
    common: Common,
    scenarios: Vec<Scenario>,
}

#[derive(Deserialize)]
struct Common {
    #[serde(rename = "apiKey")]
    api_key: String,
    headers: Headers,
}

#[derive(Deserialize)]
struct Headers {
    #[serde(rename = "x-api-key")]
    x_api_key: String,
    #[serde(rename = "user-agent-pattern")]
    user_agent_pattern: String,
}

#[derive(Deserialize)]
struct Scenario {
    name: String,
    input: Option<serde_json::Value>,
    expect: Expect,
}

#[derive(Deserialize)]
struct Expect {
    method: String,
    path: String,
    body: Option<serde_json::Value>,
}

async fn dispatch(c: &DimeSchedulerClient, name: &str, input: Option<serde_json::Value>) {
    let input = input.unwrap_or(serde_json::Value::Null);
    match name {
        "categories.getAll" => {
            c.categories.get_all().await.unwrap();
        }
        "categories.create" => {
            let cat: entities::Category = serde_json::from_value(input).unwrap();
            c.categories.create(&cat).await.unwrap();
        }
        "resources.getAll" => {
            c.resources.get_all().await.unwrap();
        }
        "jobs.getAll" => {
            c.jobs.get_all().await.unwrap();
        }
        "tasks.getAll" => {
            c.tasks.get_all().await.unwrap();
        }
        "tags.getAll" => {
            c.tags.get_all().await.unwrap();
        }
        "tags.create" => {
            let tag: entities::Tag = serde_json::from_value(input).unwrap();
            c.tags.create(&tag).await.unwrap();
        }
        other => panic!("unknown scenario: {other}. Add a case in dispatch()."),
    }
}

#[tokio::test]
async fn cross_sdk_contract() {
    let raw =
        std::fs::read_to_string("../../tests/contract/scenarios.json").expect("read scenarios");
    let contract: Contract = serde_json::from_str(&raw).expect("parse scenarios");
    // The shared pattern enumerates the other SDKs' names; this crate's
    // User-Agent follows the same `dimescheduler-<lang>/<semver>` shape.
    assert!(
        contract
            .common
            .headers
            .user_agent_pattern
            .starts_with("^dimescheduler-"),
        "contract pattern changed shape: {}",
        contract.common.headers.user_agent_pattern
    );

    for scenario in contract.scenarios {
        let server = MockServer::start().await;
        Mock::given(any())
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
            .mount(&server)
            .await;
        let c = DimeSchedulerClient::from_transport(Transport::new(
            server.uri(),
            contract.common.api_key.clone(),
        ));

        dispatch(&c, &scenario.name, scenario.input).await;

        let reqs = server.received_requests().await.unwrap();
        assert_eq!(
            reqs.len(),
            1,
            "{}: expected exactly one request",
            scenario.name
        );
        let req = &reqs[0];
        assert_eq!(req.url.path(), scenario.expect.path, "{}", scenario.name);
        assert_eq!(
            req.method.as_str(),
            scenario.expect.method,
            "{}",
            scenario.name
        );
        assert_eq!(
            req.headers.get("x-api-key").unwrap(),
            &contract.common.headers.x_api_key,
            "{}",
            scenario.name
        );
        let ua = req.headers.get("user-agent").unwrap().to_str().unwrap();
        assert!(
            ua.starts_with("dimescheduler-rust/"),
            "{}: {ua}",
            scenario.name
        );
        if let Some(expected) = scenario.expect.body {
            let sent: serde_json::Value = serde_json::from_slice(&req.body).unwrap();
            assert_eq!(sent, expected, "{}", scenario.name);
        }
    }
}