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}