#![forbid(unsafe_code)]
#![deny(clippy::all, missing_docs)]
mod anon_rc;
use anon_rc::AnonRc;
use std::{
any::{Any, TypeId},
cell::RefCell,
collections::BTreeMap,
fmt::{Debug, Formatter, Result as FmtResult},
mem::replace,
ops::Deref,
rc::Rc,
};
#[doc(inline)]
pub use illicit_macro::from_env;
thread_local! {
static CURRENT_SCOPE: RefCell<Rc<Env>> = RefCell::new(Rc::new(
Env {
depth: 0,
location: (file!(), column!(), line!()),
values: Default::default(),
}
));
}
#[macro_export]
macro_rules! child_env {
($($env_item_ty:ty => $env_item:expr),*) => {{
let location = (file!(), line!(), column!());
#[allow(unused_mut)]
let mut _new_env = $crate::Env::unstable_new(location);
$( _new_env.unstable_insert::<$env_item_ty>($env_item); )*
_new_env
}}
}
#[derive(Clone, Eq, PartialEq)]
pub struct Env {
depth: u32,
location: (&'static str, u32, u32),
values: Vec<(TypeId, AnonRc)>,
}
impl Env {
#[doc(hidden)]
pub fn unstable_new(location: (&'static str, u32, u32)) -> Self {
let mut values = Vec::new();
let mut depth = 0;
CURRENT_SCOPE.with(|current| {
let current = current.borrow();
depth = current.depth + 1;
values = current.values.clone();
});
Self {
values,
depth,
location,
}
}
#[doc(hidden)]
pub fn unstable_insert<E>(&mut self, new_item: E)
where
E: Debug + 'static,
{
let anon = AnonRc::new(new_item, self.location, self.depth);
self.add_anon(anon);
}
fn add_anon(&mut self, anon: AnonRc) {
if let Some(existing) = self
.values
.iter_mut()
.find(|(id, _)| *id == anon.id())
.map(|(_, e)| e)
{
*existing = anon;
} else {
self.values.push((anon.id(), anon));
}
}
pub fn depth(&self) -> u32 {
self.depth
}
pub fn enter<R>(self, child_fn: impl FnOnce() -> R) -> R {
let _reset_when_done_please = CURRENT_SCOPE.with(|parent| {
let mut parent = parent.borrow_mut();
let parent = replace(&mut *parent, Rc::new(self));
scopeguard::guard(parent, move |prev| {
CURRENT_SCOPE.with(|p| p.replace(prev));
})
});
child_fn()
}
pub fn get<E>() -> Option<impl Deref<Target = E> + 'static>
where
E: Any + 'static,
{
let key = TypeId::of::<E>();
let anon = CURRENT_SCOPE.with(|current| {
current
.borrow()
.values
.iter()
.find(|(id, _)| id == &key)
.map(|(_, a)| a.clone())
});
if let Some(anon) = anon {
Some(
anon.downcast_deref()
.expect("used type for storage and lookup, should match"),
)
} else {
None
}
}
pub fn expect<E>() -> impl Deref<Target = E> + 'static
where
E: Any + 'static,
{
if let Some(val) = Self::get() {
val
} else {
panic!(
"expected a `{}` from the environment, did not find it in current env: {:#?}",
std::any::type_name::<E>(),
Env::snapshot(),
)
}
}
pub fn snapshot() -> EnvSnapshot {
let mut stacked = EnvSnapshot {
by_depth: BTreeMap::new(),
};
CURRENT_SCOPE.with(|e| {
for (_, anon) in &e.borrow().values {
stacked
.by_depth
.entry(anon.depth())
.or_insert_with(|| Env {
values: Default::default(),
depth: anon.depth(),
location: anon.location(),
})
.add_anon(anon.clone());
}
});
stacked
}
pub fn hide<E: 'static>() {
CURRENT_SCOPE.with(|current| {
let mut env = current.borrow_mut();
let mut without_e = env.values.clone();
let excluded_ty = TypeId::of::<E>();
without_e.retain(|(ty, _)| ty != &excluded_ty);
*env = Rc::new(Env {
values: without_e,
depth: env.depth,
location: env.location,
});
})
}
}
impl Debug for Env {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
let (file, line, col) = self.location;
let env_w_loc = format!("Env @ {}:{}:{}", file, line, col);
let mut f = f.debug_struct(&env_w_loc);
for (ty, anon) in self
.values
.iter()
.map(|(_, v)| (v.ty(), v))
.collect::<BTreeMap<_, _>>()
{
f.field(ty, anon.debug());
}
f.finish()
}
}
#[derive(Clone)]
pub struct EnvSnapshot {
by_depth: BTreeMap<u32, Env>,
}
impl Debug for EnvSnapshot {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
f.debug_list().entries(self.by_depth.values()).finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn snapshot_has_correct_structure() {
let empty_scope = Env::snapshot();
let mut u8_present = (empty_scope.clone(), 0);
let mut u8_and_string_present = u8_present.clone();
child_env!(u8 => 42).enter(|| {
u8_present = (Env::snapshot(), line!() - 1);
child_env!(String => String::from("owo")).enter(|| {
u8_and_string_present = (Env::snapshot(), line!() - 1);
});
});
let empty_scope_after = Env::snapshot();
assert_eq!(
empty_scope.by_depth.len(),
0,
"environment should be empty without additions"
);
assert_eq!(
empty_scope.by_depth.len(),
empty_scope_after.by_depth.len(),
"snapshots must be the same before and after calls"
);
assert_eq!(u8_present.0.by_depth.len(), 1);
assert_eq!(u8_and_string_present.0.by_depth.len(), 2);
let (_, first_layer) = u8_present.0.by_depth.into_iter().next().unwrap();
assert_eq!(first_layer.depth, 1);
assert_eq!(first_layer.location, (file!(), u8_present.1, 9));
assert_eq!(first_layer.values.len(), 1);
let first_u8 = first_layer
.values
.iter()
.find(|(id, _)| id == &TypeId::of::<u8>())
.map(|(_, v)| v)
.unwrap();
assert_eq!(
(first_u8.location(), first_u8.depth()),
(first_layer.location, first_layer.depth),
);
let mut u8_and_string_layers = u8_and_string_present.0.by_depth.values();
let (first_layer, second_layer) = (
u8_and_string_layers.next().unwrap(),
u8_and_string_layers.next().unwrap(),
);
assert_eq!(first_layer.depth, 1);
assert_eq!(first_layer.location, (file!(), u8_present.1, 9));
assert_eq!(first_layer.values.len(), 1);
let first_u8 = first_layer
.values
.iter()
.find(|(id, _)| id == &TypeId::of::<u8>())
.map(|(_, v)| v)
.unwrap();
assert_eq!(
(first_u8.location(), first_u8.depth()),
(first_layer.location, first_layer.depth),
);
assert_eq!(second_layer.depth, 2);
assert_eq!(
second_layer.location,
(file!(), u8_and_string_present.1, 13)
);
assert_eq!(second_layer.values.len(), 1);
let second_string = second_layer
.values
.iter()
.find(|(id, _)| id == &TypeId::of::<String>())
.map(|(_, v)| v)
.unwrap();
assert_eq!(
(second_string.location(), second_string.depth()),
(second_layer.location, second_layer.depth),
);
}
#[test]
fn child_env_replaces_parent_env_values() {
let mut first_called = false;
let mut second_called = false;
assert!(Env::get::<u8>().is_none());
child_env!(u8 => 0u8).enter(|| {
let curr_byte = *Env::expect::<u8>();
assert_eq!(curr_byte, 0);
first_called = true;
child_env!(u8 => 1u8).enter(|| {
let curr_byte = *Env::expect::<u8>();
assert_eq!(curr_byte, 1);
second_called = true;
});
assert!(second_called);
assert_eq!(curr_byte, 0);
});
assert!(first_called);
assert!(Env::get::<u8>().is_none());
}
#[test]
fn child_sees_parent_env() {
assert!(Env::get::<u8>().is_none());
child_env!(u8 => 0u8).enter(|| {
let curr_byte = *Env::expect::<u8>();
assert_eq!(curr_byte, 0);
child_env!(u16 => 1u16).enter(|| {
let curr_byte = *Env::expect::<u8>();
assert_eq!(curr_byte, 0, "must see u8 from enclosing environment");
let curr_uh_twobyte = *Env::expect::<u16>();
assert_eq!(curr_uh_twobyte, 1, "must see locally installed u16");
});
assert_eq!(curr_byte, 0, "must see 0");
});
assert!(Env::get::<u8>().is_none());
}
#[test]
fn removing_from_env() {
assert!(Env::get::<u8>().is_none());
child_env!(u8 => 2).enter(|| {
assert_eq!(*Env::expect::<u8>(), 2, "just added 2u8");
child_env!().enter(|| {
assert_eq!(*Env::expect::<u8>(), 2, "parent added 2u8");
Env::hide::<u8>();
assert!(Env::get::<u8>().is_none(), "just removed u8 from Env");
});
assert_eq!(
*Env::get::<u8>().unwrap(),
2,
"returned to parent Env with 2u8"
);
Env::hide::<u8>();
assert!(Env::get::<u8>().is_none(), "just removed u8 from Env");
})
}
}