Skip to content

Commit

Permalink
feat(mrml-core): create multi include loader
Browse files Browse the repository at this point in the history
Signed-off-by: Jérémie Drouet <[email protected]>
  • Loading branch information
jdrouet committed Aug 17, 2023
1 parent 8010725 commit 97590ca
Show file tree
Hide file tree
Showing 2 changed files with 179 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/mrml-core/src/prelude/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
178 changes: 178 additions & 0 deletions packages/mrml-core/src/prelude/parser/multi_loader.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
//! 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", "<mj-button>Hello</mj-button>")])))
/// .with_any(Box::new(NoopIncludeLoader));
/// let opts = ParserOptions {
/// include_loader: Box::new(resolver),
/// };
/// let json = r#"<mjml>
/// <mj-body>
/// <mj-include path="file://basic.mjml" />
/// </mj-body>
/// </mjml>"#;
/// match mrml::parse_with_options(json, Arc::new(opts)) {
/// Ok(_) => println!("Success!"),
/// Err(err) => eprintln!("Couldn't parse template: {err:?}"),
/// }
/// ```
pub struct MultiIncludeLoader(Vec<MultiIncludeLoaderItem>);

impl From<Vec<MultiIncludeLoaderItem>> for MultiIncludeLoader {
fn from(value: Vec<MultiIncludeLoaderItem>) -> Self {
Self(value)
}

Check warning on line 44 in packages/mrml-core/src/prelude/parser/multi_loader.rs

View check run for this annotation

Codecov / codecov/patch

packages/mrml-core/src/prelude/parser/multi_loader.rs#L42-L44

Added lines #L42 - L44 were not covered by tests
}

impl MultiIncludeLoader {
fn with_item(
mut self,
filter: MultiIncludeLoaderFilter,
loader: Box<dyn IncludeLoader>,
) -> Self {
self.0.push(MultiIncludeLoaderItem { filter, loader });
self
}

pub fn with_any(self, loader: Box<dyn IncludeLoader>) -> Self {
self.with_item(MultiIncludeLoaderFilter::Any, loader)
}

pub fn with_starts_with<S: ToString>(
self,
starts_with: S,
loader: Box<dyn IncludeLoader>,
) -> Self {
self.with_item(
MultiIncludeLoaderFilter::StartsWith {
value: starts_with.to_string(),
},
loader,
)
}

fn add_item(&mut self, filter: MultiIncludeLoaderFilter, loader: Box<dyn IncludeLoader>) {
self.0.push(MultiIncludeLoaderItem { filter, loader });
}

Check warning on line 76 in packages/mrml-core/src/prelude/parser/multi_loader.rs

View check run for this annotation

Codecov / codecov/patch

packages/mrml-core/src/prelude/parser/multi_loader.rs#L74-L76

Added lines #L74 - L76 were not covered by tests

pub fn add_any(&mut self, loader: Box<dyn IncludeLoader>) {
self.add_item(MultiIncludeLoaderFilter::Any, loader);
}

Check warning on line 80 in packages/mrml-core/src/prelude/parser/multi_loader.rs

View check run for this annotation

Codecov / codecov/patch

packages/mrml-core/src/prelude/parser/multi_loader.rs#L78-L80

Added lines #L78 - L80 were not covered by tests

pub fn add_starts_with<S: ToString>(&mut self, starts_with: S, loader: Box<dyn IncludeLoader>) {
self.add_item(
MultiIncludeLoaderFilter::StartsWith {
value: starts_with.to_string(),
},
loader,
);
}

Check warning on line 89 in packages/mrml-core/src/prelude/parser/multi_loader.rs

View check run for this annotation

Codecov / codecov/patch

packages/mrml-core/src/prelude/parser/multi_loader.rs#L82-L89

Added lines #L82 - L89 were not covered by tests
}

#[derive(Debug)]

Check warning on line 92 in packages/mrml-core/src/prelude/parser/multi_loader.rs

View check run for this annotation

Codecov / codecov/patch

packages/mrml-core/src/prelude/parser/multi_loader.rs#L92

Added line #L92 was not covered by tests
enum MultiIncludeLoaderFilter {
StartsWith { value: String },
Any,
}

impl Default for MultiIncludeLoaderFilter {
fn default() -> Self {
Self::Any
}

Check warning on line 101 in packages/mrml-core/src/prelude/parser/multi_loader.rs

View check run for this annotation

Codecov / codecov/patch

packages/mrml-core/src/prelude/parser/multi_loader.rs#L99-L101

Added lines #L99 - L101 were not covered by tests
}

impl MultiIncludeLoaderFilter {
pub fn matches(&self, path: &str) -> bool {
match self {
Self::Any => true,
Self::StartsWith { value } => path.starts_with(value),
}
}
}

#[derive(Debug)]

Check warning on line 113 in packages/mrml-core/src/prelude/parser/multi_loader.rs

View check run for this annotation

Codecov / codecov/patch

packages/mrml-core/src/prelude/parser/multi_loader.rs#L113

Added line #L113 was not covered by tests
struct MultiIncludeLoaderItem {
pub filter: MultiIncludeLoaderFilter,
pub loader: Box<dyn IncludeLoader>,
}

impl IncludeLoader for MultiIncludeLoader {
fn resolve(&self, path: &str) -> Result<String, IncludeLoaderError> {
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",
"<mj-button>Hello</mj-button>",
)])),
)
.with_any(Box::new(NoopIncludeLoader));

assert_eq!(
resolver.resolve("file://basic.mjml").unwrap(),
"<mj-button>Hello</mj-button>"
);

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");
}
}

0 comments on commit 97590ca

Please sign in to comment.