Skip to main content

coil_cms/model/
navigation.rs

1use 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}