Menu
Public repository / 9df51040bf1687e4dc8a91557f02df52e8d5cf3a
AkurAI-Framework / crates/cli/src/routes.rs
Latest change a94840a662c50999b602c6b59227d2c46dc33dfa - feat(router): wire akurai-router into serve via backend/routes.json by Ólafur Búi Ólafsson
//! Declarative page routing from `backend/routes.json`.
//!
//! A route table maps URL patterns to template names, e.g.
//! ```json
//! { "routes": [
//! { "path": "/", "template": "index" },
//! { "path": "/posts/:id", "template": "post" },
//! { "path": "/files/*path", "template": "file" }
//! ] }
//! ```
//! Patterns use [`akurai_router`] semantics: `:param` captures one segment,
//! `*wildcard` captures the rest, and the most specific pattern wins. Captured
//! params are injected into the render context under `params` (so a template can
//! use `{{ params.id }}`). The file is optional — without it the server keeps
//! its original "template named after the path" behaviour, so existing projects
//! are unaffected.
use akurai_json::Value;
use akurai_router::Router;
use std::fs;
use std::path::Path;
/// Load the route table for the project rooted at `frontend` dir `root`, or
/// `None` if `backend/routes.json` is absent or unreadable. A malformed entry is
/// skipped rather than failing the whole table.
pub fn load(root: &Path) -> Option<Router<String>> {
let path = root.parent()?.join("backend/routes.json");
let text = fs::read_to_string(path).ok()?;
let value = akurai_json::parse(&text).ok()?;
let Value::Array(entries) = value.get("routes")? else {
return None;
};
let mut router = Router::new();
let mut any = false;
for entry in entries {
if let (Some(pattern), Some(template)) = (
entry.get("path").and_then(Value::as_str),
entry.get("template").and_then(Value::as_str),
) {
router.route(pattern, template.to_string());
any = true;
}
}
any.then_some(router)
}
/// Turn captured route params into a `params` object for the render context, so
/// templates resolve `{{ params.<name> }}`.
pub fn params_value(params: &[(String, String)]) -> Value {
Value::Object(
params
.iter()
.map(|(k, v)| (k.clone(), Value::Str(v.clone())))
.collect(),
)
}
#[cfg(test)]
mod tests {
use super::*;
fn router_from(json: &str) -> Option<Router<String>> {
// Build a router straight from JSON text, bypassing the filesystem.
let value = akurai_json::parse(json).ok()?;
let Value::Array(entries) = value.get("routes")? else {
return None;
};
let mut router = Router::new();
let mut any = false;
for entry in entries {
if let (Some(p), Some(t)) = (
entry.get("path").and_then(Value::as_str),
entry.get("template").and_then(Value::as_str),
) {
router.route(p, t.to_string());
any = true;
}
}
any.then_some(router)
}
#[test]
fn parses_and_matches_a_param_route() {
let r = router_from(
r#"{"routes":[{"path":"/","template":"index"},{"path":"/posts/:id","template":"post"}]}"#,
)
.unwrap();
let m = r.match_path("/posts/42").unwrap();
assert_eq!(m.value, &"post".to_string());
assert_eq!(m.param("id"), Some("42"));
assert_eq!(r.match_path("/").unwrap().value, &"index".to_string());
}
#[test]
fn empty_or_missing_table_is_none() {
assert!(router_from(r#"{"routes":[]}"#).is_none());
assert!(router_from(r#"{}"#).is_none());
}
#[test]
fn params_become_a_context_object() {
let v = params_value(&[("id".into(), "7".into())]);
assert_eq!(v.get("id").and_then(Value::as_str), Some("7"));
}
}