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/>.
*/
use crate::renderer::VAR_REGEX;
use log::error;
use std::collections::HashMap;
use thiserror::Error;
use crate::{
renderer::render_variables, Extension, ExtensionOutput, ExtensionResult, Params, Value,
};
use crate::{Extension, ExtensionOutput, ExtensionResult, Params, Value};
lazy_static! {
static ref EMPTY_PARAMS: Params = Params::new();
@ -59,7 +56,7 @@ impl<'a> Extension for FormExtension<'a> {
fn calculate(
&self,
_: &crate::Context,
scope: &crate::Scope,
_: &crate::Scope,
params: &Params,
) -> crate::ExtensionResult {
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());
};
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()
} else {
Params::new()
};
// Inject scope variables into fields (if needed)
inject_scope(&mut fields, scope);
match self.provider.show(layout, &fields, &EMPTY_PARAMS) {
FormProviderResult::Success(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)]
pub enum FormExtensionError {
#[error("missing layout parameter")]

View File

@ -98,6 +98,7 @@ impl Default for Template {
pub struct Variable {
pub name: String,
pub var_type: String,
pub inject_vars: bool,
pub params: Params,
}
@ -106,6 +107,7 @@ impl Default for Variable {
Self {
name: "".to_string(),
var_type: "".to_string(),
inject_vars: true,
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
*
@ -17,18 +17,22 @@
* 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::{
CasingStyle, Context, Extension, ExtensionOutput, ExtensionResult, RenderOptions, RenderResult,
Renderer, Scope, Template, Value, Variable,
};
use anyhow::Result;
use log::{error, warn};
use regex::{Captures, Regex};
use thiserror::Error;
use util::get_body_variable_names;
use self::util::{inject_variables_into_params, render_variables};
mod util;
lazy_static! {
@ -123,7 +127,22 @@ impl<'a> Renderer for DefaultRenderer<'a> {
return RenderResult::Error(RendererError::MissingSubMatch.into());
}
} 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) => {
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>(
variable: &Variable,
templates: &'a [&Template],
@ -324,6 +297,7 @@ mod tests {
params: vec![("echo".to_string(), Value::String((*value).to_string()))]
.into_iter()
.collect::<Params>(),
..Default::default()
})
.collect();
Template {
@ -415,6 +389,7 @@ mod tests {
"echo".to_string(),
Value::String("world".to_string()),
)]),
..Default::default()
}],
..Default::default()
},
@ -435,6 +410,7 @@ mod tests {
params: vec![("echo".to_string(), Value::String("Bob".to_string()))]
.into_iter()
.collect::<Params>(),
..Default::default()
},
Variable {
name: "var".to_string(),
@ -454,6 +430,7 @@ mod tests {
"read".to_string(),
Value::String("local".to_string()),
)]),
..Default::default()
}],
..Default::default()
},
@ -473,6 +450,7 @@ mod tests {
params: vec![("trigger".to_string(), Value::String("nested".to_string()))]
.into_iter()
.collect::<Params>(),
..Default::default()
}],
..Default::default()
};
@ -503,6 +481,7 @@ mod tests {
params: vec![("trigger".to_string(), Value::String("nested".to_string()))]
.into_iter()
.collect::<Params>(),
..Default::default()
}],
..Default::default()
};
@ -527,6 +506,7 @@ mod tests {
params: vec![("abort".to_string(), Value::Null)]
.into_iter()
.collect::<Params>(),
..Default::default()
}],
..Default::default()
};
@ -545,10 +525,93 @@ mod tests {
params: vec![("error".to_string(), Value::Null)]
.into_iter()
.collect::<Params>(),
..Default::default()
}],
..Default::default()
};
let res = renderer.render(&template, &Default::default(), &Default::default());
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/>.
*/
use crate::{renderer::RendererError, ExtensionOutput, Params, Scope, Value};
use anyhow::Result;
use log::error;
use regex::Captures;
use super::VAR_REGEX;
use std::collections::HashSet;
@ -29,10 +34,91 @@ pub(crate) fn get_body_variable_names(body: &str) -> HashSet<&str> {
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)]
mod tests {
use super::*;
use std::iter::FromIterator;
use std::{collections::HashMap, iter::FromIterator};
#[test]
fn get_body_variable_names_no_vars() {
@ -49,4 +135,44 @@ mod tests {
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()))
);
}
}