feat(render): create FormExtension

This commit is contained in:
Federico Terzi 2021-04-24 17:59:50 +02:00
parent d7ebd2a4dd
commit a2522af57a
3 changed files with 305 additions and 128 deletions

View File

@ -0,0 +1,120 @@
/*
* 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 crate::renderer::VAR_REGEX;
use log::error;
use std::collections::HashMap;
use thiserror::Error;
use crate::{
renderer::render_variables, Extension, ExtensionOutput, ExtensionResult, Params, Value,
};
lazy_static! {
static ref EMPTY_PARAMS: Params = Params::new();
}
pub trait FormProvider {
fn show(&self, layout: &str, fields: &Params, options: &Params) -> FormProviderResult;
}
pub enum FormProviderResult {
Success(HashMap<String, String>),
Aborted,
Error(anyhow::Error),
}
pub struct FormExtension<'a> {
provider: &'a dyn FormProvider,
}
#[allow(clippy::new_without_default)]
impl<'a> FormExtension<'a> {
pub fn new(provider: &'a dyn FormProvider) -> Self {
Self { provider }
}
}
impl<'a> Extension for FormExtension<'a> {
fn name(&self) -> &str {
"form"
}
fn calculate(
&self,
_: &crate::Context,
scope: &crate::Scope,
params: &Params,
) -> crate::ExtensionResult {
let layout = if let Some(Value::String(layout)) = params.get("layout") {
layout
} else {
return crate::ExtensionResult::Error(FormExtensionError::MissingLayout.into());
};
let mut 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))
}
FormProviderResult::Aborted => ExtensionResult::Aborted,
FormProviderResult::Error(error) => ExtensionResult::Error(error),
}
}
}
// TODO: test
fn inject_scope(fields: &mut HashMap<String, Value>, scope: &HashMap<&str, ExtensionOutput>) -> () {
for (_, value) in fields {
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")]
MissingLayout,
}
#[cfg(test)]
mod tests {
use super::*;
// TODO: test
}

View File

@ -19,6 +19,7 @@
pub mod date;
pub mod echo;
pub mod form;
pub mod clipboard;
pub mod shell;
pub mod script;

View File

@ -21,8 +21,9 @@ use std::collections::{HashMap, HashSet};
use crate::{
CasingStyle, Context, Extension, ExtensionOutput, ExtensionResult, RenderOptions, RenderResult,
Renderer, Scope, Template, Value, Variable
Renderer, Scope, Template, Value, Variable,
};
use anyhow::Result;
use log::{error, warn};
use regex::{Captures, Regex};
use thiserror::Error;
@ -40,7 +41,7 @@ pub(crate) struct DefaultRenderer<'a> {
extensions: HashMap<String, &'a dyn Extension>,
}
impl <'a> DefaultRenderer<'a> {
impl<'a> DefaultRenderer<'a> {
pub fn new(extensions: Vec<&'a dyn Extension>) -> Self {
let extensions = extensions
.into_iter()
@ -50,7 +51,7 @@ impl <'a> DefaultRenderer<'a> {
}
}
impl <'a> Renderer for DefaultRenderer<'a> {
impl<'a> Renderer for DefaultRenderer<'a> {
fn render(
&self,
template: &Template,
@ -150,42 +151,12 @@ impl <'a> Renderer for DefaultRenderer<'a> {
}
// Replace the variables
let mut replacing_error = None;
let output = VAR_REGEX
.replace_all(&template.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 RenderResult::Error(error.into());
match render_variables(&template.body, &scope) {
Ok(output) => output,
Err(error) => {
return RenderResult::Error(error.into());
}
}
output
} else {
template.body.clone()
};
@ -199,19 +170,21 @@ impl <'a> Renderer for DefaultRenderer<'a> {
let mut v: Vec<char> = body.chars().collect();
v[0] = v[0].to_uppercase().next().unwrap();
v.into_iter().collect()
},
}
CasingStyle::CapitalizeWords => {
// Capitalize the first letter of each word
WORD_REGEX.replace_all(&body, |caps: &Captures| {
if let Some(word_match) = caps.get(0) {
let mut v: Vec<char> = word_match.as_str().chars().collect();
v[0] = v[0].to_uppercase().next().unwrap();
let capitalized_word: String = v.into_iter().collect();
capitalized_word
} else {
"".to_string()
}
}).to_string()
WORD_REGEX
.replace_all(&body, |caps: &Captures| {
if let Some(word_match) = caps.get(0) {
let mut v: Vec<char> = word_match.as_str().chars().collect();
v[0] = v[0].to_uppercase().next().unwrap();
let capitalized_word: String = v.into_iter().collect();
capitalized_word
} else {
"".to_string()
}
})
.to_string()
}
};
@ -219,6 +192,52 @@ 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],
@ -280,7 +299,7 @@ mod tests {
return ExtensionResult::Aborted;
}
if params.get("error").is_some() {
return ExtensionResult::Error(RendererError::MissingVariable("missing".to_string()).into())
return ExtensionResult::Error(RendererError::MissingVariable("missing".to_string()).into());
}
ExtensionResult::Aborted
}
@ -294,56 +313,77 @@ mod tests {
Template {
ids: vec!["id".to_string()],
body: str.to_string(),
vars: Vec::new(),
vars: Vec::new(),
}
}
pub fn template(body: &str, vars: &[(&str, &str)]) -> Template {
let vars = vars.iter().map(|(name, value)| {
Variable {
let vars = vars
.iter()
.map(|(name, value)| Variable {
name: (*name).to_string(),
var_type: "mock".to_string(),
params: Params::from_iter(vec![("echo".to_string(), Value::String((*value).to_string()))].into_iter())
}
}).collect();
params: Params::from_iter(
vec![("echo".to_string(), Value::String((*value).to_string()))].into_iter(),
),
})
.collect();
Template {
ids: vec!["id".to_string()],
body: body.to_string(),
vars,
vars,
}
}
#[test]
fn no_variable_no_styling() {
let renderer = get_renderer();
let res = renderer.render(&template_for_str("plain body"), &Default::default(), &Default::default());
let res = renderer.render(
&template_for_str("plain body"),
&Default::default(),
&Default::default(),
);
assert!(matches!(res, RenderResult::Success(str) if str == "plain body"));
}
#[test]
fn no_variable_capitalize() {
let renderer = get_renderer();
let res = renderer.render(&template_for_str("plain body"), &Default::default(), &RenderOptions {
casing_style: CasingStyle::Capitalize,
});
let res = renderer.render(
&template_for_str("plain body"),
&Default::default(),
&RenderOptions {
casing_style: CasingStyle::Capitalize,
},
);
assert!(matches!(res, RenderResult::Success(str) if str == "Plain body"));
}
#[test]
fn no_variable_capitalize_words() {
let renderer = get_renderer();
let res = renderer.render(&template_for_str("ordinary least squares, with other.punctuation !Marks"), &Default::default(), &RenderOptions {
casing_style: CasingStyle::CapitalizeWords,
});
assert!(matches!(res, RenderResult::Success(str) if str == "Ordinary Least Squares, With Other.Punctuation !Marks"));
let res = renderer.render(
&template_for_str("ordinary least squares, with other.punctuation !Marks"),
&Default::default(),
&RenderOptions {
casing_style: CasingStyle::CapitalizeWords,
},
);
assert!(
matches!(res, RenderResult::Success(str) if str == "Ordinary Least Squares, With Other.Punctuation !Marks")
);
}
#[test]
fn no_variable_uppercase() {
let renderer = get_renderer();
let res = renderer.render(&template_for_str("plain body"), &Default::default(), &RenderOptions {
casing_style: CasingStyle::Uppercase,
});
let res = renderer.render(
&template_for_str("plain body"),
&Default::default(),
&RenderOptions {
casing_style: CasingStyle::Uppercase,
},
);
assert!(matches!(res, RenderResult::Success(str) if str == "PLAIN BODY"));
}
@ -367,16 +407,21 @@ mod tests {
fn global_variable() {
let renderer = get_renderer();
let template = template("hello {{var}}", &[]);
let res = renderer.render(&template, &Context {
global_vars: vec![
&Variable {
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()
}, &Default::default());
params: Params::from_iter(vec![(
"echo".to_string(),
Value::String("world".to_string()),
)]),
}],
..Default::default()
},
&Default::default(),
);
assert!(matches!(res, RenderResult::Success(str) if str == "hello world"));
}
@ -384,31 +429,38 @@ mod tests {
fn global_variable_explicit_ordering() {
let renderer = get_renderer();
let template = Template {
body: "hello {{var}} {{local}}".to_string(),
body: "hello {{var}} {{local}}".to_string(),
vars: vec![
Variable {
name: "local".to_string(),
var_type: "mock".to_string(),
params: Params::from_iter(vec![("echo".to_string(), Value::String("Bob".to_string()))].into_iter())
params: Params::from_iter(
vec![("echo".to_string(), Value::String("Bob".to_string()))].into_iter(),
),
},
Variable {
name: "var".to_string(),
var_type: "global".to_string(),
..Default::default()
}
},
],
..Default::default()
};
let res = renderer.render(&template, &Context {
global_vars: vec![
&Variable {
let res = renderer.render(
&template,
&Context {
global_vars: vec![&Variable {
name: "var".to_string(),
var_type: "mock".to_string(),
params: Params::from_iter(vec![("read".to_string(), Value::String("local".to_string()))])
}
],
..Default::default()
}, &Default::default());
params: Params::from_iter(vec![(
"read".to_string(),
Value::String("local".to_string()),
)]),
}],
..Default::default()
},
&Default::default(),
);
assert!(matches!(res, RenderResult::Success(str) if str == "hello Bob Bob"));
}
@ -416,25 +468,29 @@ mod tests {
fn nested_match() {
let renderer = get_renderer();
let template = Template {
body: "hello {{var}}".to_string(),
vars: vec![
Variable {
name: "var".to_string(),
var_type: "match".to_string(),
params: Params::from_iter(vec![("trigger".to_string(), Value::String("nested".to_string()))].into_iter())
},
],
body: "hello {{var}}".to_string(),
vars: vec![Variable {
name: "var".to_string(),
var_type: "match".to_string(),
params: Params::from_iter(
vec![("trigger".to_string(), Value::String("nested".to_string()))].into_iter(),
),
}],
..Default::default()
};
let nested_template = Template {
ids: vec!["nested".to_string()],
body: "world".to_string(),
body: "world".to_string(),
..Default::default()
};
let res = renderer.render(&template, &Context {
templates: vec![&nested_template],
..Default::default()
}, &Default::default());
let res = renderer.render(
&template,
&Context {
templates: vec![&nested_template],
..Default::default()
},
&Default::default(),
);
assert!(matches!(res, RenderResult::Success(str) if str == "hello world"));
}
@ -442,35 +498,37 @@ mod tests {
fn missing_nested_match() {
let renderer = get_renderer();
let template = Template {
body: "hello {{var}}".to_string(),
vars: vec![
Variable {
name: "var".to_string(),
var_type: "match".to_string(),
params: Params::from_iter(vec![("trigger".to_string(), Value::String("nested".to_string()))].into_iter())
},
],
body: "hello {{var}}".to_string(),
vars: vec![Variable {
name: "var".to_string(),
var_type: "match".to_string(),
params: Params::from_iter(
vec![("trigger".to_string(), Value::String("nested".to_string()))].into_iter(),
),
}],
..Default::default()
};
let res = renderer.render(&template, &Context {
..Default::default()
}, &Default::default());
let res = renderer.render(
&template,
&Context {
..Default::default()
},
&Default::default(),
);
assert!(matches!(res, RenderResult::Error(_)));
}
#[test]
fn extension_aborting_propagates() {
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: Params::from_iter(vec![("abort".to_string(), Value::Null)].into_iter()),
}
],
..Default::default()
vars: vec![Variable {
name: "var".to_string(),
var_type: "mock".to_string(),
params: Params::from_iter(vec![("abort".to_string(), Value::Null)].into_iter()),
}],
..Default::default()
};
let res = renderer.render(&template, &Default::default(), &Default::default());
assert!(matches!(res, RenderResult::Aborted));
@ -481,14 +539,12 @@ mod tests {
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: Params::from_iter(vec![("error".to_string(), Value::Null)].into_iter()),
}
],
..Default::default()
vars: vec![Variable {
name: "var".to_string(),
var_type: "mock".to_string(),
params: Params::from_iter(vec![("error".to_string(), Value::Null)].into_iter()),
}],
..Default::default()
};
let res = renderer.render(&template, &Default::default(), &Default::default());
assert!(matches!(res, RenderResult::Error(_)));