Skip to main content

planter_core/
resources.rs

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