coil_cms/model/
navigation.rs1use super::*;
2
3#[derive(Debug, Clone, PartialEq, Eq)]
4pub enum NavigationTarget {
5 Page(PageId),
6 ExternalUrl(String),
7}
8
9impl NavigationTarget {
10 pub fn external(url: impl Into<String>) -> Result<Self, CmsModelError> {
11 Ok(Self::ExternalUrl(validate_path(
12 "external_url",
13 url.into(),
14 )?))
15 }
16}
17
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct NavigationItem {
20 pub id: NavigationItemId,
21 pub label: String,
22 pub target: NavigationTarget,
23 pub children: Vec<NavigationItem>,
24}
25
26impl NavigationItem {
27 pub fn page(
28 id: NavigationItemId,
29 label: impl Into<String>,
30 page_id: PageId,
31 ) -> Result<Self, CmsModelError> {
32 Ok(Self {
33 id,
34 label: require_non_empty("navigation_label", label.into())?,
35 target: NavigationTarget::Page(page_id),
36 children: Vec::new(),
37 })
38 }
39
40 pub fn external(
41 id: NavigationItemId,
42 label: impl Into<String>,
43 url: impl Into<String>,
44 ) -> Result<Self, CmsModelError> {
45 Ok(Self {
46 id,
47 label: require_non_empty("navigation_label", label.into())?,
48 target: NavigationTarget::external(url)?,
49 children: Vec::new(),
50 })
51 }
52
53 pub fn with_child(mut self, child: NavigationItem) -> Self {
54 self.children.push(child);
55 self
56 }
57}
58
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct ResolvedNavigationItem {
61 pub id: NavigationItemId,
62 pub label: String,
63 pub href: String,
64 pub children: Vec<ResolvedNavigationItem>,
65}
66
67#[derive(Debug, Clone, PartialEq, Eq)]
68pub struct NavigationTree {
69 pub id: NavigationId,
70 pub items: Vec<NavigationItem>,
71}
72
73impl NavigationTree {
74 pub fn new(id: NavigationId, items: Vec<NavigationItem>) -> Result<Self, CmsModelError> {
75 let tree = Self { id, items };
76 tree.validate()?;
77 Ok(tree)
78 }
79
80 pub fn validate(&self) -> Result<(), CmsModelError> {
81 let mut seen = BTreeSet::new();
82 for item in &self.items {
83 validate_navigation_item(item, &mut Vec::new(), &mut seen)?;
84 }
85 Ok(())
86 }
87
88 pub fn resolve(
89 &self,
90 pages: &BTreeMap<PageId, CmsPage>,
91 ) -> Result<Vec<ResolvedNavigationItem>, CmsModelError> {
92 let mut resolved = Vec::new();
93 for item in &self.items {
94 if let Some(item) = resolve_navigation_item(item, pages)? {
95 resolved.push(item);
96 }
97 }
98 Ok(resolved)
99 }
100}
101
102fn validate_navigation_item(
103 item: &NavigationItem,
104 stack: &mut Vec<NavigationItemId>,
105 seen: &mut BTreeSet<NavigationItemId>,
106) -> Result<(), CmsModelError> {
107 if stack.contains(&item.id) {
108 return Err(CmsModelError::NavigationCycle {
109 item_id: item.id.to_string(),
110 });
111 }
112
113 if !seen.insert(item.id.clone()) {
114 return Err(CmsModelError::DuplicateNavigationItem {
115 item_id: item.id.to_string(),
116 });
117 }
118
119 stack.push(item.id.clone());
120 for child in &item.children {
121 validate_navigation_item(child, stack, seen)?;
122 }
123 stack.pop();
124
125 Ok(())
126}
127
128fn resolve_navigation_item(
129 item: &NavigationItem,
130 pages: &BTreeMap<PageId, CmsPage>,
131) -> Result<Option<ResolvedNavigationItem>, CmsModelError> {
132 let href = match &item.target {
133 NavigationTarget::ExternalUrl(url) => url.clone(),
134 NavigationTarget::Page(page_id) => match pages.get(page_id) {
135 Some(page) if page.publication().is_live() => page.live_path()?,
136 _ => return Ok(None),
137 },
138 };
139
140 let mut children = Vec::new();
141 for child in &item.children {
142 if let Some(child) = resolve_navigation_item(child, pages)? {
143 children.push(child);
144 }
145 }
146
147 Ok(Some(ResolvedNavigationItem {
148 id: item.id.clone(),
149 label: item.label.clone(),
150 href,
151 children,
152 }))
153}