microsandbox-types 0.5.8

Shared task and wire contract types for microsandbox.
Documentation
//! TypeScript binding generation helpers.

use ts_rs::TS;

use crate::{
    CloudCreateSandboxRequest, CloudErrorBody, CloudErrorDetails, CloudMessageResponse,
    CloudPaginated, CloudSandbox, CloudSandboxStatus, DiskImageFormat, EnvVar, HandoffInit,
    HostPermissions, LogSource, MountOptions, NamedVolumeCreate, NamedVolumeMode, NetworkSpec,
    OciRootfsSource, Patch, PortProtocol, PublishedPortSpec, PullPolicy, Rlimit, RlimitResource,
    RootfsSource, SandboxLogLevel, SandboxPolicy, SandboxResources, SandboxRuntimeOptions,
    SandboxSpec, SecurityProfile, SnapshotDestination, SnapshotSpec, StatVirtualization,
    VolumeKind, VolumeMount, VolumeSpec,
};

//--------------------------------------------------------------------------------------------------
// Constants
//--------------------------------------------------------------------------------------------------

const HEADER: &str = "// @generated by microsandbox-types. Do not edit by hand.\n\n";

//--------------------------------------------------------------------------------------------------
// Functions
//--------------------------------------------------------------------------------------------------

/// Render the complete generated TypeScript bindings file.
pub fn render_bindings() -> String {
    let mut output = String::from(HEADER);
    output.push_str(
        &declarations()
            .into_iter()
            .map(export_declaration)
            .map(trim_line_endings)
            .collect::<Vec<_>>()
            .join("\n\n"),
    );
    output.push('\n');
    output
}

/// Render raw `ts-rs` declarations for the shared microsandbox contracts.
pub fn declarations() -> Vec<String> {
    let cfg = ts_rs::Config::new().with_large_int("number");

    vec![
        serde_json::Value::decl(&cfg),
        DiskImageFormat::decl(&cfg),
        OciRootfsSource::decl(&cfg),
        RootfsSource::decl(&cfg),
        PullPolicy::decl(&cfg),
        StatVirtualization::decl(&cfg),
        HostPermissions::decl(&cfg),
        SecurityProfile::decl(&cfg),
        MountOptions::decl(&cfg),
        VolumeKind::decl(&cfg),
        VolumeSpec::decl(&cfg),
        NamedVolumeMode::decl(&cfg),
        NamedVolumeCreate::decl(&cfg),
        VolumeMount::decl(&cfg),
        Patch::decl(&cfg),
        NetworkSpec::decl(&cfg),
        PublishedPortSpec::decl(&cfg),
        PortProtocol::decl(&cfg),
        HandoffInit::decl(&cfg),
        SandboxPolicy::decl(&cfg),
        SnapshotDestination::decl(&cfg),
        SnapshotSpec::decl(&cfg),
        SandboxSpec::decl(&cfg),
        SandboxResources::decl(&cfg),
        SandboxRuntimeOptions::decl(&cfg),
        EnvVar::decl(&cfg),
        SandboxLogLevel::decl(&cfg),
        RlimitResource::decl(&cfg),
        Rlimit::decl(&cfg),
        LogSource::decl(&cfg),
        CloudCreateSandboxRequest::decl(&cfg),
        CloudSandbox::decl(&cfg),
        CloudSandboxStatus::decl(&cfg),
        CloudPaginated::<CloudSandbox>::decl(&cfg),
        CloudMessageResponse::decl(&cfg),
        CloudErrorBody::decl(&cfg),
        CloudErrorDetails::decl(&cfg),
    ]
}

fn export_declaration(declaration: String) -> String {
    if declaration.starts_with("export ") {
        return declaration;
    }

    for keyword in ["type ", "interface "] {
        if let Some(rest) = declaration.strip_prefix(keyword) {
            return format!("export {keyword}{rest}");
        }
    }

    declaration
}

fn trim_line_endings(declaration: String) -> String {
    declaration
        .lines()
        .map(str::trim_end)
        .collect::<Vec<_>>()
        .join("\n")
}

//--------------------------------------------------------------------------------------------------
// Tests
//--------------------------------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use std::fs;
    use std::path::PathBuf;

    use super::*;

    #[test]
    fn checked_in_bindings_match_generated_output() {
        let generated = render_bindings();
        let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
        let package_root = manifest_dir
            .parent()
            .expect("microsandbox-types rust crate should live under <package>/rust");
        let bindings_path = package_root.join("typescript/src/index.ts");
        let bindings = fs::read_to_string(&bindings_path).unwrap_or_else(|err| {
            panic!("failed to read {}: {err}", bindings_path.display());
        });

        assert_eq!(bindings, generated, "{} is stale", bindings_path.display());
    }

    #[test]
    fn ts_rs_renders_cloud_contract_declarations() {
        let declarations = declarations();

        assert_eq!(declarations.len(), 37);
        assert!(
            declarations
                .iter()
                .any(|decl| decl.contains("RootfsSource"))
        );
        assert!(declarations.iter().any(|decl| decl.contains("SandboxSpec")));
        assert!(declarations.iter().any(|decl| decl.contains("Rlimit")));
        assert!(
            declarations
                .iter()
                .any(|decl| decl.contains("CloudCreateSandboxRequest"))
        );
        assert!(
            declarations
                .iter()
                .any(|decl| decl.contains("CloudSandboxStatus"))
        );
    }
}