use ts_rs::TS;
use crate::snapshot::cloud_manifest::{
CheckpointSnapshotState, FileSnapshotState, ImageRef, Manifest as SnapshotManifest,
SnapshotFormat, SnapshotScope, SnapshotState, UpperIntegrity, UpperLayer,
};
use crate::{
Action, CloudCreateSandboxRequest, CloudCreateSandboxResponse, CloudCreateSnapshotRequest,
CloudDiskImageFormat, CloudErrorBody, CloudErrorDetails, CloudHostPattern,
CloudMessageResponse, CloudNetworkSpec, CloudPaginated, CloudPatch, CloudPullPolicy,
CloudRlimit, CloudRlimitResource, CloudRootfsSource, CloudSandboxComputeResources,
CloudSandboxResources, CloudSandboxRuntimeOptions, CloudSandboxSpec, CloudSandboxStatus,
CloudSandboxStatusReason, CloudSecretEntry, CloudSecretSource, CloudSecretsConfig,
CloudSnapshot, CloudSnapshotDetails, CloudSnapshotKind, CloudSnapshotLocation,
CloudSnapshotOperation, CloudSnapshotOperationStatus, CloudSnapshotSpec, CloudViolationAction,
CloudVolumeMount, Destination, DestinationGroup, Direction, EnvVar, HandoffInit,
HostPermissions, MountOptions, NetworkPolicy, OwnedVolumeStorage, PortRange, Protocol, Rule,
SandboxLogLevel, SandboxPolicy, SecretSubstitution, SecurityProfile, StatVirtualization,
};
const HEADER: &str = "// @generated by microsandbox-types. Do not edit by hand.\n\n";
const DOMAIN_TYPE_NAMES: &[&str] = &[
"Action",
"Destination",
"DestinationGroup",
"Direction",
"EnvVar",
"HandoffInit",
"HostPermissions",
"MountOptions",
"OwnedVolumeStorage",
"NetworkPolicy",
"PortRange",
"Protocol",
"Rule",
"SandboxLogLevel",
"SandboxPolicy",
"SecretSubstitution",
"SecurityProfile",
"StatVirtualization",
];
const SNAPSHOT_TYPE_NAMES: &[&str] = &[
"SnapshotManifest",
"SnapshotScope",
"SnapshotFormat",
"SnapshotState",
"ImageRef",
"UpperLayer",
"UpperIntegrity",
"FileSnapshotState",
"CheckpointSnapshotState",
];
pub fn render_domain() -> String {
format_ts(&format!("{HEADER}{}", body(&domain_declarations())))
}
pub fn render_snapshot() -> String {
format_ts(&format!("{HEADER}{}", body(&snapshot_declarations())))
}
pub fn render_cloud() -> String {
let decls = cloud_declarations();
let mut output = String::from(HEADER);
output.push_str(&import_block(DOMAIN_TYPE_NAMES, &decls, "./domain.js"));
output.push_str(&import_block(SNAPSHOT_TYPE_NAMES, &decls, "./snapshot.js"));
output.push('\n');
output.push_str(&body(&decls));
format_ts(&output)
}
fn import_block(names: &[&str], decls: &[String], module: &str) -> String {
let imports: Vec<&str> = names
.iter()
.copied()
.filter(|name| decls.iter().any(|decl| mentions(decl, name)))
.collect();
format!(
"import type {{ {} }} from \"{module}\";\nexport type * from \"{module}\";\n",
imports.join(", ")
)
}
fn format_ts(source: &str) -> String {
use dprint_plugin_typescript::configuration::ConfigurationBuilder;
use dprint_plugin_typescript::{FormatTextOptions, format_text};
use std::path::Path;
let config = ConfigurationBuilder::new().deno().build();
let options = FormatTextOptions {
config: &config,
path: Path::new("bindings.ts"),
text: source.to_string(),
extension: None,
external_formatter: None,
};
match format_text(options) {
Ok(Some(formatted)) => formatted,
Ok(None) => source.to_string(),
Err(err) => panic!("dprint failed to format generated bindings: {err}"),
}
}
pub fn domain_declarations() -> Vec<String> {
let cfg = ts_rs::Config::new().with_large_int("number");
vec![
EnvVar::decl(&cfg),
HandoffInit::decl(&cfg),
SecurityProfile::decl(&cfg),
SandboxPolicy::decl(&cfg),
SandboxLogLevel::decl(&cfg),
MountOptions::decl(&cfg),
OwnedVolumeStorage::decl(&cfg),
StatVirtualization::decl(&cfg),
HostPermissions::decl(&cfg),
SecretSubstitution::decl(&cfg),
NetworkPolicy::decl(&cfg),
Rule::decl(&cfg),
Action::decl(&cfg),
Direction::decl(&cfg),
Protocol::decl(&cfg),
Destination::decl(&cfg),
DestinationGroup::decl(&cfg),
PortRange::decl(&cfg),
]
}
pub fn snapshot_declarations() -> Vec<String> {
let cfg = ts_rs::Config::new().with_large_int("number");
vec![
SnapshotManifest::decl(&cfg),
SnapshotScope::decl(&cfg),
ImageRef::decl(&cfg),
SnapshotState::decl(&cfg),
FileSnapshotState::decl(&cfg),
CheckpointSnapshotState::decl(&cfg),
SnapshotFormat::decl(&cfg),
UpperLayer::decl(&cfg),
UpperIntegrity::decl(&cfg),
]
}
pub fn cloud_declarations() -> Vec<String> {
let cfg = ts_rs::Config::new().with_large_int("number");
vec![
CloudCreateSandboxRequest::decl(&cfg),
CloudSandboxSpec::decl(&cfg),
CloudSandboxComputeResources::decl(&cfg),
CloudRootfsSource::decl(&cfg),
CloudVolumeMount::decl(&cfg),
CloudSandboxResources::decl(&cfg),
CloudSandboxRuntimeOptions::decl(&cfg),
CloudPullPolicy::decl(&cfg),
CloudDiskImageFormat::decl(&cfg),
CloudRlimitResource::decl(&cfg),
CloudRlimit::decl(&cfg),
CloudPatch::decl(&cfg),
CloudNetworkSpec::decl(&cfg),
CloudSecretsConfig::decl(&cfg),
CloudSecretEntry::decl(&cfg),
CloudSecretSource::decl(&cfg),
CloudHostPattern::decl(&cfg),
CloudViolationAction::decl(&cfg),
CloudCreateSandboxResponse::decl(&cfg),
CloudSandboxStatus::decl(&cfg),
CloudSandboxStatusReason::decl(&cfg),
CloudCreateSnapshotRequest::decl(&cfg),
CloudSnapshotSpec::decl(&cfg),
CloudSnapshot::decl(&cfg),
CloudSnapshotDetails::decl(&cfg),
CloudSnapshotLocation::decl(&cfg),
CloudSnapshotOperation::decl(&cfg),
CloudSnapshotOperationStatus::decl(&cfg),
CloudSnapshotKind::decl(&cfg),
CloudPaginated::<CloudCreateSandboxResponse>::decl(&cfg),
CloudMessageResponse::decl(&cfg),
CloudErrorBody::decl(&cfg),
CloudErrorDetails::decl(&cfg),
]
}
fn body(declarations: &[String]) -> String {
let mut output = declarations
.iter()
.cloned()
.map(export_declaration)
.map(trim_line_endings)
.collect::<Vec<_>>()
.join("\n\n");
output.push('\n');
output
}
fn mentions(decl: &str, name: &str) -> bool {
decl.match_indices(name).any(|(i, _)| {
let boundary = |c: Option<char>| c.is_none_or(|c| !c.is_alphanumeric() && c != '_');
boundary(decl[..i].chars().next_back()) && boundary(decl[i + name.len()..].chars().next())
})
}
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")
}
#[cfg(test)]
mod tests {
use std::fs;
use std::path::PathBuf;
use super::*;
fn src_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("microsandbox-types rust crate should live under <package>/rust")
.join("typescript/src")
}
#[test]
fn checked_in_bindings_match_generated_output() {
for (file, generated) in [
("domain.ts", render_domain()),
("snapshot.ts", render_snapshot()),
("cloud.ts", render_cloud()),
] {
let path = src_dir().join(file);
let current = fs::read_to_string(&path)
.unwrap_or_else(|err| panic!("failed to read {}: {err}", path.display()));
assert_eq!(current, generated, "{} is stale", path.display());
}
}
#[test]
fn cloud_network_limits_are_optional_in_typescript_and_on_the_wire() {
let network: crate::cloud::CloudNetworkSpec = serde_json::from_str("{}").unwrap();
let wire = serde_json::to_value(network).unwrap();
let declaration = crate::cloud::CloudNetworkSpec::decl(&ts_rs::Config::new());
for field in ["max_connections", "max_udp_connections"] {
assert!(wire.get(field).is_none());
assert!(declaration.contains(&format!("{field}?:")));
}
}
#[test]
fn cloud_bindings_import_domain_and_stay_scoped() {
assert_eq!(domain_declarations().len(), 17);
assert_eq!(snapshot_declarations().len(), 9);
assert_eq!(cloud_declarations().len(), 33);
let cloud = render_cloud();
for source in ["oci", "bind", "disk_image", "disk_snapshot"] {
assert!(cloud.contains(&format!("\"source\": \"{source}\"")));
}
assert!(cloud.contains("export type CloudSandboxSpec"));
assert!(cloud.contains("export type CloudSandboxComputeResources"));
assert!(cloud.contains("import type {"));
assert!(cloud.contains("EnvVar"));
assert!(cloud.contains("export type * from \"./domain.js\""));
assert!(cloud.contains("export type CloudSnapshot"));
assert!(cloud.contains("import type { SnapshotManifest } from \"./snapshot.js\""));
assert!(cloud.contains("export type * from \"./snapshot.js\""));
let domain = render_domain();
assert!(domain.contains("export type NetworkPolicy"));
assert!(domain.contains("export type Rule"));
assert!(!domain.contains("SandboxModificationPlan"));
assert!(!domain.contains("export type SandboxSpec ="));
let snapshot = render_snapshot();
assert!(snapshot.contains("export type SnapshotManifest"));
assert!(snapshot.contains("export type SnapshotState"));
assert!(!snapshot.contains("export type Manifest"));
}
}