wasm-capability-core 0.1.0

This pattern's own default, zero-external-technology implementation: InMemoryCapabilityRegistry (SEA core/, local ADR-001).
Documentation
//! [`InMemoryCapabilityRegistry`] — concrete, data-assembled `CapabilityRegistry`,
//! built on `svc-registry-adapter`'s real `DefaultRegistry<T: Named>` rather
//! than hand-rolled storage.

use svc_registry::{Named, Registry};
use svc_registry_adapter::DefaultRegistry;
use wasm_capability_contract::{CapabilityDescriptor, CapabilityProtocol, CapabilityRegistry};

/// Pairs a registry key with the [`CapabilityDescriptor`] registered under
/// it. `CapabilityDescriptor::import_name` alone cannot serve as the
/// [`Named`] key: a deployer-scoped instance (e.g.
/// `"grpc-egress:inventory-svc"`) shares the same `import_name`
/// (`"grpc-egress"`) as the family's default entry, so two descriptors can
/// legitimately share an `import_name` while needing distinct registry
/// keys.
struct NamedCapabilityDescriptor {
    key: String,
    descriptor: CapabilityDescriptor,
}

impl Named for NamedCapabilityDescriptor {
    fn name(&self) -> &str {
        &self.key
    }
}

/// In-memory `CapabilityRegistry`, seeded with ADR-001's six built-in
/// capabilities. A deployer registers additional named instances (e.g.
/// `grpc-egress:inventory-svc`) via [`Self::register`] without editing
/// this crate's source.
pub struct InMemoryCapabilityRegistry {
    entries: DefaultRegistry<NamedCapabilityDescriptor>,
}

impl InMemoryCapabilityRegistry {
    /// Seeds the six built-in capabilities: `http-egress`, `grpc-egress`,
    /// `llm-complete`, `mcp-egress`, `database`, `secrets` — each
    /// `import_name` equal to its own capability name.
    #[must_use]
    pub fn with_defaults() -> Self {
        let mut entries = DefaultRegistry::default();
        for (name, protocol) in [
            ("http-egress", CapabilityProtocol::Http),
            ("grpc-egress", CapabilityProtocol::Grpc),
            ("llm-complete", CapabilityProtocol::Complete),
            ("mcp-egress", CapabilityProtocol::Mcp),
            ("database", CapabilityProtocol::Database),
            ("secrets", CapabilityProtocol::Secrets),
        ] {
            entries.register(NamedCapabilityDescriptor {
                key: name.to_string(),
                descriptor: CapabilityDescriptor {
                    import_name: name,
                    protocol,
                },
            });
        }
        Self { entries }
    }

    /// Registers `descriptor` under `name`, replacing any existing entry
    /// with the same name. Fluent — chainable after [`Self::with_defaults`]
    /// without disturbing the six built-ins it seeded.
    #[must_use]
    pub fn register(mut self, name: impl Into<String>, descriptor: CapabilityDescriptor) -> Self {
        self.entries.register(NamedCapabilityDescriptor {
            key: name.into(),
            descriptor,
        });
        self
    }
}

impl CapabilityRegistry for InMemoryCapabilityRegistry {
    fn descriptor(&self, capability: &str) -> Option<&CapabilityDescriptor> {
        self.entries.get(capability).map(|entry| &entry.descriptor)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// @covers: InMemoryCapabilityRegistry::with_defaults
    /// Every one of the six built-in capabilities must resolve to exactly
    /// the `CapabilityProtocol` ADR-001 assigns it — a wrong protocol
    /// would wire a capability's host import to the wrong dispatcher
    /// family entirely.
    #[test]
    fn test_with_defaults_resolves_all_six_built_in_capabilities_to_correct_protocol() {
        let registry = InMemoryCapabilityRegistry::with_defaults();
        let expected = [
            ("http-egress", CapabilityProtocol::Http),
            ("grpc-egress", CapabilityProtocol::Grpc),
            ("llm-complete", CapabilityProtocol::Complete),
            ("mcp-egress", CapabilityProtocol::Mcp),
            ("database", CapabilityProtocol::Database),
            ("secrets", CapabilityProtocol::Secrets),
        ];
        for (name, protocol) in expected {
            let descriptor = registry
                .descriptor(name)
                .unwrap_or_else(|| panic!("expected a descriptor for '{name}'"));
            assert_eq!(descriptor.import_name, name);
            assert_eq!(descriptor.protocol, protocol);
        }
    }

    /// @covers: InMemoryCapabilityRegistry::descriptor
    /// An unregistered capability name must return `None`, not panic or
    /// fabricate a descriptor — deny-by-default extends to lookup misses.
    #[test]
    fn test_descriptor_returns_none_for_unregistered_capability() {
        let registry = InMemoryCapabilityRegistry::with_defaults();
        assert!(registry.descriptor("not-a-real-capability").is_none());
    }

    /// @covers: InMemoryCapabilityRegistry::register
    /// Registering a new named instance must not alter or remove any of
    /// the six defaults — a deployer extending the registry must never
    /// accidentally break an existing capability.
    #[test]
    fn test_register_adds_new_entry_without_disturbing_defaults() {
        let registry = InMemoryCapabilityRegistry::with_defaults().register(
            "grpc-egress:inventory-svc",
            CapabilityDescriptor {
                import_name: "grpc-egress",
                protocol: CapabilityProtocol::Grpc,
            },
        );

        let scoped = registry
            .descriptor("grpc-egress:inventory-svc")
            .unwrap_or_else(|| panic!("expected the newly registered instance"));
        assert_eq!(scoped.import_name, "grpc-egress");

        let default = registry
            .descriptor("grpc-egress")
            .unwrap_or_else(|| panic!("expected the default 'grpc-egress' to still resolve"));
        assert_eq!(default.protocol, CapabilityProtocol::Grpc);

        for name in [
            "http-egress",
            "llm-complete",
            "mcp-egress",
            "database",
            "secrets",
        ] {
            assert!(
                registry.descriptor(name).is_some(),
                "expected default '{name}' to still resolve after register()"
            );
        }
    }

    /// @covers: InMemoryCapabilityRegistry::register
    /// Re-registering under an already-used key must replace the old
    /// descriptor, not create a duplicate or silently no-op — matching
    /// `DefaultRegistry::register`'s own documented replace-on-collision
    /// behavior.
    #[test]
    fn test_register_replaces_existing_entry_with_same_key() {
        let registry = InMemoryCapabilityRegistry::with_defaults().register(
            "http-egress",
            CapabilityDescriptor {
                import_name: "http-egress-v2",
                protocol: CapabilityProtocol::Http,
            },
        );

        let descriptor = registry
            .descriptor("http-egress")
            .unwrap_or_else(|| panic!("expected 'http-egress' to still resolve"));
        assert_eq!(descriptor.import_name, "http-egress-v2");
    }
}