From 40e8dace330509bef56c3e2d9e1b3d4471b218dc Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sat, 6 Nov 2021 23:32:14 +0100 Subject: [PATCH] feat(render): add variable injection mechanism to renderer #856 --- espanso-render/src/extension/form.rs | 31 +---- espanso-render/src/lib.rs | 2 + espanso-render/src/renderer/mod.rs | 163 +++++++++++++++++++-------- espanso-render/src/renderer/util.rs | 128 ++++++++++++++++++++- 4 files changed, 245 insertions(+), 79 deletions(-) diff --git a/espanso-render/src/extension/form.rs b/espanso-render/src/extension/form.rs index c75c75d..623b482 100644 --- a/espanso-render/src/extension/form.rs +++ b/espanso-render/src/extension/form.rs @@ -17,14 +17,11 @@ * along with espanso. If not, see . */ -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, 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")] diff --git a/espanso-render/src/lib.rs b/espanso-render/src/lib.rs index b9932da..c72bb81 100644 --- a/espanso-render/src/lib.rs +++ b/espanso-render/src/lib.rs @@ -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(), } } diff --git a/espanso-render/src/renderer/mod.rs b/espanso-render/src/renderer/mod.rs index 917d921..8fb2a17 100644 --- a/espanso-render/src/renderer/mod.rs +++ b/espanso-render/src/renderer/mod.rs @@ -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 . */ -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 { - 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::(), + ..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::(), + ..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::(), + ..Default::default() }], ..Default::default() }; @@ -503,6 +481,7 @@ mod tests { params: vec![("trigger".to_string(), Value::String("nested".to_string()))] .into_iter() .collect::(), + ..Default::default() }], ..Default::default() }; @@ -527,6 +506,7 @@ mod tests { params: vec![("abort".to_string(), Value::Null)] .into_iter() .collect::(), + ..Default::default() }], ..Default::default() }; @@ -545,10 +525,93 @@ mod tests { params: vec![("error".to_string(), Value::Null)] .into_iter() .collect::(), + ..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(_))); + } } diff --git a/espanso-render/src/renderer/util.rs b/espanso-render/src/renderer/util.rs index e053cf7..52ecdf3 100644 --- a/espanso-render/src/renderer/util.rs +++ b/espanso-render/src/renderer/util.rs @@ -17,6 +17,11 @@ * along with espanso. If not, see . */ +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 { + 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 { + 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(¶ms, &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())) + ); + } }