use std::collections::HashSet;
use indexmap::IndexMap;
use super::{AppConfig, AppError};
pub type AppRegistry = Apps;
#[derive(Debug, Default)]
pub struct Apps {
app_configs: IndexMap<String, AppConfig>,
ready: bool,
}
impl Apps {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn populate<I>(&mut self, installed_apps: I) -> Result<(), AppError>
where
I: IntoIterator<Item = AppConfig>,
{
if self.ready {
return Ok(());
}
let app_configs = installed_apps.into_iter().collect::<Vec<_>>();
validate_unique_labels(&app_configs)?;
validate_unique_names(&app_configs)?;
self.app_configs.clear();
for app_config in app_configs {
self.app_configs
.insert(app_config.label.clone(), app_config);
}
for app_config in self.app_configs.values() {
app_config.ready();
}
self.ready = true;
Ok(())
}
#[must_use]
pub fn is_ready(&self) -> bool {
self.ready
}
#[must_use]
pub fn get_app_configs(&self) -> Vec<&AppConfig> {
self.app_configs.values().collect()
}
#[must_use]
pub fn get_app_config(&self, label: &str) -> Option<&AppConfig> {
self.app_configs.get(label)
}
#[must_use]
pub fn is_installed(&self, label: &str) -> bool {
self.app_configs.contains_key(label)
}
#[must_use]
pub fn get_models(&self) -> Vec<String> {
vec![]
}
pub fn get_model(&self, app_label: &str, model_name: &str) -> Result<&str, AppError> {
self.check_apps_ready()?;
let app_config = self
.get_app_config(app_label)
.ok_or_else(|| AppError::NoSuchApp(app_label.to_owned()))?;
app_config.get_model(model_name)
}
pub fn has_app_name(&self, app_name: &str) -> Result<bool, AppError> {
self.check_apps_ready()?;
Ok(self
.app_configs
.values()
.any(|config| config.name == app_name))
}
pub fn check_apps_ready(&self) -> Result<(), AppError> {
if self.ready {
Ok(())
} else {
Err(AppError::NotReady)
}
}
#[must_use]
pub fn app_count(&self) -> usize {
self.app_configs.len()
}
}
fn validate_unique_labels(app_configs: &[AppConfig]) -> Result<(), AppError> {
let mut seen_labels = HashSet::with_capacity(app_configs.len());
for app_config in app_configs {
if !seen_labels.insert(app_config.label.as_str()) {
return Err(AppError::DuplicateApp {
kind: "label",
identifier: app_config.label.clone(),
});
}
}
Ok(())
}
fn validate_unique_names(app_configs: &[AppConfig]) -> Result<(), AppError> {
let mut seen_names = HashSet::with_capacity(app_configs.len());
for app_config in app_configs {
if !seen_names.insert(app_config.name.as_str()) {
return Err(AppError::DuplicateApp {
kind: "name",
identifier: app_config.name.clone(),
});
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use std::sync::atomic::{AtomicUsize, Ordering};
use super::{AppConfig, AppError, AppRegistry, Apps};
static READY_CALLS: AtomicUsize = AtomicUsize::new(0);
fn ready_hook() {
READY_CALLS.fetch_add(1, Ordering::SeqCst);
}
fn config(name: &str) -> AppConfig {
AppConfig::new(name)
}
#[test]
fn app_registry_alias_matches_apps_type() {
let registry = AppRegistry::new();
assert_eq!(registry.app_count(), 0);
}
#[test]
fn apps_new_starts_empty_and_not_ready() {
let apps = Apps::new();
assert!(!apps.is_ready());
assert!(matches!(apps.check_apps_ready(), Err(AppError::NotReady)));
assert!(apps.get_app_configs().is_empty());
assert!(apps.get_app_config("blog").is_none());
assert!(!apps.is_installed("blog"));
assert!(apps.get_models().is_empty());
assert_eq!(apps.app_count(), 0);
}
#[test]
fn apps_populate_registers_configs_and_marks_ready() {
READY_CALLS.store(0, Ordering::SeqCst);
let mut apps = Apps::new();
apps.populate([
config("project.blog")
.with_models(["Post", "Comment"])
.with_ready(ready_hook),
config("project.shop").with_models(["Product"]),
])
.expect("unique apps should populate successfully");
assert!(apps.is_ready());
assert_eq!(READY_CALLS.load(Ordering::SeqCst), 1);
assert_eq!(
apps.get_model("blog", "post")
.expect("blog.Post should exist after populate"),
"Post"
);
let labels = apps
.get_app_configs()
.into_iter()
.map(|config| config.label.as_str())
.collect::<Vec<_>>();
assert_eq!(labels, vec!["blog", "shop"]);
assert!(apps.is_installed("blog"));
assert_eq!(apps.app_count(), 2);
}
#[test]
fn apps_get_app_config_returns_matching_label() {
let mut apps = Apps::new();
apps.populate([config("project.blog")])
.expect("blog app should populate");
let app = apps
.get_app_config("blog")
.expect("blog app should be registered by label");
assert_eq!(app.name, "project.blog");
}
#[test]
fn apps_get_app_config_returns_none_for_unknown_label() {
let mut apps = Apps::new();
apps.populate([config("project.blog")])
.expect("blog app should populate");
assert!(apps.get_app_config("missing").is_none());
}
#[test]
fn apps_get_models_returns_empty_stub() {
let mut apps = Apps::new();
apps.populate([config("project.blog")])
.expect("blog app should populate");
assert!(apps.get_models().is_empty());
}
#[test]
fn apps_populate_rejects_duplicate_labels() {
let mut apps = Apps::new();
let error = apps
.populate([config("project.blog"), config("another.blog")])
.expect_err("duplicate labels should fail");
assert!(matches!(
error,
AppError::DuplicateApp { kind, identifier }
if kind == "label" && identifier == "blog"
));
assert!(!apps.is_ready());
}
#[test]
fn apps_populate_rejects_duplicate_names() {
let mut apps = Apps::new();
let error = apps
.populate([
config("project.blog"),
config("project.blog").with_label("publishing"),
])
.expect_err("duplicate names should fail");
assert!(matches!(
error,
AppError::DuplicateApp { kind, identifier }
if kind == "name" && identifier == "project.blog"
));
}
#[test]
fn apps_has_app_name_checks_full_app_name_once_ready() {
let mut apps = Apps::new();
apps.populate([config("project.blog"), config("project.shop")])
.expect("apps should populate");
assert!(
apps.has_app_name("project.blog")
.expect("registry should be ready")
);
assert!(
!apps
.has_app_name("blog")
.expect("label should not match a full app name")
);
assert!(apps.is_installed("blog"));
assert!(!apps.is_installed("project.blog"));
assert!(
!apps
.has_app_name("project.accounts")
.expect("missing app should not be installed")
);
}
}