diff --git a/Cargo.lock b/Cargo.lock index 4519285..7fb3d7d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3381,6 +3381,7 @@ dependencies = [ name = "nexum-sdk-test" version = "0.1.0" dependencies = [ + "derive_more", "nexum-sdk", "tracing", ] diff --git a/crates/nexum-runtime/src/digest.rs b/crates/nexum-runtime/src/digest.rs index 038e473..bace36c 100644 --- a/crates/nexum-runtime/src/digest.rs +++ b/crates/nexum-runtime/src/digest.rs @@ -1,6 +1,5 @@ //! Content digests for loaded component artifacts. -use std::fmt; use std::path::PathBuf; use std::str::FromStr; @@ -9,8 +8,10 @@ use thiserror::Error; const SCHEME: &str = "sha256"; -/// sha256 digest of an artifact's bytes. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +/// sha256 digest of an artifact's bytes; `Display` is the canonical +/// lowercase `sha256:` the manifest grammar parses back. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, derive_more::Display)] +#[display("{SCHEME}:{}", alloy_primitives::hex::encode(_0))] pub struct ContentDigest([u8; 32]); impl ContentDigest { @@ -56,13 +57,6 @@ impl FromStr for ContentDigest { } } -impl fmt::Display for ContentDigest { - /// Canonical lowercase `sha256:`. - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{SCHEME}:{}", alloy_primitives::hex::encode(self.0)) - } -} - #[derive(Debug, Error)] #[non_exhaustive] pub enum DigestParseError { diff --git a/crates/nexum-runtime/src/test_utils/manifest.rs b/crates/nexum-runtime/src/test_utils/manifest.rs index 0ba19ff..62d70f0 100644 --- a/crates/nexum-runtime/src/test_utils/manifest.rs +++ b/crates/nexum-runtime/src/test_utils/manifest.rs @@ -2,13 +2,15 @@ use std::path::{Path, PathBuf}; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, derive_more::From)] pub enum ManifestSource { /// No explicit path; the loader falls back to discovery beside the component. Beside, /// A path handed to the loader as-is, existing or not. + #[from] Path(PathBuf), /// Manifest text written out at boot. + #[from] Toml(String), } @@ -32,18 +34,6 @@ impl From for ManifestSource { } } -impl From for ManifestSource { - fn from(toml: String) -> Self { - Self::Toml(toml) - } -} - -impl From for ManifestSource { - fn from(path: PathBuf) -> Self { - Self::Path(path) - } -} - /// Builder for positive-path manifest TOML. #[derive(Debug, Clone)] pub struct TestManifest { diff --git a/crates/nexum-sdk-test/Cargo.toml b/crates/nexum-sdk-test/Cargo.toml index 8aea33e..7a9ca9b 100644 --- a/crates/nexum-sdk-test/Cargo.toml +++ b/crates/nexum-sdk-test/Cargo.toml @@ -12,4 +12,5 @@ description = "In-memory host mocks for nexum module unit tests. Implements nexu [dependencies] nexum-sdk = { path = "../nexum-sdk" } +derive_more.workspace = true tracing.workspace = true diff --git a/crates/nexum-sdk-test/src/lib.rs b/crates/nexum-sdk-test/src/lib.rs index 7c637c7..86869eb 100644 --- a/crates/nexum-sdk-test/src/lib.rs +++ b/crates/nexum-sdk-test/src/lib.rs @@ -883,7 +883,7 @@ pub struct CapturedEvent { } /// A field value as tracing's `Visit` delivered it. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, derive_more::Display)] pub enum FieldValue { /// A `record_str` value. Str(String), @@ -898,17 +898,6 @@ pub enum FieldValue { Debug(String), } -impl fmt::Display for FieldValue { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - FieldValue::Str(v) | FieldValue::Debug(v) => f.write_str(v), - FieldValue::U64(v) => write!(f, "{v}"), - FieldValue::I64(v) => write!(f, "{v}"), - FieldValue::Bool(v) => write!(f, "{v}"), - } - } -} - impl CapturedEvent { /// The value recorded for `name`, if the event carried it. pub fn field(&self, name: &str) -> Option<&FieldValue> { diff --git a/crates/nexum-sdk/src/wit_bindgen_macro.rs b/crates/nexum-sdk/src/wit_bindgen_macro.rs index 39558de..585eb4d 100644 --- a/crates/nexum-sdk/src/wit_bindgen_macro.rs +++ b/crates/nexum-sdk/src/wit_bindgen_macro.rs @@ -7,7 +7,8 @@ //! zero-argument form emits the full six-interface set. Either way the //! wit-bindgen output for the world must already be in scope, so //! selecting a capability the world does not import is a compile error. -//! A domain SDK layers its own interfaces on the same `WitBindgenHost`. +//! A domain SDK layers its own interfaces on the same `WitBindgenHost`, +//! or binds logging alone via [`crate::bind_host_logging_via_wit_bindgen!`]. //! //! ```ignore //! wit_bindgen::generate!({ /* ... */ }); @@ -326,12 +327,25 @@ macro_rules! __bind_host_cap_via_wit_bindgen { } }; (logging) => { + $crate::bind_host_logging_via_wit_bindgen!(); + impl $crate::host::LoggingHost for WitBindgenHost { fn log(&self, level: $crate::Level, message: &str) { nexum::host::logging::log(nexum::host::logging::Level::from(level), message); } } + }; +} +/// Logging-only slice of [`bind_host_via_wit_bindgen!`]: needs only the +/// generated `nexum::host::logging` in scope, never `nexum::host::types` +/// or `WitBindgenHost`. +/// +/// The generated names `HostLogSink` and `install_tracing` are visible +/// in the caller's scope (`macro_rules!` is not hygienic for items). +#[macro_export] +macro_rules! bind_host_logging_via_wit_bindgen { + () => { /// Translate a `tracing_core::Level` into the wit-bindgen /// `logging::Level` wire enum. impl ::core::convert::From<$crate::Level> for nexum::host::logging::Level { @@ -355,7 +369,7 @@ macro_rules! __bind_host_cap_via_wit_bindgen { impl $crate::tracing::LogSink for HostLogSink { fn log(&self, level: $crate::Level, message: &str) { - ::log(&WitBindgenHost, level, message); + nexum::host::logging::log(::core::convert::From::from(level), message); } } diff --git a/crates/nexum-sdk/tests/wit_bindgen_logging.rs b/crates/nexum-sdk/tests/wit_bindgen_logging.rs new file mode 100644 index 0000000..d613f03 --- /dev/null +++ b/crates/nexum-sdk/tests/wit_bindgen_logging.rs @@ -0,0 +1,69 @@ +//! A domain-SDK-shaped consumer binds `bind_host_logging_via_wit_bindgen!` +//! against its own generated `nexum::host::logging`, with no base block +//! (no `WitBindgenHost`, no `nexum:host/types`) in scope. + +mod nexum { + pub mod host { + /// Stands in for the per-cdylib wit-bindgen `logging` output. + pub mod logging { + use std::sync::Mutex; + + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub enum Level { + Trace, + Debug, + Info, + Warn, + Error, + } + + pub static RECORDED: Mutex> = Mutex::new(Vec::new()); + + pub fn log(level: Level, message: &str) { + RECORDED.lock().unwrap().push((level, message.to_owned())); + } + } + } +} + +nexum_sdk::bind_host_logging_via_wit_bindgen!(); + +use nexum::host::logging::Level as Wire; + +/// The recorder is process-wide, so every assertion is a containment +/// check rather than an equality on the whole log. +fn recorded(line: &str) -> Option { + let recorded = nexum::host::logging::RECORDED.lock().unwrap(); + recorded + .iter() + .find(|(_, message)| message == line) + .map(|(level, _)| *level) +} + +#[test] +fn sink_forwards_to_the_bound_logging_call() { + use nexum_sdk::tracing::LogSink as _; + + HostLogSink.log(nexum_sdk::Level::INFO, "ready"); + assert_eq!(recorded("ready"), Some(Wire::Info)); +} + +#[test] +fn level_mapping_covers_the_wire_enum() { + for (level, wire) in [ + (nexum_sdk::Level::ERROR, Wire::Error), + (nexum_sdk::Level::WARN, Wire::Warn), + (nexum_sdk::Level::INFO, Wire::Info), + (nexum_sdk::Level::DEBUG, Wire::Debug), + (nexum_sdk::Level::TRACE, Wire::Trace), + ] { + assert_eq!(Wire::from(level), wire); + } +} + +#[test] +fn facade_install_routes_events_to_the_bound_logging_call() { + install_tracing(); + tracing::warn!("through the facade"); + assert_eq!(recorded("through the facade"), Some(Wire::Warn)); +}