feat(render): add variable injection mechanism to renderer #856

This commit is contained in:
Federico Terzi 2021-11-06 23:32:14 +01:00
parent 9a2c27a202
commit 40e8dace33
4 changed files with 245 additions and 79 deletions

View File

@ -17,14 +17,11 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use crate::renderer::VAR_REGEX;
use log::error; use log::error;
use std::collections::HashMap; use std::collections::HashMap;
use thiserror::Error; use thiserror::Error;
use crate::{ use crate::{Extension, ExtensionOutput, ExtensionResult, Params, Value};
renderer::render_variables, Extension, ExtensionOutput, ExtensionResult, Params, Value,
};
lazy_static! { lazy_static! {
static ref EMPTY_PARAMS: Params = Params::new(); static ref EMPTY_PARAMS: Params = Params::new();
@ -59,7 +56,7 @@ impl<'a> Extension for FormExtension<'a> {
fn calculate( fn calculate(
&self, &self,
_: &crate::Context, _: &crate::Context,
scope: &crate::Scope, _: &crate::Scope,
params: &Params, params: &Params,
) -> crate::ExtensionResult { ) -> crate::ExtensionResult {
let layout = if let Some(Value::String(layout)) = params.get("layout") { let layout = if let Some(Value::String(layout)) = params.get("layout") {
@ -68,15 +65,12 @@ impl<'a> Extension for FormExtension<'a> {
return crate::ExtensionResult::Error(FormExtensionError::MissingLayout.into()); return crate::ExtensionResult::Error(FormExtensionError::MissingLayout.into());
}; };
let mut fields = if let Some(Value::Object(fields)) = params.get("fields") { let fields = if let Some(Value::Object(fields)) = params.get("fields") {
fields.clone() fields.clone()
} else { } else {
Params::new() Params::new()
}; };
// Inject scope variables into fields (if needed)
inject_scope(&mut fields, scope);
match self.provider.show(layout, &fields, &EMPTY_PARAMS) { match self.provider.show(layout, &fields, &EMPTY_PARAMS) {
FormProviderResult::Success(values) => { FormProviderResult::Success(values) => {
ExtensionResult::Success(ExtensionOutput::Multiple(values)) ExtensionResult::Success(ExtensionOutput::Multiple(values))
@ -87,25 +81,6 @@ impl<'a> Extension for FormExtension<'a> {
} }
} }
// TODO: test
fn inject_scope(fields: &mut HashMap<String, Value>, scope: &HashMap<&str, ExtensionOutput>) {
for value in fields.values_mut() {
if let Value::Object(field_options) = value {
if let Some(Value::String(default_value)) = field_options.get_mut("default") {
if VAR_REGEX.is_match(default_value) {
match render_variables(default_value, scope) {
Ok(rendered) => *default_value = rendered,
Err(err) => error!(
"error while injecting variable in form default value: {}",
err
),
}
}
}
}
}
}
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum FormExtensionError { pub enum FormExtensionError {
#[error("missing layout parameter")] #[error("missing layout parameter")]

View File

@ -98,6 +98,7 @@ impl Default for Template {
pub struct Variable { pub struct Variable {
pub name: String, pub name: String,
pub var_type: String, pub var_type: String,
pub inject_vars: bool,
pub params: Params, pub params: Params,
} }
@ -106,6 +107,7 @@ impl Default for Variable {
Self { Self {
name: "".to_string(), name: "".to_string(),
var_type: "".to_string(), var_type: "".to_string(),
inject_vars: true,
params: Params::new(), params: Params::new(),
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* This file is part of esp name: (), var_type: (), params: ()anso. * This file is part of espanso.
* *
* Copyright (C) 2019-2021 Federico Terzi * Copyright (C) 2019-2021 Federico Terzi
* *
@ -17,18 +17,22 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use std::collections::{HashMap, HashSet}; use std::{
borrow::Cow,
collections::{HashMap, HashSet},
};
use crate::{ use crate::{
CasingStyle, Context, Extension, ExtensionOutput, ExtensionResult, RenderOptions, RenderResult, CasingStyle, Context, Extension, ExtensionOutput, ExtensionResult, RenderOptions, RenderResult,
Renderer, Scope, Template, Value, Variable, Renderer, Scope, Template, Value, Variable,
}; };
use anyhow::Result;
use log::{error, warn}; use log::{error, warn};
use regex::{Captures, Regex}; use regex::{Captures, Regex};
use thiserror::Error; use thiserror::Error;
use util::get_body_variable_names; use util::get_body_variable_names;
use self::util::{inject_variables_into_params, render_variables};
mod util; mod util;
lazy_static! { lazy_static! {
@ -123,7 +127,22 @@ impl<'a> Renderer for DefaultRenderer<'a> {
return RenderResult::Error(RendererError::MissingSubMatch.into()); return RenderResult::Error(RendererError::MissingSubMatch.into());
} }
} else if let Some(extension) = self.extensions.get(&variable.var_type) { } else if let Some(extension) = self.extensions.get(&variable.var_type) {
match extension.calculate(context, &scope, &variable.params) { let variable_params = if variable.inject_vars {
match inject_variables_into_params(&variable.params, &scope) {
Ok(augmented_params) => Cow::Owned(augmented_params),
Err(err) => {
error!(
"unable to inject variables into params of variable '{}': {}",
variable.name, err
);
return RenderResult::Error(err);
}
}
} else {
Cow::Borrowed(&variable.params)
};
match extension.calculate(context, &scope, &variable_params) {
ExtensionResult::Success(output) => { ExtensionResult::Success(output) => {
scope.insert(&variable.name, output); scope.insert(&variable.name, output);
} }
@ -192,52 +211,6 @@ impl<'a> Renderer for DefaultRenderer<'a> {
} }
} }
// TODO: test
pub(crate) fn render_variables(body: &str, scope: &Scope) -> Result<String> {
let mut replacing_error = None;
let output = VAR_REGEX
.replace_all(body, |caps: &Captures| {
let var_name = caps.name("name").unwrap().as_str();
let var_subname = caps.name("subname");
match scope.get(var_name) {
Some(output) => match output {
ExtensionOutput::Single(output) => output,
ExtensionOutput::Multiple(results) => match var_subname {
Some(var_subname) => {
let var_subname = var_subname.as_str();
results.get(var_subname).map_or("", |value| &*value)
}
None => {
error!(
"nested name missing from multi-value variable: {}",
var_name
);
replacing_error = Some(RendererError::MissingVariable(format!(
"nested name missing from multi-value variable: {}",
var_name
)));
""
}
},
},
None => {
replacing_error = Some(RendererError::MissingVariable(format!(
"variable '{}' is missing",
var_name
)));
""
}
}
})
.to_string();
if let Some(error) = replacing_error {
return Err(error.into());
}
Ok(output)
}
fn get_matching_template<'a>( fn get_matching_template<'a>(
variable: &Variable, variable: &Variable,
templates: &'a [&Template], templates: &'a [&Template],
@ -324,6 +297,7 @@ mod tests {
params: vec![("echo".to_string(), Value::String((*value).to_string()))] params: vec![("echo".to_string(), Value::String((*value).to_string()))]
.into_iter() .into_iter()
.collect::<Params>(), .collect::<Params>(),
..Default::default()
}) })
.collect(); .collect();
Template { Template {
@ -415,6 +389,7 @@ mod tests {
"echo".to_string(), "echo".to_string(),
Value::String("world".to_string()), Value::String("world".to_string()),
)]), )]),
..Default::default()
}], }],
..Default::default() ..Default::default()
}, },
@ -435,6 +410,7 @@ mod tests {
params: vec![("echo".to_string(), Value::String("Bob".to_string()))] params: vec![("echo".to_string(), Value::String("Bob".to_string()))]
.into_iter() .into_iter()
.collect::<Params>(), .collect::<Params>(),
..Default::default()
}, },
Variable { Variable {
name: "var".to_string(), name: "var".to_string(),
@ -454,6 +430,7 @@ mod tests {
"read".to_string(), "read".to_string(),
Value::String("local".to_string()), Value::String("local".to_string()),
)]), )]),
..Default::default()
}], }],
..Default::default() ..Default::default()
}, },
@ -473,6 +450,7 @@ mod tests {
params: vec![("trigger".to_string(), Value::String("nested".to_string()))] params: vec![("trigger".to_string(), Value::String("nested".to_string()))]
.into_iter() .into_iter()
.collect::<Params>(), .collect::<Params>(),
..Default::default()
}], }],
..Default::default() ..Default::default()
}; };
@ -503,6 +481,7 @@ mod tests {
params: vec![("trigger".to_string(), Value::String("nested".to_string()))] params: vec![("trigger".to_string(), Value::String("nested".to_string()))]
.into_iter() .into_iter()
.collect::<Params>(), .collect::<Params>(),
..Default::default()
}], }],
..Default::default() ..Default::default()
}; };
@ -527,6 +506,7 @@ mod tests {
params: vec![("abort".to_string(), Value::Null)] params: vec![("abort".to_string(), Value::Null)]
.into_iter() .into_iter()
.collect::<Params>(), .collect::<Params>(),
..Default::default()
}], }],
..Default::default() ..Default::default()
}; };
@ -545,10 +525,93 @@ mod tests {
params: vec![("error".to_string(), Value::Null)] params: vec![("error".to_string(), Value::Null)]
.into_iter() .into_iter()
.collect::<Params>(), .collect::<Params>(),
..Default::default()
}], }],
..Default::default() ..Default::default()
}; };
let res = renderer.render(&template, &Default::default(), &Default::default()); let res = renderer.render(&template, &Default::default(), &Default::default());
assert!(matches!(res, RenderResult::Error(_))); assert!(matches!(res, RenderResult::Error(_)));
} }
#[test]
fn variable_injection() {
let renderer = get_renderer();
let mut template = template_for_str("hello {{fullname}}");
template.vars = vec![
Variable {
name: "firstname".to_string(),
var_type: "mock".to_string(),
params: Params::from_iter(vec![(
"echo".to_string(),
Value::String("John".to_string()),
)]),
..Default::default()
},
Variable {
name: "lastname".to_string(),
var_type: "mock".to_string(),
params: Params::from_iter(vec![(
"echo".to_string(),
Value::String("Snow".to_string()),
)]),
..Default::default()
},
Variable {
name: "fullname".to_string(),
var_type: "mock".to_string(),
params: Params::from_iter(vec![(
"echo".to_string(),
Value::String("{{firstname}} {{lastname}}".to_string()),
)]),
inject_vars: true,
},
];
let res = renderer.render(&template, &Default::default(), &Default::default());
assert!(matches!(res, RenderResult::Success(str) if str == "hello John Snow"));
}
#[test]
fn disable_variable_injection() {
let renderer = get_renderer();
let mut template = template_for_str("hello {{second}}");
template.vars = vec![
Variable {
name: "first".to_string(),
var_type: "mock".to_string(),
params: Params::from_iter(vec![("echo".to_string(), Value::String("one".to_string()))]),
..Default::default()
},
Variable {
name: "second".to_string(),
var_type: "mock".to_string(),
params: Params::from_iter(vec![(
"echo".to_string(),
Value::String("{{first}} two".to_string()),
)]),
inject_vars: false,
},
];
let res = renderer.render(&template, &Default::default(), &Default::default());
assert!(matches!(res, RenderResult::Success(str) if str == "hello {{first}} two"));
}
#[test]
fn variable_injection_missing_var() {
let renderer = get_renderer();
let mut template = template_for_str("hello {{second}}");
template.vars = vec![Variable {
name: "second".to_string(),
var_type: "mock".to_string(),
params: Params::from_iter(vec![(
"echo".to_string(),
Value::String("the next is {{missing}}".to_string()),
)]),
..Default::default()
}];
let res = renderer.render(&template, &Default::default(), &Default::default());
assert!(matches!(res, RenderResult::Error(_)));
}
} }

View File

@ -17,6 +17,11 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use crate::{renderer::RendererError, ExtensionOutput, Params, Scope, Value};
use anyhow::Result;
use log::error;
use regex::Captures;
use super::VAR_REGEX; use super::VAR_REGEX;
use std::collections::HashSet; use std::collections::HashSet;
@ -29,10 +34,91 @@ pub(crate) fn get_body_variable_names(body: &str) -> HashSet<&str> {
variables variables
} }
pub(crate) fn render_variables(body: &str, scope: &Scope) -> Result<String> {
let mut replacing_error = None;
let output = VAR_REGEX
.replace_all(body, |caps: &Captures| {
let var_name = caps.name("name").unwrap().as_str();
let var_subname = caps.name("subname");
match scope.get(var_name) {
Some(output) => match output {
ExtensionOutput::Single(output) => output,
ExtensionOutput::Multiple(results) => match var_subname {
Some(var_subname) => {
let var_subname = var_subname.as_str();
results.get(var_subname).map_or("", |value| &*value)
}
None => {
error!(
"nested name missing from multi-value variable: {}",
var_name
);
replacing_error = Some(RendererError::MissingVariable(format!(
"nested name missing from multi-value variable: {}",
var_name
)));
""
}
},
},
None => {
replacing_error = Some(RendererError::MissingVariable(format!(
"variable '{}' is missing",
var_name
)));
""
}
}
})
.to_string();
if let Some(error) = replacing_error {
return Err(error.into());
}
Ok(output)
}
pub(crate) fn inject_variables_into_params(params: &Params, scope: &Scope) -> Result<Params> {
let mut params = params.clone();
for (_, value) in params.iter_mut() {
inject_variables_into_value(value, scope)?;
}
Ok(params)
}
fn inject_variables_into_value(value: &mut Value, scope: &Scope) -> Result<()> {
match value {
Value::String(s_value) => {
let new_value = render_variables(s_value, scope)?;
if &new_value != s_value {
s_value.clear();
s_value.push_str(&new_value);
}
}
Value::Array(values) => {
for value in values {
inject_variables_into_value(value, scope)?;
}
}
Value::Object(fields) => {
for value in fields.values_mut() {
inject_variables_into_value(value, scope)?;
}
}
_ => {}
}
Ok(())
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use std::iter::FromIterator; use std::{collections::HashMap, iter::FromIterator};
#[test] #[test]
fn get_body_variable_names_no_vars() { fn get_body_variable_names_no_vars() {
@ -49,4 +135,44 @@ mod tests {
HashSet::from_iter(vec!["world", "greet"]), HashSet::from_iter(vec!["world", "greet"]),
); );
} }
#[test]
fn test_inject_variables_into_params() {
let mut params = Params::new();
params.insert(
"field1".to_string(),
Value::String("this contains {{first}}".to_string()),
);
params.insert("field2".to_string(), Value::Bool(true));
params.insert(
"field3".to_string(),
Value::Array(vec![Value::String("this contains {{first}}".to_string())]),
);
let mut nested = HashMap::new();
nested.insert(
"subfield1".to_string(),
Value::String("also contains {{first}}".to_string()),
);
params.insert("field4".to_string(), Value::Object(nested));
let mut scope = Scope::new();
scope.insert("first", ExtensionOutput::Single("one".to_string()));
let result = inject_variables_into_params(&params, &scope).unwrap();
assert_eq!(result.len(), 4);
assert_eq!(
result.get("field1").unwrap(),
&Value::String("this contains one".to_string())
);
assert_eq!(result.get("field2").unwrap(), &Value::Bool(true));
assert_eq!(
result.get("field3").unwrap(),
&Value::Array(vec![Value::String("this contains one".to_string())])
);
assert!(
matches!(result.get("field4").unwrap(), Value::Object(fields) if fields.get("subfield1").unwrap() == &Value::String("also contains one".to_string()))
);
}
} }