#![cfg_attr(not(feature = "builder"), allow(unused))]
mod resolvable;
pub use self::resolvable::{NoConfig, Resolvable};
use crate::views::BoxedView;
use parking_lot::Mutex;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use std::any::Any;
type MakerTrait<T> = dyn Fn(&Config, &Context) -> Result<T, Error> + Send + Sync;
type Maker<T> = Box<MakerTrait<T>>;
type AnyMaker = Maker<Box<dyn Any>>;
pub type Config = serde_json::Value;
pub type Object = serde_json::Map<String, serde_json::Value>;
pub type BareBuilder = fn(&serde_json::Value, &Context) -> Result<BoxedView, Error>;
type BoxedBuilder = Box<dyn Fn(&Config, &Context) -> Result<BoxedView, Error> + Send + Sync>;
pub type BareWrapperBuilder = fn(&serde_json::Value, &Context) -> Result<Wrapper, Error>;
type BoxedWrapperBuilder =
Box<dyn Fn(&serde_json::Value, &Context) -> Result<Wrapper, Error> + Send + Sync>;
pub type Wrapper = Box<dyn FnOnce(BoxedView) -> BoxedView + Send>;
pub type BareVarBuilder = fn(&serde_json::Value, &Context) -> Result<Box<dyn Any>, Error>;
pub type BoxedVarBuilder =
Arc<dyn Fn(&serde_json::Value, &Context) -> Result<Box<dyn Any>, Error> + Send + Sync>;
#[derive(Clone)]
pub struct Context {
variables: Arc<Variables>,
blueprints: Arc<Blueprints>,
}
impl std::fmt::Debug for Context {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let vars: Vec<_> = self.variables.keys().collect();
let blueprints: Vec<_> = self.blueprints.keys().collect();
let wrappers: Vec<_> = self.blueprints.wrapper_keys().collect();
write!(f, "Variables: {vars:?}, ")?;
write!(f, "Blueprints: {blueprints:?}, ")?;
write!(f, "Wrappers: {wrappers:?}")?;
Ok(())
}
}
struct Blueprints {
blueprints: HashMap<String, BoxedBuilder>,
wrappers: HashMap<String, BoxedWrapperBuilder>,
parent: Option<Arc<Blueprints>>,
}
pub struct ResolveOnce<T>(Arc<Mutex<Option<T>>>);
pub fn resolve_once<T>(value: T) -> impl Fn(&Config, &Context) -> Result<T, Error>
where
T: Send,
{
let value = Mutex::new(Some(value));
move |_, _| {
value
.lock()
.take()
.ok_or_else(|| Error::MakerFailed("variable was already resolved".to_string()))
}
}
impl<T> ResolveOnce<T> {
pub fn new(value: T) -> Self {
Self(Arc::new(Mutex::new(Some(value))))
}
pub fn take(&self) -> Option<T> {
self.0.lock().take()
}
pub fn is_some(&self) -> bool {
self.0.lock().is_some()
}
}
impl Blueprints {
fn wrapper_keys(&self) -> impl Iterator<Item = &String> {
self.wrappers
.keys()
.chain(self.parent.iter().flat_map(|parent| {
let parent: Box<dyn Iterator<Item = &String>> = Box::new(parent.wrapper_keys());
parent
}))
}
fn keys(&self) -> impl Iterator<Item = &String> {
self.blueprints
.keys()
.chain(self.parent.iter().flat_map(|parent| {
let parent: Box<dyn Iterator<Item = &String>> = Box::new(parent.keys());
parent
}))
}
fn build(&self, name: &str, config: &Config, context: &Context) -> Result<BoxedView, Error> {
if let Some(blueprint) = self.blueprints.get(name) {
(blueprint)(config, context)
.map_err(|e| Error::BlueprintFailed(name.into(), Box::new(e)))
} else {
match self.parent {
Some(ref parent) => parent.build(name, config, context),
None => Err(Error::BlueprintNotFound(name.into())),
}
}
}
fn build_wrapper(
&self,
name: &str,
config: &Config,
context: &Context,
) -> Result<Wrapper, Error> {
if let Some(blueprint) = self.wrappers.get(name) {
(blueprint)(config, context)
.map_err(|e| Error::BlueprintFailed(name.into(), Box::new(e)))
} else {
match self.parent {
Some(ref parent) => parent.build_wrapper(name, config, context),
None => Err(Error::BlueprintNotFound(name.into())),
}
}
}
}
enum VarEntry {
Proxy(Arc<String>),
Maker(AnyMaker),
Config(Config),
}
impl VarEntry {
fn proxy(var_name: impl Into<String>) -> Self {
VarEntry::Proxy(Arc::new(var_name.into()))
}
fn maker(maker: AnyMaker) -> Self {
VarEntry::Maker(maker)
}
fn config(config: impl Into<Config>) -> Self {
VarEntry::Config(config.into())
}
}
struct Variables {
variables: HashMap<String, VarEntry>,
parent: Option<Arc<Variables>>,
}
#[derive(Debug)]
#[non_exhaustive]
pub enum Error {
InvalidConfig {
message: String,
config: Config,
},
NoSuchVariable(String),
IncorrectVariableType {
name: String,
expected_type: String,
},
CouldNotLoad {
expected_type: String,
config: Config,
},
AllVariantsFailed {
config: Config,
errors: Vec<Error>,
},
BlueprintNotFound(String),
BlueprintFailed(String, Box<Error>),
MakerFailed(String),
ResolveFailed {
var_failure: Box<Error>,
config_failure: Box<Error>,
},
}
impl Error {
pub fn invalid_config<S: Into<String>, C: Clone + Into<Config>>(
message: S,
config: &C,
) -> Self {
let message = message.into();
let config = config.clone().into();
Error::InvalidConfig { message, config }
}
}
#[derive(Debug)]
pub struct ConfigError {
pub duplicate_vars: HashSet<String>,
pub missing_vars: HashSet<String>,
}
impl ConfigError {
fn from(duplicate_vars: HashSet<String>, missing_vars: HashSet<String>) -> Result<(), Self> {
if duplicate_vars.is_empty() && missing_vars.is_empty() {
Ok(())
} else {
Err(Self {
duplicate_vars,
missing_vars,
})
}
}
}
fn inspect_variables<F: FnMut(&str)>(config: &Config, on_var: &mut F) {
match config {
Config::String(name) => {
if let Some(name) = name.strip_prefix('$') {
on_var(name);
}
}
Config::Array(array) => {
for value in array {
inspect_variables(value, on_var);
}
}
Config::Object(object) => {
for value in object.values() {
inspect_variables(value, on_var);
}
}
_ => (),
}
}
new_default!(Context);
impl Context {
pub fn new() -> Self {
#[cfg(feature = "builder")]
let blueprints = inventory::iter::<Blueprint>()
.map(|blueprint| blueprint.as_tuple())
.collect();
#[cfg(not(feature = "builder"))]
let blueprints = Default::default();
#[cfg(feature = "builder")]
let wrappers = inventory::iter::<WrapperBlueprint>()
.map(|blueprint| blueprint.as_tuple())
.collect();
#[cfg(not(feature = "builder"))]
let wrappers = Default::default();
#[cfg(feature = "builder")]
let variables = inventory::iter::<CallbackBlueprint>()
.map(|blueprint| blueprint.as_tuple())
.collect();
#[cfg(not(feature = "builder"))]
let variables = Default::default();
let blueprints = Arc::new(Blueprints {
blueprints,
wrappers,
parent: None,
});
let variables = Arc::new(Variables {
variables,
parent: None,
});
Self {
blueprints,
variables,
}
}
pub fn resolve_as_var<T: 'static + Resolvable>(&self, config: &Config) -> Result<T, Error> {
if let Some(name) = parse_var(config) {
self.load(name, &Config::Null)
} else if let Some(config) = config.as_object() {
let (key, value) = config.iter().next().ok_or_else(|| Error::InvalidConfig {
message: "Expected non-empty body".into(),
config: config.clone().into(),
})?;
let key = key.strip_prefix('$').ok_or_else(|| Error::InvalidConfig {
message: format!("Expected variable as $key; but found {key}."),
config: config.clone().into(),
})?;
self.load(key, value)
} else {
Err(Error::CouldNotLoad {
expected_type: std::any::type_name::<T>().into(),
config: config.clone(),
})
}
}
fn resolve_as_builder<T: Resolvable + 'static>(&self, config: &Config) -> Result<T, Error> {
let config = config
.as_object()
.ok_or_else(|| Error::invalid_config("Expected string", config))?;
let (key, value) = config.iter().next().ok_or_else(|| Error::InvalidConfig {
message: "Expected non-empty body".into(),
config: config.clone().into(),
})?;
let key = key.strip_prefix('$').ok_or_else(|| Error::InvalidConfig {
message: "Expected variable as key".into(),
config: config.clone().into(),
})?;
self.load(key, value)
}
pub fn resolve_as_config<T: Resolvable + 'static>(&self, config: &Config) -> Result<T, Error> {
if let Some(name) = parse_var(config) {
if let Ok(var) = self.load(name, &Config::Null) {
return Ok(var);
}
}
if let Ok(value) = self.resolve_as_builder(config) {
return Ok(value);
}
T::from_config(config, self)
}
pub fn resolve<T: Resolvable + 'static>(&self, config: &Config) -> Result<T, Error> {
let var_failure = Box::new(match self.resolve_as_var(config) {
Ok(value) => return Ok(value),
Err(err) => err,
});
let config_failure = Box::new(match self.resolve_as_config(config) {
Ok(value) => return Ok(value),
Err(err) => err,
});
Err(Error::ResolveFailed {
var_failure,
config_failure,
})
}
pub fn resolve_or<T: Resolvable + 'static>(
&self,
config: &Config,
if_missing: T,
) -> Result<T, Error> {
Ok(self.resolve::<Option<T>>(config)?.unwrap_or(if_missing))
}
fn store_entry(&mut self, name: impl Into<String>, entry: VarEntry) {
let name = name.into();
if let Some(variables) = Arc::get_mut(&mut self.variables) {
variables.store(name, entry);
} else {
log::error!("Context was not available to store variable `{name}`.");
}
}
pub fn store_once<T>(&mut self, name: impl Into<String>, value: T)
where
T: Send + 'static,
{
self.store_with(name, resolve_once(value));
}
pub fn store_with<T: 'static>(
&mut self,
name: impl Into<String>,
maker: impl 'static + Fn(&Config, &Context) -> Result<T, Error> + Send + Sync,
) {
let name = name.into();
let maker: AnyMaker = Box::new(move |config, context| {
let res: T = (maker)(config, context)?;
let b: Box<dyn Any> = Box::new(res);
Ok(b)
});
self.store_entry(name, VarEntry::Maker(maker));
}
pub fn store<S, T>(&mut self, name: S, value: T)
where
S: Into<String>,
T: Clone + Send + Sync + 'static,
{
self.store_with(name, move |_, _| Ok(value.clone()));
}
pub fn store_view<S, T>(&mut self, name: S, view: T)
where
S: Into<String>,
T: crate::view::IntoBoxedView,
{
self.store_once(name, BoxedView::new(view.into_boxed_view()));
}
pub fn store_config(&mut self, name: impl Into<String>, config: impl Into<Config>) {
self.store_entry(name, VarEntry::config(config));
}
pub fn store_proxy(&mut self, name: impl Into<String>, new_name: impl Into<String>) {
self.store_entry(name, VarEntry::proxy(new_name));
}
pub fn register_blueprint<F>(&mut self, name: impl Into<String>, blueprint: F)
where
F: Fn(&Config, &Context) -> Result<BoxedView, Error> + 'static + Send + Sync,
{
if let Some(blueprints) = Arc::get_mut(&mut self.blueprints) {
blueprints
.blueprints
.insert(name.into(), Box::new(blueprint));
}
}
pub fn register_wrapper_blueprint<F>(&mut self, name: impl Into<String>, blueprint: F)
where
F: Fn(&Config, &Context) -> Result<Wrapper, Error> + 'static + Send + Sync,
{
if let Some(blueprints) = Arc::get_mut(&mut self.blueprints) {
blueprints.wrappers.insert(name.into(), Box::new(blueprint));
}
}
fn on_maker<T: 'static + Resolvable>(
&self,
maker: &AnyMaker,
name: &str,
config: &Config,
) -> Result<T, Error> {
let res: Box<dyn Any> = (maker)(config, self)?;
T::from_any(res).ok_or_else(|| {
Error::IncorrectVariableType {
name: name.into(),
expected_type: std::any::type_name::<T>().into(),
}
})
}
pub fn load<T: Resolvable + Any>(&self, name: &str, config: &Config) -> Result<T, Error> {
self.variables.call_on_any(
name,
|maker| self.on_maker(maker, name, config),
|config| self.resolve(config),
)
}
pub fn build_wrapper(&self, config: &Config) -> Result<Wrapper, Error> {
let (key, value) = match config {
Config::String(key) => (key, &Config::Null),
Config::Object(config) => config.into_iter().next().ok_or(Error::InvalidConfig {
message: "Expected non-empty object".into(),
config: config.clone().into(),
})?,
_ => {
return Err(Error::InvalidConfig {
message: "Expected string or object".into(),
config: config.clone(),
})
}
};
let wrapper = self.blueprints.build_wrapper(key, value, self)?;
Ok(wrapper)
}
pub fn validate(&self, config: &Config) -> Result<(), ConfigError> {
let mut vars = HashSet::new();
let mut duplicates = HashSet::new();
inspect_variables(config, &mut |variable| {
if !vars.insert(variable.to_string()) {
duplicates.insert(variable.to_string());
}
});
let not_found: HashSet<String> = vars
.into_iter()
.filter(|var| !self.variables.variables.contains_key(var))
.collect();
ConfigError::from(duplicates, not_found)
}
fn get_wrappers(&self, config: &Config) -> Result<Vec<Wrapper>, Error> {
fn get_with(config: &Config) -> Option<&Vec<Config>> {
config.as_object()?.get("with")?.as_array()
}
let with = match get_with(config) {
Some(with) => with,
None => return Ok(Vec::new()),
};
with.iter().map(|with| self.build_wrapper(with)).collect()
}
pub fn build(&self, config: &Config) -> Result<BoxedView, Error> {
let (key, value) = match config {
Config::String(name) => (name, &serde_json::Value::Null),
Config::Object(config) => {
config.iter().next().ok_or(Error::InvalidConfig {
message: "Expected non-empty object".into(),
config: config.clone().into(),
})?
}
_ => {
return Err(Error::InvalidConfig {
message: "Expected object or string.".into(),
config: config.clone(),
})
}
};
let with = self.get_wrappers(value)?;
let mut view = self.blueprints.build(key, value, self)?;
for wrapper in with {
view = (wrapper)(view);
}
Ok(view)
}
pub fn sub_context<F>(&self, f: F) -> Context
where
F: FnOnce(&mut Context),
{
let variables = Arc::new(Variables {
variables: HashMap::new(),
parent: Some(Arc::clone(&self.variables)),
});
let blueprints = Arc::new(Blueprints {
blueprints: HashMap::new(),
wrappers: HashMap::new(),
parent: Some(Arc::clone(&self.blueprints)),
});
let mut context = Context {
blueprints,
variables,
};
f(&mut context);
context
}
pub fn build_template(&self, config: &Config, template: &Config) -> Result<BoxedView, Error> {
let res = self
.sub_context(|c| {
if let Some(config) = config.as_object() {
for (key, value) in config.iter() {
if let Some(var) = parse_var(value) {
c.store_proxy(key, var);
} else {
c.store_config(key, value.clone());
}
}
} else {
c.store_config(".", config.clone());
}
})
.build(template)?;
Ok(res)
}
}
fn parse_var(value: &Config) -> Option<&str> {
value.as_str().and_then(|s| s.strip_prefix('$'))
}
impl Variables {
fn keys(&self) -> impl Iterator<Item = &String> {
self.variables
.keys()
.chain(self.parent.iter().flat_map(|parent| {
let parent: Box<dyn Iterator<Item = &String>> = Box::new(parent.keys());
parent
}))
}
fn store<S>(&mut self, name: S, value: VarEntry)
where
S: Into<String>,
{
let name = name.into();
self.variables.insert(name, value);
}
fn call_on_any<OnMaker, OnConfig, T>(
&self,
name: &str,
mut on_maker: OnMaker,
mut on_config: OnConfig,
) -> Result<T, Error>
where
OnConfig: FnMut(&Config) -> Result<T, Error>,
OnMaker: FnMut(&AnyMaker) -> Result<T, Error>,
T: 'static,
{
let new_name = match self.variables.get(name) {
None => None,
Some(VarEntry::Proxy(proxy)) => Some(Arc::clone(proxy)),
Some(VarEntry::Maker(maker)) => return (on_maker)(maker),
Some(VarEntry::Config(config)) => return (on_config)(config),
};
let name = new_name
.as_ref()
.map(|s| s.as_ref().as_str())
.unwrap_or(name);
self.parent.as_ref().map_or_else(
|| Err(Error::NoSuchVariable(name.into())),
|parent| parent.call_on_any(name, on_maker, on_config),
)
}
}
pub struct CallbackBlueprint {
pub name: &'static str,
pub builder: BareVarBuilder,
}
impl CallbackBlueprint {
fn as_tuple(&self) -> (String, VarEntry) {
let cb: AnyMaker = Box::new(self.builder);
(self.name.into(), VarEntry::maker(cb))
}
}
pub struct Blueprint {
pub name: &'static str,
pub builder: BareBuilder,
}
impl Blueprint {
fn as_tuple(&self) -> (String, BoxedBuilder) {
(self.name.into(), Box::new(self.builder))
}
}
pub struct WrapperBlueprint {
pub name: &'static str,
pub builder: BareWrapperBuilder,
}
impl WrapperBlueprint {
fn as_tuple(&self) -> (String, BoxedWrapperBuilder) {
(self.name.into(), Box::new(self.builder))
}
}
#[cfg(feature = "builder")]
inventory::collect!(Blueprint);
#[cfg(feature = "builder")]
inventory::collect!(CallbackBlueprint);
#[cfg(feature = "builder")]
inventory::collect!(WrapperBlueprint);
#[cfg(not(feature = "builder"))]
#[macro_export]
macro_rules! manual_blueprint {
($name:ident from $config_builder:expr) => {};
(with $name:ident, $builder:expr) => {};
($name:ident, $builder:expr) => {};
}
#[cfg(feature = "builder")]
#[macro_export]
macro_rules! manual_blueprint {
($name:ident from $config_builder:expr) => {
$crate::submit! {
$crate::builder::Blueprint {
name: stringify!($name),
builder: |config, context| {
let template = $config_builder;
context.build_template(config, &template)
},
}
}
};
(with $name:ident, $builder:expr) => {
$crate::submit! {
$crate::builder::WrapperBlueprint {
name: stringify!($name),
builder: |config, context| {
let builder: fn(&$crate::reexports::serde_json::Value, &$crate::builder::Context) -> Result<_, $crate::builder::Error> = $builder;
let wrapper = (builder)(config, context)?;
Ok(Box::new(move |view| {
let view = (wrapper)(view);
$crate::views::BoxedView::boxed(view)
}))
}
}
}
};
($name:ident, $builder:expr) => {
$crate::submit! {
$crate::builder::Blueprint {
name: stringify!($name),
builder: |config, context| {
let builder: fn(&$crate::reexports::serde_json::Value, &$crate::builder::Context) -> Result<_,$crate::builder::Error> = $builder;
(builder)(config, context).map($crate::views::BoxedView::boxed)
},
}
}
};
}
#[cfg(not(feature = "builder"))]
#[macro_export]
macro_rules! fn_blueprint {
($name: expr, $builder:expr) => {};
}
#[cfg(feature = "builder")]
#[macro_export]
macro_rules! fn_blueprint {
($name: expr, $builder:expr) => {
$crate::submit! {
$crate::builder::CallbackBlueprint {
name: $name,
builder: |config, context| {
let builder: fn(&::serde_json::Value, &$crate::builder::Context) -> Result<_, $crate::builder::Error> = $builder;
Ok(Box::new((builder)(config, context)?))
},
}
}
};
}
manual_blueprint!(View, |config, context| {
let view: BoxedView = context.resolve(&config["view"])?;
Ok(view)
});
fn_blueprint!("concat", |config, context| {
let values = config
.as_array()
.ok_or_else(|| Error::invalid_config("Expected array", config))?;
values
.iter()
.map(|value| {
context.resolve::<String>(value)
})
.collect::<Result<String, _>>()
});
fn_blueprint!("cursup", |config, context| {
let text: String = context.resolve(config)?;
Ok(crate::utils::markup::cursup::parse(text))
});
#[cfg(feature = "builder")]
#[cfg(test)]
mod tests {
#[test]
fn test_load_config() {
use crate::view::Finder;
let config = r#"
LinearLayout:
children:
- TextView:
content: $foo
with:
- name: text
- DummyView
- TextView: bar
- LinearLayout:
orientation: horizontal
children:
- TextView: "Age?"
- DummyView
- EditView:
with:
- name: edit
with:
- full_screen
"#;
let foo = "Foo";
let config: crate::builder::Config = serde_yaml::from_str(config).unwrap();
let mut context = crate::builder::Context::new();
assert!(context.validate(&config).is_err());
context.store("foo", foo.to_string());
context.validate(&config).unwrap();
let mut res = context.build(&config).unwrap();
assert!(res
.downcast_ref::<crate::views::ResizedView<crate::views::BoxedView>>()
.is_some());
let content = res
.call_on_name("text", |v: &mut crate::views::TextView| v.get_content())
.unwrap();
assert_eq!(content.source(), foo);
}
}