sdk

Radroots SDK and bindings
git clone https://radroots.dev/git/sdk.git
Log | Files | Refs | README

commit 5101e03ab69fd669c70f2c427007511223f84341
parent 71659acf4bb32ddbec8ce0f3ef7706e56b6dc1bd
Author: triesap <tyson@radroots.org>
Date:   Thu, 11 Jun 2026 06:09:49 -0700

feat(xtask): add generation command skeleton

Diffstat:
MCargo.lock | 3+++
Mcrates/xtask/Cargo.toml | 3+++
Acrates/xtask/src/check.rs | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/xtask/src/fs.rs | 46++++++++++++++++++++++++++++++++++++++++++++++
Acrates/xtask/src/generate.rs | 22++++++++++++++++++++++
Mcrates/xtask/src/main.rs | 46+++++++++++++++++++++++++++++++++++++++++++++-
Acrates/xtask/src/manifest.rs | 36++++++++++++++++++++++++++++++++++++
Acrates/xtask/src/package_matrix.rs | 130+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/xtask/src/ts.rs | 39+++++++++++++++++++++++++++++++++++++++
9 files changed, 395 insertions(+), 1 deletion(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -679,6 +679,9 @@ dependencies = [ [[package]] name = "radroots_sdk_xtask" version = "0.1.0" +dependencies = [ + "serde_json", +] [[package]] name = "radroots_secret_vault" diff --git a/crates/xtask/Cargo.toml b/crates/xtask/Cargo.toml @@ -11,3 +11,6 @@ publish = false [[bin]] name = "radroots_sdk_xtask" path = "src/main.rs" + +[dependencies] +serde_json = "1" diff --git a/crates/xtask/src/check.rs b/crates/xtask/src/check.rs @@ -0,0 +1,71 @@ +use std::{fs, path::Path}; + +use crate::{ + fs::workspace_root, + package_matrix::{FORBIDDEN_PACKAGE_NAMES, package_specs, validate_package_matrix}, +}; + +pub fn check() -> Result<(), String> { + validate_package_matrix()?; + let root = workspace_root()?; + check_forbidden_packages(&root)?; + for spec in package_specs() { + let package_dir = root.join(spec.package_dir); + let package_json_path = package_dir.join("package.json"); + let index_path = package_dir.join("src/index.ts"); + check_package_json(&package_json_path, spec.package_name)?; + if !index_path.is_file() { + return Err(format!("missing package index: {}", index_path.display())); + } + } + Ok(()) +} + +fn check_forbidden_packages(root: &Path) -> Result<(), String> { + for forbidden in FORBIDDEN_PACKAGE_NAMES { + let package_leaf = forbidden.trim_start_matches("@radroots/").to_owned(); + let forbidden_dir = root.join("packages").join(package_leaf); + if forbidden_dir.exists() { + return Err(format!( + "forbidden package directory exists: {}", + forbidden_dir.display() + )); + } + } + Ok(()) +} + +fn check_package_json(path: &Path, expected_name: &str) -> Result<(), String> { + let raw = fs::read_to_string(path) + .map_err(|error| format!("failed to read {}: {error}", path.display()))?; + let json = serde_json::from_str::<serde_json::Value>(&raw) + .map_err(|error| format!("failed to parse {}: {error}", path.display()))?; + let actual_name = json + .get("name") + .and_then(serde_json::Value::as_str) + .ok_or_else(|| format!("package.json missing name: {}", path.display()))?; + if actual_name != expected_name { + return Err(format!( + "package name mismatch in {}: expected {expected_name}, found {actual_name}", + path.display() + )); + } + let private = json + .get("private") + .and_then(serde_json::Value::as_bool) + .unwrap_or(false); + if !private { + return Err(format!("package must be private: {}", path.display())); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::check; + + #[test] + fn package_skeleton_is_valid() { + check().expect("package skeleton validates"); + } +} diff --git a/crates/xtask/src/fs.rs b/crates/xtask/src/fs.rs @@ -0,0 +1,46 @@ +use std::{ + fs, + path::{Path, PathBuf}, +}; + +pub fn workspace_root() -> Result<PathBuf, String> { + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + manifest_dir + .parent() + .and_then(Path::parent) + .map(Path::to_path_buf) + .ok_or_else(|| { + format!( + "cannot resolve workspace root from {}", + manifest_dir.display() + ) + }) +} + +#[allow(dead_code)] +pub fn write_if_changed(path: &Path, contents: &str) -> Result<bool, String> { + if let Ok(existing) = fs::read_to_string(path) { + if existing == contents { + return Ok(false); + } + } + if let Some(parent) = path.parent() { + fs::create_dir_all(parent) + .map_err(|error| format!("failed to create {}: {error}", parent.display()))?; + } + fs::write(path, contents) + .map_err(|error| format!("failed to write {}: {error}", path.display()))?; + Ok(true) +} + +#[cfg(test)] +mod tests { + use super::workspace_root; + + #[test] + fn resolves_workspace_root() { + let root = workspace_root().expect("workspace root resolves"); + assert!(root.join("Cargo.toml").is_file()); + assert!(root.join("packages").is_dir()); + } +} diff --git a/crates/xtask/src/generate.rs b/crates/xtask/src/generate.rs @@ -0,0 +1,22 @@ +use crate::{ + manifest::{manifest_file_name, package_manifest}, + package_matrix::{package_specs, validate_package_matrix}, + ts::{generated_constants_file, generated_header, generated_types_file, normalize_lf}, +}; + +pub fn generate_ts() -> Result<(), String> { + validate_package_matrix()?; + let header = normalize_lf(generated_header()); + for spec in package_specs() { + let manifest = package_manifest(*spec); + println!( + "planned TypeScript generation for {} with {}, {}, {}, and {}", + manifest["package"], + generated_types_file(), + generated_constants_file(), + manifest_file_name(), + header.lines().next().unwrap_or_default() + ); + } + Ok(()) +} diff --git a/crates/xtask/src/main.rs b/crates/xtask/src/main.rs @@ -1,3 +1,47 @@ +mod check; +mod fs; +mod generate; +mod manifest; +mod package_matrix; +mod ts; + fn main() { - println!("radroots sdk xtask"); + if let Err(error) = run(std::env::args().skip(1)) { + eprintln!("{error}"); + std::process::exit(1); + } +} + +fn run(args: impl IntoIterator<Item = String>) -> Result<(), String> { + let args = args.into_iter().collect::<Vec<_>>(); + match args.as_slice() { + [command, target] if command == "generate" && target == "ts" => generate::generate_ts(), + [command] if command == "check" => check::check(), + [] => Err(usage()), + _ => Err(usage()), + } +} + +fn usage() -> String { + "usage: cargo xtask generate ts | cargo xtask check".to_owned() +} + +#[cfg(test)] +mod tests { + use super::run; + + #[test] + fn accepts_generate_ts() { + assert!(run(["generate".to_owned(), "ts".to_owned()]).is_ok()); + } + + #[test] + fn accepts_check() { + assert!(run(["check".to_owned()]).is_ok()); + } + + #[test] + fn rejects_unknown_command() { + assert!(run(["generate".to_owned(), "swift".to_owned()]).is_err()); + } } diff --git a/crates/xtask/src/manifest.rs b/crates/xtask/src/manifest.rs @@ -0,0 +1,36 @@ +use serde_json::json; + +use crate::package_matrix::PackageSpec; + +pub fn manifest_file_name() -> &'static str { + "sdk-manifest.json" +} + +pub fn package_manifest(spec: PackageSpec) -> serde_json::Value { + json!({ + "package": spec.package_name, + "crate": spec.crate_name, + "generator": "radroots_sdk_xtask", + "generated": false + }) +} + +#[cfg(test)] +mod tests { + use crate::{ + manifest::{manifest_file_name, package_manifest}, + package_matrix::package_specs, + }; + + #[test] + fn manifest_name_is_stable() { + assert_eq!(manifest_file_name(), "sdk-manifest.json"); + } + + #[test] + fn manifest_records_package_and_crate() { + let manifest = package_manifest(package_specs()[0]); + assert_eq!(manifest["package"], package_specs()[0].package_name); + assert_eq!(manifest["crate"], package_specs()[0].crate_name); + } +} diff --git a/crates/xtask/src/package_matrix.rs b/crates/xtask/src/package_matrix.rs @@ -0,0 +1,130 @@ +use std::collections::BTreeSet; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct PackageSpec { + pub key: &'static str, + pub crate_name: &'static str, + pub crate_dir: &'static str, + pub package_name: &'static str, + pub package_dir: &'static str, +} + +pub const PACKAGE_SPECS: [PackageSpec; 7] = [ + PackageSpec { + key: "core", + crate_name: "radroots_core_bindings", + crate_dir: "crates/core_bindings", + package_name: "@radroots/core-bindings", + package_dir: "packages/core-bindings", + }, + PackageSpec { + key: "events", + crate_name: "radroots_events_bindings", + crate_dir: "crates/events_bindings", + package_name: "@radroots/events-bindings", + package_dir: "packages/events-bindings", + }, + PackageSpec { + key: "events_indexed", + crate_name: "radroots_events_indexed_bindings", + crate_dir: "crates/events_indexed_bindings", + package_name: "@radroots/events-indexed-bindings", + package_dir: "packages/events-indexed-bindings", + }, + PackageSpec { + key: "identity", + crate_name: "radroots_identity_bindings", + crate_dir: "crates/identity_bindings", + package_name: "@radroots/identity-bindings", + package_dir: "packages/identity-bindings", + }, + PackageSpec { + key: "replica_db_schema", + crate_name: "radroots_replica_db_schema_bindings", + crate_dir: "crates/replica_db_schema_bindings", + package_name: "@radroots/replica-db-schema-bindings", + package_dir: "packages/replica-db-schema-bindings", + }, + PackageSpec { + key: "trade", + crate_name: "radroots_trade_bindings", + crate_dir: "crates/trade_bindings", + package_name: "@radroots/trade-bindings", + package_dir: "packages/trade-bindings", + }, + PackageSpec { + key: "types", + crate_name: "radroots_types_bindings", + crate_dir: "crates/types_bindings", + package_name: "@radroots/types-bindings", + package_dir: "packages/types-bindings", + }, +]; + +pub const FORBIDDEN_PACKAGE_NAMES: [&str; 2] = + ["@radroots/tangle-db-schema-bindings", "@radroots/contracts"]; + +pub fn package_specs() -> &'static [PackageSpec] { + &PACKAGE_SPECS +} + +pub fn validate_package_matrix() -> Result<(), String> { + let mut crate_names = BTreeSet::new(); + let mut package_names = BTreeSet::new(); + let mut package_dirs = BTreeSet::new(); + for spec in package_specs() { + if FORBIDDEN_PACKAGE_NAMES.contains(&spec.package_name) { + return Err(format!( + "forbidden package in matrix: {}", + spec.package_name + )); + } + if !crate_names.insert(spec.crate_name) { + return Err(format!("duplicate crate in matrix: {}", spec.crate_name)); + } + if !package_names.insert(spec.package_name) { + return Err(format!( + "duplicate package in matrix: {}", + spec.package_name + )); + } + if !package_dirs.insert(spec.package_dir) { + return Err(format!( + "duplicate package directory in matrix: {}", + spec.package_dir + )); + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::{FORBIDDEN_PACKAGE_NAMES, package_specs, validate_package_matrix}; + + #[test] + fn package_matrix_is_valid() { + validate_package_matrix().expect("package matrix is valid"); + } + + #[test] + fn approved_package_count_is_stable() { + assert_eq!(package_specs().len(), 7); + } + + #[test] + fn forbidden_names_are_absent() { + for spec in package_specs() { + assert!(!FORBIDDEN_PACKAGE_NAMES.contains(&spec.package_name)); + } + } + + #[test] + fn replica_schema_package_uses_current_name() { + assert!( + package_specs() + .iter() + .any(|spec| spec.package_name == "@radroots/replica-db-schema-bindings") + ); + } +} diff --git a/crates/xtask/src/ts.rs b/crates/xtask/src/ts.rs @@ -0,0 +1,39 @@ +pub fn generated_header() -> &'static str { + "// @generated by cargo xtask generate ts\n// Do not edit by hand.\n" +} + +pub fn generated_types_file() -> &'static str { + "types.ts" +} + +pub fn generated_constants_file() -> &'static str { + "constants.ts" +} + +pub fn normalize_lf(value: &str) -> String { + value.replace("\r\n", "\n") +} + +#[cfg(test)] +mod tests { + use super::{generated_constants_file, generated_header, generated_types_file, normalize_lf}; + + #[test] + fn generated_header_matches_contract() { + assert_eq!( + generated_header(), + "// @generated by cargo xtask generate ts\n// Do not edit by hand.\n" + ); + } + + #[test] + fn generated_file_names_are_stable() { + assert_eq!(generated_types_file(), "types.ts"); + assert_eq!(generated_constants_file(), "constants.ts"); + } + + #[test] + fn normalizes_line_endings() { + assert_eq!(normalize_lf("a\r\nb\n"), "a\nb\n"); + } +}