use std::collections::HashMap;
use crate::error::{CoreError, CoreResult};
use crate::migration::MigrationSet;
use crate::registry::Registry;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ModuleId(&'static str);
impl ModuleId {
pub const fn new(code: &'static str) -> Self {
Self(code)
}
pub const fn as_str(&self) -> &'static str {
self.0
}
}
impl std::fmt::Display for ModuleId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Capability(&'static str);
impl Capability {
pub const fn new(name: &'static str) -> Self {
Self(name)
}
pub const fn as_str(&self) -> &'static str {
self.0
}
}
pub trait Module: Send + Sync + 'static {
fn id(&self) -> ModuleId;
fn migrations(&self) -> MigrationSet {
MigrationSet::new(self.id().as_str(), Vec::new())
}
fn requires(&self) -> &'static [ModuleId] {
&[]
}
fn requires_db_capabilities(&self) -> &'static [Capability] {
&[]
}
fn optional_db_capabilities(&self) -> &'static [Capability] {
&[]
}
fn register(&self, registry: &mut Registry) {
let _ = registry;
}
fn catalogs(&self) -> &'static [(&'static str, &'static str)] {
&[]
}
}
#[derive(Default)]
pub struct ModuleRegistry {
modules: Vec<Box<dyn Module>>,
}
impl ModuleRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn register(&mut self, module: impl Module) -> &mut Self {
self.modules.push(Box::new(module));
self
}
pub fn register_boxed(&mut self, module: Box<dyn Module>) -> &mut Self {
self.modules.push(module);
self
}
pub fn has(&self, id: ModuleId) -> bool {
self.modules.iter().any(|m| m.id() == id)
}
pub fn get(&self, id: ModuleId) -> Option<&dyn Module> {
self.modules
.iter()
.map(|m| m.as_ref())
.find(|m| m.id() == id)
}
pub fn iter(&self) -> impl Iterator<Item = &dyn Module> + '_ {
self.modules.iter().map(|m| m.as_ref())
}
pub fn len(&self) -> usize {
self.modules.len()
}
pub fn is_empty(&self) -> bool {
self.modules.is_empty()
}
pub fn ordered(&self) -> CoreResult<Vec<&dyn Module>> {
let mut by_id: HashMap<ModuleId, &dyn Module> = HashMap::with_capacity(self.modules.len());
for m in &self.modules {
if by_id.insert(m.id(), m.as_ref()).is_some() {
return Err(CoreError::DuplicateModule {
module: m.id().to_string(),
});
}
}
for m in &self.modules {
for dep in m.requires() {
if !by_id.contains_key(dep) {
return Err(CoreError::UnknownModuleDependency {
module: m.id().to_string(),
dependency: dep.to_string(),
});
}
}
}
let mut state: HashMap<ModuleId, Visit> = HashMap::with_capacity(self.modules.len());
let mut order: Vec<ModuleId> = Vec::with_capacity(self.modules.len());
for m in &self.modules {
visit(m.id(), &by_id, &mut state, &mut order)?;
}
Ok(order.into_iter().map(|id| by_id[&id]).collect())
}
pub fn ordered_migration_sets(&self) -> CoreResult<Vec<MigrationSet>> {
Ok(self
.ordered()?
.into_iter()
.map(|m| m.migrations())
.collect())
}
}
#[derive(Clone, Copy)]
enum Visit {
Active,
Done,
}
fn visit(
id: ModuleId,
by_id: &HashMap<ModuleId, &dyn Module>,
state: &mut HashMap<ModuleId, Visit>,
order: &mut Vec<ModuleId>,
) -> CoreResult<()> {
match state.get(&id) {
Some(Visit::Done) => return Ok(()),
Some(Visit::Active) => {
return Err(CoreError::ModuleDependencyCycle {
module: id.to_string(),
})
}
None => {}
}
state.insert(id, Visit::Active);
for dep in by_id[&id].requires() {
visit(*dep, by_id, state, order)?;
}
state.insert(id, Visit::Done);
order.push(id);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
struct Mod(ModuleId, &'static [ModuleId]);
impl Module for Mod {
fn id(&self) -> ModuleId {
self.0
}
fn requires(&self) -> &'static [ModuleId] {
self.1
}
}
const BLOG: ModuleId = ModuleId::new("acme.blog");
const USER: ModuleId = ModuleId::new("acme.user");
const MEDIA: ModuleId = ModuleId::new("acme.media");
fn order_of(reg: &ModuleRegistry) -> Vec<&'static str> {
reg.ordered()
.unwrap()
.into_iter()
.map(|m| m.id().as_str())
.collect()
}
#[test]
fn dependencies_come_before_dependents() {
let mut reg = ModuleRegistry::new();
reg.register(Mod(BLOG, &[USER, MEDIA]))
.register(Mod(MEDIA, &[]))
.register(Mod(USER, &[]));
let order = order_of(®);
let pos = |id: &str| order.iter().position(|x| *x == id).unwrap();
assert!(pos("acme.user") < pos("acme.blog"));
assert!(pos("acme.media") < pos("acme.blog"));
}
#[test]
fn registration_order_breaks_ties() {
let mut reg = ModuleRegistry::new();
reg.register(Mod(ModuleId::new("a"), &[]))
.register(Mod(ModuleId::new("b"), &[]))
.register(Mod(ModuleId::new("c"), &[]));
assert_eq!(order_of(®), ["a", "b", "c"]);
}
#[test]
fn has_and_get_find_by_id() {
let mut reg = ModuleRegistry::new();
reg.register(Mod(MEDIA, &[]));
assert!(reg.has(MEDIA));
assert!(!reg.has(USER));
assert_eq!(reg.get(MEDIA).unwrap().id(), MEDIA);
assert!(reg.get(ModuleId::new("missing")).is_none());
}
#[test]
fn unknown_dependency_errors() {
let mut reg = ModuleRegistry::new();
reg.register(Mod(BLOG, &[USER]));
assert!(matches!(
reg.ordered(),
Err(CoreError::UnknownModuleDependency { .. })
));
}
#[test]
fn cycle_errors() {
const A_REQS: &[ModuleId] = &[ModuleId::new("acme.b")];
const B_REQS: &[ModuleId] = &[ModuleId::new("acme.a")];
let mut reg = ModuleRegistry::new();
reg.register(Mod(ModuleId::new("acme.a"), A_REQS))
.register(Mod(ModuleId::new("acme.b"), B_REQS));
assert!(matches!(
reg.ordered(),
Err(CoreError::ModuleDependencyCycle { .. })
));
}
#[test]
fn duplicate_id_errors() {
let mut reg = ModuleRegistry::new();
reg.register(Mod(ModuleId::new("a"), &[]))
.register(Mod(ModuleId::new("a"), &[]));
assert!(matches!(
reg.ordered(),
Err(CoreError::DuplicateModule { .. })
));
}
}