sdk

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

commit 76053de71d5fec5076b864e2ff79a26b17fc71d1
parent ee040ce23067eeee888556086f4a65417622985b
Author: triesap <tyson@radroots.org>
Date:   Thu, 11 Jun 2026 06:17:51 -0700

feat(bindings): generate core and types packages

Diffstat:
MCargo.lock | 2++
Mcrates/core_bindings/src/lib.rs | 15+++++++++++++++
Mcrates/types_bindings/src/lib.rs | 15+++++++++++++++
Mcrates/xtask/Cargo.toml | 2++
Mcrates/xtask/src/check.rs | 13+++++++++++++
Mcrates/xtask/src/generate.rs | 26++++++++++----------------
Mcrates/xtask/src/main.rs | 1+
Acrates/xtask/src/output.rs | 120+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/xtask/src/ts.rs | 22+++++++++++++++++++++-
Apackages/core-bindings/src/generated/sdk-manifest.json | 6++++++
Apackages/core-bindings/src/generated/types.ts | 25+++++++++++++++++++++++++
Mpackages/core-bindings/src/index.ts | 2+-
Apackages/types-bindings/src/generated/sdk-manifest.json | 6++++++
Apackages/types-bindings/src/generated/types.ts | 9+++++++++
Mpackages/types-bindings/src/index.ts | 2+-
15 files changed, 247 insertions(+), 19 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -680,6 +680,8 @@ dependencies = [ name = "radroots_sdk_xtask" version = "0.1.0" dependencies = [ + "radroots_core_bindings", + "radroots_types_bindings", "serde_json", ] diff --git a/crates/core_bindings/src/lib.rs b/crates/core_bindings/src/lib.rs @@ -1 +1,16 @@ pub use radroots_core as upstream; + +pub const TYPES_TS: &str = + include_str!("../../../testdata/baseline/current-radroots-generated/core/types.ts"); + +#[cfg(test)] +mod tests { + use super::TYPES_TS; + + #[test] + fn preserves_core_type_exports() { + assert!(TYPES_TS.contains("export type RadrootsCoreMoney")); + assert!(TYPES_TS.contains("export type RadrootsCoreQuantityPrice")); + assert!(TYPES_TS.contains("\"each\"")); + } +} diff --git a/crates/types_bindings/src/lib.rs b/crates/types_bindings/src/lib.rs @@ -1 +1,16 @@ pub use radroots_types as upstream; + +pub const TYPES_TS: &str = + include_str!("../../../testdata/baseline/current-radroots-generated/types/types.ts"); + +#[cfg(test)] +mod tests { + use super::TYPES_TS; + + #[test] + fn preserves_result_wrapper_exports() { + assert!(TYPES_TS.contains("export type IError")); + assert!(TYPES_TS.contains("export type IResultList")); + assert!(TYPES_TS.contains("export type IResultPass")); + } +} diff --git a/crates/xtask/Cargo.toml b/crates/xtask/Cargo.toml @@ -13,4 +13,6 @@ name = "radroots_sdk_xtask" path = "src/main.rs" [dependencies] +radroots_core_bindings = { path = "../core_bindings" } +radroots_types_bindings = { path = "../types_bindings" } serde_json = "1" diff --git a/crates/xtask/src/check.rs b/crates/xtask/src/check.rs @@ -2,6 +2,7 @@ use std::{fs, path::Path}; use crate::{ fs::workspace_root, + output::package_outputs, package_matrix::{FORBIDDEN_PACKAGE_NAMES, package_specs, validate_package_matrix}, }; @@ -19,6 +20,18 @@ pub fn check() -> Result<(), String> { return Err(format!("missing package index: {}", index_path.display())); } } + for output in package_outputs() { + for expected in output.files() { + let path = root + .join(output.spec.package_dir) + .join(expected.relative_path); + let actual = fs::read_to_string(&path) + .map_err(|error| format!("failed to read {}: {error}", path.display()))?; + if actual != expected.contents { + return Err(format!("stale generated output: {}", path.display())); + } + } + } Ok(()) } diff --git a/crates/xtask/src/generate.rs b/crates/xtask/src/generate.rs @@ -1,22 +1,16 @@ -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}, -}; +use crate::{fs::workspace_root, output::package_outputs, package_matrix::validate_package_matrix}; 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() - ); + let root = workspace_root()?; + for output in package_outputs() { + for generated_file in output.files() { + let path = root + .join(output.spec.package_dir) + .join(generated_file.relative_path); + crate::fs::write_if_changed(&path, &generated_file.contents)?; + } + println!("generated TypeScript package {}", output.spec.package_name); } Ok(()) } diff --git a/crates/xtask/src/main.rs b/crates/xtask/src/main.rs @@ -2,6 +2,7 @@ mod check; mod fs; mod generate; mod manifest; +mod output; mod package_matrix; mod ts; diff --git a/crates/xtask/src/output.rs b/crates/xtask/src/output.rs @@ -0,0 +1,120 @@ +use crate::{ + manifest::manifest_file_name, + manifest::package_manifest, + package_matrix::{PackageSpec, package_specs}, + ts::{ + generated_constants_file, generated_header, generated_types_file, + strip_legacy_generated_header, + }, +}; + +pub struct PackageOutput { + pub spec: PackageSpec, + pub types_ts: Option<&'static str>, + pub constants_ts: Option<&'static str>, +} + +pub struct GeneratedFile { + pub relative_path: String, + pub contents: String, +} + +impl PackageOutput { + pub fn files(&self) -> Vec<GeneratedFile> { + let mut files = Vec::new(); + if let Some(types_ts) = self.types_ts { + files.push(GeneratedFile { + relative_path: format!("src/generated/{}", generated_types_file()), + contents: render_ts(types_ts), + }); + } + if let Some(constants_ts) = self.constants_ts { + files.push(GeneratedFile { + relative_path: format!("src/generated/{}", generated_constants_file()), + contents: render_ts(constants_ts), + }); + } + files.push(GeneratedFile { + relative_path: format!("src/generated/{}", manifest_file_name()), + contents: render_manifest(self.spec), + }); + files.push(GeneratedFile { + relative_path: "src/index.ts".to_owned(), + contents: render_index(self), + }); + files + } +} + +pub fn package_outputs() -> Vec<PackageOutput> { + vec![ + PackageOutput { + spec: spec_by_key("core"), + types_ts: Some(radroots_core_bindings::TYPES_TS), + constants_ts: None, + }, + PackageOutput { + spec: spec_by_key("types"), + types_ts: Some(radroots_types_bindings::TYPES_TS), + constants_ts: None, + }, + ] +} + +fn spec_by_key(key: &str) -> PackageSpec { + package_specs() + .iter() + .copied() + .find(|spec| spec.key == key) + .unwrap_or_else(|| panic!("missing package spec for {key}")) +} + +fn render_ts(source: &str) -> String { + let body = strip_legacy_generated_header(source); + format!("{}{}", generated_header(), body.trim_start()) +} + +fn render_manifest(spec: PackageSpec) -> String { + let mut value = package_manifest(spec); + value["generated"] = serde_json::Value::Bool(true); + format!( + "{}\n", + serde_json::to_string_pretty(&value).expect("manifest json serializes") + ) +} + +fn render_index(output: &PackageOutput) -> String { + let mut lines = Vec::new(); + if output.types_ts.is_some() { + lines.push("export * from \"./generated/types.js\";"); + } + if output.constants_ts.is_some() { + lines.push("export * from \"./generated/constants.js\";"); + } + if lines.is_empty() { + lines.push("export {};"); + } + format!("{}\n", lines.join("\n")) +} + +#[cfg(test)] +mod tests { + use super::{package_outputs, render_ts}; + + #[test] + fn renders_sdk_header() { + let output = render_ts("// legacy\n\nexport type A = string;\n"); + assert!(output.starts_with("// @generated by cargo xtask generate ts")); + assert!(output.contains("export type A = string;")); + } + + #[test] + fn includes_core_and_types_outputs() { + let package_names = package_outputs() + .into_iter() + .map(|output| output.spec.package_name) + .collect::<Vec<_>>(); + assert!(package_names.contains(&"@radroots/core-bindings")); + assert!(package_names.contains(&"@radroots/types-bindings")); + } +} diff --git a/crates/xtask/src/ts.rs b/crates/xtask/src/ts.rs @@ -14,9 +14,20 @@ pub fn normalize_lf(value: &str) -> String { value.replace("\r\n", "\n") } +pub fn strip_legacy_generated_header(value: &str) -> String { + let normalized = normalize_lf(value); + normalized + .strip_prefix("// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.\n\n") + .unwrap_or(&normalized) + .to_owned() +} + #[cfg(test)] mod tests { - use super::{generated_constants_file, generated_header, generated_types_file, normalize_lf}; + use super::{ + generated_constants_file, generated_header, generated_types_file, normalize_lf, + strip_legacy_generated_header, + }; #[test] fn generated_header_matches_contract() { @@ -36,4 +47,13 @@ mod tests { fn normalizes_line_endings() { assert_eq!(normalize_lf("a\r\nb\n"), "a\nb\n"); } + + #[test] + fn strips_legacy_ts_rs_header() { + let source = "// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.\n\nexport type A = string;\n"; + assert_eq!( + strip_legacy_generated_header(source), + "export type A = string;\n" + ); + } } diff --git a/packages/core-bindings/src/generated/sdk-manifest.json b/packages/core-bindings/src/generated/sdk-manifest.json @@ -0,0 +1,6 @@ +{ + "crate": "radroots_core_bindings", + "generated": true, + "generator": "radroots_sdk_xtask", + "package": "@radroots/core-bindings" +} diff --git a/packages/core-bindings/src/generated/types.ts b/packages/core-bindings/src/generated/types.ts @@ -0,0 +1,25 @@ +// @generated by cargo xtask generate ts +// Do not edit by hand. +export type RadrootsCoreCurrency = string; + +export type RadrootsCoreDecimal = string; + +export type RadrootsCoreDiscount = { scope: RadrootsCoreDiscountScope, threshold: RadrootsCoreDiscountThreshold, value: RadrootsCoreDiscountValue, }; + +export type RadrootsCoreDiscountScope = "bin" | "order_total"; + +export type RadrootsCoreDiscountThreshold = { "kind": "bin_count", "amount": { bin_id: string, min: number, } } | { "kind": "order_quantity", "amount": { min: RadrootsCoreQuantity, } }; + +export type RadrootsCoreDiscountValue = { "kind": "money_per_bin", "amount": RadrootsCoreMoney } | { "kind": "percent", "amount": RadrootsCorePercent }; + +export type RadrootsCoreMoney = { amount: string, currency: string, }; + +export type RadrootsCorePercent = { value: string, }; + +export type RadrootsCoreQuantity = { amount: string, unit: RadrootsCoreUnit, label: string | null, }; + +export type RadrootsCoreQuantityPrice = { amount: RadrootsCoreMoney, quantity: RadrootsCoreQuantity, }; + +export type RadrootsCoreUnit = "each" | "kg" | "g" | "oz" | "lb" | "l" | "ml"; + +export type RadrootsCoreUnitDimension = "count" | "mass" | "volume"; diff --git a/packages/core-bindings/src/index.ts b/packages/core-bindings/src/index.ts @@ -1 +1 @@ -export {}; +export * from "./generated/types.js"; diff --git a/packages/types-bindings/src/generated/sdk-manifest.json b/packages/types-bindings/src/generated/sdk-manifest.json @@ -0,0 +1,6 @@ +{ + "crate": "radroots_types_bindings", + "generated": true, + "generator": "radroots_sdk_xtask", + "package": "@radroots/types-bindings" +} diff --git a/packages/types-bindings/src/generated/types.ts b/packages/types-bindings/src/generated/types.ts @@ -0,0 +1,9 @@ +// @generated by cargo xtask generate ts +// Do not edit by hand. +export type IError<T> = { err: T, }; + +export type IResult<T> = { result: T, }; + +export type IResultList<T> = { results: Array<T>, }; + +export type IResultPass = { pass: boolean, }; diff --git a/packages/types-bindings/src/index.ts b/packages/types-bindings/src/index.ts @@ -1 +1 @@ -export {}; +export * from "./generated/types.js";