spin_manifest/
normalize.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
//! Manifest normalization functions.

use std::collections::HashSet;

use crate::schema::v2::{AppManifest, ComponentSpec, KebabId};

/// Normalizes some optional [`AppManifest`] features into a canonical form:
/// - Inline components in trigger configs are moved into top-level
///   components and replaced with a reference.
/// - Any triggers without an ID are assigned a generated ID.
pub fn normalize_manifest(manifest: &mut AppManifest) {
    normalize_trigger_ids(manifest);
    normalize_inline_components(manifest);
}

fn normalize_inline_components(manifest: &mut AppManifest) {
    // Normalize inline components
    let components = &mut manifest.components;

    for trigger in manifest.triggers.values_mut().flatten() {
        let trigger_id = &trigger.id;

        let component_specs = trigger
            .component
            .iter_mut()
            .chain(
                trigger
                    .components
                    .values_mut()
                    .flat_map(|specs| specs.0.iter_mut()),
            )
            .collect::<Vec<_>>();
        let multiple_components = component_specs.len() > 1;

        let mut counter = 1;
        for spec in component_specs {
            if !matches!(spec, ComponentSpec::Inline(_)) {
                continue;
            };

            let inline_id = {
                // Try a "natural" component ID...
                let mut id = KebabId::try_from(format!("{trigger_id}-component"));
                // ...falling back to a counter-based component ID
                if multiple_components
                    || id.is_err()
                    || components.contains_key(id.as_ref().unwrap())
                {
                    id = Ok(loop {
                        let id = KebabId::try_from(format!("inline-component{counter}")).unwrap();
                        if !components.contains_key(&id) {
                            break id;
                        }
                        counter += 1;
                    });
                }
                id.unwrap()
            };

            // Replace the inline component with a reference...
            let inline_spec = std::mem::replace(spec, ComponentSpec::Reference(inline_id.clone()));
            let ComponentSpec::Inline(component) = inline_spec else {
                unreachable!();
            };
            // ...moving the inline component into the top-level components map.
            components.insert(inline_id.clone(), *component);
        }
    }
}

fn normalize_trigger_ids(manifest: &mut AppManifest) {
    let mut trigger_ids = manifest
        .triggers
        .values()
        .flatten()
        .cloned()
        .map(|t| t.id)
        .collect::<HashSet<_>>();
    for (trigger_type, triggers) in &mut manifest.triggers {
        let mut counter = 1;
        for trigger in triggers {
            if !trigger.id.is_empty() {
                continue;
            }
            // Try to assign a "natural" ID to this trigger
            if let Some(ComponentSpec::Reference(component_id)) = &trigger.component {
                let candidate_id = format!("{component_id}-{trigger_type}-trigger");
                if !trigger_ids.contains(&candidate_id) {
                    trigger.id.clone_from(&candidate_id);
                    trigger_ids.insert(candidate_id);
                    continue;
                }
            }
            // Fall back to assigning a counter-based trigger ID
            trigger.id = loop {
                let id = format!("{trigger_type}-trigger{counter}");
                if !trigger_ids.contains(&id) {
                    trigger_ids.insert(id.clone());
                    break id;
                }
                counter += 1;
            }
        }
    }
}