use ts_rs::TS;
use crate::{
Action, CloudCreateSandboxResponse, CloudDiskImageFormat, CloudErrorBody, CloudErrorDetails,
CloudHostPattern, CloudMessageResponse, CloudNetworkSpec, CloudPaginated, CloudPatch,
CloudPullPolicy, CloudRlimit, CloudRlimitResource, CloudRootfsSource, CloudSandboxResources,
CloudSandboxRuntimeOptions, CloudSandboxSpec, CloudSandboxStatus, CloudSandboxStatusReason,
CloudSecretEntry, CloudSecretSource, CloudSecretsConfig, CloudViolationAction,
CloudVolumeMount, Destination, DestinationGroup, Direction, EnvVar, HandoffInit,
HostPermissions, MountOptions, NetworkPolicy, PortRange, Protocol, Rule, SandboxLogLevel,
SandboxPolicy, SecretInjection, 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",
"NetworkPolicy",
"PortRange",
"Protocol",
"Rule",
"SandboxLogLevel",
"SandboxPolicy",
"SecretInjection",
"SecurityProfile",
"StatVirtualization",
];
pub fn render_domain() -> String {
format_ts(&format!("{HEADER}{}", body(&domain_declarations())))
}
pub fn render_cloud() -> String {
let decls = cloud_declarations();
let imports: Vec<&str> = DOMAIN_TYPE_NAMES
.iter()
.copied()
.filter(|name| decls.iter().any(|decl| mentions(decl, name)))
.collect();
let mut output = String::from(HEADER);
output.push_str(&format!(
"import type {{ {} }} from \"./domain.js\";\n",
imports.join(", ")
));
output.push_str("export type * from \"./domain.js\";\n\n");
output.push_str(&body(&decls));
format_ts(&output)
}
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),
StatVirtualization::decl(&cfg),
HostPermissions::decl(&cfg),
SecretInjection::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 cloud_declarations() -> Vec<String> {
let cfg = ts_rs::Config::new().with_large_int("number");
vec![
CloudSandboxSpec::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),
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()), ("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_bindings_import_domain_and_stay_scoped() {
assert_eq!(domain_declarations().len(), 17);
assert_eq!(cloud_declarations().len(), 23);
let cloud = render_cloud();
assert!(cloud.contains("export type CloudSandboxSpec"));
assert!(cloud.contains("import type {"));
assert!(cloud.contains("EnvVar"));
assert!(cloud.contains("export type * from \"./domain.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 ="));
}
}