From 9febf99dc4c266043a136a60059e391182604344 Mon Sep 17 00:00:00 2001 From: mfw78 Date: Wed, 22 Jul 2026 17:03:45 +0000 Subject: [PATCH] world: enums over const strings for the vocabularies `caps` becomes `Cap` with a hand-written `const fn as_str`, so the `CORE` table, `core_iface_count` and `CORE_IFACES` still evaluate in const context; `Capability::name` is now typed. `fault_labels` becomes `FaultLabel` with `IntoStaticStr` for the label and `VariantNames` in place of the hand-maintained `ALL` array. `EnumString` gives both a fail-closed parse; new tests pin the hand-written accessor to the derived vocabulary. Closes #547 --- Cargo.lock | 1 + crates/nexum-runtime/src/host/error.rs | 19 +-- .../src/manifest/capabilities.rs | 8 +- crates/nexum-sdk/src/host.rs | 19 ++- crates/nexum-world/Cargo.toml | 3 + crates/nexum-world/src/lib.rs | 144 +++++++++++------- crates/videre-macros/src/world.rs | 2 +- 7 files changed, 123 insertions(+), 73 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index adc5b212..b5d973d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3700,6 +3700,7 @@ dependencies = [ name = "nexum-world" version = "0.1.0" dependencies = [ + "strum", "syn 2.0.118", "tempfile", "toml 1.1.2+spec-1.1.0", diff --git a/crates/nexum-runtime/src/host/error.rs b/crates/nexum-runtime/src/host/error.rs index 65fbe9d7..e31f803c 100644 --- a/crates/nexum-runtime/src/host/error.rs +++ b/crates/nexum-runtime/src/host/error.rs @@ -16,19 +16,20 @@ pub(crate) fn chain_denied(detail: impl Into) -> ChainError { /// Stable snake_case label for a [`Fault`], used as a metric label and /// structured-log `kind` field. Emitted from the single-source -/// `nexum_world::fault_labels` vocabulary the SDK `HostFault::label` +/// [`nexum_world::FaultLabel`] vocabulary the SDK `HostFault::label` /// mirrors. pub fn fault_label(fault: &Fault) -> &'static str { - use nexum_world::fault_labels as labels; + use nexum_world::FaultLabel as Label; match fault { - Fault::Unsupported(_) => labels::UNSUPPORTED, - Fault::Unavailable(_) => labels::UNAVAILABLE, - Fault::Denied(_) => labels::DENIED, - Fault::RateLimited(_) => labels::RATE_LIMITED, - Fault::Timeout => labels::TIMEOUT, - Fault::InvalidInput(_) => labels::INVALID_INPUT, - Fault::Internal(_) => labels::INTERNAL, + Fault::Unsupported(_) => Label::Unsupported, + Fault::Unavailable(_) => Label::Unavailable, + Fault::Denied(_) => Label::Denied, + Fault::RateLimited(_) => Label::RateLimited, + Fault::Timeout => Label::Timeout, + Fault::InvalidInput(_) => Label::InvalidInput, + Fault::Internal(_) => Label::Internal, } + .into() } /// Human-readable detail carried by a [`Fault`], for the log `message` diff --git a/crates/nexum-runtime/src/manifest/capabilities.rs b/crates/nexum-runtime/src/manifest/capabilities.rs index 3492634f..12b3a53a 100644 --- a/crates/nexum-runtime/src/manifest/capabilities.rs +++ b/crates/nexum-runtime/src/manifest/capabilities.rs @@ -44,8 +44,10 @@ pub const CORE_NAMESPACE: NamespaceCaps = NamespaceCaps { /// moves bytes to and from its counterparty and nothing else. `http` is /// not listed here for the same reason it is not in the core set: it /// gates `wasi:http/*` and is handled by the registry directly. -pub const PROVIDER_CAPABILITIES: &[&str] = - &[nexum_world::caps::CHAIN, nexum_world::caps::MESSAGING]; +pub const PROVIDER_CAPABILITIES: &[&str] = &[ + nexum_world::Cap::Chain.as_str(), + nexum_world::Cap::Messaging.as_str(), +]; /// The provider namespace: the same `nexum:host/` prefix as core but only /// the scoped-transport interfaces. Validating a provider manifest against @@ -63,7 +65,7 @@ const WASI_HTTP_PREFIX: &str = "wasi:http/"; /// Capability name a module declares to import any `wasi:http/*` /// interface; the per-module `[capabilities.http].allow` list scopes it. -const HTTP_CAPABILITY: &str = nexum_world::caps::HTTP; +const HTTP_CAPABILITY: &str = nexum_world::Cap::Http.as_str(); /// Gated WASI capability names. Declaring one grants the matching `wasi:` /// interface group; see [`classify_wasi`]. `wasi:io`, `wasi:clocks`, diff --git a/crates/nexum-sdk/src/host.rs b/crates/nexum-sdk/src/host.rs index 9fc0cb68..254c8787 100644 --- a/crates/nexum-sdk/src/host.rs +++ b/crates/nexum-sdk/src/host.rs @@ -509,18 +509,21 @@ mod tests { #[test] fn fault_labels_match_the_single_source_vocabulary() { - use nexum_world::fault_labels as labels; + use nexum_world::FaultLabel as Label; let cases: [(Fault, &str); 7] = [ - (Fault::Unsupported(String::new()), labels::UNSUPPORTED), - (Fault::Unavailable(String::new()), labels::UNAVAILABLE), - (Fault::Denied(String::new()), labels::DENIED), + (Fault::Unsupported(String::new()), Label::Unsupported.into()), + (Fault::Unavailable(String::new()), Label::Unavailable.into()), + (Fault::Denied(String::new()), Label::Denied.into()), ( Fault::RateLimited(RateLimit::default()), - labels::RATE_LIMITED, + Label::RateLimited.into(), ), - (Fault::Timeout, labels::TIMEOUT), - (Fault::InvalidInput(String::new()), labels::INVALID_INPUT), - (Fault::Internal(String::new()), labels::INTERNAL), + (Fault::Timeout, Label::Timeout.into()), + ( + Fault::InvalidInput(String::new()), + Label::InvalidInput.into(), + ), + (Fault::Internal(String::new()), Label::Internal.into()), ]; for (fault, label) in cases { assert_eq!(fault.label(), label); diff --git a/crates/nexum-world/Cargo.toml b/crates/nexum-world/Cargo.toml index 5bb38f0b..8b33d24b 100644 --- a/crates/nexum-world/Cargo.toml +++ b/crates/nexum-world/Cargo.toml @@ -15,6 +15,9 @@ workspace = true macros = ["dep:syn"] [dependencies] +# Derives the closed capability / fault-label vocabularies: `VariantNames` +# supersedes a hand-maintained list, `EnumString` parses fail-closed. +strum.workspace = true syn = { workspace = true, optional = true } toml.workspace = true diff --git a/crates/nexum-world/src/lib.rs b/crates/nexum-world/src/lib.rs index c412e5a1..9a451c70 100644 --- a/crates/nexum-world/src/lib.rs +++ b/crates/nexum-world/src/lib.rs @@ -16,59 +16,74 @@ //! so this crate carries no downstream name. use std::path::{Path, PathBuf}; +use strum::{EnumString, IntoStaticStr, VariantNames}; -/// Capability name consts: the single source the [`CORE`] table and the +/// A core capability name: the single source the [`CORE`] table and the /// runtime's capability registry emit from. -pub mod caps { +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, EnumString, VariantNames)] +#[strum(serialize_all = "kebab-case")] +#[non_exhaustive] +pub enum Cap { /// `nexum:host/chain`. - pub const CHAIN: &str = "chain"; + Chain, /// `nexum:host/identity`. - pub const IDENTITY: &str = "identity"; + Identity, /// `nexum:host/local-store`. - pub const LOCAL_STORE: &str = "local-store"; + LocalStore, /// `nexum:host/remote-store`. - pub const REMOTE_STORE: &str = "remote-store"; + RemoteStore, /// `nexum:host/messaging`. - pub const MESSAGING: &str = "messaging"; + Messaging, /// `nexum:host/logging`. - pub const LOGGING: &str = "logging"; + Logging, /// Gates `wasi:http/*`; no world import. - pub const HTTP: &str = "http"; + Http, } -/// Snake_case labels of the `nexum:host/types.fault` cases, in +impl Cap { + /// The declared name, as a manifest spells it. Hand-written rather + /// than derived: [`CORE`] and [`CORE_IFACES`] evaluate it in const + /// context, and strum's `IntoStaticStr` emits a non-const `From`. + pub const fn as_str(self) -> &'static str { + match self { + Self::Chain => "chain", + Self::Identity => "identity", + Self::LocalStore => "local-store", + Self::RemoteStore => "remote-store", + Self::Messaging => "messaging", + Self::Logging => "logging", + Self::Http => "http", + } + } +} + +/// A `nexum:host/types.fault` case as a stable snake_case label, in WIT /// declaration order: the single source every label mirror emits from. -pub mod fault_labels { +/// `IntoStaticStr` yields the label, `VARIANTS` the whole vocabulary. +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, EnumString, IntoStaticStr, VariantNames)] +#[strum(serialize_all = "snake_case")] +#[non_exhaustive] +pub enum FaultLabel { /// `fault.unsupported`. - pub const UNSUPPORTED: &str = "unsupported"; + Unsupported, /// `fault.unavailable`. - pub const UNAVAILABLE: &str = "unavailable"; + Unavailable, /// `fault.denied`. - pub const DENIED: &str = "denied"; + Denied, /// `fault.rate-limited`. - pub const RATE_LIMITED: &str = "rate_limited"; + RateLimited, /// `fault.timeout`. - pub const TIMEOUT: &str = "timeout"; + Timeout, /// `fault.invalid-input`. - pub const INVALID_INPUT: &str = "invalid_input"; + InvalidInput, /// `fault.internal`. - pub const INTERNAL: &str = "internal"; - /// All seven, in declaration order. - pub const ALL: [&str; 7] = [ - UNSUPPORTED, - UNAVAILABLE, - DENIED, - RATE_LIMITED, - TIMEOUT, - INVALID_INPUT, - INTERNAL, - ]; + Internal, } /// One manifest capability and its world wiring. pub struct Capability { /// The name declared under `[capabilities].required` / `optional`. - pub name: &'static str, + pub name: Cap, /// The WIT import the declaration turns into, or `None` for /// capabilities with no world import (`http` is granted through the /// SDK's wasi:http client and the host allowlist, not the world). @@ -86,43 +101,43 @@ pub struct Capability { /// core registry and nothing else; extension rows are the caller's. pub const CORE: &[Capability] = &[ Capability { - name: caps::CHAIN, + name: Cap::Chain, import: Some("nexum:host/chain@0.1.0"), packages: &[], adapter: Some("chain"), }, Capability { - name: caps::IDENTITY, + name: Cap::Identity, import: Some("nexum:host/identity@0.1.0"), packages: &[], adapter: Some("identity"), }, Capability { - name: caps::LOCAL_STORE, + name: Cap::LocalStore, import: Some("nexum:host/local-store@0.1.0"), packages: &[], adapter: Some("local_store"), }, Capability { - name: caps::REMOTE_STORE, + name: Cap::RemoteStore, import: Some("nexum:host/remote-store@0.1.0"), packages: &[], adapter: Some("remote_store"), }, Capability { - name: caps::MESSAGING, + name: Cap::Messaging, import: Some("nexum:host/messaging@0.1.0"), packages: &[], adapter: Some("messaging"), }, Capability { - name: caps::LOGGING, + name: Cap::Logging, import: Some("nexum:host/logging@0.1.0"), packages: &[], adapter: Some("logging"), }, Capability { - name: caps::HTTP, + name: Cap::Http, import: None, packages: &[], adapter: None, @@ -151,7 +166,7 @@ pub const CORE_IFACES: [&str; core_iface_count()] = { let mut i = 0; while i < CORE.len() { if CORE[i].import.is_some() { - out[n] = CORE[i].name; + out[n] = CORE[i].name.as_str(); n += 1; } i += 1; @@ -311,7 +326,7 @@ pub fn find_extensions_manifest(start: &Path) -> Option { /// colliding registry cannot emit a duplicate import. pub fn synthesize(declared: &[String], extensions: &[ExtensionRow]) -> Result { for (idx, ext) in extensions.iter().enumerate() { - if CORE.iter().any(|c| c.name == ext.name) + if CORE.iter().any(|c| c.name.as_str() == ext.name) || extensions[..idx].iter().any(|prior| prior.name == ext.name) { return Err(format!( @@ -324,7 +339,7 @@ pub fn synthesize(declared: &[String], extensions: &[ExtensionRow]) -> Result Result = CORE.iter().map(|c| c.name.as_str()).collect(); + assert_eq!(names, Cap::VARIANTS); + for name in Cap::VARIANTS { + assert_eq!(name.parse::().unwrap().as_str(), *name); + } } #[test] fn fault_labels_are_snake_case_and_distinct() { - for label in fault_labels::ALL { + for label in FaultLabel::VARIANTS { assert!(label.chars().all(|c| c.is_ascii_lowercase() || c == '_')); } - let mut labels = fault_labels::ALL.to_vec(); + let mut labels = FaultLabel::VARIANTS.to_vec(); labels.sort_unstable(); labels.dedup(); - assert_eq!(labels.len(), fault_labels::ALL.len()); + assert_eq!(labels.len(), FaultLabel::VARIANTS.len()); + } + + #[test] + fn fault_label_parses_back_from_its_label() { + for label in FaultLabel::VARIANTS { + let parsed: FaultLabel = label.parse().unwrap(); + assert_eq!(<&'static str>::from(parsed), *label); + } + assert!("nonesuch".parse::().is_err()); } #[test] @@ -554,13 +589,18 @@ mod tests { // `http` has no world import (SDK wasi:http client) and no // adapter; every other core row has both. for cap in CORE { - assert_eq!(cap.import.is_some(), cap.adapter.is_some(), "{}", cap.name); + assert_eq!( + cap.import.is_some(), + cap.adapter.is_some(), + "{}", + cap.name.as_str() + ); } } #[test] fn full_declaration_emits_the_six_adapters_in_core_order() { - let declared: Vec = CORE.iter().map(|c| c.name.to_string()).collect(); + let declared: Vec = CORE.iter().map(|c| c.name.as_str().to_owned()).collect(); let world = synthesize(&declared, &[]).unwrap(); assert_eq!( world.adapters, diff --git a/crates/videre-macros/src/world.rs b/crates/videre-macros/src/world.rs index 30a059ad..081a4b3f 100644 --- a/crates/videre-macros/src/world.rs +++ b/crates/videre-macros/src/world.rs @@ -45,7 +45,7 @@ pub fn synthesize_venue(declared: &[String]) -> Result { .map(str::to_owned) .into(); for cap in nexum_world::CORE { - if !declared.iter().any(|d| d == cap.name) { + if !declared.iter().any(|d| d == cap.name.as_str()) { continue; } if let Some(import) = cap.import {