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}