#![allow(clippy::allow_attributes)]
use wasmtime::component::Instance;
pub mod v0_2_0 {
wasmtime::component::bindgen!({
path: "wit-vendor/extension-provider/v0_2_0",
world: "greentic:extension-provider/full-provider",
});
}
pub mod v0_1_0 {
wasmtime::component::bindgen!({
path: "wit-vendor/extension-provider/v0_1_0",
world: "greentic:extension-provider/full-provider",
});
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProviderWorld {
V0_2_0,
V0_1_0,
}
impl ProviderWorld {
pub const fn version(self) -> &'static str {
match self {
ProviderWorld::V0_2_0 => "0.2.0",
ProviderWorld::V0_1_0 => "0.1.0",
}
}
}
pub fn probe_provider_world(
store: &mut wasmtime::Store<impl Send>,
instance: &Instance,
) -> Option<ProviderWorld> {
for world in [ProviderWorld::V0_2_0, ProviderWorld::V0_1_0] {
let iface = format!("greentic:extension-provider/messaging@{}", world.version());
if instance
.get_export_index(&mut *store, None, &iface)
.is_some()
{
return Some(world);
}
}
None
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ProviderInvokeError {
#[error("invalid input: {0}")]
InvalidInput(String),
#[error("missing capability: {0}")]
MissingCapability(String),
#[error("permission denied: {0}")]
PermissionDenied(String),
#[error("not found: {0}")]
NotFound(String),
#[error("schema invalid: {0}")]
SchemaInvalid(String),
#[error("internal: {0}")]
Internal(String),
}
impl ProviderInvokeError {
pub const fn code(&self) -> &'static str {
match self {
ProviderInvokeError::InvalidInput(_) => "invalid-input",
ProviderInvokeError::MissingCapability(_) => "missing-capability",
ProviderInvokeError::PermissionDenied(_) => "permission-denied",
ProviderInvokeError::NotFound(_) => "not-found",
ProviderInvokeError::SchemaInvalid(_) => "schema-invalid",
ProviderInvokeError::Internal(_) => "internal",
}
}
}
pub fn map_v2(err: v0_2_0::greentic::extension_base::types::ExtensionError) -> ProviderInvokeError {
use v0_2_0::greentic::extension_base::types::ExtensionError as E;
match err {
E::InvalidInput(s) => ProviderInvokeError::InvalidInput(s),
E::MissingCapability(s) => ProviderInvokeError::MissingCapability(s),
E::PermissionDenied(s) => ProviderInvokeError::PermissionDenied(s),
E::NotFound(s) => ProviderInvokeError::NotFound(s),
E::SchemaInvalid(s) => ProviderInvokeError::SchemaInvalid(s),
E::Internal(s) => ProviderInvokeError::Internal(s),
}
}
pub fn map_v1(err: v0_1_0::greentic::extension_provider::types::Error) -> ProviderInvokeError {
use v0_1_0::greentic::extension_provider::types::Error as E;
match err {
E::NotFound(s) => ProviderInvokeError::NotFound(s),
E::SchemaInvalid(s) => ProviderInvokeError::SchemaInvalid(s),
E::Internal(s) => ProviderInvokeError::Internal(s),
}
}
pub fn map_legacy(message: impl Into<String>) -> ProviderInvokeError {
ProviderInvokeError::Internal(message.into())
}
pub fn into_anyhow(err: ProviderInvokeError) -> anyhow::Error {
let code = err.code();
anyhow::Error::new(err).context(format!("provider error [{code}]"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn worlds_bind_to_distinct_types() {
let v2 = std::any::type_name::<v0_2_0::FullProviderPre<()>>();
let v1 = std::any::type_name::<v0_1_0::FullProviderPre<()>>();
assert_ne!(v2, v1);
}
#[test]
fn provider_world_version_strings() {
assert_eq!(ProviderWorld::V0_2_0.version(), "0.2.0");
assert_eq!(ProviderWorld::V0_1_0.version(), "0.1.0");
}
#[test]
fn v2_error_maps_all_six_variants_losslessly() {
use v0_2_0::greentic::extension_base::types::ExtensionError as E;
let cases = [
(E::InvalidInput("a".into()), "invalid-input"),
(E::MissingCapability("b".into()), "missing-capability"),
(E::PermissionDenied("c".into()), "permission-denied"),
(E::NotFound("d".into()), "not-found"),
(E::SchemaInvalid("e".into()), "schema-invalid"),
(E::Internal("f".into()), "internal"),
];
for (wit, expected_code) in cases {
let mapped = map_v2(wit);
assert_eq!(mapped.code(), expected_code);
}
}
#[test]
fn v2_error_preserves_message() {
use v0_2_0::greentic::extension_base::types::ExtensionError as E;
let mapped = map_v2(E::InvalidInput("bad payload".into()));
assert_eq!(
mapped,
ProviderInvokeError::InvalidInput("bad payload".into())
);
assert_eq!(mapped.to_string(), "invalid input: bad payload");
}
#[test]
fn v1_error_maps_three_variants_losslessly() {
use v0_1_0::greentic::extension_provider::types::Error as E;
let cases = [
(E::NotFound("x".into()), "not-found"),
(E::SchemaInvalid("y".into()), "schema-invalid"),
(E::Internal("z".into()), "internal"),
];
for (wit, expected_code) in cases {
let mapped = map_v1(wit);
assert_eq!(mapped.code(), expected_code);
}
}
#[test]
fn v1_error_preserves_message() {
use v0_1_0::greentic::extension_provider::types::Error as E;
let mapped = map_v1(E::NotFound("no such channel".into()));
assert_eq!(
mapped,
ProviderInvokeError::NotFound("no such channel".into())
);
}
#[test]
fn legacy_maps_to_internal() {
let mapped = map_legacy("boom");
assert_eq!(mapped, ProviderInvokeError::Internal("boom".into()));
assert_eq!(mapped.code(), "internal");
}
#[test]
fn all_codes_are_canonical_kebab() {
let all = [
ProviderInvokeError::InvalidInput(String::new()),
ProviderInvokeError::MissingCapability(String::new()),
ProviderInvokeError::PermissionDenied(String::new()),
ProviderInvokeError::NotFound(String::new()),
ProviderInvokeError::SchemaInvalid(String::new()),
ProviderInvokeError::Internal(String::new()),
];
let codes: Vec<_> = all.iter().map(|e| e.code()).collect();
assert_eq!(
codes,
vec![
"invalid-input",
"missing-capability",
"permission-denied",
"not-found",
"schema-invalid",
"internal",
]
);
}
#[test]
fn into_anyhow_preserves_downcast_and_code_in_display() {
let typed = ProviderInvokeError::PermissionDenied("nope".into());
let err = into_anyhow(typed.clone());
assert!(err.to_string().contains("permission-denied"));
let recovered = err
.downcast_ref::<ProviderInvokeError>()
.expect("typed error preserved in anyhow chain");
assert_eq!(recovered, &typed);
assert_eq!(recovered.code(), "permission-denied");
}
}