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}