From a2522af57a31cd87e9bdd59f291ed1ba9658aac2 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sat, 24 Apr 2021 17:59:50 +0200 Subject: [PATCH] feat(render): create FormExtension --- espanso-render/src/extension/form.rs | 120 +++++++++++ espanso-render/src/extension/mod.rs | 1 + espanso-render/src/renderer/mod.rs | 312 ++++++++++++++++----------- 3 files changed, 305 insertions(+), 128 deletions(-) create mode 100644 espanso-render/src/extension/form.rs diff --git a/espanso-render/src/extension/form.rs b/espanso-render/src/extension/form.rs new file mode 100644 index 0000000..11ca0cd --- /dev/null +++ b/espanso-render/src/extension/form.rs @@ -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 . + */ + +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), + 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, 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 +} diff --git a/espanso-render/src/extension/mod.rs b/espanso-render/src/extension/mod.rs index 836fe76..c7294c5 100644 --- a/espanso-render/src/extension/mod.rs +++ b/espanso-render/src/extension/mod.rs @@ -19,6 +19,7 @@ pub mod date; pub mod echo; +pub mod form; pub mod clipboard; pub mod shell; pub mod script; diff --git a/espanso-render/src/renderer/mod.rs b/espanso-render/src/renderer/mod.rs index 958ac0e..85db7e5 100644 --- a/espanso-render/src/renderer/mod.rs +++ b/espanso-render/src/renderer/mod.rs @@ -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, } -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 = 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 = 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 = 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 { + 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(_)));