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()))
+ );
+ }
}