Skip to main content

dioxus_showcase_core/
runtime.rs

1use crate::manifest::{StoryDefinition, StoryManifest};
2
3#[derive(Debug, Clone, PartialEq, Eq)]
4pub struct StoryEntry {
5    pub definition: StoryDefinition,
6    pub renderer_symbol: &'static str,
7}
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct StoryNavigationNode {
11    pub segment: String,
12    pub title_path: String,
13    pub story_id: Option<String>,
14    pub children: Vec<Self>,
15}
16
17pub trait StoryTreeEntry {
18    fn story_id(&self) -> &str;
19    fn story_title(&self) -> &str;
20}
21
22#[derive(Debug, Default)]
23pub struct ShowcaseRegistry {
24    stories: Vec<StoryEntry>,
25}
26
27impl ShowcaseRegistry {
28    pub fn register(&mut self, entry: StoryEntry) {
29        self.stories.push(entry);
30    }
31
32    pub fn manifest(&self) -> StoryManifest {
33        let mut manifest = StoryManifest::new(1);
34        for story in &self.stories {
35            manifest.add_story(story.definition.clone());
36        }
37        manifest
38    }
39
40    pub fn story_count(&self) -> usize {
41        self.stories.len()
42    }
43}
44
45pub fn build_story_navigation<T: StoryTreeEntry>(stories: &[T]) -> Vec<StoryNavigationNode> {
46    let mut nodes = Vec::new();
47
48    for story in stories {
49        let title = story.story_title().trim();
50        let segments = split_story_title(title);
51        if segments.is_empty() {
52            continue;
53        }
54
55        insert_story_node(&mut nodes, &segments, story.story_id());
56    }
57
58    nodes
59}
60
61fn split_story_title(title: &str) -> Vec<&str> {
62    title.split('/').map(str::trim).filter(|segment| !segment.is_empty()).collect()
63}
64
65fn insert_story_node(nodes: &mut Vec<StoryNavigationNode>, segments: &[&str], story_id: &str) {
66    let segment = segments[0];
67    let node_index = match nodes.iter().position(|node| node.segment == segment) {
68        Some(index) => index,
69        None => {
70            let title_path = segment.to_owned();
71            nodes.push(StoryNavigationNode {
72                segment: segment.to_owned(),
73                title_path,
74                story_id: None,
75                children: Vec::new(),
76            });
77            nodes.len() - 1
78        }
79    };
80
81    let node = &mut nodes[node_index];
82    if segments.len() == 1 {
83        node.story_id = Some(story_id.to_owned());
84        return;
85    }
86
87    insert_story_child(&mut node.children, &node.title_path, &segments[1..], story_id);
88}
89
90fn insert_story_child(
91    children: &mut Vec<StoryNavigationNode>,
92    parent_path: &str,
93    segments: &[&str],
94    story_id: &str,
95) {
96    let segment = segments[0];
97    let node_index = match children.iter().position(|node| node.segment == segment) {
98        Some(index) => index,
99        None => {
100            let title_path = format!("{parent_path}/{segment}");
101            children.push(StoryNavigationNode {
102                segment: segment.to_owned(),
103                title_path,
104                story_id: None,
105                children: Vec::new(),
106            });
107            children.len() - 1
108        }
109    };
110
111    let node = &mut children[node_index];
112    if segments.len() == 1 {
113        node.story_id = Some(story_id.to_owned());
114        return;
115    }
116
117    insert_story_child(&mut node.children, &node.title_path, &segments[1..], story_id);
118}
119
120impl StoryTreeEntry for StoryEntry {
121    fn story_id(&self) -> &str {
122        &self.definition.id
123    }
124
125    fn story_title(&self) -> &str {
126        &self.definition.title
127    }
128}
129
130impl<T> StoryTreeEntry for &T
131where
132    T: StoryTreeEntry + ?Sized,
133{
134    fn story_id(&self) -> &str {
135        (*self).story_id()
136    }
137
138    fn story_title(&self) -> &str {
139        (*self).story_title()
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn register_updates_count_and_manifest() {
149        let mut registry = ShowcaseRegistry::default();
150        registry.register(StoryEntry {
151            definition: StoryDefinition {
152                id: "atoms-button-default".to_owned(),
153                title: "Atoms/Button/Default".to_owned(),
154                source_path: "showcase/button.stories.rs".to_owned(),
155                module_path: "showcase::button_default".to_owned(),
156                renderer_symbol: "button_default".to_owned(),
157                tags: vec!["atoms".to_owned()],
158            },
159            renderer_symbol: "button_default",
160        });
161
162        assert_eq!(registry.story_count(), 1);
163        let manifest = registry.manifest();
164        assert_eq!(manifest.schema_version, 1);
165        assert_eq!(manifest.stories.len(), 1);
166        assert_eq!(manifest.stories[0].id, "atoms-button-default");
167    }
168
169    #[test]
170    fn build_story_navigation_groups_titles_into_tree() {
171        let stories = vec![
172            StoryEntry {
173                definition: StoryDefinition {
174                    id: "atoms-dropdown-link".to_owned(),
175                    title: "Atoms/Dropdown Link".to_owned(),
176                    source_path: "showcase/button.stories.rs".to_owned(),
177                    module_path: "showcase::dropdown_link".to_owned(),
178                    renderer_symbol: "dropdown_link".to_owned(),
179                    tags: vec![],
180                },
181                renderer_symbol: "dropdown_link",
182            },
183            StoryEntry {
184                definition: StoryDefinition {
185                    id: "atoms-button".to_owned(),
186                    title: "Atoms/Button".to_owned(),
187                    source_path: "showcase/button.stories.rs".to_owned(),
188                    module_path: "showcase::button".to_owned(),
189                    renderer_symbol: "button".to_owned(),
190                    tags: vec![],
191                },
192                renderer_symbol: "button",
193            },
194        ];
195
196        let navigation = build_story_navigation(&stories);
197        assert_eq!(navigation.len(), 1);
198        assert_eq!(navigation[0].segment, "Atoms");
199        assert_eq!(navigation[0].title_path, "Atoms");
200        assert_eq!(navigation[0].children.len(), 2);
201        assert_eq!(navigation[0].children[0].segment, "Dropdown Link");
202        assert_eq!(navigation[0].children[0].story_id.as_deref(), Some("atoms-dropdown-link"));
203        assert_eq!(navigation[0].children[1].segment, "Button");
204        assert_eq!(navigation[0].children[1].story_id.as_deref(), Some("atoms-button"));
205    }
206
207    #[test]
208    fn build_story_navigation_allows_branch_to_be_story_and_parent() {
209        let stories = vec![
210            StoryEntry {
211                definition: StoryDefinition {
212                    id: "atoms".to_owned(),
213                    title: "Atoms".to_owned(),
214                    source_path: "showcase/atoms.rs".to_owned(),
215                    module_path: "showcase::atoms".to_owned(),
216                    renderer_symbol: "atoms".to_owned(),
217                    tags: vec![],
218                },
219                renderer_symbol: "atoms",
220            },
221            StoryEntry {
222                definition: StoryDefinition {
223                    id: "atoms-button".to_owned(),
224                    title: "Atoms/Button".to_owned(),
225                    source_path: "showcase/button.rs".to_owned(),
226                    module_path: "showcase::button".to_owned(),
227                    renderer_symbol: "button".to_owned(),
228                    tags: vec![],
229                },
230                renderer_symbol: "button",
231            },
232        ];
233
234        let navigation = build_story_navigation(&stories);
235        assert_eq!(navigation.len(), 1);
236        assert_eq!(navigation[0].story_id.as_deref(), Some("atoms"));
237        assert_eq!(navigation[0].children.len(), 1);
238        assert_eq!(navigation[0].children[0].segment, "Button");
239    }
240
241    #[test]
242    fn build_story_navigation_accepts_slices_of_references() {
243        let stories = [StoryEntry {
244            definition: StoryDefinition {
245                id: "atoms-button".to_owned(),
246                title: "Atoms/Button".to_owned(),
247                source_path: "showcase/button.rs".to_owned(),
248                module_path: "showcase::button".to_owned(),
249                renderer_symbol: "button".to_owned(),
250                tags: vec![],
251            },
252            renderer_symbol: "button",
253        }];
254        let filtered = stories.iter().collect::<Vec<_>>();
255
256        let navigation = build_story_navigation(&filtered);
257
258        assert_eq!(navigation.len(), 1);
259        assert_eq!(navigation[0].segment, "Atoms");
260        assert_eq!(navigation[0].children.len(), 1);
261        assert_eq!(navigation[0].children[0].story_id.as_deref(), Some("atoms-button"));
262    }
263}