1use serde::{Deserialize, Serialize};
17use std::collections::HashMap;
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct Activity {
26 pub id: String,
28 pub task_id: String,
30 pub sequence: i32,
32 pub duration: ActivityDuration,
34 pub resource_requirements: Vec<ResourceRequirement>,
36 pub predecessors: Vec<String>,
38 pub splittable: bool,
40 pub min_split_ms: i64,
42 pub attributes: HashMap<String, String>,
44}
45
46impl Activity {
47 pub fn new(id: impl Into<String>, task_id: impl Into<String>, sequence: i32) -> Self {
49 Self {
50 id: id.into(),
51 task_id: task_id.into(),
52 sequence,
53 duration: ActivityDuration::default(),
54 resource_requirements: Vec::new(),
55 predecessors: Vec::new(),
56 splittable: false,
57 min_split_ms: 0,
58 attributes: HashMap::new(),
59 }
60 }
61
62 pub fn with_duration(mut self, duration: ActivityDuration) -> Self {
64 self.duration = duration;
65 self
66 }
67
68 pub fn with_process_time(mut self, process_ms: i64) -> Self {
70 self.duration = ActivityDuration::fixed(process_ms);
71 self
72 }
73
74 pub fn with_requirement(mut self, req: ResourceRequirement) -> Self {
76 self.resource_requirements.push(req);
77 self
78 }
79
80 pub fn with_predecessor(mut self, predecessor_id: impl Into<String>) -> Self {
82 self.predecessors.push(predecessor_id.into());
83 self
84 }
85
86 pub fn with_splitting(mut self, min_split_ms: i64) -> Self {
88 self.splittable = true;
89 self.min_split_ms = min_split_ms;
90 self
91 }
92
93 pub fn candidate_resources(&self) -> Vec<&str> {
95 self.resource_requirements
96 .iter()
97 .flat_map(|r| r.candidates.iter().map(|s| s.as_str()))
98 .collect()
99 }
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct ActivityDuration {
111 pub setup_ms: i64,
113 pub process_ms: i64,
115 pub teardown_ms: i64,
117}
118
119impl ActivityDuration {
120 pub fn new(setup_ms: i64, process_ms: i64, teardown_ms: i64) -> Self {
122 Self {
123 setup_ms,
124 process_ms,
125 teardown_ms,
126 }
127 }
128
129 pub fn fixed(process_ms: i64) -> Self {
131 Self::new(0, process_ms, 0)
132 }
133
134 pub fn total_ms(&self) -> i64 {
136 self.setup_ms + self.process_ms + self.teardown_ms
137 }
138}
139
140impl Default for ActivityDuration {
141 fn default() -> Self {
142 Self::fixed(0)
143 }
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct ResourceRequirement {
152 pub resource_type: String,
154 pub quantity: i32,
156 pub candidates: Vec<String>,
159 pub required_skills: Vec<String>,
161}
162
163impl ResourceRequirement {
164 pub fn new(resource_type: impl Into<String>) -> Self {
166 Self {
167 resource_type: resource_type.into(),
168 quantity: 1,
169 candidates: Vec::new(),
170 required_skills: Vec::new(),
171 }
172 }
173
174 pub fn with_quantity(mut self, quantity: i32) -> Self {
176 self.quantity = quantity;
177 self
178 }
179
180 pub fn with_candidates(mut self, candidates: Vec<String>) -> Self {
182 self.candidates = candidates;
183 self
184 }
185
186 pub fn with_skill(mut self, skill: impl Into<String>) -> Self {
188 self.required_skills.push(skill.into());
189 self
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196
197 #[test]
198 fn test_activity_builder() {
199 let act = Activity::new("O1", "J1", 0)
200 .with_duration(ActivityDuration::new(100, 500, 50))
201 .with_requirement(ResourceRequirement::new("Machine").with_quantity(1))
202 .with_predecessor("O0")
203 .with_splitting(200);
204
205 assert_eq!(act.id, "O1");
206 assert_eq!(act.task_id, "J1");
207 assert_eq!(act.sequence, 0);
208 assert_eq!(act.duration.total_ms(), 650);
209 assert_eq!(act.resource_requirements.len(), 1);
210 assert_eq!(act.predecessors, vec!["O0"]);
211 assert!(act.splittable);
212 assert_eq!(act.min_split_ms, 200);
213 }
214
215 #[test]
216 fn test_activity_duration_fixed() {
217 let d = ActivityDuration::fixed(1000);
218 assert_eq!(d.setup_ms, 0);
219 assert_eq!(d.process_ms, 1000);
220 assert_eq!(d.teardown_ms, 0);
221 assert_eq!(d.total_ms(), 1000);
222 }
223
224 #[test]
225 fn test_activity_duration_components() {
226 let d = ActivityDuration::new(100, 500, 50);
227 assert_eq!(d.total_ms(), 650);
228 }
229
230 #[test]
231 fn test_resource_requirement() {
232 let req = ResourceRequirement::new("CNC")
233 .with_quantity(2)
234 .with_candidates(vec!["M1".into(), "M2".into(), "M3".into()])
235 .with_skill("milling");
236
237 assert_eq!(req.resource_type, "CNC");
238 assert_eq!(req.quantity, 2);
239 assert_eq!(req.candidates.len(), 3);
240 assert_eq!(req.required_skills, vec!["milling"]);
241 }
242
243 #[test]
244 fn test_candidate_resources() {
245 let act = Activity::new("O1", "J1", 0)
246 .with_requirement(
247 ResourceRequirement::new("Machine").with_candidates(vec!["M1".into(), "M2".into()]),
248 )
249 .with_requirement(
250 ResourceRequirement::new("Operator").with_candidates(vec!["W1".into()]),
251 );
252
253 let candidates = act.candidate_resources();
254 assert_eq!(candidates.len(), 3);
255 assert!(candidates.contains(&"M1"));
256 assert!(candidates.contains(&"W1"));
257 }
258}