Skip to main content

planter_core/
resources.rs

1use crate::person::Person;
2
3#[derive(Debug, Clone, PartialEq, Eq)]
4/// Represents a resource that can be used in a project. A resource can be either a material or personnel.
5pub enum Resource {
6    /// Represents a material resource that can be used in a project.
7    Material(Material),
8    /// Represents a personnel resource. Personnel is usually a resource that can complete tasks
9    Personnel {
10        /// Information about the person.
11        person: Person,
12        /// Hourly rate of the person.
13        hourly_rate: Option<u16>,
14    },
15}
16
17#[derive(Debug, Clone, PartialEq, Eq)]
18/// Represents a material resource that can be used in a project.
19/// It can be either consumable or non-consumable.
20pub enum Material {
21    /// A consumable resource is a material that needs to be resupplied after use.
22    Consumable(Consumable),
23    /// A non-consumable resource is a material that does not need to be resupplied after use.
24    NonConsumable(NonConsumable),
25}
26
27#[derive(Debug, Clone, Default, PartialEq, Eq)]
28/// Represents a consumable material resource that can be used in a project.
29pub struct Consumable {
30    /// Name of the consumable material.
31    name: String,
32    /// Available quantity of the consumable material.
33    quantity: Option<u16>,
34    /// Cost to buy this material.
35    cost_per_unit: Option<u16>,
36}
37
38#[derive(Debug, Clone, Default, PartialEq, Eq)]
39/// Represents a non-consumable material resource that can be used in a project.
40pub struct NonConsumable {
41    /// Name of the non-consumable material.
42    name: String,
43    /// Available quantity of the non-consumable material.
44    quantity: Option<u16>,
45    /// Cost to buy this material.
46    cost_per_unit: Option<u16>,
47    /// Some non consumable materials can have a hourly rate. For example, due to energy consumption.
48    hourly_rate: Option<u16>,
49}
50
51impl From<NonConsumable> for Consumable {
52    fn from(value: NonConsumable) -> Self {
53        Consumable {
54            name: value.name,
55            quantity: value.quantity,
56            cost_per_unit: value.cost_per_unit,
57        }
58    }
59}
60
61impl From<Consumable> for NonConsumable {
62    fn from(value: Consumable) -> Self {
63        NonConsumable {
64            name: value.name,
65            quantity: value.quantity,
66            cost_per_unit: value.cost_per_unit,
67            hourly_rate: None,
68        }
69    }
70}
71
72impl Material {
73    /// Returns a consumable material by default, with the given name.
74    ///
75    /// # Example
76    /// ```
77    /// use planter_core::resources::Material;
78    ///
79    /// let material = Material::new("Steel".to_owned());
80    /// assert_eq!(material.name(), "Steel");
81    /// ```
82    #[must_use]
83    pub fn new(name: impl Into<String>) -> Self {
84        Material::Consumable(Consumable::new(name))
85    }
86
87    /// Returns the name of the material.
88    /// # Example
89    /// ```
90    /// use planter_core::resources::Material;
91    ///
92    /// let material = Material::new("Steel".to_owned());
93    /// assert_eq!(material.name(), "Steel");
94    /// ```
95    #[must_use]
96    pub fn name(&self) -> &str {
97        match self {
98            Material::Consumable(consumable) => &consumable.name,
99            Material::NonConsumable(non_consumable) => &non_consumable.name,
100        }
101    }
102
103    /// Updates the name of the material.
104    /// # Example
105    /// ```
106    /// use planter_core::resources::Material;
107    ///
108    /// let mut material = Material::new("Steel".to_owned());
109    /// material.update_name("Iron".to_owned());
110    /// assert_eq!(material.name(), "Iron");
111    /// ```
112    pub fn update_name(&mut self, name: impl Into<String>) {
113        match self {
114            Material::Consumable(consumable) => consumable.name = name.into(),
115            Material::NonConsumable(non_consumable) => non_consumable.name = name.into(),
116        }
117    }
118
119    /// Returns the quantity of materials.
120    /// # Example
121    /// ```
122    /// use planter_core::resources::Material;
123    ///
124    /// let material = Material::new("Steel".to_owned());
125    /// assert_eq!(material.quantity(), None);
126    /// ```
127    #[must_use]
128    pub const fn quantity(&self) -> Option<u16> {
129        match self {
130            Material::Consumable(consumable) => consumable.quantity,
131            Material::NonConsumable(non_consumable) => non_consumable.quantity,
132        }
133    }
134
135    /// Updates the quantity of materials.
136    /// # Example
137    /// ```
138    /// use planter_core::resources::Material;
139    ///
140    /// let mut material = Material::new("Steel".to_owned());
141    /// material.update_quantity(3);
142    /// assert_eq!(material.quantity(), Some(3));
143    /// ```
144    pub const fn update_quantity(&mut self, quantity: u16) {
145        match self {
146            Material::Consumable(consumable) => consumable.quantity = Some(quantity),
147            Material::NonConsumable(non_consumable) => non_consumable.quantity = Some(quantity),
148        }
149    }
150
151    /// Remove the quantity of materials.
152    /// # Example
153    /// ```
154    /// use planter_core::resources::Material;
155    ///
156    /// let mut material = Material::new("Steel".to_owned());
157    /// material.update_quantity(3);
158    /// assert_eq!(material.quantity(), Some(3));
159    /// material.remove_quantity();
160    /// assert_eq!(material.quantity(), None);
161    /// ```
162    pub const fn remove_quantity(&mut self) {
163        match self {
164            Material::Consumable(consumable) => consumable.quantity = None,
165            Material::NonConsumable(non_consumable) => non_consumable.quantity = None,
166        }
167    }
168    /// Returns the cost per unit of the material.
169    /// # Example
170    /// ```
171    /// use planter_core::resources::Material;
172    ///
173    /// let material = Material::new("Steel".to_owned());
174    /// assert_eq!(material.cost_per_unit(), None);
175    /// ```
176    #[must_use]
177    pub const fn cost_per_unit(&self) -> Option<u16> {
178        match self {
179            Material::Consumable(consumable) => consumable.cost_per_unit,
180            Material::NonConsumable(non_consumable) => non_consumable.cost_per_unit,
181        }
182    }
183
184    /// Updates the cost per unit of the material.
185    /// # Example
186    /// ```
187    /// use planter_core::resources::Material;
188    ///
189    /// let mut material = Material::new("Steel".to_owned());
190    /// material.update_cost_per_unit(3);
191    /// assert_eq!(material.cost_per_unit(), Some(3));
192    /// ```
193    pub const fn update_cost_per_unit(&mut self, cost_per_unit: u16) {
194        match self {
195            Material::Consumable(consumable) => consumable.cost_per_unit = Some(cost_per_unit),
196            Material::NonConsumable(non_consumable) => {
197                non_consumable.cost_per_unit = Some(cost_per_unit)
198            }
199        }
200    }
201
202    /// Remove the cost per unit of the material.
203    /// # Example
204    /// ```
205    /// use planter_core::resources::Material;
206    ///
207    /// let mut material = Material::new("Steel".to_owned());
208    /// material.update_cost_per_unit(3);
209    /// assert_eq!(material.cost_per_unit(), Some(3));
210    /// material.remove_cost_per_unit();
211    /// assert_eq!(material.cost_per_unit(), None);
212    /// ```
213    pub const fn remove_cost_per_unit(&mut self) {
214        match self {
215            Material::Consumable(consumable) => consumable.cost_per_unit = None,
216            Material::NonConsumable(non_consumable) => non_consumable.cost_per_unit = None,
217        }
218    }
219}
220
221impl Consumable {
222    /// Creates a new consumable material resource with the given name.
223    ///
224    /// # Example
225    /// ```
226    /// use planter_core::resources::{Consumable, Material};
227    ///
228    /// let consumable = Consumable::new("Steel".to_owned());
229    /// let material = Material::Consumable(consumable);
230    /// assert_eq!(material.name(), "Steel");
231    /// ```
232    #[must_use]
233    pub fn new(name: impl Into<String>) -> Self {
234        Consumable {
235            name: name.into(),
236            quantity: None,
237            cost_per_unit: None,
238        }
239    }
240}
241
242impl NonConsumable {
243    /// Creates a new non-consumable material resource with the given name.
244    ///
245    /// # Example
246    /// ```
247    /// use planter_core::resources::{NonConsumable, Material};
248    ///
249    /// let non_consumable = NonConsumable::new("Drill".to_owned());
250    /// let material = Material::NonConsumable(non_consumable);
251    /// assert_eq!(material.name(), "Drill");
252    /// ```
253    #[must_use]
254    pub fn new(name: impl Into<String>) -> Self {
255        NonConsumable {
256            name: name.into(),
257            quantity: None,
258            hourly_rate: None,
259            cost_per_unit: None,
260        }
261    }
262
263    /// Returns the hourly rate of the non-consumable material.
264    /// # Example
265    /// ```
266    /// use planter_core::resources::NonConsumable;
267    ///
268    /// let non_consumable = NonConsumable::new("Steel".to_owned());
269    /// assert_eq!(non_consumable.hourly_rate(), None);
270    /// ```
271    #[must_use]
272    pub const fn hourly_rate(&self) -> Option<u16> {
273        self.hourly_rate
274    }
275
276    /// Updates the hourly_rate of the non consumable material.
277    /// # Example
278    /// ```
279    /// use planter_core::resources::NonConsumable;
280    ///
281    /// let mut non_consumable = NonConsumable::new("Steel".to_owned());
282    /// non_consumable.update_hourly_rate(3);
283    /// assert_eq!(non_consumable.hourly_rate(), Some(3));
284    /// ```
285    pub const fn update_hourly_rate(&mut self, hourly_rate: u16) {
286        self.hourly_rate = Some(hourly_rate);
287    }
288
289    /// Remove the cost per unit of the non consumable material.
290    /// # Example
291    /// ```
292    /// use planter_core::resources::NonConsumable;
293    ///
294    /// let mut non_consumable = NonConsumable::new("Steel".to_owned());
295    /// non_consumable.update_hourly_rate(3);
296    /// assert_eq!(non_consumable.hourly_rate(), Some(3));
297    /// non_consumable.remove_hourly_rate();
298    /// assert_eq!(non_consumable.hourly_rate(), None);
299    /// ```
300    pub const fn remove_hourly_rate(&mut self) {
301        self.hourly_rate = None;
302    }
303}
304
305#[cfg(test)]
306mod tests {
307    use proptest::prelude::*;
308
309    use crate::resources::{Consumable, Material, NonConsumable};
310
311    fn material_name() -> impl Strategy<Value = String> {
312        r"[a-zA-Z0-9 ]{1,30}".prop_map(|s: String| s)
313    }
314
315    fn quantity() -> impl Strategy<Value = u16> {
316        1..u16::MAX
317    }
318
319    fn unit_cost() -> impl Strategy<Value = u16> {
320        1..u16::MAX
321    }
322
323    proptest! {
324        #[test]
325        fn consumable_setters_roundtrip(name in material_name(), qty in quantity(), cost in unit_cost()) {
326            let mut m = Material::Consumable(Consumable::new(name.clone()));
327            assert_eq!(m.name(), name);
328
329            assert_eq!(m.quantity(), None);
330            m.update_quantity(qty);
331            assert_eq!(m.quantity(), Some(qty));
332            m.remove_quantity();
333            assert_eq!(m.quantity(), None);
334
335            assert_eq!(m.cost_per_unit(), None);
336            m.update_cost_per_unit(cost);
337            assert_eq!(m.cost_per_unit(), Some(cost));
338            m.remove_cost_per_unit();
339            assert_eq!(m.cost_per_unit(), None);
340        }
341
342        #[test]
343        fn nonconsumable_setters_roundtrip(name in material_name(), qty in quantity(), cost in unit_cost()) {
344            let mut m = Material::NonConsumable(NonConsumable::new(name.clone()));
345            assert_eq!(m.name(), name);
346
347            assert_eq!(m.quantity(), None);
348            m.update_quantity(qty);
349            assert_eq!(m.quantity(), Some(qty));
350            m.remove_quantity();
351            assert_eq!(m.quantity(), None);
352
353            assert_eq!(m.cost_per_unit(), None);
354            m.update_cost_per_unit(cost);
355            assert_eq!(m.cost_per_unit(), Some(cost));
356            m.remove_cost_per_unit();
357            assert_eq!(m.cost_per_unit(), None);
358        }
359
360        #[test]
361        fn nonconsumable_hourly_rate_roundtrip(rate in 1u16..5000u16) {
362            let mut nc = NonConsumable::new("item");
363            assert_eq!(nc.hourly_rate(), None);
364            nc.update_hourly_rate(rate);
365            assert_eq!(nc.hourly_rate(), Some(rate));
366            nc.remove_hourly_rate();
367            assert_eq!(nc.hourly_rate(), None);
368        }
369
370        #[test]
371        fn consumable_to_nonconsumable_preserves_fields(name in material_name(), qty in quantity(), cost in unit_cost()) {
372            let mut m = Material::Consumable(Consumable::new(name.clone()));
373            m.update_quantity(qty);
374            m.update_cost_per_unit(cost);
375
376            let Material::Consumable(ref inner) = m else { panic!() };
377            let nc = NonConsumable::from(inner.clone());
378
379            assert_eq!(nc.name, inner.name);
380            assert_eq!(nc.quantity, inner.quantity);
381            assert_eq!(nc.cost_per_unit, inner.cost_per_unit);
382        }
383
384        #[test]
385        fn nonconsumable_to_consumable_preserves_fields(qty in quantity(), cost in unit_cost()) {
386            let mut m = Material::NonConsumable(NonConsumable::new("item"));
387            m.update_quantity(qty);
388            m.update_cost_per_unit(cost);
389            let Material::NonConsumable(ref nc) = m else { panic!() };
390            let c = Consumable::from(nc.clone());
391            assert_eq!(c.quantity, Some(qty));
392            assert_eq!(c.cost_per_unit, Some(cost));
393        }
394    }
395}