Skip to main content

sofie_cli/
components.rs

1use std::fmt::Write;
2
3use sofie_engine::{Children, ComponentSpec, SPECS};
4
5/// With the app's sofie features, each feature a component needs is marked on or off.
6#[must_use]
7pub fn component_list(enabled: Option<&[String]>) -> String {
8    let mut text = String::from(
9        "Screen components, written in templates as `<Name prop=\"…\" />`. Templates are \
10         HTML besides: plain elements, `{data.path}` text, `{#if …}`/`{:else}`/`{/if}`, \
11         `{#each list as item, index}`/`{/each}`; conditions may ask `can(\"…\")`, \
12         `enabled(\"…\")` and `entitled(\"…\")`, and text may be `t(\"key\", name = …)`, \
13         `number(…)` or `money(…)` (the `i18n` feature) or `date(…)`. `explain <Name>` gives \
14         one component's props.\n",
15    );
16    if let Some(enabled) = enabled {
17        let mut features: Vec<&str> = SPECS.iter().filter_map(|spec| spec.feature).collect();
18        features.dedup();
19        let states: Vec<String> = features
20            .iter()
21            .map(|feature| format!("`{feature}` {}", state(feature, enabled)))
22            .collect();
23        let _ = writeln!(text, "In this app: {}.", states.join(", "));
24    }
25    for spec in SPECS {
26        let _ = write!(text, "\n{}", component_section(spec, enabled));
27    }
28    text
29}
30
31fn state(feature: &str, enabled: &[String]) -> &'static str {
32    if enabled.iter().any(|on| on == feature) {
33        "on"
34    } else {
35        "off"
36    }
37}
38
39/// `enabled` is asked only for a component behind a feature, as reading it runs Cargo.
40pub fn component(name: &str, enabled: impl FnOnce() -> Option<Vec<String>>) -> Option<String> {
41    let spec = SPECS.iter().find(|spec| spec.name == name)?;
42    let enabled = spec.feature.and_then(|_| enabled());
43    Some(component_section(spec, enabled.as_deref()))
44}
45
46fn component_section(spec: &ComponentSpec, enabled: Option<&[String]>) -> String {
47    let mut text = format!("component {}: {}\n", spec.name, spec.summary);
48    if let Some(feature) = spec.feature {
49        let in_app = match enabled.map(|enabled| state(feature, enabled)) {
50            Some("on") => ", on in this app".to_owned(),
51            Some(_) => format!(
52                ", off in this app: `sofie feature add {feature}`, or the MCP tool `feature`, \
53                 turns it on"
54            ),
55            None => String::new(),
56        };
57        let _ = writeln!(
58            text,
59            "  feature: `{feature}` (sofie's feature of the same name{in_app})"
60        );
61    }
62    let _ = writeln!(text, "  children: {}", children(spec.children));
63    for prop in spec.props {
64        let need = if prop.required {
65            "required"
66        } else {
67            "optional"
68        };
69        let _ = writeln!(
70            text,
71            "  {} ({need}, {}): {}",
72            prop.name,
73            prop.kind.describe(),
74            prop.summary
75        );
76    }
77    text
78}
79
80fn children(children: Children) -> &'static str {
81    match children {
82        Children::None => "none; write it as `<Name … />`",
83        Children::Any => "any markup and components",
84        Children::FormParts => "`Field` and `Submit`, in the order shown",
85        Children::RowContent => "rendered in a last cell of every row, the row named by `as`",
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    #[test]
94    fn a_component_names_its_props_and_what_each_takes() {
95        let table = component("Table", || unreachable!("`Table` needs no feature")).unwrap();
96
97        assert!(table.starts_with("component Table: "), "{table}");
98        assert!(
99            table.contains("  of (required, a data path such as `{data.items}`): "),
100            "{table}"
101        );
102        assert!(
103            table.contains("  children: rendered in a last cell"),
104            "{table}"
105        );
106        assert_eq!(component("Nothing", || None), None);
107    }
108
109    #[test]
110    fn the_list_holds_every_component() {
111        let list = component_list(None);
112
113        for spec in SPECS {
114            assert!(
115                list.contains(&format!("component {}: ", spec.name)),
116                "{}",
117                spec.name
118            );
119        }
120    }
121
122    #[test]
123    fn a_feature_reads_as_on_or_off_when_the_app_is_known() {
124        let enabled = || Some(vec!["engine".to_owned(), "i18n".to_owned()]);
125
126        let badge = component("Badge", enabled).unwrap();
127        let card = component("Card", enabled).unwrap();
128        let unknown = component("Card", || None).unwrap();
129
130        assert!(badge.contains("same name, on in this app)"), "{badge}");
131        assert!(
132            card.contains("off in this app: `sofie feature add layout`, or the MCP tool `feature`"),
133            "{card}"
134        );
135        assert!(
136            unknown.contains("(sofie's feature of the same name)\n"),
137            "{unknown}"
138        );
139    }
140
141    #[test]
142    fn table_documents_its_row_link() {
143        let table = component("Table", || None).unwrap();
144
145        assert!(table.contains("  link (optional, "), "{table}");
146    }
147}