use std::collections::BTreeMap;
use std::fmt;
use oxide_batch::{DefinitionIdentity, JobName};
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum CatalogError {
AnonymousDefinition,
DuplicateJob {
job_name: JobName,
},
}
impl fmt::Display for CatalogError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::AnonymousDefinition => {
formatter.write_str("the definition identity carries no job name")
}
Self::DuplicateJob { job_name } => {
write!(formatter, "job {job_name} is already registered")
}
}
}
}
impl std::error::Error for CatalogError {}
#[derive(Clone, Debug, Default)]
pub struct DefinitionCatalog {
entries: BTreeMap<JobName, DefinitionIdentity>,
}
impl DefinitionCatalog {
#[must_use]
pub fn new() -> Self {
Self {
entries: BTreeMap::new(),
}
}
pub fn register(&mut self, identity: DefinitionIdentity) -> Result<(), CatalogError> {
let job_name = identity
.job_name()
.cloned()
.ok_or(CatalogError::AnonymousDefinition)?;
if self.entries.contains_key(&job_name) {
return Err(CatalogError::DuplicateJob { job_name });
}
self.entries.insert(job_name, identity);
Ok(())
}
pub fn with(mut self, identity: DefinitionIdentity) -> Result<Self, CatalogError> {
self.register(identity)?;
Ok(self)
}
#[must_use]
pub fn get(&self, job_name: &JobName) -> Option<&DefinitionIdentity> {
self.entries.get(job_name)
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn job_names(&self) -> impl ExactSizeIterator<Item = &JobName> {
self.entries.keys()
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::panic)]
use oxide_batch::{
ComponentRevision, DefinitionIdentity, DefinitionRevision, JobName, StepName,
};
use super::{CatalogError, DefinitionCatalog};
fn identity(job: &str) -> DefinitionIdentity {
let job_name = JobName::new(job).expect("the job name is valid");
let step_name = StepName::new("only").expect("the step name is valid");
let revision = DefinitionRevision::new("r1").expect("the revision is valid");
let component = ComponentRevision::new("c1").expect("the component revision is valid");
DefinitionIdentity::tasklet(&job_name, &step_name, revision, &component)
.expect("the manifest encodes")
}
#[test]
fn an_empty_catalog_registers_nothing() {
let catalog = DefinitionCatalog::new();
assert!(catalog.is_empty());
assert_eq!(catalog.len(), 0);
}
#[test]
fn a_registered_job_resolves_by_name() {
let catalog = DefinitionCatalog::new()
.with(identity("orders"))
.expect("the registration succeeds");
let job_name = JobName::new("orders").expect("the job name is valid");
assert!(catalog.get(&job_name).is_some());
}
#[test]
fn a_duplicate_job_name_is_rejected() {
let mut catalog = DefinitionCatalog::new();
catalog
.register(identity("orders"))
.expect("the first registration succeeds");
let error = catalog
.register(identity("orders"))
.expect_err("the second registration is a duplicate");
assert!(matches!(error, CatalogError::DuplicateJob { .. }));
}
}