use std::any::{type_name, Any, TypeId};
use std::collections::BTreeMap;
#[derive(Debug)]
pub struct Recipe {
converts: BTreeMap<(TypeId, TypeId), Box<dyn Any>>,
named_converts: BTreeMap<(String, TypeId, TypeId), Box<dyn Any>>,
defaults: BTreeMap<TypeId, Box<dyn Any>>,
named_defaults: BTreeMap<(String, TypeId), Box<dyn Any>>,
}
impl Default for Recipe {
fn default() -> Self {
Self::new()
}
}
impl Recipe {
pub fn new() -> Self {
Self {
converts: BTreeMap::new(),
named_converts: BTreeMap::new(),
defaults: BTreeMap::new(),
named_defaults: BTreeMap::new(),
}
}
pub fn add<From, To>(&mut self, converter: fn(&Self, From) -> To)
where
From: Sized + Any,
To: Sized + Any,
{
let type_ids = (TypeId::of::<From>(), TypeId::of::<To>());
self.converts.insert(type_ids, Box::new(converter));
}
pub fn add_as<From, To>(&mut self, name: &str, converter: fn(&Self, From) -> To)
where
From: Sized + Any,
To: Sized + Any,
{
let type_ids = (name.to_string(), TypeId::of::<From>(), TypeId::of::<To>());
self.named_converts.insert(type_ids, Box::new(converter));
}
pub fn put<T: Sized + Any>(&mut self, default: fn(&Self) -> T) {
let type_id = TypeId::of::<T>();
self.defaults.insert(type_id, Box::new(default));
}
pub fn put_as<T: Sized + Any>(&mut self, name: &str, f: fn(&Self) -> T) {
let type_id = TypeId::of::<T>();
self.named_defaults
.insert((name.to_string(), type_id), Box::new(f));
}
pub fn make<From, To>(&self, x: From) -> To
where
From: Sized + Any,
To: Sized + Any,
{
match self.get::<From, To>() {
Some(f) => f(x),
None => panic!(
"Undefined conversion from {:?} to {:?}",
type_name::<From>(),
type_name::<To>()
),
}
}
pub fn make_as<From, To>(&self, name: &str, x: From) -> To
where
From: Sized + Any,
To: Sized + Any,
{
match self.get_as::<From, To>(name) {
Some(f) => f(x),
None => panic!(
"Undefined conversion named '{}' from {:?} to {:?}",
name,
type_name::<From>(),
type_name::<To>()
),
}
}
pub fn take<T: Sized + Any>(&self) -> T {
match self.get_default::<T>() {
Some(f) => f(),
None => panic!("Undefined default for {:?}", type_name::<T>(),),
}
}
pub fn take_as<T: Sized + Any>(&self, name: &str) -> T {
match self.get_default_as::<T>(name) {
Some(f) => f(),
None => panic!(
"Undefined default named '{}' for {:?}",
name,
type_name::<T>(),
),
}
}
fn get<From, To>(&self) -> Option<Box<dyn Fn(From) -> To + '_>>
where
From: Sized + Any,
To: Sized + Any,
{
let type_ids = (TypeId::of::<From>(), TypeId::of::<To>());
self.converts.get(&type_ids).and_then(|f| {
f.downcast_ref::<fn(&Self, From) -> To>()
.map(|f| Box::new(move |x: From| f(self, x)) as Box<dyn Fn(From) -> To>)
})
}
fn get_as<From, To>(&self, name: &str) -> Option<Box<dyn Fn(From) -> To + '_>>
where
From: Sized + Any,
To: Sized + Any,
{
let type_ids = (name.to_string(), TypeId::of::<From>(), TypeId::of::<To>());
self.named_converts.get(&type_ids).and_then(|f| {
f.downcast_ref::<fn(&Self, From) -> To>()
.map(|f| Box::new(move |x: From| f(self, x)) as Box<dyn Fn(From) -> To>)
})
}
fn get_default<T: Sized + Any>(&self) -> Option<Box<dyn Fn() -> T + '_>> {
let type_id = TypeId::of::<T>();
self.defaults.get(&type_id).and_then(|f| {
f.downcast_ref::<fn(&Self) -> T>()
.map(|f| Box::new(move || f(self)) as Box<dyn Fn() -> T>)
})
}
fn get_default_as<T: Sized + Any>(&self, name: &str) -> Option<Box<dyn Fn() -> T + '_>> {
let type_id = TypeId::of::<T>();
self.named_defaults
.get(&(name.to_string(), type_id))
.and_then(|f| {
f.downcast_ref::<fn(&Self) -> T>()
.map(|f| Box::new(move || f(self)) as Box<dyn Fn() -> T>)
})
}
}
#[cfg(test)]
mod tests {
use super::*;
struct Record {
name: String,
address: Address,
landline: Phone,
mobile: Phone,
}
struct Address {
postal_code: u32,
_city: String,
_street: String,
_door: u32,
}
struct Phone {
area_code: u32,
_number: u32,
}
fn check_record(r: Record) -> bool {
r.name.len() >= 6 &&
r.name.len() <= 100 &&
r.address.postal_code >= 10000 &&
r.address.postal_code <= 99999 &&
r.landline.area_code >= 100 &&
r.landline.area_code <= 999 &&
r.mobile.area_code >= 100 &&
r.mobile.area_code <= 999
}
#[test]
fn test_check_record() {
let mut r = Recipe::new();
r.put(|_| Phone {
area_code: 123,
_number: 456_789,
});
r.put(|_| Address {
postal_code: 10179,
_city: "Berlin".to_string(),
_street: "Molkenmarkt".to_string(),
_door: 1,
});
r.add(|r, name: String| Record {
name,
address: r.take(),
landline: r.take(),
mobile: r.take(),
});
assert!(check_record(r.make("John Smith".to_string())));
assert!(!check_record(r.make("short".to_string())));
assert!(!check_record(r.make("long".repeat(100))));
r.add(|_, phone: (u32, u32)| Phone {
area_code: phone.0,
_number: phone.1,
});
r.add(|r, phone: (u32, u32)| Record {
name: "John Smith".to_string(),
address: r.take(),
landline: r.make(phone),
mobile: r.take(),
});
let test_phone = |r: &Recipe| {
assert!(check_record(r.make((123_u32, 1_234_567_u32))));
assert!(!check_record(r.make((1_u32, 1_234_567_u32))));
assert!(!check_record(r.make((1234_u32, 1_234_567_u32))));
};
test_phone(&r);
r.add(|r, phone: (u32, u32)| Record {
name: "John Smith".to_string(),
address: r.take(),
landline: r.take(),
mobile: r.make(phone),
});
test_phone(&r);
}
#[derive(Debug, PartialEq)]
struct Chain {
name: String,
id: u64,
default_provider: Provider,
}
#[derive(Debug, PartialEq)]
struct Provider {
name: String,
id: u64,
}
#[derive(Debug, PartialEq)]
struct Block {
chain: Chain,
height: u64,
id: u64,
provider: Provider,
}
#[derive(Debug, PartialEq)]
struct AbstractBlock {
chain: String,
height: u64,
provider: String,
}
#[test]
fn test() {
let mut r = Recipe::new();
r.put_as("height", |_| 1_u64);
r.put_as("id", |_| 0_u64);
r.put(|r| Provider {
name: "default_provider".to_string(),
id: r.take_as("id"),
});
r.add(|r, name: String| Provider {
name,
id: r.take_as("id"),
});
r.add(|r, name: String| Chain {
name,
id: r.take_as("id"),
default_provider: r.take(),
});
r.add(|r, b: AbstractBlock| Block {
chain: r.make(b.chain),
height: b.height,
id: r.take_as("id"),
provider: r.make(b.provider),
});
let a_block = AbstractBlock {
chain: "chain1".to_string(),
height: 1,
provider: "provider2".to_string(),
};
let block: Block = r.make(a_block);
let expected = Block {
chain: Chain {
name: "chain1".to_string(),
id: 0,
default_provider: Provider {
name: "default_provider".to_string(),
id: 0,
},
},
height: 1,
id: 0,
provider: Provider {
name: "provider2".to_string(),
id: 0,
},
};
assert_eq!(block, expected);
}
}