use std::collections::HashMap;
pub trait Catalogued {
type Metadata;
}
pub trait Requirement<U: Catalogued + ?Sized> {
fn admits_type(&self, metadata: &U::Metadata) -> bool;
fn admits_instance(&self, instance: &U) -> bool;
}
pub trait DependsOn<U: Catalogued + ?Sized> {
type Requirement: Requirement<U>;
fn requirement(&self) -> Self::Requirement;
}
pub trait Configured<U: Catalogued + ?Sized> {
fn instances(&self) -> Vec<(String, &U)>;
fn catalog(&self) -> HashMap<&'static str, U::Metadata>;
fn config_schema(&self, type_name: &str) -> Option<schemars::Schema>;
}
#[derive(Debug)]
pub struct Resolution {
pub existing_instances: Vec<String>,
pub configurable_types: Vec<&'static str>,
}
impl Resolution {
pub fn is_unsatisfiable(&self) -> bool {
self.existing_instances.is_empty() && self.configurable_types.is_empty()
}
}
pub fn resolve<T, U>(dependent: &T, source: &impl Configured<U>) -> Resolution
where
U: Catalogued + ?Sized,
T: DependsOn<U>,
{
let requirement = dependent.requirement();
let existing_instances: Vec<String> = source
.instances()
.into_iter()
.filter(|(_, instance)| requirement.admits_instance(*instance))
.map(|(id, _)| id)
.collect();
let configurable_types: Vec<&'static str> = source
.catalog()
.into_iter()
.filter(|(_, metadata)| requirement.admits_type(metadata))
.map(|(name, _)| name)
.collect();
Resolution {
existing_instances,
configurable_types,
}
}
#[cfg(test)]
mod tests {
use super::*;
trait Paint {
fn color(&self) -> &str;
}
struct PaintMetadata {
claimed_colors: Vec<&'static str>,
}
impl Catalogued for dyn Paint {
type Metadata = PaintMetadata;
}
struct MixedPaint(&'static str);
impl Paint for MixedPaint {
fn color(&self) -> &str {
self.0
}
}
struct WantsColor(&'static str);
struct ColorRequirement(&'static str);
impl Requirement<dyn Paint> for ColorRequirement {
fn admits_type(&self, metadata: &PaintMetadata) -> bool {
metadata.claimed_colors.contains(&self.0)
}
fn admits_instance(&self, instance: &dyn Paint) -> bool {
instance.color() == self.0
}
}
impl DependsOn<dyn Paint> for WantsColor {
type Requirement = ColorRequirement;
fn requirement(&self) -> ColorRequirement {
ColorRequirement(self.0)
}
}
#[derive(schemars::JsonSchema)]
struct MixRatio {
#[allow(unused)] parts_base: u32,
}
struct PaintShop {
cans: Vec<(String, Box<dyn Paint>)>,
recipes: HashMap<&'static str, PaintMetadata>,
recipe_schemas: HashMap<&'static str, schemars::Schema>,
}
impl Configured<dyn Paint> for PaintShop {
fn instances(&self) -> Vec<(String, &(dyn Paint + 'static))> {
self.cans
.iter()
.map(|(id, p)| (id.clone(), p.as_ref()))
.collect()
}
fn catalog(&self) -> HashMap<&'static str, PaintMetadata> {
self.recipes
.iter()
.map(|(name, meta)| {
(
*name,
PaintMetadata {
claimed_colors: meta.claimed_colors.clone(),
},
)
})
.collect()
}
fn config_schema(&self, type_name: &str) -> Option<schemars::Schema> {
self.recipe_schemas.get(type_name).cloned()
}
}
fn empty_shop() -> PaintShop {
PaintShop {
cans: vec![],
recipes: HashMap::new(),
recipe_schemas: HashMap::new(),
}
}
#[test]
fn resolution_includes_matching_instances() {
let shop = PaintShop {
cans: vec![
("can-1".to_string(), Box::new(MixedPaint("red"))),
("can-2".to_string(), Box::new(MixedPaint("blue"))),
],
recipes: HashMap::new(),
recipe_schemas: HashMap::new(),
};
let resolution = resolve(&WantsColor("blue"), &shop);
assert_eq!(resolution.existing_instances, vec!["can-2".to_string()]);
assert!(resolution.configurable_types.is_empty());
assert!(!resolution.is_unsatisfiable());
}
#[test]
fn resolution_includes_configurable_types_alongside_instances() {
let mut shop = empty_shop();
shop.cans
.push(("can-1".to_string(), Box::new(MixedPaint("blue"))));
shop.recipes.insert(
"cyan-mix",
PaintMetadata {
claimed_colors: vec!["blue", "green"],
},
);
shop.recipes.insert(
"warm-mix",
PaintMetadata {
claimed_colors: vec!["red", "orange"],
},
);
let resolution = resolve(&WantsColor("blue"), &shop);
assert_eq!(resolution.existing_instances, vec!["can-1".to_string()]);
assert_eq!(resolution.configurable_types, vec!["cyan-mix"]);
assert!(!resolution.is_unsatisfiable());
}
#[test]
fn resolution_is_configurable_only_when_no_instance_matches() {
let mut shop = empty_shop();
shop.cans
.push(("can-1".to_string(), Box::new(MixedPaint("red"))));
shop.recipes.insert(
"cyan-mix",
PaintMetadata {
claimed_colors: vec!["blue", "green"],
},
);
let resolution = resolve(&WantsColor("blue"), &shop);
assert!(resolution.existing_instances.is_empty());
assert_eq!(resolution.configurable_types, vec!["cyan-mix"]);
assert!(!resolution.is_unsatisfiable());
}
#[test]
fn configurable_type_names_resolve_to_their_config_schema() {
let mut shop = empty_shop();
shop.recipes.insert(
"cyan-mix",
PaintMetadata {
claimed_colors: vec!["blue", "green"],
},
);
shop.recipe_schemas
.insert("cyan-mix", schemars::schema_for!(MixRatio));
let resolution = resolve(&WantsColor("blue"), &shop);
assert_eq!(resolution.configurable_types, vec!["cyan-mix"]);
let schema = shop
.config_schema(resolution.configurable_types[0])
.expect("configurable type should have a config schema");
let properties = schema
.get("properties")
.and_then(|p| p.as_object())
.expect("object schema with properties");
assert!(properties.contains_key("parts_base"));
}
#[test]
fn unsatisfiable_when_nothing_could_ever_match() {
let mut shop = empty_shop();
shop.recipes.insert(
"warm-mix",
PaintMetadata {
claimed_colors: vec!["red", "orange"],
},
);
let resolution = resolve(&WantsColor("blue"), &shop);
assert!(resolution.existing_instances.is_empty());
assert!(resolution.configurable_types.is_empty());
assert!(resolution.is_unsatisfiable());
}
}