1use kotoba_core::prelude::*;
6use crate::frontend::component_ir::{ComponentIR, ComponentType};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
12pub enum RouteType {
13 Static,
15 Dynamic,
17 CatchAll,
19 OptionalCatchAll,
21 Group,
23 Parallel,
25 Intercept,
27}
28
29#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
31pub struct RouteSegment {
32 pub name: String,
33 pub segment_type: RouteType,
34 pub params: Vec<String>, pub is_optional: bool,
36}
37
38impl RouteSegment {
39 pub fn static_segment(name: String) -> Self {
40 Self {
41 name,
42 segment_type: RouteType::Static,
43 params: Vec::new(),
44 is_optional: false,
45 }
46 }
47
48 pub fn dynamic_segment(param: String) -> Self {
49 Self {
50 name: format!("[{}]", param),
51 segment_type: RouteType::Dynamic,
52 params: vec![param],
53 is_optional: false,
54 }
55 }
56
57 pub fn catch_all_segment(param: String) -> Self {
58 Self {
59 name: format!("[...{}]", param),
60 segment_type: RouteType::CatchAll,
61 params: vec![param],
62 is_optional: false,
63 }
64 }
65
66 pub fn optional_catch_all_segment(param: String) -> Self {
67 Self {
68 name: format!("[[...{}]]", param),
69 segment_type: RouteType::OptionalCatchAll,
70 params: vec![param],
71 is_optional: true,
72 }
73 }
74}
75
76#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
78pub struct RouteIR {
79 pub path: String,
80 pub segments: Vec<RouteSegment>,
81 pub components: RouteComponents,
82 pub metadata: RouteMetadata,
83 pub children: Vec<RouteIR>, }
85
86#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
87pub struct RouteComponents {
88 pub page: Option<ComponentIR>,
89 pub layout: Option<ComponentIR>,
90 pub loading: Option<ComponentIR>,
91 pub error: Option<ComponentIR>,
92 pub template: Option<ComponentIR>,
93 pub not_found: Option<ComponentIR>,
94}
95
96impl RouteComponents {
97 pub fn new() -> Self {
98 Self {
99 page: None,
100 layout: None,
101 loading: None,
102 error: None,
103 template: None,
104 not_found: None,
105 }
106 }
107}
108
109#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
110pub struct RouteMetadata {
111 pub title: Option<String>,
112 pub description: Option<String>,
113 pub keywords: Vec<String>,
114 pub open_graph: Option<OpenGraphMetadata>,
115 pub twitter_card: Option<TwitterCardMetadata>,
116 pub canonical_url: Option<String>,
117 pub robots: Option<RobotsMetadata>,
118}
119
120#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
121pub struct OpenGraphMetadata {
122 pub title: Option<String>,
123 pub description: Option<String>,
124 pub image: Option<String>,
125 pub url: Option<String>,
126 pub type_: Option<String>, }
128
129#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
130pub struct TwitterCardMetadata {
131 pub card: Option<String>, pub title: Option<String>,
133 pub description: Option<String>,
134 pub image: Option<String>,
135}
136
137#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
138pub struct RobotsMetadata {
139 pub index: bool,
140 pub follow: bool,
141 pub noarchive: bool,
142 pub nosnippet: bool,
143 pub noimageindex: bool,
144 pub nocache: bool,
145}
146
147impl RouteIR {
148 pub fn new(path: String) -> Self {
149 Self {
150 path: path.clone(),
151 segments: Self::parse_path_segments(&path),
152 components: RouteComponents::new(),
153 metadata: RouteMetadata {
154 title: None,
155 description: None,
156 keywords: Vec::new(),
157 open_graph: None,
158 twitter_card: None,
159 canonical_url: None,
160 robots: None,
161 },
162 children: Vec::new(),
163 }
164 }
165
166 fn parse_path_segments(path: &str) -> Vec<RouteSegment> {
168 path.trim_matches('/')
169 .split('/')
170 .filter(|s| !s.is_empty())
171 .map(|segment| {
172 if segment.starts_with("[[") && segment.ends_with("]]") {
173 let param = segment.strip_prefix("[[").unwrap()
175 .strip_suffix("]]").unwrap()
176 .strip_prefix("...").unwrap();
177 RouteSegment::optional_catch_all_segment(param.to_string())
178 } else if segment.starts_with("[...") && segment.ends_with("]") {
179 let param = segment.strip_prefix("[...").unwrap()
181 .strip_suffix("]").unwrap();
182 RouteSegment::catch_all_segment(param.to_string())
183 } else if segment.starts_with('[') && segment.ends_with(']') {
184 let param = segment.strip_prefix('[').unwrap()
186 .strip_suffix(']').unwrap();
187 RouteSegment::dynamic_segment(param.to_string())
188 } else if segment.starts_with('(') && segment.ends_with(')') {
189 RouteSegment {
191 name: segment.to_string(),
192 segment_type: RouteType::Group,
193 params: Vec::new(),
194 is_optional: false,
195 }
196 } else if segment.starts_with('@') {
197 RouteSegment {
199 name: segment.to_string(),
200 segment_type: RouteType::Parallel,
201 params: Vec::new(),
202 is_optional: false,
203 }
204 } else {
205 RouteSegment::static_segment(segment.to_string())
207 }
208 })
209 .collect()
210 }
211
212 pub fn set_page(&mut self, component: ComponentIR) {
214 self.components.page = Some(component);
215 }
216
217 pub fn set_layout(&mut self, component: ComponentIR) {
219 self.components.layout = Some(component);
220 }
221
222 pub fn set_loading(&mut self, component: ComponentIR) {
224 self.components.loading = Some(component);
225 }
226
227 pub fn set_error(&mut self, component: ComponentIR) {
229 self.components.error = Some(component);
230 }
231
232 pub fn add_child(&mut self, child: RouteIR) {
234 self.children.push(child);
235 }
236
237 pub fn matches_path(&self, request_path: &str) -> Option<HashMap<String, String>> {
239 let request_segments: Vec<&str> = request_path.trim_matches('/')
240 .split('/')
241 .filter(|s| !s.is_empty())
242 .collect();
243
244 if self.segments.len() != request_segments.len() {
245 if let Some(last_segment) = self.segments.last() {
247 if matches!(last_segment.segment_type, RouteType::OptionalCatchAll) && request_segments.len() < self.segments.len() {
248 } else {
250 return None;
251 }
252 } else {
253 return None;
254 }
255 }
256
257 let mut params = HashMap::new();
258
259 for (i, segment) in self.segments.iter().enumerate() {
260 let request_segment = request_segments.get(i);
261
262 match segment.segment_type {
263 RouteType::Static => {
264 if request_segment != Some(&segment.name.as_str()) {
265 return None;
266 }
267 }
268 RouteType::Dynamic => {
269 if let Some(param_name) = segment.params.first() {
270 if let Some(value) = request_segment {
271 params.insert(param_name.clone(), value.to_string());
272 } else {
273 return None;
274 }
275 }
276 }
277 RouteType::CatchAll => {
278 if let Some(param_name) = segment.params.first() {
279 let remaining_segments: Vec<String> = request_segments[i..].iter()
280 .map(|s| s.to_string())
281 .collect();
282 params.insert(param_name.clone(), remaining_segments.join("/"));
283 break; }
285 }
286 RouteType::OptionalCatchAll => {
287 if let Some(param_name) = segment.params.first() {
288 if i < request_segments.len() {
289 let remaining_segments: Vec<String> = request_segments[i..].iter()
290 .map(|s| s.to_string())
291 .collect();
292 params.insert(param_name.clone(), remaining_segments.join("/"));
293 } else {
294 params.insert(param_name.clone(), "".to_string());
295 }
296 break;
297 }
298 }
299 RouteType::Group | RouteType::Parallel | RouteType::Intercept => {
300 continue;
302 }
303 }
304 }
305
306 Some(params)
307 }
308}
309
310#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
312pub struct RouteTableIR {
313 pub routes: Vec<RouteIR>,
314 pub base_path: String,
315 pub middleware: Vec<ComponentIR>, }
317
318impl RouteTableIR {
319 pub fn new() -> Self {
320 Self {
321 routes: Vec::new(),
322 base_path: "/".to_string(),
323 middleware: Vec::new(),
324 }
325 }
326
327 pub fn add_route(&mut self, route: RouteIR) {
329 self.routes.push(route);
330 }
331
332 pub fn add_middleware(&mut self, middleware: ComponentIR) {
334 self.middleware.push(middleware);
335 }
336
337 pub fn find_route(&self, path: &str) -> Option<(&RouteIR, HashMap<String, String>)> {
339 for route in &self.routes {
340 if let Some(params) = route.matches_path(path) {
341 return Some((route, params));
342 }
343 }
344 None
345 }
346
347 pub fn build_nested_routes(&mut self) {
349 }
352}
353
354#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
356pub struct NavigationIR {
357 pub from_path: String,
358 pub to_path: String,
359 pub navigation_type: NavigationType,
360 pub state: Properties,
361 pub replace: bool,
362}
363
364#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
365pub enum NavigationType {
366 Push,
367 Replace,
368 Back,
369 Forward,
370 Reload,
371}
372
373impl NavigationIR {
374 pub fn new(from_path: String, to_path: String) -> Self {
375 Self {
376 from_path,
377 to_path,
378 navigation_type: NavigationType::Push,
379 state: Properties::new(),
380 replace: false,
381 }
382 }
383
384 pub fn replace(mut self, replace: bool) -> Self {
385 self.replace = replace;
386 self
387 }
388
389 pub fn with_state(mut self, state: Properties) -> Self {
390 self.state = state;
391 self
392 }
393}
394
395#[cfg(test)]
396mod tests {
397 use super::*;
398
399 #[test]
400 fn test_route_parsing() {
401 let route = RouteIR::new("/about".to_string());
403 assert_eq!(route.segments.len(), 1);
404 assert_eq!(route.segments[0].name, "about");
405 assert_eq!(route.segments[0].segment_type, RouteType::Static);
406
407 let route = RouteIR::new("/users/[id]".to_string());
409 assert_eq!(route.segments.len(), 2);
410 assert_eq!(route.segments[1].segment_type, RouteType::Dynamic);
411 assert_eq!(route.segments[1].params, vec!["id"]);
412
413 let route = RouteIR::new("/docs/[...slug]".to_string());
415 assert_eq!(route.segments[1].segment_type, RouteType::CatchAll);
416 assert_eq!(route.segments[1].params, vec!["slug"]);
417 }
418
419 #[test]
420 fn test_route_matching() {
421 let route = RouteIR::new("/users/[id]".to_string());
422
423 let params = route.matches_path("/users/123").unwrap();
425 assert_eq!(params.get("id"), Some(&"123".to_string()));
426
427 assert!(route.matches_path("/users").is_none());
429 assert!(route.matches_path("/users/123/profile").is_none());
430
431 let catch_all_route = RouteIR::new("/docs/[...slug]".to_string());
433 let params = catch_all_route.matches_path("/docs/getting-started/installation").unwrap();
434 assert_eq!(params.get("slug"), Some(&"getting-started/installation".to_string()));
435 }
436
437 #[test]
438 fn test_route_table() {
439 let mut table = RouteTableIR::new();
440
441 let mut route1 = RouteIR::new("/".to_string());
442 let page1 = ComponentIR::new("HomePage".to_string(), ComponentType::Page);
443 route1.set_page(page1);
444 table.add_route(route1);
445
446 let mut route2 = RouteIR::new("/about".to_string());
447 let page2 = ComponentIR::new("AboutPage".to_string(), ComponentType::Page);
448 route2.set_page(page2);
449 table.add_route(route2);
450
451 let (route, params) = table.find_route("/about").unwrap();
453 assert_eq!(route.path, "/about");
454 assert!(params.is_empty());
455
456 assert!(table.find_route("/nonexistent").is_none());
457 }
458}