planter_core/project.rs
1use std::collections::HashSet;
2
3use anyhow::{Context, bail};
4use bon::{Builder, builder};
5use chrono::{DateTime, Utc};
6use daggy::{
7 Dag,
8 petgraph::{
9 Direction,
10 visit::{IntoNeighborsDirected, IntoNodeIdentifiers},
11 },
12};
13use thiserror::Error;
14
15use crate::{
16 resources::{Material, Resource},
17 stakeholders::Stakeholder,
18 task::Task,
19};
20
21#[derive(Debug, Default, Builder)]
22#[builder(on(String, into))]
23/// Represents a project with a name and a list of resources.
24pub struct Project {
25 /// The name of the project.
26 name: String,
27 /// The description of the project.
28 description: Option<String>,
29 /// The start date of the project.
30 start_date: Option<DateTime<Utc>>,
31 /// The tasks associated with the project.
32 #[builder(default, with = |tasks: impl IntoIterator<Item = Task>| {
33 let mut dag = Dag::new();
34 tasks.into_iter().for_each(|t| _ = dag.add_node(t));
35 dag
36 })]
37 tasks: Dag<Task, TimeRelationship, usize>,
38 #[builder(default)]
39 subtask_relationships: Vec<SubtaskRelationship>,
40 /// The list of resources associated with the project.
41 #[builder(default)]
42 resources: Vec<Resource>,
43 /// The list of stakeholders associated with the project.
44 #[builder(default)]
45 stakeholders: Vec<Stakeholder>,
46}
47
48#[derive(Debug, Default, Clone, Copy)]
49/// A given task, might be composed of different subtasks.
50pub struct SubtaskRelationship {
51 task: usize,
52 subtask: usize,
53}
54
55#[derive(Debug, Default, Clone, Copy)]
56/// The predecessor - successor relationship between tasks.
57pub enum TimeRelationship {
58 /// The predecessor has to start for the successor to finish.
59 StartToFinish,
60 /// The predecessor has to start for the successor to finish.
61 FinishToFinish,
62 #[default]
63 /// The predecessor has to finish for the successor to start.
64 FinishToStart,
65 /// The predecessor has to start for the successor to finish.
66 StartToStart,
67}
68
69impl Project {
70 /// Creates a new project with the given name.
71 ///
72 /// # Arguments
73 ///
74 /// * `name` - The name of the project.
75 ///
76 /// # Returns
77 ///
78 /// A new `Project` instance.
79 ///
80 /// # Example
81 ///
82 /// ```
83 /// use planter_core::project::Project;
84 ///
85 /// let project = Project::new("World domination");
86 /// assert_eq!(project.name(), "World domination");
87 /// ```
88 pub fn new(name: impl Into<String>) -> Self {
89 Self {
90 name: name.into(),
91 description: None,
92 start_date: None,
93 resources: Vec::new(),
94 stakeholders: Vec::new(),
95 tasks: Dag::new(),
96 subtask_relationships: Vec::new(),
97 }
98 }
99
100 /// Returns the name of the project.
101 ///
102 /// # Example
103 ///
104 /// ```
105 /// use planter_core::project::Project;
106 ///
107 /// let project = Project::new("World domination");
108 /// assert_eq!(project.name(), "World domination");
109 /// ```
110 pub fn name(&self) -> &str {
111 &self.name
112 }
113
114 /// Returns the description of the project.
115 ///
116 /// # Example
117 ///
118 /// ```
119 /// use planter_core::project::Project;
120 ///
121 /// let project = Project::new("World domination");
122 /// assert_eq!(project.description(), None);
123 /// ```
124 pub fn description(&self) -> Option<&str> {
125 self.description.as_deref()
126 }
127
128 /// Adds a task to the project.
129 ///
130 /// # Arguments
131 ///
132 /// * `task` - The task to add to the project.
133 ///
134 /// # Example
135 ///
136 /// ```
137 /// use planter_core::{project::Project, task::Task};
138 ///
139 /// let mut project = Project::new("World domination");
140 /// assert_eq!(project.tasks().count(), 0);
141 /// project.add_task(Task::new("Become world leader"));
142 /// assert_eq!(project.tasks().count(), 1);
143 /// ```
144 pub fn add_task(&mut self, task: Task) {
145 self.tasks.add_node(task);
146 }
147
148 /// Deletes a task and all references to it from the project.
149 ///
150 /// # Arguments
151 ///
152 /// * `i` - The index of the task.
153 ///
154 /// # Errors
155 /// TODO!
156 ///
157 /// # Example
158 ///
159 /// ```
160 /// use planter_core::{project::Project, task::Task};
161 ///
162 /// let mut project = Project::new("World domination");
163 /// project.add_task(Task::new("Become world leader"));
164 /// assert_eq!(project.tasks().count(), 1);
165 /// assert!(project.rm_task(0).is_ok());
166 /// assert_eq!(project.tasks().count(), 0);
167 /// ```
168 pub fn rm_task(&mut self, i: usize) -> anyhow::Result<()> {
169 self.tasks
170 .remove_node(i.into())
171 .context("Tried removing a non existing node from Dag")?;
172 Ok(())
173 }
174
175 /// Gets a reference to the task with the given index from the project.
176 ///
177 /// # Arguments
178 ///
179 /// * `index` - The index to identify the task.
180 ///
181 /// # Example
182 ///
183 /// ```
184 /// use planter_core::{project::Project, task::Task};
185 ///
186 /// let mut project = Project::new("World domination");
187 /// project.add_task(Task::new("Become world leader"));
188 /// assert_eq!(project.task(0).unwrap().name(), "Become world leader");
189 /// ```
190 pub fn task(&self, index: usize) -> Option<&Task> {
191 self.tasks.node_weight(index.into())
192 }
193
194 /// Gets a mutable reference to the task with the given index from the project.
195 ///
196 /// # Arguments
197 ///
198 /// * `index` - The index to identify the task.
199 ///
200 /// # Example
201 ///
202 /// ```
203 /// use planter_core::{project::Project, task::Task};
204 ///
205 /// let mut project = Project::new("World domination");
206 /// project.add_task(Task::new("Become world leader"));
207 /// let task = project.task_mut(0).unwrap();
208 /// assert_eq!(task.name(), "Become world leader");
209 ///
210 /// task.edit_name("Become world's biggest loser");
211 /// assert_eq!(task.name(), "Become world's biggest loser")
212 /// ```
213 pub fn task_mut(&mut self, index: usize) -> Option<&mut Task> {
214 self.tasks.node_weight_mut(index.into())
215 }
216
217 /// Returns the tasks of the project.
218 ///
219 /// # Example
220 ///
221 /// ```
222 /// use planter_core::{project::Project, task::Task};
223 ///
224 /// let mut project = Project::new("World domination");
225 /// project.add_task(Task::new("Become world leader"));
226 /// assert_eq!(project.tasks().count(), 1);
227 /// ```
228 pub fn tasks(&self) -> impl Iterator<Item = &Task> {
229 self.tasks.raw_nodes().iter().map(|node| &node.weight)
230 }
231
232 /// Returns a mutable reference to the tasks of the project.
233 ///
234 /// # Example
235 ///
236 /// ```
237 /// use planter_core::{project::Project, task::Task};
238 ///
239 /// let mut project = Project::new("World domination");
240 /// project.add_task(Task::new("Become world leader"));
241 /// assert_eq!(project.tasks().count(), 1);
242 /// ```
243 pub fn tasks_mut(&mut self) -> impl Iterator<Item = &mut Task> {
244 self.tasks.node_weights_mut()
245 }
246
247 /// Adds a relationship betwen tasks, where one is the predecessor and the other one a successor.
248 ///
249 /// # Errors
250 /// TODO!
251 ///
252 /// # Example
253 ///
254 /// ```
255 /// use planter_core::{project::{Project, TimeRelationship}, task::Task};
256 ///
257 /// let mut project = Project::new("World domination");
258 /// project.add_task(Task::new("Get rich"));
259 /// project.add_task(Task::new("Become world leader"));
260 /// project.add_time_relationship(0, 1, TimeRelationship::default());
261 ///
262 /// assert_eq!(project.successors(0).next().unwrap().name(), "Become world leader")
263 /// ```
264 pub fn add_time_relationship(
265 &mut self,
266 predecessor_index: usize,
267 successor_index: usize,
268 kind: TimeRelationship,
269 ) -> anyhow::Result<()> {
270 self.tasks
271 .update_edge(predecessor_index.into(), successor_index.into(), kind)
272 .context("Tried to add a relationship between non existing nodes")?;
273 anyhow::Ok(())
274 }
275
276 /// Removes a relationship betwen tasks, where one is the predecessor and the other one a successor.
277 ///
278 /// # Errors
279 /// TODO!
280 ///
281 /// # Example
282 ///
283 /// ```
284 /// use planter_core::{project::{Project, TimeRelationship}, task::Task};
285 ///
286 /// let mut project = Project::new("World domination");
287 /// project.add_task(Task::new("Get rich"));
288 /// project.add_task(Task::new("Become world leader"));
289 /// project.add_time_relationship(0, 1, TimeRelationship::default());
290 /// project.remove_time_relationship(0, 1);
291 ///
292 /// assert_eq!(project.successors(0).count(), 0);
293 /// ```
294 pub fn remove_time_relationship(
295 &mut self,
296 predecessor_index: usize,
297 successor_index: usize,
298 ) -> anyhow::Result<()> {
299 let edge_index = self
300 .tasks
301 .find_edge(predecessor_index.into(), successor_index.into())
302 .context(
303 "Tried to remove a relationship that doesn't exist or between non existing nodes",
304 )?;
305
306 self.tasks.remove_edge(edge_index);
307 anyhow::Ok(())
308 }
309
310 /// Gets the list of successors for a given node.
311 ///
312 /// # Example
313 ///
314 /// ```
315 /// use planter_core::{project::{Project, TimeRelationship}, task::Task};
316 ///
317 /// let mut project = Project::new("World domination");
318 /// project.add_task(Task::new("Get rich"));
319 /// project.add_task(Task::new("Become world leader"));
320 /// project.add_time_relationship(0, 1, TimeRelationship::default());
321 ///
322 /// assert_eq!(project.successors(0).next().unwrap().name(), "Become world leader")
323 /// ```
324 pub fn successors(&self, node_index: usize) -> impl Iterator<Item = &Task> {
325 self.tasks
326 .neighbors_directed(node_index.into(), Direction::Outgoing)
327 .map(|index| &self.tasks[index])
328 }
329
330 /// Gets the indices of all successors for a given node.
331 ///
332 /// # Example
333 ///
334 /// ```
335 /// use planter_core::{project::{Project, TimeRelationship}, task::Task};
336 ///
337 /// let mut project = Project::new("World domination");
338 /// project.add_task(Task::new("Get rich"));
339 /// project.add_task(Task::new("Become world leader"));
340 /// project.add_time_relationship(0, 1, TimeRelationship::default());
341 ///
342 /// assert_eq!(project.successors_indices(0).next().unwrap(), 1)
343 /// ```
344 pub fn successors_indices(&self, node_index: usize) -> impl Iterator<Item = usize> {
345 self.tasks
346 .neighbors_directed(node_index.into(), Direction::Outgoing)
347 .map(|index| index.index())
348 }
349
350 /// Gets the list of predecessors for a given node.
351 ///
352 /// # Example
353 ///
354 /// ```
355 /// use planter_core::{project::{Project, TimeRelationship}, task::Task};
356 ///
357 /// let mut project = Project::new("World domination");
358 /// project.add_task(Task::new("Get rich"));
359 /// project.add_task(Task::new("Become world leader"));
360 /// project.add_time_relationship(1, 0, TimeRelationship::default());
361 ///
362 /// assert_eq!(project.predecessors(0).next().unwrap().name(), "Become world leader")
363 /// ```
364 pub fn predecessors(&self, node_index: usize) -> impl Iterator<Item = &Task> {
365 self.tasks
366 .neighbors_directed(node_index.into(), Direction::Incoming)
367 .map(|index| &self.tasks[index])
368 }
369
370 /// Gets the indices of all predecessors for a given node.
371 ///
372 /// # Example
373 ///
374 /// ```
375 /// use planter_core::{project::{Project, TimeRelationship}, task::Task};
376 ///
377 /// let mut project = Project::new("World domination");
378 /// project.add_task(Task::new("Get rich"));
379 /// project.add_task(Task::new("Become world leader"));
380 /// project.add_time_relationship(1, 0, TimeRelationship::default());
381 ///
382 /// assert_eq!(project.predecessors_indices(0).next().unwrap(), 1)
383 /// ```
384 pub fn predecessors_indices(&self, node_index: usize) -> impl Iterator<Item = usize> {
385 self.tasks
386 .neighbors_directed(node_index.into(), Direction::Incoming)
387 .map(|index| index.index())
388 }
389
390 /// Updates the project by making sure the predecessors for the task with
391 /// index `node_index` are exactly the ones listed in `predecessors_indices`
392 ///
393 /// # Arguments
394 ///
395 /// * `task_index` - The index whose predecessors need to be updated.
396 /// * `predecessors_indices` - The indices of the predecessors.
397 ///
398 /// # Errors
399 /// TODO!
400 ///
401 /// # Example
402 ///
403 /// ```
404 /// use planter_core::{project::Project, task::Task};
405 ///
406 /// let tasks = vec![
407 /// Task::new("Become world leader"),
408 /// Task::new("Get rich"),
409 /// Task::new("Be evil")
410 /// ];
411 /// let mut project = Project::builder().name("World domination").tasks(tasks).build();
412 ///
413 /// project.update_predecessors(2, &[0, 1]);
414 /// assert_eq!(project.predecessors(2).count(), 2);
415 pub fn update_predecessors(
416 &mut self,
417 task_index: usize,
418 predecessors_indices: &[usize],
419 ) -> anyhow::Result<()> {
420 self.validate_indices(task_index, predecessors_indices)?;
421
422 // Update predecessors in a cloned data structure for tasks.
423 // If this gives an error, the actual data structure won't be polluted.
424 // TODO: benchmark and see if there is a better way to do this without cloning.
425 let mut tasks_clone = self.tasks.clone();
426 for &i in predecessors_indices {
427 tasks_clone
428 .add_edge(i.into(), task_index.into(), TimeRelationship::FinishToStart)
429 .context(format!(
430 "A cycle was detected between tasks {i} and {task_index}"
431 ))?;
432 }
433
434 // Remove all predecessors.
435 for i in self
436 .predecessors_indices(task_index)
437 .collect::<Vec<usize>>()
438 {
439 self.remove_time_relationship(i, task_index)
440 .context("It should have been possible to remove a predecessor. This is a bug.")?;
441 }
442 // Update predecessors.
443 for &i in predecessors_indices {
444 self.tasks
445 .add_edge(i.into(), task_index.into(), TimeRelationship::FinishToStart)
446 .context("This shouldn't have happened because the data structure was just checked for cycles.")?;
447 }
448 Ok(())
449 }
450
451 /// Checks that all the tasks with indices passed as parameters actually exist in the project.
452 fn validate_indices(&self, task_index: usize, related_indices: &[usize]) -> anyhow::Result<()> {
453 let graph_edges: HashSet<usize> =
454 self.tasks.node_identifiers().map(|i| i.index()).collect();
455 // Make sure all the listed predecessors exist within the graph.
456 if !related_indices.iter().all(|i| graph_edges.contains(i)) {
457 bail!("Some index in the predecessors list doesn't exist in the graph");
458 }
459
460 // Make sure the task index exists withing the graph.
461 if !graph_edges.contains(&task_index) {
462 bail!(format!(
463 "Task index {task_index} doesn't exist in the graph"
464 ));
465 }
466
467 Ok(())
468 }
469
470 /// Updates the project by making sure the successors for the task with
471 /// index `node_index` are exactly the ones listed in `successors_indices`
472 ///
473 /// # Arguments
474 ///
475 /// * `task_index` - The index whose successors need to be updated.
476 /// * `successors_indices` - The indices of the successors.
477 ///
478 /// # Errors
479 /// TODO!
480 ///
481 /// # Example
482 ///
483 /// ```
484 /// use planter_core::{project::Project, task::Task};
485 ///
486 /// let tasks = vec![
487 /// Task::new("Become world leader"),
488 /// Task::new("Get rich"),
489 /// Task::new("Be evil")
490 /// ];
491 /// let mut project = Project::builder().name("World domination").tasks(tasks).build();
492 ///
493 /// project.update_successors(0, &[1, 2]);
494 /// assert_eq!(project.successors(0).count(), 2);
495 pub fn update_successors(
496 &mut self,
497 task_index: usize,
498 successors_indices: &[usize],
499 ) -> anyhow::Result<()> {
500 self.validate_indices(task_index, successors_indices)?;
501
502 // Update successors in a cloned data structure for tasks.
503 // If this gives an error, the actual data structure won't be polluted.
504 // TODO: benchmark and see if there is a better way to do this without cloning.
505 let mut tasks_clone = self.tasks.clone();
506 for &i in successors_indices {
507 tasks_clone
508 .add_edge(task_index.into(), i.into(), TimeRelationship::FinishToStart)
509 .context(format!(
510 "A cycle was detected between tasks {i} and {task_index}"
511 ))?;
512 }
513
514 // Remove all successors.
515 for i in self.successors_indices(task_index).collect::<Vec<usize>>() {
516 self.remove_time_relationship(task_index, i)
517 .context("It should have been possible to remove a predecessor. This is a bug.")?;
518 }
519 // Update successors.
520 for &i in successors_indices {
521 self.tasks
522 .add_edge( task_index.into(), i.into(), TimeRelationship::FinishToStart)
523 .context("This shouldn't have happened because the data structure was just checked for cycles.")?;
524 }
525 Ok(())
526 }
527
528 /// Adds a subtask to a given task. This relationship means that the parent
529 /// task is completed when all the children are completed. Also, the parent's cost
530 /// and duration is the cumulative cost and duration of the children.
531 ///
532 /// Example
533 ///
534 /// ```
535 /// use planter_core::{project::Project, task::Task};
536 ///
537 /// let tasks = vec![
538 /// Task::new("Become world leader"),
539 /// Task::new("Get rich"),
540 /// Task::new("Be evil")
541 /// ];
542 /// let mut project = Project::builder().name("World domination").tasks(tasks).build();
543 ///
544 /// project.add_subtask(0, 1);
545 /// project.add_subtask(0, 2);
546 /// assert_eq!(project.subtasks(0).len(), 2);
547 /// ```
548 pub fn add_subtask(&mut self, parent_index: usize, child_index: usize) {
549 self.subtask_relationships.push(SubtaskRelationship {
550 task: parent_index,
551 subtask: child_index,
552 });
553 }
554
555 /// Gets a list of all the subtasks of the given task.
556 ///
557 /// Example
558 ///
559 /// ```
560 /// use planter_core::{project::Project, task::Task};
561 ///
562 /// let mut project = Project::new("World domination");
563 /// project.add_task(Task::new("Become world leader"));
564 /// project.add_task(Task::new("Get rich"));
565 /// assert_eq!(project.subtasks(0).len(), 0);
566 ///
567 /// project.add_subtask(0, 1);
568 /// assert_eq!(project.subtasks(0).len(), 1);
569 /// ```
570 pub fn subtasks(&self, task_index: usize) -> Vec<usize> {
571 self.subtask_relationships
572 .iter()
573 .filter(|r| r.task == task_index)
574 .map(|r| r.subtask)
575 .collect()
576 }
577
578 /// Returns the start date of the project.
579 ///
580 /// # Example
581 ///
582 /// ```
583 /// use planter_core::{project::Project};
584 /// use chrono::Utc;
585 ///
586 /// let start_date = Utc::now();
587 /// let mut project = Project::builder().name("World domination").start_date(start_date).build();
588 /// assert_eq!(project.start_date(), Some(start_date));
589 /// ```
590 pub fn start_date(&self) -> Option<DateTime<Utc>> {
591 self.start_date
592 }
593
594 /// Adds a resource to the project.
595 ///
596 /// # Arguments
597 ///
598 /// * `resource` - The resource to add to the project.
599 ///
600 /// # Example
601 ///
602 /// ```
603 /// use planter_core::{resources::Resource, project::Project, person::Person};
604 ///
605 /// let mut project = Project::new("World domination");
606 /// project.add_resource(Resource::Personnel {
607 /// person: Person::new("Sebastiano", "Giordano").unwrap(),
608 /// hourly_rate: None,
609 /// });
610 /// assert_eq!(project.resources().len(), 1);
611 /// ```
612 pub fn add_resource(&mut self, resource: Resource) {
613 self.resources.push(resource);
614 }
615
616 /// Get a reference to a resource used in the project.
617 ///
618 /// # Example
619 ///
620 /// ```
621 /// use planter_core::{resources::Resource, project::Project, person::Person};
622 ///
623 /// let mut project = Project::new("World domination");
624 /// project.add_resource(Resource::Personnel {
625 /// person: Person::new("Sebastiano", "Giordano").unwrap(),
626 /// hourly_rate: None,
627 /// });
628 ///
629 /// assert!(project.resource(0).is_some());
630 /// ```
631 pub fn resource(&self, index: usize) -> Option<&Resource> {
632 self.resources.get(index)
633 }
634
635 /// Remove a resource from the project.
636 ///
637 /// # Panics
638 ///
639 /// Panics if the resource index is out of bounds.
640 ///
641 /// # Example
642 ///
643 /// ```
644 /// use planter_core::{resources::Resource, project::Project, person::Person};
645 ///
646 /// let mut project = Project::new("World domination");
647 /// project.add_resource(Resource::Personnel {
648 /// person: Person::new("Sebastiano", "Giordano").unwrap(),
649 /// hourly_rate: None,
650 /// });
651 ///
652 /// assert!(project.resource(0).is_some());
653 /// project.rm_resource(0);
654 /// assert!(project.resource(0).is_none());
655 ///
656 /// let result = std::panic::catch_unwind(move || {
657 /// project.rm_resource(0);
658 /// });
659 /// assert!(result.is_err());
660 /// ```
661 pub fn rm_resource(&mut self, index: usize) -> Resource {
662 self.resources.remove(index)
663 }
664
665 /// Get a mutable reference to a resource used in the project.
666 ///
667 /// # Example
668 ///
669 /// ```
670 /// use planter_core::{resources::Resource, project::Project, person::Person};
671 ///
672 /// let mut project = Project::new("World domination");
673 /// project.add_resource(Resource::Personnel {
674 /// person: Person::new("Sebastiano", "Giordano").unwrap(),
675 /// hourly_rate: None,
676 /// });
677 ///
678 /// let resource = project.resource_mut(0).unwrap();
679 /// match resource {
680 /// Resource::Material(_) => panic!(),
681 /// Resource::Personnel {
682 /// person,
683 /// ..
684 /// } => {
685 /// person.update_first_name("Alessandro");
686 /// assert_eq!(person.first_name(), "Alessandro");
687 /// }
688 /// }
689 /// ```
690 pub fn resource_mut(&mut self, index: usize) -> Option<&mut Resource> {
691 self.resources.get_mut(index)
692 }
693
694 /// Returns a reference to the list of resources associated with the project.
695 ///
696 /// # Example
697 ///
698 /// ```
699 /// use planter_core::{resources::{Resource, Material, NonConsumable}, project::Project};
700 ///
701 /// let mut project = Project::new("World domination");
702 /// project.add_resource(Resource::Material(Material::NonConsumable(
703 /// NonConsumable::new("Crowbar"),
704 /// )));
705 /// assert_eq!(project.resources().len(), 1);
706 /// ```
707 pub fn resources(&self) -> &[Resource] {
708 &self.resources
709 }
710
711 /// Converts a resource into a `Consumable`, if that's possible.
712 ///
713 /// # Arguments
714 ///
715 /// * resource_index - The index of a `Material`, that's not a `Consumable`
716 ///
717 /// # Errors
718 ///
719 /// * `ResourceConversionError::ResourceNotFound` - If a resource with the specified index does not exist.
720 /// * `ResourceConversionError::ConversionNotPossible` - When trying to convert a resource that's not a `Material`.
721 ///
722 /// # Example
723 ///
724 /// ```
725 /// use planter_core::{resources::{Resource, Material, NonConsumable}, project::Project};
726 ///
727 /// let mut project = Project::new("World domination");
728 /// project.add_resource(Resource::Material(Material::NonConsumable(
729 /// NonConsumable::new("Crowbar"),
730 /// )));
731 /// assert!(project.res_into_consumable(0).is_ok());
732 /// ```
733 ///
734 pub fn res_into_consumable(
735 &mut self,
736 resource_index: usize,
737 ) -> Result<(), ResourceConversionError> {
738 let res = self
739 .resources
740 .get_mut(resource_index)
741 .ok_or(ResourceConversionError::ResourceNotFound)?;
742 match res {
743 Resource::Material(Material::NonConsumable(non_consumable)) => {
744 *res = Resource::Material(Material::Consumable(non_consumable.clone().into()))
745 }
746 Resource::Material(Material::Consumable(_)) => {}
747 _ => return Err(ResourceConversionError::ConversionNotPossible),
748 }
749 Ok(())
750 }
751
752 /// Converts a resource into a `NonConsumable`, if that's possible.
753 ///
754 /// # Arguments
755 ///
756 /// * resource_index - The index of a `Material`, that's not a `NonConsumable`
757 ///
758 /// # Errors
759 ///
760 /// * `ResourceConversionError::ResourceNotFound` - If a resource with the specified index does not exist.
761 /// * `ResourceConversionError::ConversionNotPossible` - When trying to convert a resource that's not a `Material`.
762 ///
763 /// # Example
764 ///
765 /// ```
766 /// use planter_core::{resources::{Resource, Material, Consumable}, project::Project};
767 ///
768 /// let mut project = Project::new("World domination");
769 /// project.add_resource(Resource::Material(Material::Consumable(
770 /// Consumable::new("Stimpack"),
771 /// )));
772 /// assert!(project.res_into_nonconsumable(0).is_ok());
773 /// ```
774 pub fn res_into_nonconsumable(
775 &mut self,
776 resource_index: usize,
777 ) -> Result<(), ResourceConversionError> {
778 let res = self
779 .resources
780 .get_mut(resource_index)
781 .ok_or(ResourceConversionError::ResourceNotFound)?;
782
783 match res {
784 Resource::Material(Material::Consumable(consumable)) => {
785 *res = Resource::Material(Material::NonConsumable(consumable.clone().into()));
786 }
787 Resource::Material(Material::NonConsumable(_)) => {}
788 _ => return Err(ResourceConversionError::ConversionNotPossible),
789 }
790 Ok(())
791 }
792
793 /// Adds a stakeholder to the project.
794 ///
795 /// # Arguments
796 ///
797 /// * `stakeholder` - The stakeholder to add to the project.
798 ///
799 /// # Example
800 ///
801 /// ```
802 /// use planter_core::{stakeholders::Stakeholder, project::Project, person::Person};
803 ///
804 /// let mut project = Project::new("World domination");
805 /// let person = Person::new("Margherita", "Hack").unwrap();
806 /// project.add_stakeholder(Stakeholder::Individual {
807 /// person,
808 /// description: None,
809 /// });
810 /// assert_eq!(project.stakeholders().len(), 1);
811 /// ```
812 pub fn add_stakeholder(&mut self, stakeholder: Stakeholder) {
813 self.stakeholders.push(stakeholder);
814 }
815
816 /// Returns a reference to the list of stakeholders associated with the project.
817 ///
818 /// # Example
819 ///
820 /// ```
821 /// use planter_core::{stakeholders::Stakeholder, project::Project, person::Person};
822 ///
823 /// let mut project = Project::new("World domination");
824 /// let person = Person::new("Margherita", "Hack").unwrap();
825 /// project.add_stakeholder(Stakeholder::Individual {
826 /// person,
827 /// description: None,
828 /// });
829 /// assert_eq!(project.stakeholders().len(), 1);
830 /// ```
831 pub fn stakeholders(&self) -> &[Stakeholder] {
832 &self.stakeholders
833 }
834}
835
836/// Represents an error that can occur when trying to convert `Material` resources variants to another variant.
837#[derive(Error, Debug, PartialEq, Eq)]
838pub enum ResourceConversionError {
839 /// Used when trying to convert a resource with an index out of bounds.
840 #[error("The resource with the specified index wasn't found")]
841 ResourceNotFound,
842 /// Used when trying to convert a resource that's not a material, for example personnel.
843 #[error("Tried to convert a resource that's not a material")]
844 ConversionNotPossible,
845}
846
847#[cfg(test)]
848/// Utilities to test `[Project]`
849pub mod test_utils {
850 use proptest::{collection, prelude::*};
851
852 use crate::task::{Task, test_utils::task_strategy};
853
854 use super::{Project, TimeRelationship};
855
856 const MAX_TASKS: usize = 100;
857 const MIN_TASKS: usize = 5;
858
859 /// Generates a random task relationship kind.
860 pub fn task_relationship_strategy() -> impl Strategy<Value = TimeRelationship> {
861 prop_oneof![
862 Just(TimeRelationship::StartToFinish),
863 Just(TimeRelationship::StartToStart),
864 Just(TimeRelationship::FinishToFinish),
865 Just(TimeRelationship::FinishToStart),
866 ]
867 }
868
869 /// Generate a random amount of randomly generated `[Tasks]`, between `[MIN_TASKS]` and `[MAX_TASKS]`.
870 pub fn tasks_strategy() -> impl Strategy<Value = Vec<Task>> {
871 collection::vec(task_strategy(), MIN_TASKS..MAX_TASKS)
872 }
873
874 /// Generate a random `[Project]` where every node is connected to the previous one.
875 pub fn project_graph_strategy() -> impl Strategy<Value = Project> {
876 (".*", tasks_strategy()).prop_map(|(n, tasks)| {
877 let indices = 0..tasks.len();
878 let mut project = Project::builder().name(n).tasks(tasks).build();
879
880 let mut previous = None;
881 indices.for_each(|current| {
882 if let Some(prev) = previous {
883 project.update_successors(prev, &[current]).unwrap();
884 }
885 previous = Some(current);
886 });
887 project
888 })
889 }
890
891 /// Generate a random `[Project]` with `[Task]`s, but no predecessors/successors relationships.
892 pub fn project_strategy() -> impl Strategy<Value = Project> {
893 (".*", tasks_strategy())
894 .prop_map(|(n, tasks)| Project::builder().name(n).tasks(tasks).build())
895 }
896}
897
898#[cfg(test)]
899mod tests {
900 use proptest::prelude::*;
901 use rand::{Rng, rng};
902
903 use crate::{
904 person::Person,
905 project::{
906 Project, ResourceConversionError,
907 test_utils::{project_graph_strategy, project_strategy},
908 },
909 resources::{Consumable, Material, NonConsumable, Resource},
910 };
911 proptest! {
912 #[test]
913 fn update_predecessors_rejects_circular_graphs(mut project in project_graph_strategy()) {
914 assert!(project.update_predecessors(0, &[project.tasks().count() - 1]).is_err());
915 }
916
917 #[test]
918 fn update_successors_rejects_circular_graphs(mut project in project_graph_strategy()) {
919 assert!(project.update_successors(project.tasks().count() - 1, &[0] ).is_err());
920 }
921
922 #[test]
923 fn update_predecessors_rejects_non_existent_indices(mut project in project_strategy()) {
924 let count: usize = project.tasks().count();
925
926 assert!(project.update_predecessors(0, &[count]).is_err())
927 }
928
929 #[test]
930 fn update_successors_rejects_non_existent_indices(mut project in project_strategy()) {
931 let count: usize = project.tasks().count();
932
933 assert!(project.update_successors(0, &[count]).is_err())
934 }
935
936 #[test]
937 fn update_predecessors_removes_them_if_input_is_empty(mut project in project_strategy()) {
938 let mut rng = rng();
939 let task_index1 = rng.random_range(0..project.tasks().count());
940 let mut task_index2 = task_index1;
941
942 while task_index2 == task_index1 {
943 task_index2 = rng.random_range(0..project.tasks().count());
944 }
945
946 project.update_predecessors(task_index1, &[task_index2]).unwrap();
947 project.update_predecessors(task_index1, &[]).unwrap();
948
949 assert_eq!(project.predecessors(task_index1).count(), 0);
950 }
951
952 #[test]
953 fn update_predecessors_removes_indices_not_present_in_input(mut project in project_strategy()) {
954 let mut rng = rng();
955 let task_index1 = rng.random_range(0..project.tasks().count());
956 let mut task_index2 = task_index1;
957 let mut task_index3 = task_index1;
958
959 while task_index2 == task_index1 {
960 task_index2 = rng.random_range(0..project.tasks().count());
961 }
962 while task_index3 == task_index1 || task_index3 == task_index2 {
963 task_index3 = rng.random_range(0..project.tasks().count());
964 }
965
966 project.update_predecessors(task_index1, &[task_index2, task_index3]).unwrap();
967 project.update_predecessors(task_index1, &[task_index2]).unwrap();
968
969 let mut predecessors = project.predecessors(task_index1);
970 assert_eq!(predecessors.next(), project.task(task_index2));
971 assert!(predecessors.next().is_none());
972 }
973
974 #[test]
975 fn update_predecessors_works(mut project in project_strategy()) {
976 let mut rng = rng();
977 let task_index1 = rng.random_range(0..project.tasks().count());
978 let mut task_index2 = task_index1;
979
980 while task_index2 == task_index1 {
981 task_index2 = rng.random_range(0..project.tasks().count());
982 }
983
984 project.update_predecessors(task_index1, &[task_index2]).unwrap();
985
986 let mut predecessors = project.predecessors(task_index1);
987 assert_eq!(project.predecessors(task_index1).count(), 1);
988 assert_eq!(predecessors.next(), project.task(task_index2));
989 }
990
991 #[test]
992 fn update_successors_works(mut project in project_strategy()) {
993 let mut rng = rng();
994 let task_index1 = rng.random_range(0..project.tasks().count());
995 let mut task_index2 = task_index1;
996
997 while task_index2 == task_index1 {
998 task_index2 = rng.random_range(0..project.tasks().count());
999 }
1000
1001 project.update_successors(task_index1, &[task_index2]).unwrap();
1002
1003 let mut successors = project.successors(task_index1);
1004 assert_eq!(successors.next(), project.task(task_index2));
1005 assert!(successors.next().is_none());
1006 }
1007
1008 #[test]
1009 fn update_successors_removes_them_if_input_is_empty(mut project in project_strategy()) {
1010 let mut rng = rng();
1011 let task_index1 = rng.random_range(0..project.tasks().count());
1012 let mut task_index2 = task_index1;
1013
1014 while task_index2 == task_index1 {
1015 task_index2 = rng.random_range(0..project.tasks().count());
1016 }
1017
1018 project.update_successors(task_index1, &[task_index2]).unwrap();
1019 project.update_successors(task_index1, &[]).unwrap();
1020
1021 assert_eq!(project.successors(task_index1).count(), 0);
1022 }
1023
1024 #[test]
1025 fn update_successors_removes_indices_not_present_in_input(mut project in project_strategy()) {
1026 let mut rng = rng();
1027 let task_index1 = rng.random_range(0..project.tasks().count());
1028 let mut task_index2 = task_index1;
1029 let mut task_index3 = task_index1;
1030
1031 while task_index2 == task_index1 {
1032 task_index2 = rng.random_range(0..project.tasks().count());
1033 }
1034 while task_index3 == task_index1 || task_index3 == task_index2 {
1035 task_index3 = rng.random_range(0..project.tasks().count());
1036 }
1037
1038 project.update_successors(task_index1, &[task_index2, task_index3]).unwrap();
1039 project.update_successors(task_index1, &[task_index2]).unwrap();
1040
1041 let mut successors = project.successors(task_index1);
1042 assert_eq!(successors.next(), project.task(task_index2));
1043 assert!(successors.next().is_none());
1044 }
1045 }
1046
1047 #[test]
1048 fn res_into_consumable_returns_the_correct_errors() {
1049 let mut project = Project::new("World domination");
1050
1051 project.add_resource(Resource::Personnel {
1052 person: Person::new("Sebastiano", "Giordano").unwrap(),
1053 hourly_rate: None,
1054 });
1055
1056 // The correct error when trying to convert a reasource that's not a `Material`.
1057 assert_eq!(
1058 project.res_into_consumable(0),
1059 Err(ResourceConversionError::ConversionNotPossible)
1060 );
1061
1062 // The correct error when trying to convert a reasource that does not exist.
1063 assert_eq!(
1064 project.res_into_consumable(1),
1065 Err(ResourceConversionError::ResourceNotFound)
1066 );
1067
1068 // Doesn't return an error when trying to convert a `Consumable` into a `Consumable`.
1069 project.add_resource(Resource::Material(Material::Consumable(Consumable::new(
1070 "Stimpack",
1071 ))));
1072
1073 assert!(project.res_into_consumable(1).is_ok());
1074
1075 // Doesn't change the type of the resource when not needed.
1076 if let Resource::Material(Material::Consumable(_)) = project.resources()[1] {
1077 } else {
1078 panic!("It changed the resource type");
1079 }
1080
1081 // It changes it when needed.
1082 project.add_resource(Resource::Material(Material::NonConsumable(
1083 NonConsumable::new("Crowbar"),
1084 )));
1085 project.res_into_consumable(2).unwrap();
1086 if let Resource::Material(Material::Consumable(_)) = project.resources()[2] {
1087 } else {
1088 panic!("It didn't change the resource type");
1089 }
1090 }
1091
1092 #[test]
1093 fn res_into_nonconsumable_returns_the_correct_errors() {
1094 let mut project = Project::new("World domination");
1095
1096 project.add_resource(Resource::Personnel {
1097 person: Person::new("Sebastiano", "Giordano").unwrap(),
1098 hourly_rate: None,
1099 });
1100
1101 // The correct error when trying to convert a resource that's not a `Material`.
1102 assert_eq!(
1103 project.res_into_nonconsumable(0),
1104 Err(ResourceConversionError::ConversionNotPossible)
1105 );
1106
1107 // The correct error when trying to convert a resource that does not exist.
1108 assert_eq!(
1109 project.res_into_nonconsumable(1),
1110 Err(ResourceConversionError::ResourceNotFound)
1111 );
1112
1113 // Doesn't return an error when trying to convert a `NonConsumable` into a `NonConsumable`.
1114 project.add_resource(Resource::Material(Material::NonConsumable(
1115 NonConsumable::new("Crowbar"),
1116 )));
1117
1118 assert!(project.res_into_nonconsumable(1).is_ok());
1119
1120 // Doesn't change the type of the resource when not needed.
1121 if let Resource::Material(Material::NonConsumable(_)) = project.resources()[1] {
1122 } else {
1123 panic!("It changed the resource type");
1124 }
1125
1126 // It changes it when needed.
1127 project.add_resource(Resource::Material(Material::Consumable(Consumable::new(
1128 "Stimpack",
1129 ))));
1130 project.res_into_nonconsumable(2).unwrap();
1131 if let Resource::Material(Material::NonConsumable(_)) = project.resources()[2] {
1132 } else {
1133 panic!("It didn't change the resource type");
1134 }
1135 }
1136}