oxide_batch_cli/
catalog.rs1use std::collections::BTreeMap;
21use std::fmt;
22
23use oxide_batch::{DefinitionIdentity, JobName};
24
25#[derive(Clone, Debug, Eq, PartialEq)]
27#[non_exhaustive]
28pub enum CatalogError {
29 AnonymousDefinition,
31 DuplicateJob {
33 job_name: JobName,
35 },
36}
37
38impl fmt::Display for CatalogError {
39 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
40 match self {
41 Self::AnonymousDefinition => {
42 formatter.write_str("the definition identity carries no job name")
43 }
44 Self::DuplicateJob { job_name } => {
45 write!(formatter, "job {job_name} is already registered")
46 }
47 }
48 }
49}
50
51impl std::error::Error for CatalogError {}
52
53#[derive(Clone, Debug, Default)]
55pub struct DefinitionCatalog {
56 entries: BTreeMap<JobName, DefinitionIdentity>,
57}
58
59impl DefinitionCatalog {
60 #[must_use]
65 pub fn new() -> Self {
66 Self {
67 entries: BTreeMap::new(),
68 }
69 }
70
71 pub fn register(&mut self, identity: DefinitionIdentity) -> Result<(), CatalogError> {
79 let job_name = identity
80 .job_name()
81 .cloned()
82 .ok_or(CatalogError::AnonymousDefinition)?;
83 if self.entries.contains_key(&job_name) {
84 return Err(CatalogError::DuplicateJob { job_name });
85 }
86 self.entries.insert(job_name, identity);
87 Ok(())
88 }
89
90 pub fn with(mut self, identity: DefinitionIdentity) -> Result<Self, CatalogError> {
96 self.register(identity)?;
97 Ok(self)
98 }
99
100 #[must_use]
102 pub fn get(&self, job_name: &JobName) -> Option<&DefinitionIdentity> {
103 self.entries.get(job_name)
104 }
105
106 #[must_use]
108 pub fn is_empty(&self) -> bool {
109 self.entries.is_empty()
110 }
111
112 #[must_use]
114 pub fn len(&self) -> usize {
115 self.entries.len()
116 }
117
118 #[must_use]
120 pub fn job_names(&self) -> impl ExactSizeIterator<Item = &JobName> {
121 self.entries.keys()
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 #![allow(clippy::expect_used, clippy::panic)]
128
129 use oxide_batch::{
130 ComponentRevision, DefinitionIdentity, DefinitionRevision, JobName, StepName,
131 };
132
133 use super::{CatalogError, DefinitionCatalog};
134
135 fn identity(job: &str) -> DefinitionIdentity {
136 let job_name = JobName::new(job).expect("the job name is valid");
137 let step_name = StepName::new("only").expect("the step name is valid");
138 let revision = DefinitionRevision::new("r1").expect("the revision is valid");
139 let component = ComponentRevision::new("c1").expect("the component revision is valid");
140 DefinitionIdentity::tasklet(&job_name, &step_name, revision, &component)
141 .expect("the manifest encodes")
142 }
143
144 #[test]
145 fn an_empty_catalog_registers_nothing() {
146 let catalog = DefinitionCatalog::new();
147 assert!(catalog.is_empty());
148 assert_eq!(catalog.len(), 0);
149 }
150
151 #[test]
152 fn a_registered_job_resolves_by_name() {
153 let catalog = DefinitionCatalog::new()
154 .with(identity("orders"))
155 .expect("the registration succeeds");
156 let job_name = JobName::new("orders").expect("the job name is valid");
157 assert!(catalog.get(&job_name).is_some());
158 }
159
160 #[test]
161 fn a_duplicate_job_name_is_rejected() {
162 let mut catalog = DefinitionCatalog::new();
163 catalog
164 .register(identity("orders"))
165 .expect("the first registration succeeds");
166 let error = catalog
167 .register(identity("orders"))
168 .expect_err("the second registration is a duplicate");
169 assert!(matches!(error, CatalogError::DuplicateJob { .. }));
170 }
171}