microsandbox-types 0.6.5

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

use ts_rs::TS;

use crate::{
    Action, CertCacheConfig, ChangeKind, CloudCreateSandboxResponse, CloudErrorBody,
    CloudErrorDetails, CloudMessageResponse, CloudNetworkSpec, CloudPaginated, CloudRootfsSource,
    CloudSandboxResources, CloudSandboxRuntimeOptions, CloudSandboxSpec, CloudSandboxStatus,
    ConfigPlannedChange, Destination, DestinationGroup, Direction, DiskImageFormat, DnsConfig,
    EnvVar, HandoffInit, HostPattern, HostPermissions, InterceptCaConfig, InterfaceOverrides,
    ModificationConflict, ModificationDisposition, ModificationPolicy, ModificationWarning,
    MountOptions, NamedVolumeCreate, NamedVolumeMode, NetworkPolicy, NetworkSpec, OciRootfsSource,
    Patch, PlannedChange, PortProtocol, PortRange, Protocol, PublishedPortSpec, PullPolicy,
    ResourceConvergenceState, ResourceKind, ResourceResizeStatus, Rlimit, RlimitResource,
    RootfsSource, Rule, SandboxLogLevel, SandboxModificationPatch, SandboxModificationPlan,
    SandboxPolicy, SandboxResources, SandboxRuntimeOptions, SandboxSpec, ScopedUpstreamCaCert,
    ScopedVerifyUpstream, SecretChangeKind, SecretEntry, SecretInjection, SecretModificationPatch,
    SecretPlannedChange, SecretSource, SecretsConfig, SecurityProfile, StatVirtualization,
    TlsConfig, ViolationAction, 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![
        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),
        HostPattern::decl(&cfg),
        SecretInjection::decl(&cfg),
        ViolationAction::decl(&cfg),
        SecretEntry::decl(&cfg),
        SecretsConfig::decl(&cfg),
        InterceptCaConfig::decl(&cfg),
        CertCacheConfig::decl(&cfg),
        ScopedUpstreamCaCert::decl(&cfg),
        ScopedVerifyUpstream::decl(&cfg),
        TlsConfig::decl(&cfg),
        Action::decl(&cfg),
        Direction::decl(&cfg),
        Protocol::decl(&cfg),
        DestinationGroup::decl(&cfg),
        Destination::decl(&cfg),
        PortRange::decl(&cfg),
        Rule::decl(&cfg),
        NetworkPolicy::decl(&cfg),
        DnsConfig::decl(&cfg),
        InterfaceOverrides::decl(&cfg),
        NetworkSpec::decl(&cfg),
        PublishedPortSpec::decl(&cfg),
        PortProtocol::decl(&cfg),
        HandoffInit::decl(&cfg),
        SandboxPolicy::decl(&cfg),
        SandboxSpec::decl(&cfg),
        SandboxResources::decl(&cfg),
        SandboxRuntimeOptions::decl(&cfg),
        EnvVar::decl(&cfg),
        SandboxLogLevel::decl(&cfg),
        RlimitResource::decl(&cfg),
        Rlimit::decl(&cfg),
        SandboxModificationPatch::decl(&cfg),
        ModificationPolicy::decl(&cfg),
        SecretModificationPatch::decl(&cfg),
        SecretSource::decl(&cfg),
        SandboxModificationPlan::decl(&cfg),
        PlannedChange::decl(&cfg),
        ConfigPlannedChange::decl(&cfg),
        SecretPlannedChange::decl(&cfg),
        ChangeKind::decl(&cfg),
        SecretChangeKind::decl(&cfg),
        ModificationDisposition::decl(&cfg),
        ModificationConflict::decl(&cfg),
        ModificationWarning::decl(&cfg),
        ResourceKind::decl(&cfg),
        ResourceConvergenceState::decl(&cfg),
        ResourceResizeStatus::decl(&cfg),
        CloudNetworkSpec::decl(&cfg),
        CloudRootfsSource::decl(&cfg),
        CloudSandboxResources::decl(&cfg),
        CloudSandboxRuntimeOptions::decl(&cfg),
        CloudSandboxSpec::decl(&cfg),
        CloudCreateSandboxResponse::decl(&cfg),
        CloudSandboxStatus::decl(&cfg),
        CloudPaginated::<CloudCreateSandboxResponse>::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");

        // `just gen` (and the CI staleness gate) set this to rewrite the
        // checked-in bindings from the ts-rs declarations.
        if std::env::var_os("REGEN_TS_BINDINGS").is_some() {
            fs::write(&bindings_path, &generated).expect("write bindings");
            return;
        }

        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; run `just gen`",
            bindings_path.display()
        );
    }

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

        assert_eq!(declarations.len(), 71);
        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("CloudSandboxSpec"))
        );
        assert!(
            declarations
                .iter()
                .any(|decl| decl.contains("CloudSandboxStatus"))
        );
    }
}