use std::collections::BTreeSet;
use std::fmt;
use std::str::FromStr;
use jammi_db::config::ServiceSelection;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ServiceTier {
Core,
Train,
Event,
Eval,
}
impl ServiceTier {
pub const OPTIONAL: [ServiceTier; 3] =
[ServiceTier::Eval, ServiceTier::Event, ServiceTier::Train];
pub fn as_str(self) -> &'static str {
match self {
ServiceTier::Core => "core",
ServiceTier::Train => "train",
ServiceTier::Event => "event",
ServiceTier::Eval => "eval",
}
}
pub fn compiled_in(self) -> bool {
match self {
ServiceTier::Core | ServiceTier::Event | ServiceTier::Eval => true,
ServiceTier::Train => cfg!(feature = "train"),
}
}
}
impl fmt::Display for ServiceTier {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for ServiceTier {
type Err = TierError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"core" => Ok(ServiceTier::Core),
"train" => Ok(ServiceTier::Train),
"event" => Ok(ServiceTier::Event),
"eval" => Ok(ServiceTier::Eval),
other => Err(TierError::Unknown(other.to_string())),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum TierError {
#[error("unknown service tier '{0}'; expected one of: core, train, event, eval")]
Unknown(String),
#[error(
"service tier '{0}' is not compiled into this binary; \
rebuild with the '{0}' feature or remove it from `[server] services`"
)]
FeatureNotCompiled(ServiceTier),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TierSet {
tiers: BTreeSet<ServiceTier>,
}
impl TierSet {
pub fn resolve(optional: impl IntoIterator<Item = ServiceTier>) -> Result<Self, TierError> {
let mut tiers = BTreeSet::new();
tiers.insert(ServiceTier::Core);
for tier in optional {
if tier == ServiceTier::Core {
continue;
}
if !tier.compiled_in() {
return Err(TierError::FeatureNotCompiled(tier));
}
tiers.insert(tier);
}
Ok(Self { tiers })
}
pub fn from_config(selection: &ServiceSelection) -> Result<Self, TierError> {
match selection {
ServiceSelection::All(_) => Ok(Self::all_compiled()),
ServiceSelection::Only(tokens) => {
let tiers = tokens
.iter()
.map(|t| ServiceTier::from_str(t))
.collect::<Result<Vec<_>, _>>()?;
Self::resolve(tiers)
}
}
}
pub fn all_compiled() -> Self {
let optional = ServiceTier::OPTIONAL
.into_iter()
.filter(|t| t.compiled_in());
Self::resolve(optional).expect("compiled-in tiers always resolve")
}
pub fn contains(&self, tier: ServiceTier) -> bool {
self.tiers.contains(&tier)
}
pub fn as_wire(&self) -> Vec<String> {
let mut wire: Vec<String> = self.tiers.iter().map(|t| t.as_str().to_string()).collect();
wire.sort();
wire
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn core_is_always_present_even_with_an_empty_selection() {
let set = TierSet::resolve(std::iter::empty()).expect("empty resolves");
assert!(set.contains(ServiceTier::Core));
assert_eq!(set.as_wire(), vec!["core".to_string()]);
}
#[test]
fn resolve_adds_requested_compiled_in_tiers() {
let set = TierSet::resolve([ServiceTier::Event, ServiceTier::Eval]).expect("resolve");
assert!(set.contains(ServiceTier::Core));
assert!(set.contains(ServiceTier::Event));
assert!(set.contains(ServiceTier::Eval));
assert_eq!(
set.as_wire(),
vec!["core".to_string(), "eval".to_string(), "event".to_string()]
);
}
#[test]
fn explicit_core_in_the_selection_is_harmless() {
let set = TierSet::resolve([ServiceTier::Core, ServiceTier::Event]).expect("resolve");
assert_eq!(set.as_wire(), vec!["core".to_string(), "event".to_string()]);
}
#[test]
fn all_compiled_includes_core_and_every_compiled_optional() {
let set = TierSet::all_compiled();
assert!(set.contains(ServiceTier::Core));
assert!(set.contains(ServiceTier::Event));
assert!(set.contains(ServiceTier::Eval));
assert_eq!(
set.contains(ServiceTier::Train),
ServiceTier::Train.compiled_in()
);
}
#[test]
fn wire_tokens_are_sorted_and_round_trip() {
let set = TierSet::all_compiled();
let wire = set.as_wire();
let mut sorted = wire.clone();
sorted.sort();
assert_eq!(wire, sorted, "wire tokens are sorted (BTreeSet order)");
for token in &wire {
let tier = ServiceTier::from_str(token).expect("token parses");
assert!(set.contains(tier));
}
}
#[test]
fn unknown_tier_token_is_an_error() {
assert_eq!(
ServiceTier::from_str("registry"),
Err(TierError::Unknown("registry".to_string()))
);
}
#[test]
fn from_config_all_is_all_compiled() {
let set = TierSet::from_config(&ServiceSelection::default()).expect("default resolves");
assert_eq!(set, TierSet::all_compiled());
}
#[test]
fn from_config_empty_only_is_serve_only() {
let set =
TierSet::from_config(&ServiceSelection::Only(vec![])).expect("serve-only resolves");
assert_eq!(set.as_wire(), vec!["core".to_string()]);
}
#[test]
fn from_config_named_tier() {
let set = TierSet::from_config(&ServiceSelection::Only(vec!["event".to_string()]))
.expect("event resolves");
assert!(set.contains(ServiceTier::Event));
assert!(!set.contains(ServiceTier::Eval));
}
#[test]
fn from_config_unknown_token_is_an_error() {
let err = TierSet::from_config(&ServiceSelection::Only(vec!["registry".to_string()]))
.unwrap_err();
assert_eq!(err, TierError::Unknown("registry".to_string()));
}
#[cfg(not(feature = "train"))]
#[test]
fn requesting_a_compiled_out_tier_is_a_truthful_error() {
let err = TierSet::resolve([ServiceTier::Train]).unwrap_err();
assert_eq!(err, TierError::FeatureNotCompiled(ServiceTier::Train));
assert!(!TierSet::all_compiled().contains(ServiceTier::Train));
}
#[cfg(feature = "train")]
#[test]
fn requesting_a_compiled_in_train_tier_mounts_it() {
let set = TierSet::resolve([ServiceTier::Train]).expect("train resolves when compiled");
assert!(set.contains(ServiceTier::Train));
}
}