feat(render): add variable injection mechanism to renderer #856
This commit is contained in:
parent
9a2c27a202
commit
40e8dace33
|
@ -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")]
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(_)));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(¶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()))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user