feat(render): implement new variable resolution algorithm
This commit is contained in:
parent
57450bee32
commit
8acca4a366
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
|
197
espanso-render/src/renderer/resolve.rs
Normal file
197
espanso-render/src/renderer/resolve.rs
Normal 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(())
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user