Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 10 additions & 9 deletions crates/nexum-runtime/src/host/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,20 @@ pub(crate) fn chain_denied(detail: impl Into<String>) -> 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`
Expand Down
8 changes: 5 additions & 3 deletions crates/nexum-runtime/src/manifest/capabilities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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`,
Expand Down
19 changes: 11 additions & 8 deletions crates/nexum-sdk/src/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
3 changes: 3 additions & 0 deletions crates/nexum-world/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
144 changes: 92 additions & 52 deletions crates/nexum-world/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -311,7 +326,7 @@ pub fn find_extensions_manifest(start: &Path) -> Option<PathBuf> {
/// colliding registry cannot emit a duplicate import.
pub fn synthesize(declared: &[String], extensions: &[ExtensionRow]) -> Result<ModuleWorld, String> {
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!(
Expand All @@ -324,7 +339,7 @@ pub fn synthesize(declared: &[String], extensions: &[ExtensionRow]) -> Result<Mo

let known = || {
CORE.iter()
.map(|c| c.name)
.map(|c| c.name.as_str())
.chain(extensions.iter().map(|e| e.name.as_str()))
};
for name in declared {
Expand All @@ -346,7 +361,7 @@ pub fn synthesize(declared: &[String], extensions: &[ExtensionRow]) -> Result<Mo
let mut packages = vec!["nexum-host".to_owned()];
let mut adapters = Vec::new();
for cap in 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 {
Expand Down Expand Up @@ -518,26 +533,46 @@ mod tests {
assert_eq!(
CORE_IFACES,
[
caps::CHAIN,
caps::IDENTITY,
caps::LOCAL_STORE,
caps::REMOTE_STORE,
caps::MESSAGING,
caps::LOGGING,
Cap::Chain.as_str(),
Cap::Identity.as_str(),
Cap::LocalStore.as_str(),
Cap::RemoteStore.as_str(),
Cap::Messaging.as_str(),
Cap::Logging.as_str(),
],
);
assert!(!CORE_IFACES.contains(&caps::HTTP));
assert!(!CORE_IFACES.contains(&Cap::Http.as_str()));
}

/// The const accessor is hand-written, so pin it to the derived
/// vocabulary in both directions.
#[test]
fn cap_accessor_agrees_with_the_derived_vocabulary() {
let names: Vec<&str> = CORE.iter().map(|c| c.name.as_str()).collect();
assert_eq!(names, Cap::VARIANTS);
for name in Cap::VARIANTS {
assert_eq!(name.parse::<Cap>().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::<FaultLabel>().is_err());
}

#[test]
Expand All @@ -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<String> = CORE.iter().map(|c| c.name.to_string()).collect();
let declared: Vec<String> = CORE.iter().map(|c| c.name.as_str().to_owned()).collect();
let world = synthesize(&declared, &[]).unwrap();
assert_eq!(
world.adapters,
Expand Down
2 changes: 1 addition & 1 deletion crates/videre-macros/src/world.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ pub fn synthesize_venue(declared: &[String]) -> Result<ModuleWorld, String> {
.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 {
Expand Down
Loading