lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

commit 4550601711d55a03a7d8df0092bfd0a49ed27258
parent f633a2604f675592e4af24acadefc2098a887dc9
Author: triesap <tyson@radroots.org>
Date:   Fri, 20 Feb 2026 23:31:54 +0000

build: add contract manifest parser and validator

Diffstat:
Mcrates/xtask/Cargo.toml | 2++
Acrates/xtask/src/contract.rs | 173+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/xtask/src/main.rs | 28+++++++++++++++++++++++++++-
3 files changed, 202 insertions(+), 1 deletion(-)

diff --git a/crates/xtask/Cargo.toml b/crates/xtask/Cargo.toml @@ -6,3 +6,5 @@ rust-version.workspace = true license.workspace = true [dependencies] +serde = { workspace = true, features = ["derive"] } +toml = { workspace = true } diff --git a/crates/xtask/src/contract.rs b/crates/xtask/src/contract.rs @@ -0,0 +1,173 @@ +#![forbid(unsafe_code)] + +use serde::Deserialize; +use std::collections::BTreeMap; +use std::fs; +use std::path::{Path, PathBuf}; + +#[derive(Debug, Deserialize)] +pub struct ContractManifest { + pub contract: ManifestContract, + pub surface: Surface, + pub policy: Policy, +} + +#[derive(Debug, Deserialize)] +pub struct ManifestContract { + pub name: String, + pub version: String, + pub source: String, +} + +#[derive(Debug, Deserialize)] +pub struct Surface { + pub model_crates: Vec<String>, + pub algorithm_crates: Vec<String>, + pub wasm_crates: Vec<String>, +} + +#[derive(Debug, Deserialize)] +pub struct Policy { + pub exclude_internal_workspace_crates: bool, + pub require_reproducible_exports: bool, + pub require_conformance_vectors: bool, +} + +#[derive(Debug, Deserialize)] +pub struct VersionPolicy { + pub contract: VersionContract, + pub semver: SemverRules, + pub compatibility: CompatibilityRules, +} + +#[derive(Debug, Deserialize)] +pub struct VersionContract { + pub version: String, + pub stability: String, +} + +#[derive(Debug, Deserialize)] +pub struct SemverRules { + pub major_on: Vec<String>, + pub minor_on: Vec<String>, + pub patch_on: Vec<String>, +} + +#[derive(Debug, Deserialize)] +pub struct CompatibilityRules { + pub requires_conformance_pass: bool, + pub requires_export_manifest_diff: bool, + pub requires_release_notes: bool, +} + +#[derive(Debug, Deserialize)] +pub struct ExportMapping { + pub language: ExportLanguage, + pub packages: BTreeMap<String, String>, +} + +#[derive(Debug, Deserialize)] +pub struct ExportLanguage { + pub id: String, + pub repository: String, +} + +#[derive(Debug)] +pub struct ContractBundle { + pub root: PathBuf, + pub manifest: ContractManifest, + pub version: VersionPolicy, + pub exports: Vec<ExportMapping>, +} + +fn parse_toml<T: for<'de> Deserialize<'de>>(path: &Path) -> Result<T, String> { + let raw = fs::read_to_string(path).map_err(|e| format!("read {}: {e}", path.display()))?; + toml::from_str::<T>(&raw).map_err(|e| format!("parse {}: {e}", path.display())) +} + +fn contract_root(workspace_root: &Path) -> PathBuf { + workspace_root.join("contract") +} + +pub fn load_contract_bundle(workspace_root: &Path) -> Result<ContractBundle, String> { + let root = contract_root(workspace_root); + let manifest = parse_toml::<ContractManifest>(&root.join("manifest.toml"))?; + let version = parse_toml::<VersionPolicy>(&root.join("version.toml"))?; + let exports_dir = root.join("exports"); + let mut exports = Vec::new(); + let mut entries = fs::read_dir(&exports_dir) + .map_err(|e| format!("read dir {}: {e}", exports_dir.display()))? + .collect::<Result<Vec<_>, _>>() + .map_err(|e| format!("read dir entries {}: {e}", exports_dir.display()))?; + entries.sort_by_key(|entry| entry.file_name()); + for entry in entries { + let path = entry.path(); + if path.extension().and_then(|ext| ext.to_str()) != Some("toml") { + continue; + } + exports.push(parse_toml::<ExportMapping>(&path)?); + } + Ok(ContractBundle { + root, + manifest, + version, + exports, + }) +} + +pub fn validate_contract_bundle(bundle: &ContractBundle) -> Result<(), String> { + if bundle.manifest.contract.name.trim().is_empty() { + return Err("contract name is required".to_string()); + } + if bundle.manifest.contract.version.trim().is_empty() { + return Err("contract version is required".to_string()); + } + if bundle.manifest.contract.source.trim().is_empty() { + return Err("contract source is required".to_string()); + } + if bundle.manifest.surface.model_crates.is_empty() { + return Err("contract surface.model_crates must not be empty".to_string()); + } + if bundle.exports.is_empty() { + return Err("at least one language export mapping is required".to_string()); + } + for mapping in &bundle.exports { + if mapping.language.id.trim().is_empty() { + return Err("language.id is required".to_string()); + } + if mapping.language.repository.trim().is_empty() { + return Err(format!("language.repository is required for {}", mapping.language.id)); + } + if mapping.packages.is_empty() { + return Err(format!("packages map is required for {}", mapping.language.id)); + } + } + if bundle.version.contract.version.trim().is_empty() { + return Err("version.contract.version is required".to_string()); + } + if bundle.version.contract.stability.trim().is_empty() { + return Err("version.contract.stability is required".to_string()); + } + if bundle.version.semver.major_on.is_empty() + || bundle.version.semver.minor_on.is_empty() + || bundle.version.semver.patch_on.is_empty() + { + return Err("version.semver rules must all be non-empty".to_string()); + } + if !bundle.version.compatibility.requires_conformance_pass { + return Err("compatibility.requires_conformance_pass must be true".to_string()); + } + if !bundle.version.compatibility.requires_export_manifest_diff { + return Err("compatibility.requires_export_manifest_diff must be true".to_string()); + } + if !bundle.version.compatibility.requires_release_notes { + return Err("compatibility.requires_release_notes must be true".to_string()); + } + if !bundle.manifest.policy.exclude_internal_workspace_crates + || !bundle.manifest.policy.require_reproducible_exports + || !bundle.manifest.policy.require_conformance_vectors + { + return Err("contract policy flags must all be true".to_string()); + } + Ok(()) +} diff --git a/crates/xtask/src/main.rs b/crates/xtask/src/main.rs @@ -1,6 +1,9 @@ #![forbid(unsafe_code)] +mod contract; + use std::env; +use std::path::{Path, PathBuf}; use std::process::ExitCode; fn usage() { @@ -9,10 +12,33 @@ fn usage() { eprintln!(" cargo xtask sdk validate"); } +fn workspace_root() -> Result<PathBuf, String> { + let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); + let Some(crates_dir) = manifest_dir.parent() else { + return Err("failed to resolve crates dir".to_string()); + }; + let Some(root) = crates_dir.parent() else { + return Err("failed to resolve workspace root".to_string()); + }; + Ok(root.to_path_buf()) +} + +fn validate_contract() -> Result<(), String> { + let root = workspace_root()?; + let bundle = contract::load_contract_bundle(&root)?; + contract::validate_contract_bundle(&bundle)?; + eprintln!( + "validated contract {} {}", + bundle.manifest.contract.name, bundle.manifest.contract.version + ); + eprintln!("contract root: {}", bundle.root.display()); + Ok(()) +} + fn run_sdk(args: &[String]) -> Result<(), String> { match args.first().map(String::as_str) { Some("export-ts") => Ok(()), - Some("validate") => Ok(()), + Some("validate") => validate_contract(), _ => Err("unknown sdk subcommand".to_string()), } }