subako-function-sdk 0.2.0

Write Subako function packages in Rust: typed functions become tools, and the program prints its own manifest.
Documentation
use serde::{Deserialize, Serialize};
use subako_function_sdk::{Output, ToolError, execute};

#[derive(Deserialize)]
struct Greet {
    name: String,
}

#[derive(Serialize)]
struct Greeting {
    text: String,
}

fn route(tool: &str, input: subako_function_sdk::Input) -> Result<Output, ToolError> {
    match tool {
        "greet" => {
            let greet: Greet = input.parse()?;
            Output::json(&Greeting {
                text: format!("hello {}", greet.name),
            })
        }
        other => Err(ToolError::unknown_tool(other)),
    }
}

fn args(tool: &str) -> Vec<String> {
    vec!["pkg".to_string(), tool.to_string()]
}

#[test]
fn a_known_tool_writes_its_output_and_exits_zero() {
    let (stdout, code) = execute(&args("greet"), br#"{"name":"ada"}"#, route);
    assert_eq!(code, 0);
    assert_eq!(
        serde_json::from_slice::<serde_json::Value>(&stdout).unwrap(),
        serde_json::json!({"text": "hello ada"})
    );
}

#[test]
fn an_unknown_tool_is_a_tool_error() {
    let (stdout, code) = execute(&args("nope"), b"{}", route);
    assert_eq!(code, 1);
    assert_eq!(
        serde_json::from_slice::<serde_json::Value>(&stdout).unwrap(),
        serde_json::json!({"error": "unknown tool: nope"})
    );
}

#[test]
fn input_that_is_not_json_is_a_tool_error() {
    let (stdout, code) = execute(&args("greet"), b"not json", route);
    assert_eq!(code, 1);
    let value: serde_json::Value = serde_json::from_slice(&stdout).unwrap();
    assert!(
        value["error"]
            .as_str()
            .unwrap()
            .starts_with("input is not JSON")
    );
}

#[test]
fn input_of_the_wrong_shape_names_the_problem() {
    let (stdout, code) = execute(&args("greet"), br#"{"name": 3}"#, route);
    assert_eq!(code, 1);
    let value: serde_json::Value = serde_json::from_slice(&stdout).unwrap();
    assert!(
        value["error"]
            .as_str()
            .unwrap()
            .starts_with("invalid input")
    );
}

#[test]
fn a_missing_tool_argument_is_a_tool_error() {
    let (stdout, code) = execute(&["pkg".to_string()], b"{}", route);
    assert_eq!(code, 1);
    let value: serde_json::Value = serde_json::from_slice(&stdout).unwrap();
    assert_eq!(value["error"], "no tool named on the command line");
}

#[test]
fn output_that_json_cannot_hold_is_a_tool_error() {
    // JSON object keys are strings, so a map keyed by pairs has no JSON form.
    let unjsonable = std::collections::BTreeMap::from([((1, 2), 0)]);
    let error = Output::json(&unjsonable).err().unwrap();
    assert!(
        error.message().starts_with("output is not JSON"),
        "{error:?}"
    );
}

#[test]
fn a_tool_error_takes_a_message_of_either_string_type() {
    assert_eq!(ToolError::from("no").message(), "no");
    assert_eq!(ToolError::from("no".to_string()).message(), "no");
}