1use std::collections::{HashMap, HashSet};
4use std::fmt;
5use std::sync::Arc;
6
7use serde::de::DeserializeOwned;
8use serde_json::Value;
9use sha2::{Digest, Sha256};
10
11use super::error::ReportingError;
12use super::task::{TaskContext, TaskResult, Workload};
13use crate::configuration::{ProjectConfig, TaskConfig};
14use crate::project::ScientificProject;
15
16#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
18pub struct PhaseId(u64);
19
20impl PhaseId {
21 pub const fn new(value: u64) -> Self {
24 Self(value)
25 }
26
27 pub const fn get(self) -> u64 {
29 self.0
30 }
31}
32
33impl From<u64> for PhaseId {
34 fn from(value: u64) -> Self {
35 Self::new(value)
36 }
37}
38
39impl fmt::Display for PhaseId {
40 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
41 self.0.fmt(formatter)
42 }
43}
44
45#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
47pub struct TaskId(Arc<str>);
48
49impl TaskId {
50 pub fn new(value: impl Into<String>) -> Self {
53 Self(value.into().into())
54 }
55
56 pub fn as_str(&self) -> &str {
58 &self.0
59 }
60}
61
62impl fmt::Display for TaskId {
63 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
64 self.0.fmt(formatter)
65 }
66}
67
68#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
70pub struct TaskKey {
71 phase: PhaseId,
72 task: TaskId,
73}
74
75impl TaskKey {
76 pub fn new(phase: impl Into<PhaseId>, task: TaskId) -> Self {
78 Self {
79 phase: phase.into(),
80 task,
81 }
82 }
83
84 pub const fn phase_id(&self) -> PhaseId {
86 self.phase
87 }
88
89 pub fn task_id(&self) -> &TaskId {
91 &self.task
92 }
93}
94
95impl fmt::Display for TaskKey {
96 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
97 write!(formatter, "{}/{}", self.phase, self.task)
98 }
99}
100
101#[derive(Clone, Copy, Debug, Eq, PartialEq)]
103#[non_exhaustive]
104pub enum TaskDisplayKind {
105 Progress,
107 Activity,
109}
110
111pub struct Task {
113 key: TaskKey,
114 kind: Arc<str>,
115 configuration: TaskConfig,
116 display_kind: TaskDisplayKind,
117 label: Arc<str>,
118 display_keys: Option<Arc<[Box<str>]>>,
119 workload: Option<Workload>,
120 reused: bool,
121}
122
123impl Task {
124 pub fn key(&self) -> &TaskKey {
126 &self.key
127 }
128
129 pub fn id(&self) -> &TaskId {
131 self.key.task_id()
132 }
133
134 pub fn kind(&self) -> &str {
136 &self.kind
137 }
138
139 pub fn label(&self) -> &str {
141 &self.label
142 }
143
144 pub const fn display_kind(&self) -> TaskDisplayKind {
146 self.display_kind
147 }
148
149 pub fn configuration_ordinal(&self) -> u64 {
151 self.configuration.task_ordinal()
152 }
153
154 pub fn configuration(&self) -> &TaskConfig {
156 &self.configuration
157 }
158
159 pub fn value(&self, key: &str) -> Option<&Value> {
161 self.configuration.value(key)
162 }
163
164 pub fn require_value(&self, key: &str) -> Result<&Value, ReportingError> {
166 self.value(key)
167 .ok_or_else(|| ReportingError::UnknownManagedTaskParameter {
168 task: self.key.to_string(),
169 key: key.to_owned(),
170 })
171 }
172
173 pub fn decode_value<T>(&self, key: &str) -> Result<T, ReportingError>
175 where
176 T: DeserializeOwned,
177 {
178 T::deserialize(self.require_value(key)?).map_err(|source| {
179 ReportingError::DecodeManagedTaskParameter {
180 task: self.key.to_string(),
181 key: key.to_owned(),
182 source,
183 }
184 })
185 }
186
187 pub fn iter(&self) -> impl Iterator<Item = (&str, &Value)> + '_ {
189 self.configuration.parameters().iter()
190 }
191
192 pub fn display_keys(&self) -> Option<&[Box<str>]> {
194 self.display_keys.as_deref()
195 }
196
197 pub(crate) fn take_workload(&mut self) -> Option<Workload> {
198 self.workload.take()
199 }
200
201 pub(crate) fn has_workload(&self) -> bool {
202 self.workload.is_some()
203 }
204
205 pub(crate) const fn is_reused(&self) -> bool {
206 self.reused
207 }
208
209 fn attach_to_phase(&mut self, phase: PhaseId) {
210 self.key.phase = phase;
211 }
212
213 fn parameter_keys(&self) -> Vec<&str> {
214 self.iter().map(|(key, _)| key).collect()
215 }
216
217 fn regenerate_label(&mut self, keys: Arc<[Box<str>]>, include_id: bool) {
218 let mut parts = Vec::with_capacity(keys.len() + usize::from(include_id));
219 for key in keys.iter() {
220 let value = self
221 .value(key)
222 .expect("validated display parameters resolve for every applicable task");
223 parts.push(format!("{key}={}", compact_value(value)));
224 }
225 if include_id {
226 parts.push(format!("id={}", self.key.task_id()));
227 }
228 self.label = if parts.is_empty() {
229 Arc::clone(&self.kind)
230 } else {
231 format!("{} {}", self.kind, parts.join(" ")).into()
232 };
233 self.display_keys = Some(keys);
234 }
235}
236
237impl fmt::Debug for Task {
238 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
239 formatter
240 .debug_struct("Task")
241 .field("key", &self.key)
242 .field("kind", &self.kind)
243 .field("label", &self.label)
244 .field("display_kind", &self.display_kind)
245 .field("parameters", &self.iter().count())
246 .finish_non_exhaustive()
247 }
248}
249
250pub struct Phase {
252 id: PhaseId,
253 label: Arc<str>,
254 tasks: Vec<Task>,
255 max_concurrent_workloads: usize,
256 queue_capacity: usize,
257 dependencies: Vec<PhaseId>,
258 failure_policy: PhaseFailurePolicy,
259 require_confirm: bool,
260}
261
262#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
264#[non_exhaustive]
265pub enum PhaseFailurePolicy {
266 #[default]
268 FailFast,
269 FinishActive,
271}
272
273impl PhaseFailurePolicy {
274 pub const fn as_str(self) -> &'static str {
276 match self {
277 Self::FailFast => "fail-fast",
278 Self::FinishActive => "finish-active",
279 }
280 }
281}
282
283impl Phase {
284 pub fn builder(id: impl Into<PhaseId>, label: impl Into<String>) -> PhaseBuilder {
286 PhaseBuilder {
287 id: id.into(),
288 label: label.into(),
289 tasks: Vec::new(),
290 display_by_kind: HashMap::new(),
291 max_concurrent_workloads: 1,
292 queue_capacity: 1,
293 dependencies: Vec::new(),
294 failure_policy: PhaseFailurePolicy::FailFast,
295 require_confirm: false,
296 }
297 }
298
299 pub const fn id(&self) -> PhaseId {
301 self.id
302 }
303
304 pub fn label(&self) -> &str {
306 &self.label
307 }
308
309 pub fn tasks(&self) -> &[Task] {
311 &self.tasks
312 }
313
314 pub const fn max_concurrent_workloads(&self) -> usize {
316 self.max_concurrent_workloads
317 }
318
319 pub const fn queue_capacity(&self) -> usize {
321 self.queue_capacity
322 }
323
324 pub fn dependencies(&self) -> &[PhaseId] {
326 &self.dependencies
327 }
328
329 pub const fn failure_policy(&self) -> PhaseFailurePolicy {
331 self.failure_policy
332 }
333
334 pub const fn requires_confirmation(&self) -> bool {
337 self.require_confirm
338 }
339
340 pub(crate) fn into_tasks(self) -> Vec<Task> {
341 self.tasks
342 }
343
344 pub fn task(&self, id: &TaskId) -> Option<&Task> {
346 self.tasks.iter().find(|task| task.key.task_id() == id)
347 }
348
349 pub fn unique_task_matching(&self, selector: &TaskSelector) -> Result<&Task, ReportingError> {
351 let mut matches = self.tasks.iter().filter(|task| selector.matches(task));
352 let first = matches
353 .next()
354 .ok_or_else(|| ReportingError::ManagedTaskNotFound {
355 selector: selector.to_string(),
356 })?;
357 if let Some(second) = matches.next() {
358 return Err(ReportingError::ManagedTaskSelectorAmbiguous {
359 selector: selector.to_string(),
360 first: first.key.to_string(),
361 second: second.key.to_string(),
362 });
363 }
364 Ok(first)
365 }
366}
367
368pub struct PhaseBuilder {
370 id: PhaseId,
371 label: String,
372 tasks: Vec<Task>,
373 display_by_kind: HashMap<String, Vec<String>>,
374 max_concurrent_workloads: usize,
375 queue_capacity: usize,
376 dependencies: Vec<PhaseId>,
377 failure_policy: PhaseFailurePolicy,
378 require_confirm: bool,
379}
380
381impl PhaseBuilder {
382 pub fn progress_workload<W>(
384 mut self,
385 configuration: TaskConfig,
386 kind: impl Into<String>,
387 workload: W,
388 ) -> Self
389 where
390 W: FnOnce(&TaskContext) -> TaskResult + Send + 'static,
391 {
392 self.push_configuration_task(
393 configuration,
394 kind.into(),
395 TaskDisplayKind::Progress,
396 Some(Box::new(workload)),
397 false,
398 );
399 self
400 }
401
402 pub fn activity_workload<W>(
404 mut self,
405 configuration: TaskConfig,
406 kind: impl Into<String>,
407 workload: W,
408 ) -> Self
409 where
410 W: FnOnce(&TaskContext) -> TaskResult + Send + 'static,
411 {
412 self.push_configuration_task(
413 configuration,
414 kind.into(),
415 TaskDisplayKind::Activity,
416 Some(Box::new(workload)),
417 false,
418 );
419 self
420 }
421
422 pub fn reused_activity(mut self, configuration: TaskConfig, kind: impl Into<String>) -> Self {
424 self.push_configuration_task(
425 configuration,
426 kind.into(),
427 TaskDisplayKind::Activity,
428 None,
429 true,
430 );
431 self
432 }
433
434 pub fn progress_tasks_from_project<W>(
440 self,
441 project: &ScientificProject,
442 kind: impl Into<String>,
443 workload: W,
444 ) -> Self
445 where
446 W: Fn(&TaskContext) -> TaskResult + Send + Sync + 'static,
447 {
448 self.progress_tasks_from_configuration(project.configuration(), kind, workload)
449 }
450
451 pub fn progress_tasks_from_configuration<W>(
453 mut self,
454 configuration: &ProjectConfig,
455 kind: impl Into<String>,
456 workload: W,
457 ) -> Self
458 where
459 W: Fn(&TaskContext) -> TaskResult + Send + Sync + 'static,
460 {
461 self.extend_shared_workload(
462 configuration,
463 kind.into(),
464 TaskDisplayKind::Progress,
465 workload,
466 );
467 self
468 }
469
470 pub fn progress_workloads_from_project<F, W>(
472 self,
473 project: &ScientificProject,
474 kind: impl Into<String>,
475 factory: F,
476 ) -> Self
477 where
478 F: Fn(&TaskConfig) -> W,
479 W: FnOnce(&TaskContext) -> TaskResult + Send + 'static,
480 {
481 self.progress_workloads_from_configuration(project.configuration(), kind, factory)
482 }
483
484 pub fn progress_workloads_from_configuration<F, W>(
486 mut self,
487 configuration: &ProjectConfig,
488 kind: impl Into<String>,
489 factory: F,
490 ) -> Self
491 where
492 F: Fn(&TaskConfig) -> W,
493 W: FnOnce(&TaskContext) -> TaskResult + Send + 'static,
494 {
495 self.extend_configuration_workloads(
496 configuration,
497 kind.into(),
498 TaskDisplayKind::Progress,
499 factory,
500 );
501 self
502 }
503
504 pub fn activity_tasks_from_project<W>(
509 self,
510 project: &ScientificProject,
511 kind: impl Into<String>,
512 workload: W,
513 ) -> Self
514 where
515 W: Fn(&TaskContext) -> TaskResult + Send + Sync + 'static,
516 {
517 self.activity_tasks_from_configuration(project.configuration(), kind, workload)
518 }
519
520 pub fn activity_tasks_from_configuration<W>(
522 mut self,
523 configuration: &ProjectConfig,
524 kind: impl Into<String>,
525 workload: W,
526 ) -> Self
527 where
528 W: Fn(&TaskContext) -> TaskResult + Send + Sync + 'static,
529 {
530 self.extend_shared_workload(
531 configuration,
532 kind.into(),
533 TaskDisplayKind::Activity,
534 workload,
535 );
536 self
537 }
538
539 pub fn activity_workloads_from_project<F, W>(
541 self,
542 project: &ScientificProject,
543 kind: impl Into<String>,
544 factory: F,
545 ) -> Self
546 where
547 F: Fn(&TaskConfig) -> W,
548 W: FnOnce(&TaskContext) -> TaskResult + Send + 'static,
549 {
550 self.activity_workloads_from_configuration(project.configuration(), kind, factory)
551 }
552
553 pub fn activity_workloads_from_configuration<F, W>(
555 mut self,
556 configuration: &ProjectConfig,
557 kind: impl Into<String>,
558 factory: F,
559 ) -> Self
560 where
561 F: Fn(&TaskConfig) -> W,
562 W: FnOnce(&TaskContext) -> TaskResult + Send + 'static,
563 {
564 self.extend_configuration_workloads(
565 configuration,
566 kind.into(),
567 TaskDisplayKind::Activity,
568 factory,
569 );
570 self
571 }
572
573 pub fn display_tasks_by<I, S>(mut self, kind: impl Into<String>, keys: I) -> Self
575 where
576 I: IntoIterator<Item = S>,
577 S: Into<String>,
578 {
579 self.display_by_kind
580 .insert(kind.into(), keys.into_iter().map(Into::into).collect());
581 self
582 }
583
584 pub fn max_concurrent_workloads(mut self, maximum: usize) -> Self {
586 self.max_concurrent_workloads = maximum;
587 self
588 }
589
590 pub fn queue_capacity(mut self, capacity: usize) -> Self {
592 self.queue_capacity = capacity;
593 self
594 }
595
596 pub fn depends_on(mut self, dependency: impl Into<PhaseId>) -> Self {
598 self.dependencies.push(dependency.into());
599 self
600 }
601
602 pub fn failure_policy(mut self, policy: PhaseFailurePolicy) -> Self {
604 self.failure_policy = policy;
605 self
606 }
607
608 pub fn require_confirm(mut self, require: bool) -> Self {
613 self.require_confirm = require;
614 self
615 }
616
617 pub fn build(mut self) -> Result<Phase, ReportingError> {
619 if self.label.trim().is_empty() {
620 return Err(ReportingError::InvalidPhaseLabel { phase: self.id.0 });
621 }
622 if self.tasks.is_empty() {
623 return Err(ReportingError::EmptyPhase { phase: self.id.0 });
624 }
625 if self.max_concurrent_workloads == 0 {
626 return Err(ReportingError::InvalidPhaseWorkloadLimit { phase: self.id.0 });
627 }
628 if self.queue_capacity == 0 {
629 return Err(ReportingError::InvalidPhaseQueueCapacity { phase: self.id.0 });
630 }
631
632 let mut ids = HashSet::with_capacity(self.tasks.len());
633 for task in &mut self.tasks {
634 if task.key.task_id().as_str().trim().is_empty() {
635 return Err(ReportingError::InvalidManagedTaskId { phase: self.id.0 });
636 }
637 if task.kind.trim().is_empty() {
638 return Err(ReportingError::InvalidManagedTaskKind {
639 task: task.key.task_id().to_string(),
640 });
641 }
642 if !ids.insert(task.key.task_id().clone()) {
643 return Err(ReportingError::DuplicateManagedTaskId {
644 phase: self.id.0,
645 task: task.key.task_id().to_string(),
646 });
647 }
648 task.attach_to_phase(self.id);
649 }
650
651 let mut dependencies = HashSet::with_capacity(self.dependencies.len());
652 for dependency in &self.dependencies {
653 if !dependencies.insert(*dependency) || *dependency == self.id {
654 return Err(ReportingError::PhaseDependencyCycle { phase: self.id.0 });
655 }
656 }
657
658 let kinds: HashSet<_> = self
659 .tasks
660 .iter()
661 .map(|task| task.kind.to_string())
662 .collect();
663 for requested in self.display_by_kind.keys() {
664 if !kinds.contains(requested) {
665 return Err(ReportingError::UnknownManagedTaskKind {
666 kind: requested.clone(),
667 });
668 }
669 }
670 for kind in kinds {
671 self.generate_labels(&kind)?;
672 }
673
674 Ok(Phase {
675 id: self.id,
676 label: self.label.into(),
677 tasks: self.tasks,
678 max_concurrent_workloads: self.max_concurrent_workloads,
679 queue_capacity: self.queue_capacity,
680 dependencies: self.dependencies,
681 failure_policy: self.failure_policy,
682 require_confirm: self.require_confirm,
683 })
684 }
685
686 fn extend_configuration_workloads<F, W>(
687 &mut self,
688 configuration: &ProjectConfig,
689 kind: String,
690 display_kind: TaskDisplayKind,
691 factory: F,
692 ) where
693 F: Fn(&TaskConfig) -> W,
694 W: FnOnce(&TaskContext) -> TaskResult + Send + 'static,
695 {
696 for config in configuration.task_configs() {
697 let workload = factory(&config);
698 self.push_configuration_task(
699 config,
700 kind.clone(),
701 display_kind,
702 Some(Box::new(workload)),
703 false,
704 );
705 }
706 }
707
708 fn extend_shared_workload<W>(
709 &mut self,
710 configuration: &ProjectConfig,
711 kind: String,
712 display_kind: TaskDisplayKind,
713 workload: W,
714 ) where
715 W: Fn(&TaskContext) -> TaskResult + Send + Sync + 'static,
716 {
717 let workload = Arc::new(workload);
718 for config in configuration.task_configs() {
719 let workload = Arc::clone(&workload);
720 self.push_configuration_task(
721 config,
722 kind.clone(),
723 display_kind,
724 Some(Box::new(move |context| workload(context))),
725 false,
726 );
727 }
728 }
729
730 fn push_configuration_task(
731 &mut self,
732 configuration: TaskConfig,
733 kind: String,
734 display_kind: TaskDisplayKind,
735 workload: Option<Workload>,
736 reused: bool,
737 ) {
738 let id = TaskId::new(format!("{kind}:{}", configuration.task_ordinal()));
739 self.tasks.push(Task {
740 key: TaskKey::new(self.id, id),
741 kind: kind.clone().into(),
742 configuration,
743 display_kind,
744 label: kind.into(),
745 display_keys: None,
746 workload,
747 reused,
748 });
749 }
750
751 fn generate_labels(&mut self, kind: &str) -> Result<(), ReportingError> {
752 let positions: Vec<_> = self
753 .tasks
754 .iter()
755 .enumerate()
756 .filter_map(|(position, task)| (task.kind() == kind).then_some(position))
757 .collect();
758 validate_parameter_keys(&self.tasks, &positions, kind)?;
759 let requested = self.display_by_kind.get(kind);
760 let keys = if let Some(requested) = requested {
761 validate_display_keys(&self.tasks, &positions, requested)?
762 } else {
763 varying_keys(&self.tasks, &positions)
764 };
765 let keys: Arc<[Box<str>]> = keys.into_iter().map(String::into_boxed_str).collect();
766 let include_id = keys.is_empty() && positions.len() > 1;
767 let mut labels = HashMap::<String, TaskKey>::with_capacity(positions.len());
768 for position in positions {
769 self.tasks[position].regenerate_label(Arc::clone(&keys), include_id);
770 let task = &self.tasks[position];
771 if let Some(first) = labels.insert(task.label().to_owned(), task.key().clone()) {
772 return Err(ReportingError::ManagedTaskDisplayCollision {
773 label: task.label().to_owned(),
774 first: first.to_string(),
775 second: task.key().to_string(),
776 });
777 }
778 }
779 Ok(())
780 }
781}
782
783impl fmt::Debug for Phase {
784 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
785 formatter
786 .debug_struct("Phase")
787 .field("id", &self.id)
788 .field("label", &self.label)
789 .field("tasks", &self.tasks.len())
790 .field("max_concurrent_workloads", &self.max_concurrent_workloads)
791 .field("queue_capacity", &self.queue_capacity)
792 .field("dependencies", &self.dependencies)
793 .field("failure_policy", &self.failure_policy)
794 .field("require_confirm", &self.require_confirm)
795 .finish()
796 }
797}
798
799impl fmt::Debug for PhaseBuilder {
800 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
801 formatter
802 .debug_struct("PhaseBuilder")
803 .field("id", &self.id)
804 .field("label", &self.label)
805 .field("tasks", &self.tasks.len())
806 .field("max_concurrent_workloads", &self.max_concurrent_workloads)
807 .field("queue_capacity", &self.queue_capacity)
808 .field("dependencies", &self.dependencies)
809 .field("require_confirm", &self.require_confirm)
810 .finish_non_exhaustive()
811 }
812}
813
814#[derive(Clone, Debug, Default)]
816pub struct TaskSelector {
817 phase: Option<PhaseId>,
818 kind: Option<Arc<str>>,
819 parameters: Vec<(Box<str>, Value)>,
820}
821
822impl TaskSelector {
823 pub fn new() -> Self {
825 Self::default()
826 }
827
828 pub fn phase(mut self, phase: impl Into<PhaseId>) -> Self {
830 self.phase = Some(phase.into());
831 self
832 }
833
834 pub fn kind(mut self, kind: impl Into<String>) -> Self {
836 self.kind = Some(kind.into().into());
837 self
838 }
839
840 pub fn parameter(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
842 let key = key.into();
843 if let Some((_, current)) = self
844 .parameters
845 .iter_mut()
846 .find(|(candidate, _)| candidate.as_ref() == key)
847 {
848 *current = value.into();
849 } else {
850 self.parameters.push((key.into_boxed_str(), value.into()));
851 }
852 self
853 }
854
855 pub(crate) fn matches(&self, task: &Task) -> bool {
856 self.phase.is_none_or(|phase| task.key.phase == phase)
857 && self.kind.as_deref().is_none_or(|kind| task.kind() == kind)
858 && self
859 .parameters
860 .iter()
861 .all(|(key, value)| task.value(key) == Some(value))
862 }
863}
864
865impl fmt::Display for TaskSelector {
866 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
867 let mut fields = Vec::new();
868 if let Some(phase) = self.phase {
869 fields.push(format!("phase={phase}"));
870 }
871 if let Some(kind) = &self.kind {
872 fields.push(format!("kind={kind}"));
873 }
874 fields.extend(
875 self.parameters
876 .iter()
877 .map(|(key, value)| format!("{key}={}", compact_value(value))),
878 );
879 formatter.write_str(&fields.join(", "))
880 }
881}
882
883fn varying_keys(tasks: &[Task], positions: &[usize]) -> Vec<String> {
884 let Some(&first) = positions.first() else {
885 return Vec::new();
886 };
887 tasks[first]
888 .parameter_keys()
889 .into_iter()
890 .filter(|key| {
891 let first_value = tasks[first].value(key);
892 positions
893 .iter()
894 .skip(1)
895 .any(|&position| tasks[position].value(key) != first_value)
896 })
897 .map(str::to_owned)
898 .collect()
899}
900
901fn validate_parameter_keys(
902 tasks: &[Task],
903 positions: &[usize],
904 kind: &str,
905) -> Result<(), ReportingError> {
906 let Some(&first) = positions.first() else {
907 return Ok(());
908 };
909 let expected: HashSet<_> = tasks[first].parameter_keys().into_iter().collect();
910 for &position in positions.iter().skip(1) {
911 let actual: HashSet<_> = tasks[position].parameter_keys().into_iter().collect();
912 if actual != expected {
913 return Err(ReportingError::InconsistentManagedTaskParameters {
914 kind: kind.to_owned(),
915 first: tasks[first].key().to_string(),
916 second: tasks[position].key().to_string(),
917 });
918 }
919 }
920 Ok(())
921}
922
923fn validate_display_keys(
924 tasks: &[Task],
925 positions: &[usize],
926 requested: &[String],
927) -> Result<Vec<String>, ReportingError> {
928 let mut seen = HashSet::with_capacity(requested.len());
929 for key in requested {
930 if !seen.insert(key.as_str()) {
931 return Err(ReportingError::DuplicateIdentityParameter { key: key.clone() });
932 }
933 if positions
934 .iter()
935 .any(|&position| tasks[position].value(key).is_none())
936 {
937 return Err(ReportingError::UnknownIdentityParameter { key: key.clone() });
938 }
939 }
940 Ok(requested.to_vec())
941}
942
943fn compact_value(value: &Value) -> String {
944 match value {
945 Value::Array(values) => format!("<array:{}:{}>", values.len(), short_hash(value)),
946 Value::Object(values) => format!("<object:{}:{}>", values.len(), short_hash(value)),
947 _ => serde_json::to_string(value)
948 .expect("serde_json::Value always serializes to valid compact JSON"),
949 }
950}
951
952fn short_hash(value: &Value) -> String {
953 let bytes = serde_json::to_vec(value)
954 .expect("serde_json::Value always serializes to valid compact JSON");
955 let digest = Sha256::digest(bytes);
956 digest[..4]
957 .iter()
958 .map(|byte| format!("{byte:02x}"))
959 .collect::<Vec<_>>()
960 .join("")
961}