lodestone-client 0.0.10

Current-only asynchronous Rust client for the Lodestone API
Documentation
use std::error::Error;

use lodestone_client::{
    CapabilityGrant, Command, Digest, DiscoverResult, Identity, LODESTONE_API_COMMAND_OUTCOMES,
    LODESTONE_API_SCHEMA_ID, LODESTONE_API_SPEC_SHA256, RequestBody, Resource, ResultBody, bulk_batch_id,
    describe_tool_operation, load_tool_pack, request_command,
};
use serde_json::{Value, json};

const EXAMPLES: &str = include_str!("../../../clients/native/generated/lodestone-api-examples.json");
const EXECUTABLE_EXAMPLES: &str =
    include_str!("../../../clients/native/generated/lodestone-api-executable-examples.json");
const FIXTURES: &str = include_str!("../../../tests/p10/lodestone-api-closed-schema.json");
const BULK_BATCH_ID_VECTORS: &str = include_str!("../../../clients/native/fixtures/lodestone-bulk-batch-id.json");

#[test]
fn public_client_derives_the_language_neutral_bulk_batch_vectors() -> Result<(), Box<dyn Error>> {
    let document: Value = serde_json::from_str(BULK_BATCH_ID_VECTORS)?;
    let vectors = document
        .get("vectors")
        .and_then(Value::as_array)
        .ok_or("bulk batch fixture omitted vectors")?;
    for vector in vectors {
        let collection = Identity {
            value: vector
                .get("collection_identity")
                .and_then(Value::as_str)
                .ok_or("bulk batch vector omitted collection")?
                .to_owned(),
        };
        let index: Resource =
            serde_json::from_value(vector.get("index").cloned().ok_or("bulk batch vector omitted index")?)?;
        let actions = vector
            .get("item_action_ids")
            .and_then(Value::as_array)
            .ok_or("bulk batch vector omitted actions")?
            .iter()
            .map(|value| {
                value
                    .as_str()
                    .map(|value| Digest {
                        value: value.to_owned(),
                    })
                    .ok_or("bulk batch vector action is not a string")
            })
            .collect::<Result<Vec<_>, _>>()?;
        let expected = vector
            .get("batch_id")
            .and_then(Value::as_str)
            .ok_or("bulk batch vector omitted result")?;
        assert_eq!(bulk_batch_id(&collection, &index, &actions)?.value, expected);
    }
    Ok(())
}

#[test]
fn every_generated_example_decodes_and_reencodes_through_the_closed_union() -> Result<(), Box<dyn Error>> {
    let document: Value = serde_json::from_str(EXAMPLES)?;
    let commands = document
        .get("commands")
        .and_then(Value::as_object)
        .ok_or("generated examples omitted commands")?;
    assert_eq!(commands.len(), lodestone_client::RUST_CLIENT_METHOD_COUNT);
    for (name, example) in commands {
        let request_value = json!({
            "command": name,
            "body": example.get("request_body").ok_or("example omitted request_body")?,
        });
        let request: RequestBody = serde_json::from_value(request_value.clone())?;
        assert_eq!(serde_json::to_value(&request)?, request_value);
        assert_eq!(
            serde_json::to_value(request_command(&request))?,
            Value::String(name.clone())
        );

        let result_value = json!({
            "command": name,
            "body": example.get("expected_result").ok_or("example omitted expected_result")?,
        });
        let result: ResultBody = serde_json::from_value(result_value.clone())?;
        assert_eq!(serde_json::to_value(result)?, result_value);
    }
    assert_eq!(
        document
            .pointer("/raw_json_text/wire/bytes_base64")
            .and_then(Value::as_str),
        Some("eyJuIjoxLjIzMDAsIm4iOjJ9")
    );
    Ok(())
}

#[test]
fn connected_tool_loader_intersects_exact_discovery_and_result_ceiling() -> Result<(), Box<dyn Error>> {
    let examples: Value = serde_json::from_str(EXAMPLES)?;
    let mut discovery: DiscoverResult = serde_json::from_value(
        examples
            .pointer("/commands/discover/expected_result")
            .cloned()
            .ok_or("discover example missing")?,
    )?;
    discovery.schema_id = LODESTONE_API_SCHEMA_ID.to_owned();
    discovery.schema_digest.value = LODESTONE_API_SPEC_SHA256.to_owned();
    let fixtures: Value = serde_json::from_str(FIXTURES)?;
    let mut grant: CapabilityGrant = serde_json::from_value(
        fixtures
            .pointer("/named_types/capability_grant")
            .cloned()
            .ok_or("capability fixture missing")?,
    )?;
    grant.command = "search".to_owned();
    grant.state = "available".to_owned();
    grant.capability_family = "data".to_owned();
    discovery.capabilities.push(grant);
    let loaded = load_tool_pack(&discovery, "data", 2 * 1024 * 1024)?;
    assert_eq!(loaded["operations"].as_array().map(Vec::len), Some(1));
    let omitted = load_tool_pack(&discovery, "data", 1)?;
    assert_eq!(omitted["operations"].as_array().map(Vec::len), Some(0));
    discovery.schema_digest.value = "0".repeat(64);
    assert!(load_tool_pack(&discovery, "data", 2 * 1024 * 1024).is_err());
    let descriptor = describe_tool_operation("search")?.ok_or("search tool descriptor missing")?;
    assert_eq!(descriptor.get("command").and_then(Value::as_str), Some("search"));
    assert!(descriptor.get("$defs").and_then(Value::as_object).is_some());
    assert_internal_references_resolve(&descriptor, &descriptor)?;
    assert!(describe_tool_operation("activate_service_credential")?.is_none());
    Ok(())
}

fn assert_internal_references_resolve(root: &Value, value: &Value) -> Result<(), Box<dyn Error>> {
    match value {
        Value::Object(object) => {
            if let Some(reference) = object.get("$ref").and_then(Value::as_str) {
                let pointer = reference.strip_prefix('#').ok_or("external schema reference")?;
                if root.pointer(pointer).is_none() {
                    return Err(format!("unresolved schema reference: {reference}").into());
                }
            }
            for child in object.values() {
                assert_internal_references_resolve(root, child)?;
            }
        }
        Value::Array(values) => {
            for child in values {
                assert_internal_references_resolve(root, child)?;
            }
        }
        Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {}
    }
    Ok(())
}

#[test]
fn executable_templates_freshen_and_cover_recovery_exact_number_and_raw_json() -> Result<(), Box<dyn Error>> {
    let document: Value = serde_json::from_str(EXECUTABLE_EXAMPLES)?;
    let templates = document
        .get("templates")
        .and_then(Value::as_object)
        .ok_or("executable examples omitted templates")?;
    assert_eq!(templates.len(), 6);
    for (ordinal, (name, template)) in templates.iter().enumerate() {
        let command = template
            .get("command")
            .and_then(Value::as_str)
            .ok_or("executable template omitted command")?;
        let mut body = template
            .get("request_body")
            .cloned()
            .ok_or("executable template omitted request body")?;
        for pointer in template
            .get("freshen")
            .and_then(Value::as_array)
            .ok_or("executable template omitted freshness paths")?
        {
            let pointer = pointer.as_str().ok_or("freshness path is not text")?;
            let value = body.pointer_mut(pointer).ok_or("freshness path does not resolve")?;
            *value = if pointer.ends_with("incarnation") {
                Value::String(format!("{:032x}", ordinal + 1))
            } else if pointer.ends_with("digest") {
                Value::String(format!("{:064x}", ordinal + 1))
            } else {
                Value::String(format!("ec1-fresh-{ordinal}"))
            };
        }
        let request_value = json!({"command": command, "body": body});
        let request: RequestBody = serde_json::from_value(request_value.clone())?;
        assert_eq!(serde_json::to_value(&request)?, request_value, "{name}");
    }
    assert_eq!(
        document.pointer("/templates/exact_number/request_body/window_end"),
        Some(&Value::String("18446744073709551615".to_owned()))
    );
    assert_eq!(
        document.pointer("/templates/raw_json/request_body/source/bytes_base64"),
        Some(&Value::String("e30=".to_owned()))
    );
    let submit = LODESTONE_API_COMMAND_OUTCOMES
        .iter()
        .find(|outcome| outcome.command == Command::SubmitDocumentMutation)
        .ok_or("submit mutation outcome contract missing")?;
    assert!(submit.possible_submission_states.contains(&"indeterminate"));
    assert_eq!(submit.resolver_command, Some(Command::ResolveDocumentMutation));
    Ok(())
}