spin_manifest/
normalize.rs

1//! Manifest normalization functions.
2
3use std::collections::HashSet;
4
5use crate::schema::v2::{AppManifest, ComponentSpec, KebabId};
6
7/// Normalizes some optional [`AppManifest`] features into a canonical form:
8/// - Inline components in trigger configs are moved into top-level
9///   components and replaced with a reference.
10/// - Any triggers without an ID are assigned a generated ID.
11pub fn normalize_manifest(manifest: &mut AppManifest) {
12    normalize_trigger_ids(manifest);
13    normalize_inline_components(manifest);
14}
15
16fn normalize_inline_components(manifest: &mut AppManifest) {
17    // Normalize inline components
18    let components = &mut manifest.components;
19
20    for trigger in manifest.triggers.values_mut().flatten() {
21        let trigger_id = &trigger.id;
22
23        let component_specs = trigger
24            .component
25            .iter_mut()
26            .chain(
27                trigger
28                    .components
29                    .values_mut()
30                    .flat_map(|specs| specs.0.iter_mut()),
31            )
32            .collect::<Vec<_>>();
33        let multiple_components = component_specs.len() > 1;
34
35        let mut counter = 1;
36        for spec in component_specs {
37            if !matches!(spec, ComponentSpec::Inline(_)) {
38                continue;
39            };
40
41            let inline_id = {
42                // Try a "natural" component ID...
43                let mut id = KebabId::try_from(format!("{trigger_id}-component"));
44                // ...falling back to a counter-based component ID
45                if multiple_components
46                    || id.is_err()
47                    || components.contains_key(id.as_ref().unwrap())
48                {
49                    id = Ok(loop {
50                        let id = KebabId::try_from(format!("inline-component{counter}")).unwrap();
51                        if !components.contains_key(&id) {
52                            break id;
53                        }
54                        counter += 1;
55                    });
56                }
57                id.unwrap()
58            };
59
60            // Replace the inline component with a reference...
61            let inline_spec = std::mem::replace(spec, ComponentSpec::Reference(inline_id.clone()));
62            let ComponentSpec::Inline(component) = inline_spec else {
63                unreachable!();
64            };
65            // ...moving the inline component into the top-level components map.
66            components.insert(inline_id.clone(), *component);
67        }
68    }
69}
70
71fn normalize_trigger_ids(manifest: &mut AppManifest) {
72    let mut trigger_ids = manifest
73        .triggers
74        .values()
75        .flatten()
76        .cloned()
77        .map(|t| t.id)
78        .collect::<HashSet<_>>();
79    for (trigger_type, triggers) in &mut manifest.triggers {
80        let mut counter = 1;
81        for trigger in triggers {
82            if !trigger.id.is_empty() {
83                continue;
84            }
85            // Try to assign a "natural" ID to this trigger
86            if let Some(ComponentSpec::Reference(component_id)) = &trigger.component {
87                let candidate_id = format!("{component_id}-{trigger_type}-trigger");
88                if !trigger_ids.contains(&candidate_id) {
89                    trigger.id.clone_from(&candidate_id);
90                    trigger_ids.insert(candidate_id);
91                    continue;
92                }
93            }
94            // Fall back to assigning a counter-based trigger ID
95            trigger.id = loop {
96                let id = format!("{trigger_type}-trigger{counter}");
97                if !trigger_ids.contains(&id) {
98                    trigger_ids.insert(id.clone());
99                    break id;
100                }
101                counter += 1;
102            }
103        }
104    }
105}