rustlavel_view/
context.rs1use rustlavel_core::Json;
4use std::collections::BTreeMap;
5
6#[derive(Debug, Clone, Default, PartialEq)]
24pub struct Context {
25 data: BTreeMap<String, Json>,
26}
27
28impl Context {
29 pub fn new() -> Self {
30 Context::default()
31 }
32
33 pub fn with(mut self, key: impl Into<String>, value: impl Into<Json>) -> Self {
35 self.insert(key, value);
36 self
37 }
38
39 pub fn insert(&mut self, key: impl Into<String>, value: impl Into<Json>) {
41 self.data.insert(key.into(), value.into());
42 }
43
44 pub fn get(&self, key: &str) -> Option<&Json> {
45 self.data.get(key)
46 }
47
48 pub fn from_json(value: Json) -> Self {
52 match value {
53 Json::Object(map) => Context { data: map.into_iter().collect() },
54 _ => Context::default(),
55 }
56 }
57}
58
59pub struct Scope<'a> {
65 context: &'a Context,
66 locals: Vec<(String, Json)>,
67}
68
69impl<'a> Scope<'a> {
70 pub fn new(context: &'a Context) -> Self {
71 Scope { context, locals: Vec::new() }
72 }
73
74 pub fn push(&mut self, name: impl Into<String>, value: Json) {
76 self.locals.push((name.into(), value));
77 }
78
79 pub fn pop(&mut self) {
81 self.locals.pop();
82 }
83
84 pub fn lookup(&self, path: &str) -> Option<&Json> {
89 let (root, rest) = match path.split_once('.') {
90 Some((root, rest)) => (root, Some(rest)),
91 None => (path, None),
92 };
93
94 let value = self
95 .locals
96 .iter()
97 .rev()
98 .find(|(name, _)| name == root)
99 .map(|(_, value)| value)
100 .or_else(|| self.context.get(root))?;
101
102 match rest {
103 Some(rest) => value.get(rest),
104 None => Some(value),
105 }
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112
113 #[test]
114 fn resolves_dotted_paths_into_nested_data() {
115 let context = Context::new().with(
116 "user",
117 Json::object([
118 ("name", Json::from("Ada")),
119 ("tags", Json::from(vec!["math", "engines"])),
120 ]),
121 );
122 let scope = Scope::new(&context);
123
124 assert_eq!(scope.lookup("user.name"), Some(&Json::from("Ada")));
125 assert_eq!(scope.lookup("user.tags.1"), Some(&Json::from("engines")));
126 assert!(scope.lookup("user.missing").is_none());
127 assert!(scope.lookup("nobody").is_none());
128 }
129
130 #[test]
131 fn a_local_shadows_the_context_until_it_is_popped() {
132 let context = Context::new().with("item", "outer");
133 let mut scope = Scope::new(&context);
134
135 scope.push("item", Json::from("inner"));
136 assert_eq!(scope.lookup("item"), Some(&Json::from("inner")));
137
138 scope.pop();
139 assert_eq!(scope.lookup("item"), Some(&Json::from("outer")));
140 }
141
142 #[test]
143 fn a_json_object_becomes_the_whole_context() {
144 let context = Context::from_json(Json::object([("count", Json::from(3))]));
145
146 assert_eq!(context.get("count"), Some(&Json::from(3)));
147 assert_eq!(Context::from_json(Json::from(1)), Context::new());
148 }
149}