use std::collections::HashMap;
use rskit_errors::{AppError, AppResult, ErrorCode};
#[derive(Debug, Clone)]
pub struct Binding<P> {
pub operation_id: String,
pub provider: P,
pub tiers: Vec<String>,
pub priority: i32,
}
pub struct Registry<P> {
bindings: HashMap<String, Vec<Binding<P>>>,
}
impl<P: Clone> Default for Registry<P> {
fn default() -> Self {
Self::new()
}
}
impl<P: Clone> Registry<P> {
pub fn new() -> Self {
Self {
bindings: HashMap::new(),
}
}
pub fn bind(&mut self, binding: Binding<P>) {
self.bindings
.entry(binding.operation_id.clone())
.or_default()
.push(binding);
}
pub fn resolve(&self, operation_id: &str, tier: &str) -> AppResult<&P> {
let bindings = self.bindings.get(operation_id).ok_or_else(|| {
AppError::new(
ErrorCode::NotFound,
format!("no bindings registered for operation '{operation_id}'"),
)
})?;
bindings
.iter()
.filter(|b| b.tiers.is_empty() || b.tiers.iter().any(|t| t == tier))
.min_by_key(|b| b.priority)
.map(|b| &b.provider)
.ok_or_else(|| {
AppError::new(
ErrorCode::NotFound,
format!(
"no provider for operation '{operation_id}' accessible to tier '{tier}'"
),
)
})
}
pub fn list_bindings(&self, operation_id: &str) -> &[Binding<P>] {
self.bindings
.get(operation_id)
.map(Vec::as_slice)
.unwrap_or(&[])
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolve_returns_highest_priority_match() {
let mut reg = Registry::new();
reg.bind(Binding {
operation_id: "transcode".into(),
provider: "slow-provider",
tiers: vec![],
priority: 10,
});
reg.bind(Binding {
operation_id: "transcode".into(),
provider: "fast-provider",
tiers: vec![],
priority: 1,
});
let p = reg.resolve("transcode", "free").unwrap();
assert_eq!(*p, "fast-provider");
}
#[test]
fn resolve_filters_by_tier() {
let mut reg = Registry::new();
reg.bind(Binding {
operation_id: "upscale".into(),
provider: "premium-backend",
tiers: vec!["pro".into()],
priority: 1,
});
reg.bind(Binding {
operation_id: "upscale".into(),
provider: "basic-backend",
tiers: vec![],
priority: 5,
});
let p = reg.resolve("upscale", "free").unwrap();
assert_eq!(*p, "basic-backend");
let p = reg.resolve("upscale", "pro").unwrap();
assert_eq!(*p, "premium-backend");
}
#[test]
fn resolve_unknown_operation_returns_not_found() {
let reg = Registry::<&str>::new();
let err = reg.resolve("nonexistent", "free").unwrap_err();
assert_eq!(err.code(), ErrorCode::NotFound);
}
#[test]
fn resolve_no_tier_match_returns_not_found() {
let mut reg = Registry::new();
reg.bind(Binding {
operation_id: "export".into(),
provider: "enterprise-only",
tiers: vec!["enterprise".into()],
priority: 1,
});
let err = reg.resolve("export", "free").unwrap_err();
assert_eq!(err.code(), ErrorCode::NotFound);
}
#[test]
fn list_bindings_returns_all_registered() {
let mut reg = Registry::new();
reg.bind(Binding {
operation_id: "encode".into(),
provider: "a",
tiers: vec![],
priority: 1,
});
reg.bind(Binding {
operation_id: "encode".into(),
provider: "b",
tiers: vec!["pro".into()],
priority: 2,
});
assert_eq!(reg.list_bindings("encode").len(), 2);
assert!(reg.list_bindings("unknown").is_empty());
}
#[test]
fn default_creates_empty_registry() {
let reg = Registry::<String>::default();
assert!(reg.list_bindings("any").is_empty());
}
}