instancebuilder/
lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
use std::any::{type_name, Any, TypeId};
use std::collections::HashMap;
use std::fmt::Formatter;

/// InstanceBuilder offers the creation of configured instances. Due to this pattern, you can for
/// example use dependency injection in your tests without exposing those.
///
/// The object to be build implements the FromInstanceBuilder trait in its module.
///
/// ```
/// use std::convert::Infallible;
/// use ::instancebuilder::{BuilderError, InstanceBuilder, FromInstanceBuilder};
///
/// struct TestImplementation {
///     inner: String,
/// }
///
/// struct TestConfig {
///     key: String,
/// }
///
/// impl FromInstanceBuilder for TestImplementation {
///     fn try_from_builder(builder: &InstanceBuilder) -> Result<Self, BuilderError> {
///         let config: &TestConfig = builder.data()?;
///         Ok(Self {
///             inner: config.key.clone(),
///         })
///     }
/// }
///
/// let config = TestConfig {
///    key: String::from("help me!"),
/// };
///
/// let mut  builder = InstanceBuilder::new();
/// builder.insert(config);
///
/// let instance = builder.build::<TestImplementation>().unwrap();
///
/// ```
pub struct InstanceBuilder {
    data: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
}

impl InstanceBuilder {
    pub fn new() -> Self {
        Self {
            data: Default::default(),
        }
    }

    pub fn insert<D: Any + Send + Sync>(&mut self, data: D) {
        self.data.insert(TypeId::of::<D>(), Box::new(data));
    }

    pub fn data<D: Any + Send + Sync>(&self) -> Result<&D, BuilderError> {
        self.data_opt()
            .ok_or_else(|| BuilderError::DataDoesNotExist {
                ty: type_name::<D>().to_string(),
            })
    }

    pub fn data_opt<D: Any + Send + Sync>(&self) -> Option<&D> {
        self.data
            .get(&TypeId::of::<D>())
            .and_then(|d| d.downcast_ref::<D>())
    }

    pub fn build<T>(&self) -> Result<T, BuilderError>
    where
        T: FromInstanceBuilder,
    {
        T::try_from_builder(self)
    }
}

impl Default for InstanceBuilder {
    fn default() -> Self {
        Self::new()
    }
}

#[derive(Debug)]
pub enum BuilderError {
    DataDoesNotExist { ty: String },
    Other(String),
}

impl ::std::error::Error for BuilderError {}

impl ::std::fmt::Display for BuilderError {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            BuilderError::DataDoesNotExist { ty } => write!(f, "data of type {ty} does not exist"),
            BuilderError::Other(err) => {
                write!(f, "other error: {err}")
            }
        }
    }
}

pub trait FromInstanceBuilder: Sized {
    fn try_from_builder(builder: &InstanceBuilder) -> Result<Self, BuilderError>;
}

#[cfg(test)]
mod tests {
    use super::{BuilderError, FromInstanceBuilder, InstanceBuilder};
    use std::any::{Any, TypeId};

    struct TestImplementation {
        inner: String,
    }

    struct TestConfig {
        key: String,
    }

    impl FromInstanceBuilder for TestImplementation {
        fn try_from_builder(builder: &InstanceBuilder) -> Result<Self, BuilderError> {
            let config: &TestConfig = builder.data()?;
            Ok(Self {
                inner: config.key.clone(),
            })
        }
    }

    #[test]
    fn it_creates_new_instance_of_demanded_impl() {
        let config = TestConfig {
            key: String::from("help me!"),
        };
        let config_key = config.key.clone();
        let mut builder = InstanceBuilder::new();
        builder.insert(config);

        let instance = builder.build::<TestImplementation>().unwrap();

        assert_eq!(instance.type_id(), TypeId::of::<TestImplementation>());
        assert_eq!(instance.inner, config_key);
    }
}