use std::collections::BTreeMap;
use greentic_deploy_spec::{CapabilitySlot, PackDescriptor};
use thiserror::Error;
use super::slot::{BUILTIN_HANDLERS, EnvPackHandler};
#[derive(Debug, Error, PartialEq, Eq)]
pub enum RegistryError {
#[error("no env-pack handler registered for `{0}`")]
Unknown(String),
#[error("env-pack `{kind}` is a `{actual}` handler but was bound to the `{expected}` slot")]
SlotMismatch {
kind: String,
expected: CapabilitySlot,
actual: CapabilitySlot,
},
#[error(
"env-pack `{kind}` pins version `{requested}` but the native handler implements `{supported}`"
)]
VersionUnsupported {
kind: String,
requested: String,
supported: String,
},
#[error("an env-pack handler is already registered for path `{0}`")]
DuplicateRegistration(String),
}
#[derive(Debug, Default)]
pub struct EnvPackRegistry {
handlers: BTreeMap<String, Box<dyn EnvPackHandler>>,
}
impl EnvPackRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn with_builtins() -> Self {
let mut registry = Self::new();
for handler in BUILTIN_HANDLERS {
registry
.register(Box::new(*handler))
.expect("built-in handler paths are unique");
}
registry
}
pub fn register(&mut self, handler: Box<dyn EnvPackHandler>) -> Result<(), RegistryError> {
let path = handler.descriptor_path().to_string();
if self.handlers.contains_key(&path) {
return Err(RegistryError::DuplicateRegistration(path));
}
self.handlers.insert(path, handler);
Ok(())
}
pub fn resolve(&self, kind: &PackDescriptor) -> Result<&dyn EnvPackHandler, RegistryError> {
let handler = self
.handlers
.get(kind.path())
.map(|h| h.as_ref())
.ok_or_else(|| RegistryError::Unknown(kind.as_str().to_string()))?;
let req = handler.supported_versions();
if !req.matches(&kind.version().0) {
return Err(RegistryError::VersionUnsupported {
kind: kind.as_str().to_string(),
requested: kind.version().to_string(),
supported: req.to_string(),
});
}
Ok(handler)
}
pub fn resolve_for_slot(
&self,
expected: CapabilitySlot,
kind: &PackDescriptor,
) -> Result<&dyn EnvPackHandler, RegistryError> {
let handler = self.resolve(kind)?;
let actual = handler.slot();
if actual != expected {
return Err(RegistryError::SlotMismatch {
kind: kind.as_str().to_string(),
expected,
actual,
});
}
Ok(handler)
}
pub fn len(&self) -> usize {
self.handlers.len()
}
pub fn is_empty(&self) -> bool {
self.handlers.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::defaults::{LOCAL_DEPLOYER_PACK, LOCAL_SECRETS_PACK};
fn descriptor(raw: &str) -> PackDescriptor {
PackDescriptor::try_new(raw).expect("descriptor parses")
}
#[test]
fn with_builtins_registers_five_handlers() {
let registry = EnvPackRegistry::with_builtins();
assert_eq!(registry.len(), 5);
}
#[test]
fn resolve_built_in_descriptor() {
let registry = EnvPackRegistry::with_builtins();
let handler = registry.resolve(&descriptor(LOCAL_SECRETS_PACK)).unwrap();
assert_eq!(handler.slot(), CapabilitySlot::Secrets);
assert_eq!(handler.descriptor_path(), "greentic.secrets.dev-store");
}
#[test]
fn resolve_accepts_compatible_version() {
let registry = EnvPackRegistry::with_builtins();
let handler = registry
.resolve(&descriptor("greentic.secrets.dev-store@0.1.7"))
.unwrap();
assert_eq!(handler.slot(), CapabilitySlot::Secrets);
}
#[test]
fn resolve_rejects_unsupported_version() {
let registry = EnvPackRegistry::with_builtins();
let err = registry
.resolve(&descriptor("greentic.secrets.dev-store@9.9.9"))
.unwrap_err();
assert!(matches!(
err,
RegistryError::VersionUnsupported {
requested,
supported,
..
} if requested == "9.9.9" && supported == "^0.1.0"
));
}
#[test]
fn resolve_unknown_descriptor_errors() {
let registry = EnvPackRegistry::with_builtins();
let err = registry
.resolve(&descriptor("greentic.secrets.acme-vault@1.0.0"))
.unwrap_err();
assert!(matches!(err, RegistryError::Unknown(k) if k.contains("acme-vault")));
}
#[test]
fn resolve_for_slot_accepts_matching_slot() {
let registry = EnvPackRegistry::with_builtins();
registry
.resolve_for_slot(CapabilitySlot::Deployer, &descriptor(LOCAL_DEPLOYER_PACK))
.unwrap();
}
#[test]
fn resolve_for_slot_rejects_mismatched_slot() {
let registry = EnvPackRegistry::with_builtins();
let err = registry
.resolve_for_slot(CapabilitySlot::Secrets, &descriptor(LOCAL_DEPLOYER_PACK))
.unwrap_err();
assert!(matches!(
err,
RegistryError::SlotMismatch {
expected: CapabilitySlot::Secrets,
actual: CapabilitySlot::Deployer,
..
}
));
}
#[test]
fn register_rejects_duplicate_path() {
let mut registry = EnvPackRegistry::with_builtins();
let err = registry
.register(Box::new(super::super::slot::BUILTIN_HANDLERS[0]))
.unwrap_err();
assert!(matches!(err, RegistryError::DuplicateRegistration(_)));
}
#[test]
fn new_registry_is_empty() {
let registry = EnvPackRegistry::new();
assert!(registry.is_empty());
assert_eq!(registry.len(), 0);
}
#[derive(Debug)]
struct ExtHandler;
impl EnvPackHandler for ExtHandler {
fn slot(&self) -> CapabilitySlot {
CapabilitySlot::Extension
}
fn descriptor_path(&self) -> &str {
"acme.oauth.auth0"
}
fn supported_versions(&self) -> semver::VersionReq {
"^1.0.0".parse().unwrap()
}
}
#[test]
fn resolve_for_slot_accepts_a_registered_extension_handler() {
let mut registry = EnvPackRegistry::with_builtins();
registry.register(Box::new(ExtHandler)).unwrap();
registry
.resolve_for_slot(
CapabilitySlot::Extension,
&descriptor("acme.oauth.auth0@1.2.0"),
)
.unwrap();
let err = registry
.resolve_for_slot(
CapabilitySlot::Secrets,
&descriptor("acme.oauth.auth0@1.2.0"),
)
.unwrap_err();
assert!(matches!(
err,
RegistryError::SlotMismatch {
expected: CapabilitySlot::Secrets,
actual: CapabilitySlot::Extension,
..
}
));
}
#[test]
fn unregistered_extension_is_unknown() {
let registry = EnvPackRegistry::with_builtins();
let err = registry
.resolve_for_slot(
CapabilitySlot::Extension,
&descriptor("acme.oauth.auth0@1.0.0"),
)
.unwrap_err();
assert!(matches!(err, RegistryError::Unknown(_)));
}
}