From 531cd95d9dc99ffe31ab540458dd3affd3108862 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Drouet?= Date: Thu, 17 Aug 2023 11:25:04 +0200 Subject: [PATCH] feat(mrml-core): create multi include loader (#322) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(mrml-core): create multi include loader Signed-off-by: Jérémie Drouet * feat(mrml-core): apply clippy suggestions Signed-off-by: Jérémie Drouet --------- Signed-off-by: Jérémie Drouet --- packages/mrml-core/src/mj_head/render.rs | 4 +- packages/mrml-core/src/prelude/parser/mod.rs | 1 + .../src/prelude/parser/multi_loader.rs | 179 ++++++++++++++++++ 3 files changed, 182 insertions(+), 2 deletions(-) create mode 100644 packages/mrml-core/src/prelude/parser/multi_loader.rs diff --git a/packages/mrml-core/src/mj_head/render.rs b/packages/mrml-core/src/mj_head/render.rs index 96593618..3cda2311 100644 --- a/packages/mrml-core/src/mj_head/render.rs +++ b/packages/mrml-core/src/mj_head/render.rs @@ -80,7 +80,7 @@ impl MjHead { .iter() .filter_map(|item| item.as_mj_attributes_class()) .fold(result, |mut res, class| { - (*res.entry(class.name()).or_insert_with(Map::new)).extend( + (*res.entry(class.name()).or_default()).extend( class .attributes() .iter() @@ -101,7 +101,7 @@ impl MjHead { .iter() .filter_map(|item| item.as_mj_attributes_element()) .fold(result, |mut res, element| { - (*res.entry(element.name()).or_insert_with(Map::new)).extend( + (*res.entry(element.name()).or_default()).extend( element .attributes() .iter() diff --git a/packages/mrml-core/src/prelude/parser/mod.rs b/packages/mrml-core/src/prelude/parser/mod.rs index 7ec7cc2c..29ded316 100644 --- a/packages/mrml-core/src/prelude/parser/mod.rs +++ b/packages/mrml-core/src/prelude/parser/mod.rs @@ -13,6 +13,7 @@ pub mod loader; #[cfg(feature = "local-loader")] pub mod local_loader; pub mod memory_loader; +pub mod multi_loader; pub mod noop_loader; #[derive(Clone, Debug, Default)] diff --git a/packages/mrml-core/src/prelude/parser/multi_loader.rs b/packages/mrml-core/src/prelude/parser/multi_loader.rs new file mode 100644 index 00000000..96c9122b --- /dev/null +++ b/packages/mrml-core/src/prelude/parser/multi_loader.rs @@ -0,0 +1,179 @@ +//! Module containing a loader that is composed of multiple loaders. + +use super::loader::IncludeLoaderError; +use crate::prelude::parser::loader::IncludeLoader; + +#[derive(Debug, Default)] +/// This struct is a +/// [`IncludeLoader`](crate::prelude::parser::loader::IncludeLoader) where +/// you can define a strategy of resolver depending on the path. +/// That way, you can have a resolver for paths starting with `https://` and +/// another resolver for local files where the paths start with `file://`. +/// If no provider match the path, a `NotFound` error will be returned. +/// +/// # Example +/// ```rust +/// use std::sync::Arc; +/// use mrml::mj_include::body::MjIncludeBodyKind; +/// use mrml::prelude::parser::memory_loader::MemoryIncludeLoader; +/// use mrml::prelude::parser::multi_loader::MultiIncludeLoader; +/// use mrml::prelude::parser::noop_loader::NoopIncludeLoader; +/// use mrml::prelude::parser::ParserOptions; +/// +/// let resolver = MultiIncludeLoader::default() +/// .with_starts_with("file://", Box::new(MemoryIncludeLoader::from(vec![("file://basic.mjml", "Hello")]))) +/// .with_any(Box::new(NoopIncludeLoader)); +/// let opts = ParserOptions { +/// include_loader: Box::new(resolver), +/// }; +/// let json = r#" +/// +/// +/// +/// "#; +/// match mrml::parse_with_options(json, Arc::new(opts)) { +/// Ok(_) => println!("Success!"), +/// Err(err) => eprintln!("Couldn't parse template: {err:?}"), +/// } +/// ``` +pub struct MultiIncludeLoader(Vec); + +impl MultiIncludeLoader { + fn with_item( + mut self, + filter: MultiIncludeLoaderFilter, + loader: Box, + ) -> Self { + self.0.push(MultiIncludeLoaderItem { filter, loader }); + self + } + + pub fn with_any(self, loader: Box) -> Self { + self.with_item(MultiIncludeLoaderFilter::Any, loader) + } + + pub fn with_starts_with( + self, + starts_with: S, + loader: Box, + ) -> Self { + self.with_item( + MultiIncludeLoaderFilter::StartsWith { + value: starts_with.to_string(), + }, + loader, + ) + } + + fn add_item(&mut self, filter: MultiIncludeLoaderFilter, loader: Box) { + self.0.push(MultiIncludeLoaderItem { filter, loader }); + } + + pub fn add_any(&mut self, loader: Box) { + self.add_item(MultiIncludeLoaderFilter::Any, loader); + } + + pub fn add_starts_with(&mut self, starts_with: S, loader: Box) { + self.add_item( + MultiIncludeLoaderFilter::StartsWith { + value: starts_with.to_string(), + }, + loader, + ); + } +} + +#[derive(Debug)] +enum MultiIncludeLoaderFilter { + StartsWith { value: String }, + Any, +} + +impl MultiIncludeLoaderFilter { + pub fn matches(&self, path: &str) -> bool { + match self { + Self::Any => true, + Self::StartsWith { value } => path.starts_with(value), + } + } +} + +#[derive(Debug)] +struct MultiIncludeLoaderItem { + pub filter: MultiIncludeLoaderFilter, + pub loader: Box, +} + +impl IncludeLoader for MultiIncludeLoader { + fn resolve(&self, path: &str) -> Result { + self.0 + .iter() + .find(|item| item.filter.matches(path)) + .ok_or_else(|| { + IncludeLoaderError::not_found(path) + .with_message("unable to find a compatible resolver") + }) + .and_then(|item| item.loader.resolve(path)) + } +} + +#[cfg(test)] +mod tests { + use std::io::ErrorKind; + + #[test] + fn should_resolve() { + use crate::prelude::parser::loader::IncludeLoader; + use crate::prelude::parser::memory_loader::MemoryIncludeLoader; + use crate::prelude::parser::multi_loader::MultiIncludeLoader; + use crate::prelude::parser::noop_loader::NoopIncludeLoader; + + let resolver = MultiIncludeLoader::default() + .with_starts_with( + "file://", + Box::new(MemoryIncludeLoader::from(vec![( + "file://basic.mjml", + "Hello", + )])), + ) + .with_any(Box::::default()); + + assert_eq!( + resolver.resolve("file://basic.mjml").unwrap(), + "Hello" + ); + + let err = resolver.resolve("file://not-found.mjml").unwrap_err(); + assert_eq!(err.reason, ErrorKind::NotFound); + // assert_eq!(err.message.unwrap(), "unable to find compatible resolver"); + + let err = resolver.resolve("noop://not-found.mjml").unwrap_err(); + assert_eq!(err.reason, ErrorKind::NotFound); + assert!(err.message.is_none()); + } + + #[test] + fn should_not_find_resolver() { + use crate::prelude::parser::loader::IncludeLoader; + use crate::prelude::parser::multi_loader::MultiIncludeLoader; + + let resolver = MultiIncludeLoader::default(); + + let err = resolver.resolve("file://not-found.mjml").unwrap_err(); + assert_eq!(err.reason, ErrorKind::NotFound); + assert_eq!(err.message.unwrap(), "unable to find a compatible resolver"); + } + + #[test] + fn should_build_resolvers() { + use crate::prelude::parser::multi_loader::MultiIncludeLoader; + use crate::prelude::parser::noop_loader::NoopIncludeLoader; + + let mut resolver = MultiIncludeLoader::default(); + resolver.add_starts_with("foo", Box::::default()); + resolver.add_any(Box::::default()); + assert_eq!(resolver.0.len(), 2); + + assert_eq!(format!("{resolver:?}"), "MultiIncludeLoader([MultiIncludeLoaderItem { filter: StartsWith { value: \"foo\" }, loader: NoopIncludeLoader }, MultiIncludeLoaderItem { filter: Any, loader: NoopIncludeLoader }])"); + } +}