Skip to main content

concinnity_cook/build_only/
provenance.rs

1// Where one asset of an expanded world came from. `prepare_world` records what
2// each pass injected, generated, and skipped; this reads those records back
3// against a name. Both `cn list --expanded` / `cn explain` and the editor's
4// Expanded tab classify rows through here, so the two cannot drift.
5
6use super::LoadedWorld;
7
8/// The origin of one expanded-world asset.
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum Provenance {
11    /// A world.jsonl line.
12    Authored,
13    /// A world.jsonl line that replaces what an expansion would have produced;
14    /// the named source no longer drives this asset.
15    AuthoredShadowing {
16        /// The authored asset whose expansion it replaces.
17        generated_by: String,
18    },
19    /// Added by an injection pass (a companion, an engine default).
20    Injected {
21        /// The injection pass that added it.
22        by: String,
23    },
24    /// Produced by the expansion of the named authored asset.
25    Generated {
26        /// The authored asset whose expansion produced it.
27        by: String,
28    },
29    /// Produced by a build-time macro expansion that does not record its output
30    /// (menus, stories, prefabs, and the other primitive-emitting passes).
31    Expanded,
32}
33
34impl Provenance {
35    /// The authored asset or pass this came from, for grouping a listing by
36    /// source. `None` for a plain authored line and the unattributed expansions.
37    pub fn source(&self) -> Option<&str> {
38        match self {
39            Provenance::AuthoredShadowing { generated_by } => Some(generated_by),
40            Provenance::Injected { by } => Some(by),
41            Provenance::Generated { by } => Some(by),
42            Provenance::Authored | Provenance::Expanded => None,
43        }
44    }
45
46    /// Whether the asset has a world.jsonl line of its own.
47    pub fn is_authored(&self) -> bool {
48        matches!(
49            self,
50            Provenance::Authored | Provenance::AuthoredShadowing { .. }
51        )
52    }
53
54    /// Whether copying this asset's entry into world.jsonl overrides it rather
55    /// than duplicating it: only the passes that skip a name the world claims
56    /// (scene imports, injections) can be overridden this way. The macro
57    /// expansions emit their primitives unconditionally, so a copy of one would
58    /// land beside the generated asset, not replace it.
59    pub fn is_overridable(&self) -> bool {
60        matches!(
61            self,
62            Provenance::Injected { .. } | Provenance::Generated { .. }
63        )
64    }
65}
66
67impl std::fmt::Display for Provenance {
68    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69        match self {
70            Provenance::Authored => write!(f, "authored"),
71            Provenance::AuthoredShadowing { generated_by } => {
72                write!(f, "authored (shadows {})", generated_by)
73            }
74            Provenance::Injected { by } => write!(f, "injected:{}", by),
75            Provenance::Generated { by } => write!(f, "generated:{}", by),
76            Provenance::Expanded => write!(f, "expanded"),
77        }
78    }
79}
80
81impl LoadedWorld {
82    /// Where the asset called `name` in this expanded world came from.
83    pub fn provenance(&self, name: &str) -> Provenance {
84        if self.authored.iter().any(|n| n == name) {
85            return match self.shadowed.iter().find(|s| s.name == name) {
86                Some(s) => Provenance::AuthoredShadowing {
87                    generated_by: s.generated_by.clone(),
88                },
89                None => Provenance::Authored,
90            };
91        }
92        if let Some(i) = self.injected.iter().find(|i| i.name == name) {
93            return Provenance::Injected {
94                by: i.injected_by.to_string(),
95            };
96        }
97        if let Some(g) = self.generated.iter().find(|g| g.name == name) {
98            return Provenance::Generated {
99                by: g.generated_by.clone(),
100            };
101        }
102        Provenance::Expanded
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use crate::build_only::{GeneratedAsset, InjectedAsset, ShadowedAsset};
110
111    fn world() -> LoadedWorld {
112        LoadedWorld {
113            assets: Vec::new(),
114            injected: vec![InjectedAsset {
115                name: "hud_font".to_string(),
116                asset_type: "Font".to_string(),
117                args: serde_json::json!({}),
118                injected_by: "debug_hud",
119            }],
120            generated: vec![GeneratedAsset {
121                name: "bistro_mat_wood".to_string(),
122                asset_type: "Material".to_string(),
123                generated_by: "bistro".to_string(),
124            }],
125            shadowed: vec![ShadowedAsset {
126                name: "bistro_mat_glass".to_string(),
127                asset_type: "Material".to_string(),
128                generated_by: "bistro".to_string(),
129                args: serde_json::json!({}),
130            }],
131            authored: vec!["cam".to_string(), "bistro_mat_glass".to_string()],
132        }
133    }
134
135    #[test]
136    fn each_record_classifies_its_names() {
137        let w = world();
138        assert_eq!(w.provenance("cam"), Provenance::Authored);
139        assert_eq!(
140            w.provenance("hud_font"),
141            Provenance::Injected {
142                by: "debug_hud".to_string()
143            }
144        );
145        assert_eq!(
146            w.provenance("bistro_mat_wood"),
147            Provenance::Generated {
148                by: "bistro".to_string()
149            }
150        );
151        assert_eq!(
152            w.provenance("bistro_mat_glass"),
153            Provenance::AuthoredShadowing {
154                generated_by: "bistro".to_string()
155            }
156        );
157        // A macro expansion's primitives record nothing, so they fall back.
158        assert_eq!(w.provenance("main_menu_tab_0"), Provenance::Expanded);
159    }
160
161    // The display strings are what `cn list --expanded` prints.
162    #[test]
163    fn display_matches_the_listing_vocabulary() {
164        let w = world();
165        assert_eq!(w.provenance("cam").to_string(), "authored");
166        assert_eq!(w.provenance("hud_font").to_string(), "injected:debug_hud");
167        assert_eq!(
168            w.provenance("bistro_mat_wood").to_string(),
169            "generated:bistro"
170        );
171        assert_eq!(
172            w.provenance("bistro_mat_glass").to_string(),
173            "authored (shadows bistro)"
174        );
175        assert_eq!(w.provenance("nothing").to_string(), "expanded");
176    }
177
178    #[test]
179    fn source_names_the_producing_asset_or_pass() {
180        let w = world();
181        assert_eq!(w.provenance("bistro_mat_wood").source(), Some("bistro"));
182        assert_eq!(w.provenance("hud_font").source(), Some("debug_hud"));
183        assert_eq!(w.provenance("bistro_mat_glass").source(), Some("bistro"));
184        assert_eq!(w.provenance("cam").source(), None);
185        assert_eq!(w.provenance("nothing").source(), None);
186    }
187
188    // Only the passes that skip a claimed name can be overridden by a copy; the
189    // unattributed macro expansions would duplicate instead.
190    #[test]
191    fn only_skipping_passes_are_overridable() {
192        let w = world();
193        assert!(w.provenance("bistro_mat_wood").is_overridable());
194        assert!(w.provenance("hud_font").is_overridable());
195        assert!(!w.provenance("nothing").is_overridable());
196        assert!(!w.provenance("cam").is_overridable());
197        // An asset already overridden is authored, so it is not offered again.
198        assert!(!w.provenance("bistro_mat_glass").is_overridable());
199    }
200
201    #[test]
202    fn is_authored_covers_a_plain_line_and_an_overriding_copy() {
203        let w = world();
204        assert!(w.provenance("cam").is_authored());
205        assert!(w.provenance("bistro_mat_glass").is_authored());
206        assert!(!w.provenance("bistro_mat_wood").is_authored());
207        assert!(!w.provenance("nothing").is_authored());
208    }
209}