use rustlavel_core::Json;
use std::collections::BTreeMap;
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Context {
data: BTreeMap<String, Json>,
}
impl Context {
pub fn new() -> Self {
Context::default()
}
pub fn with(mut self, key: impl Into<String>, value: impl Into<Json>) -> Self {
self.insert(key, value);
self
}
pub fn insert(&mut self, key: impl Into<String>, value: impl Into<Json>) {
self.data.insert(key.into(), value.into());
}
pub fn get(&self, key: &str) -> Option<&Json> {
self.data.get(key)
}
pub fn from_json(value: Json) -> Self {
match value {
Json::Object(map) => Context { data: map.into_iter().collect() },
_ => Context::default(),
}
}
}
pub struct Scope<'a> {
context: &'a Context,
locals: Vec<(String, Json)>,
}
impl<'a> Scope<'a> {
pub fn new(context: &'a Context) -> Self {
Scope { context, locals: Vec::new() }
}
pub fn push(&mut self, name: impl Into<String>, value: Json) {
self.locals.push((name.into(), value));
}
pub fn pop(&mut self) {
self.locals.pop();
}
pub fn lookup(&self, path: &str) -> Option<&Json> {
let (root, rest) = match path.split_once('.') {
Some((root, rest)) => (root, Some(rest)),
None => (path, None),
};
let value = self
.locals
.iter()
.rev()
.find(|(name, _)| name == root)
.map(|(_, value)| value)
.or_else(|| self.context.get(root))?;
match rest {
Some(rest) => value.get(rest),
None => Some(value),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolves_dotted_paths_into_nested_data() {
let context = Context::new().with(
"user",
Json::object([
("name", Json::from("Ada")),
("tags", Json::from(vec!["math", "engines"])),
]),
);
let scope = Scope::new(&context);
assert_eq!(scope.lookup("user.name"), Some(&Json::from("Ada")));
assert_eq!(scope.lookup("user.tags.1"), Some(&Json::from("engines")));
assert!(scope.lookup("user.missing").is_none());
assert!(scope.lookup("nobody").is_none());
}
#[test]
fn a_local_shadows_the_context_until_it_is_popped() {
let context = Context::new().with("item", "outer");
let mut scope = Scope::new(&context);
scope.push("item", Json::from("inner"));
assert_eq!(scope.lookup("item"), Some(&Json::from("inner")));
scope.pop();
assert_eq!(scope.lookup("item"), Some(&Json::from("outer")));
}
#[test]
fn a_json_object_becomes_the_whole_context() {
let context = Context::from_json(Json::object([("count", Json::from(3))]));
assert_eq!(context.get("count"), Some(&Json::from(3)));
assert_eq!(Context::from_json(Json::from(1)), Context::new());
}
}