feat(render): implement new variable resolution algorithm

This commit is contained in:
Federico Terzi 2021-11-10 23:17:23 +01:00
parent 57450bee32
commit 8acca4a366
4 changed files with 474 additions and 26 deletions

View File

@ -100,6 +100,8 @@ pub struct Variable {
pub var_type: String,
pub inject_vars: bool,
pub params: Params,
// Name of the variables this variable depends on
pub depends_on: Vec<String>,
}
impl Default for Variable {
@ -109,6 +111,7 @@ impl Default for Variable {
var_type: "".to_string(),
inject_vars: true,
params: Params::new(),
depends_on: Vec::new(),
}
}
}

View File

@ -17,10 +17,7 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
use std::{
borrow::Cow,
collections::{HashMap, HashSet},
};
use std::{borrow::Cow, collections::HashMap};
use crate::{
CasingStyle, Context, Extension, ExtensionOutput, ExtensionResult, RenderOptions, RenderResult,
@ -29,10 +26,10 @@ use crate::{
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 resolve;
mod util;
lazy_static! {
@ -63,23 +60,6 @@ impl<'a> Renderer for DefaultRenderer<'a> {
options: &RenderOptions,
) -> RenderResult {
let body = if VAR_REGEX.is_match(&template.body) {
// In order to define a variable evaluation order, we first need to find
// the global variables that are being used but for which an explicit order
// is not defined.
let body_variable_names = get_body_variable_names(&template.body);
let local_variable_names: HashSet<&str> =
template.vars.iter().map(|var| var.name.as_str()).collect();
let missing_global_variable_names: HashSet<&str> = body_variable_names
.difference(&local_variable_names)
.copied()
.collect();
let missing_global_variables: Vec<&Variable> = context
.global_vars
.iter()
.copied()
.filter(|global_var| missing_global_variable_names.contains(&*global_var.name))
.collect();
// Convert "global" variable type aliases when needed
let local_variables: Vec<&Variable> =
if template.vars.iter().any(|var| var.var_type == "global") {
@ -103,10 +83,16 @@ impl<'a> Renderer for DefaultRenderer<'a> {
template.vars.iter().collect()
};
// The implicit global variables will be evaluated first, followed by the local vars
let mut variables: Vec<&Variable> = Vec::new();
variables.extend(missing_global_variables);
variables.extend(local_variables.iter());
// Here we execute a graph dependency resolution algorithm to determine a valid
// evaluation order for variables.
let variables = match resolve::resolve_evaluation_order(
&template.body,
&local_variables,
&context.global_vars,
) {
Ok(variables) => variables,
Err(err) => return RenderResult::Error(err),
};
// Compute the variable outputs
let mut scope = Scope::new();
@ -257,6 +243,9 @@ pub enum RendererError {
#[error("missing sub match")]
MissingSubMatch,
#[error("circular dependency: `{0}` -> `{1}`")]
CircularDependency(String, String),
}
#[cfg(test)]
@ -462,6 +451,147 @@ mod tests {
assert!(matches!(res, RenderResult::Success(str) if str == "hello Bob Bob"));
}
#[test]
fn nested_global_variable() {
let renderer = get_renderer();
let template = template("hello {{var2}}", &[]);
let res = renderer.render(
&template,
&Context {
global_vars: vec![
&Variable {
name: "var".to_string(),
var_type: "mock".to_string(),
params: Params::from_iter(vec![(
"echo".to_string(),
Value::String("world".to_string()),
)]),
..Default::default()
},
&Variable {
name: "var2".to_string(),
var_type: "mock".to_string(),
params: Params::from_iter(vec![(
"echo".to_string(),
Value::String("{{var}}".to_string()),
)]),
..Default::default()
},
],
..Default::default()
},
&Default::default(),
);
assert!(matches!(res, RenderResult::Success(str) if str == "hello world"));
}
#[test]
fn nested_global_variable_circular_dependency_should_fail() {
let renderer = get_renderer();
let template = template("hello {{var}}", &[]);
let res = renderer.render(
&template,
&Context {
global_vars: vec![
&Variable {
name: "var".to_string(),
var_type: "mock".to_string(),
params: Params::from_iter(vec![(
"echo".to_string(),
Value::String("{{var2}}".to_string()),
)]),
..Default::default()
},
&Variable {
name: "var2".to_string(),
var_type: "mock".to_string(),
params: Params::from_iter(vec![(
"echo".to_string(),
Value::String("{{var3}}".to_string()),
)]),
..Default::default()
},
&Variable {
name: "var3".to_string(),
var_type: "mock".to_string(),
params: Params::from_iter(vec![(
"echo".to_string(),
Value::String("{{var}}".to_string()),
)]),
..Default::default()
},
],
..Default::default()
},
&Default::default(),
);
assert!(matches!(res, RenderResult::Error(_)));
}
#[test]
fn global_variable_depends_on() {
let renderer = get_renderer();
let template = template("hello {{var}}", &[]);
let res = renderer.render(
&template,
&Context {
global_vars: vec![
&Variable {
name: "var".to_string(),
var_type: "mock".to_string(),
params: Params::from_iter(vec![(
"echo".to_string(),
Value::String("world".to_string()),
)]),
depends_on: vec!["var2".to_string()],
..Default::default()
},
&Variable {
name: "var2".to_string(),
var_type: "mock".to_string(),
params: Params::from_iter(vec![("abort".to_string(), Value::Null)]),
..Default::default()
},
],
..Default::default()
},
&Default::default(),
);
assert!(matches!(res, RenderResult::Aborted));
}
#[test]
fn local_variable_explicit_ordering() {
let renderer = get_renderer();
let template = Template {
body: "hello {{var}}".to_string(),
vars: vec![Variable {
name: "var".to_string(),
var_type: "mock".to_string(),
params: vec![("echo".to_string(), Value::String("something".to_string()))]
.into_iter()
.collect::<Params>(),
depends_on: vec!["global".to_string()],
..Default::default()
}],
..Default::default()
};
let res = renderer.render(
&template,
&Context {
global_vars: vec![&Variable {
name: "global".to_string(),
var_type: "mock".to_string(),
params: Params::from_iter(vec![("abort".to_string(), Value::Null)]),
..Default::default()
}],
..Default::default()
},
&Default::default(),
);
assert!(matches!(res, RenderResult::Aborted));
}
#[test]
fn nested_match() {
let renderer = get_renderer();
@ -587,6 +717,7 @@ mod tests {
Value::String("{{firstname}} {{lastname}}".to_string()),
)]),
inject_vars: true,
..Default::default()
},
];
@ -613,6 +744,7 @@ mod tests {
Value::String("{{first}} two".to_string()),
)]),
inject_vars: false,
..Default::default()
},
];
@ -637,4 +769,88 @@ mod tests {
let res = renderer.render(&template, &Default::default(), &Default::default());
assert!(matches!(res, RenderResult::Error(_)));
}
#[test]
fn variable_injection_with_global_variable() {
let renderer = get_renderer();
let mut template = template_for_str("hello {{output}}");
template.vars = vec![
Variable {
name: "var".to_string(),
var_type: "global".to_string(),
..Default::default()
},
Variable {
name: "output".to_string(),
var_type: "mock".to_string(),
params: Params::from_iter(vec![(
"echo".to_string(),
Value::String("{{var}}".to_string()),
)]),
..Default::default()
},
];
let res = renderer.render(
&template,
&Context {
global_vars: vec![&Variable {
name: "var".to_string(),
var_type: "mock".to_string(),
params: Params::from_iter(vec![(
"echo".to_string(),
Value::String("global".to_string()),
)]),
..Default::default()
}],
..Default::default()
},
&Default::default(),
);
assert!(matches!(res, RenderResult::Success(str) if str == "hello global"));
}
#[test]
fn variable_injection_local_var_takes_precedence_over_global() {
let renderer = get_renderer();
let mut template = template_for_str("hello {{output}}");
template.vars = vec![
Variable {
name: "var".to_string(),
var_type: "mock".to_string(),
params: Params::from_iter(vec![(
"echo".to_string(),
Value::String("local".to_string()),
)]),
..Default::default()
},
Variable {
name: "output".to_string(),
var_type: "mock".to_string(),
params: Params::from_iter(vec![(
"echo".to_string(),
Value::String("{{var}}".to_string()),
)]),
..Default::default()
},
];
let res = renderer.render(
&template,
&Context {
global_vars: vec![&Variable {
name: "var".to_string(),
var_type: "mock".to_string(),
params: Params::from_iter(vec![(
"echo".to_string(),
Value::String("global".to_string()),
)]),
..Default::default()
}],
..Default::default()
},
&Default::default(),
);
assert!(matches!(res, RenderResult::Success(str) if str == "hello local"));
}
}

View File

@ -0,0 +1,197 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
use std::{
cell::RefCell,
collections::{HashMap, HashSet},
};
use anyhow::{anyhow, Result};
use crate::Variable;
use super::RendererError;
struct Node<'a> {
name: &'a str,
variable: Option<&'a Variable>,
dependencies: Option<HashSet<&'a str>>,
}
pub(crate) fn resolve_evaluation_order<'a>(
body: &'a str,
local_vars: &'a [&'a Variable],
global_vars: &'a [&'a Variable],
) -> Result<Vec<&'a Variable>> {
let node_map = generate_nodes(body, local_vars, global_vars);
let body_node = node_map
.get("__match_body")
.ok_or_else(|| anyhow!("missing body node"))?;
let eval_order = RefCell::new(Vec::new());
let resolved = RefCell::new(HashSet::new());
let seen = RefCell::new(HashSet::new());
{
resolve_dependencies(body_node, &node_map, &eval_order, &resolved, &seen)?;
}
let eval_order_ref = eval_order.borrow();
let mut ordered_variables = Vec::new();
for var_name in (*eval_order_ref).iter() {
let node = node_map
.get(var_name)
.ok_or_else(|| anyhow!("could not find dependency node for variable: {}", var_name))?;
if let Some(var) = node.variable {
ordered_variables.push(var);
}
}
Ok(ordered_variables)
}
fn generate_nodes<'a>(
body: &'a str,
local_vars: &'a [&'a Variable],
global_vars: &'a [&'a Variable],
) -> HashMap<&'a str, Node<'a>> {
let mut local_vars_nodes = Vec::new();
for (index, var) in local_vars.iter().enumerate() {
let mut dependencies = HashSet::new();
if var.inject_vars {
dependencies.extend(super::util::get_params_variable_names(&var.params));
}
dependencies.extend(var.depends_on.iter().map(|v| v.as_str()));
// Every local variable depends on the one before it.
// Needed to guarantee execution order within local vars.
if index > 0 {
let previous_var = local_vars.get(index - 1);
if let Some(previous_var) = previous_var {
dependencies.insert(&previous_var.name);
}
}
local_vars_nodes.push(Node {
name: &var.name,
variable: Some(var),
dependencies: Some(dependencies),
});
}
let global_vars_nodes = global_vars.iter().map(|var| create_node_from_var(*var));
// The body depends on all local variables + the variables read inside it (which might be global)
let mut body_dependencies: HashSet<&str> =
local_vars_nodes.iter().map(|node| node.name).collect();
body_dependencies.extend(super::util::get_body_variable_names(body));
let body_node = Node {
name: "__match_body",
variable: None,
dependencies: Some(body_dependencies),
};
let mut node_map = HashMap::new();
node_map.insert(body_node.name, body_node);
global_vars_nodes.into_iter().for_each(|node| {
node_map.insert(node.name, node);
});
local_vars_nodes.into_iter().for_each(|node| {
node_map.insert(node.name, node);
});
node_map
}
fn create_node_from_var(var: &Variable) -> Node {
let dependencies = if var.inject_vars || !var.depends_on.is_empty() {
let mut vars = HashSet::new();
if var.inject_vars {
vars.extend(super::util::get_params_variable_names(&var.params))
}
vars.extend(var.depends_on.iter().map(|s| s.as_str()));
Some(vars)
} else {
None
};
Node {
name: &var.name,
variable: Some(var),
dependencies,
}
}
fn resolve_dependencies<'a>(
node: &'a Node,
node_map: &'a HashMap<&'a str, Node<'a>>,
eval_order: &'a RefCell<Vec<&'a str>>,
resolved: &'a RefCell<HashSet<&'a str>>,
seen: &'a RefCell<HashSet<&'a str>>,
) -> Result<()> {
{
let mut seen_ref = seen.borrow_mut();
seen_ref.insert(node.name);
}
if let Some(dependencies) = &node.dependencies {
for dependency in dependencies.iter() {
let has_been_resolved = {
let resolved_ref = resolved.borrow();
resolved_ref.contains(dependency)
};
let has_been_seen = {
let seen_ref = seen.borrow();
seen_ref.contains(dependency)
};
if !has_been_resolved {
if has_been_seen {
return Err(
RendererError::CircularDependency(node.name.to_string(), dependency.to_string()).into(),
);
}
match node_map.get(dependency) {
Some(dependency_node) => {
resolve_dependencies(dependency_node, node_map, eval_order, resolved, seen)?
}
None => {
return Err(RendererError::MissingVariable(dependency.to_string()).into());
}
}
}
}
}
{
let mut eval_order_ref = eval_order.borrow_mut();
eval_order_ref.push(node.name);
let mut resolved_ref = resolved.borrow_mut();
resolved_ref.insert(node.name);
}
Ok(())
}

View File

@ -34,6 +34,38 @@ pub(crate) fn get_body_variable_names(body: &str) -> HashSet<&str> {
variables
}
pub(crate) fn get_params_variable_names(params: &Params) -> HashSet<&str> {
let mut names = HashSet::new();
for (_, value) in params.iter() {
let local_names = get_value_variable_names_recursively(value);
names.extend(local_names);
}
names
}
fn get_value_variable_names_recursively(value: &Value) -> HashSet<&str> {
match value {
Value::String(s_value) => get_body_variable_names(s_value),
Value::Array(values) => {
let mut local_names: HashSet<&str> = HashSet::new();
for value in values {
local_names.extend(get_value_variable_names_recursively(value));
}
local_names
}
Value::Object(fields) => {
let mut local_names: HashSet<&str> = HashSet::new();
for value in fields.values() {
local_names.extend(get_value_variable_names_recursively(value));
}
local_names
}
_ => HashSet::new(),
}
}
pub(crate) fn render_variables(body: &str, scope: &Scope) -> Result<String> {
let mut replacing_error = None;
let output = VAR_REGEX