subako-function-sdk 0.2.2

Write Subako function packages in Rust: typed functions become tools, and the program prints its own manifest.
Documentation
use std::collections::BTreeMap;

use serde_json::json;
use subako_function_sdk::ToolError;
use subako_function_sdk::http::{
    Answer, HttpError, HttpErrorKind, Request, RequestBody, Response, ResponseBody, ResponseOutput,
    request,
};

#[test]
fn a_request_needs_only_a_url() {
    let parsed: Request = serde_json::from_value(json!({"url": "https://a.example/"})).unwrap();
    assert_eq!(parsed, Request::get("https://a.example/"));
    assert!(parsed.follow_redirects);
    assert_eq!(parsed.output, ResponseOutput::Inline);
    assert_eq!(
        serde_json::to_value(&parsed).unwrap(),
        json!({
            "method": "GET",
            "url": "https://a.example/",
            "headers": {},
            "output": {"type": "inline"},
            "follow_redirects": true,
        })
    );
}

#[test]
fn every_field_has_its_tagged_wire_shape() {
    let full = json!({
        "method": "POST",
        "url": "https://a.example/upload",
        "headers": {"x-a": "1"},
        "body": {"type": "file", "path": "/volume/tmp/in"},
        "output": {"type": "file", "path": "/volume/tmp/out"},
        "follow_redirects": false,
        "timeout_ms": 5000,
    });
    let parsed: Request = serde_json::from_value(full.clone()).unwrap();
    assert_eq!(
        parsed.body,
        Some(RequestBody::File {
            path: "/volume/tmp/in".to_string()
        })
    );
    assert_eq!(serde_json::to_value(&parsed).unwrap(), full);
    for body in [
        json!({"type": "text", "text": "hi"}),
        json!({"type": "json", "json": {"a": [1]}}),
    ] {
        let parsed: RequestBody = serde_json::from_value(body.clone()).unwrap();
        assert_eq!(serde_json::to_value(&parsed).unwrap(), body);
    }
}

#[test]
fn an_unknown_field_is_refused() {
    for bad in [
        json!({"url": "https://a.example/", "follow": true}),
        json!({"url": "https://a.example/", "body": {"type": "text", "text": "", "x": 1}}),
    ] {
        assert!(serde_json::from_value::<Request>(bad).is_err());
    }
}

#[test]
fn an_answer_is_ok_or_error() {
    let response = Response {
        status: 201,
        headers: BTreeMap::from([("a".to_string(), "b".to_string())]),
        body: ResponseBody::File {
            path: "/volume/tmp/out".to_string(),
            bytes: 3,
        },
    };
    let ok = json!({"ok": {
        "status": 201,
        "headers": {"a": "b"},
        "body": {"type": "file", "path": "/volume/tmp/out", "bytes": 3},
    }});
    let answer: Answer = serde_json::from_value(ok).unwrap();
    assert_eq!(Result::from(answer), Ok(response));

    let error = HttpError {
        kind: HttpErrorKind::HostNotAllowed,
        message: "no".to_string(),
    };
    let answer: Answer =
        serde_json::from_value(json!({"error": {"kind": "host_not_allowed", "message": "no"}}))
            .unwrap();
    assert_eq!(Result::from(answer), Err::<Response, _>(error.clone()));
    assert_eq!(error.to_string(), "no");
    assert_eq!(ToolError::from(error).message(), "no");
}

#[test]
fn outside_the_worker_a_request_fails_without_trapping() {
    let error = request(&Request::get("https://a.example/")).unwrap_err();
    assert_eq!(error.kind, HttpErrorKind::Abi);
    assert_eq!(
        error.message,
        "subako_http exists only inside the function worker"
    );
}

#[test]
fn a_method_is_spelled_as_its_display() {
    use subako_function_sdk::http::Method;
    for method in [
        Method::Get,
        Method::Head,
        Method::Post,
        Method::Put,
        Method::Patch,
        Method::Delete,
        Method::Options,
    ] {
        assert_eq!(
            serde_json::to_value(method).unwrap(),
            json!(method.to_string())
        );
        assert_eq!(
            serde_json::from_value::<Method>(json!(method.as_str())).unwrap(),
            method
        );
    }
    for refused in ["TRACE", "CONNECT", "get"] {
        assert!(serde_json::from_value::<Method>(json!(refused)).is_err());
    }
}