use std::sync::Arc;
use indexmap::IndexMap;
use crate::ast::{ErrorLevel, Function, Node, NodeType, ValidationError, Value};
use crate::renderable::RenderableTreeNodes;
use crate::validate::Config;
use crate::validate::ValidationType;
pub type TransformHook = Arc<
dyn for<'a, 'c> Fn(&'a Node<'a>, &'c Config<'a>) -> RenderableTreeNodes + Send + Sync + 'static,
>;
pub type ValidateHook = Arc<
dyn for<'a, 'c> Fn(&'a Node<'a>, &'c Config<'a>) -> Vec<ValidationError<'a>>
+ Send
+ Sync
+ 'static,
>;
pub type AttributeValidateHook = Arc<
dyn for<'a, 'c> Fn(&Value, &'c Config<'a>, &str) -> Vec<ValidationError<'a>>
+ Send
+ Sync
+ 'static,
>;
pub type MatchesHook =
Arc<dyn for<'a, 'c> Fn(&'c Config<'a>) -> Option<SchemaMatches> + Send + Sync + 'static>;
pub type FunctionTransformHook = Arc<
dyn for<'a, 'c> Fn(&IndexMap<String, Option<Value>>, &'c Config<'a>) -> Option<Value>
+ Send
+ Sync
+ 'static,
>;
pub type FunctionValidateHook = Arc<
dyn for<'a, 'c> Fn(&Function, &'c Config<'a>) -> Vec<ValidationError<'a>>
+ Send
+ Sync
+ 'static,
>;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub enum RenderPolicy {
#[default]
Named,
Hidden,
Renamed(String),
}
impl RenderPolicy {
#[must_use]
pub fn output_name<'k>(&'k self, key: &'k str) -> Option<&'k str> {
match self {
RenderPolicy::Named => Some(key),
RenderPolicy::Hidden => None,
RenderPolicy::Renamed(name) => Some(name.as_str()),
}
}
}
#[derive(Clone)]
#[non_exhaustive]
pub enum SchemaMatches {
Values(Vec<String>),
Pattern(Arc<dyn MatchPattern + Send + Sync + 'static>),
Dynamic(MatchesHook),
}
pub trait MatchPattern {
fn is_match(&self, value: &str) -> bool;
fn display(&self) -> &str;
}
#[derive(Clone, Default)]
pub struct SchemaAttribute {
pub attribute_type: Option<ValidationType>,
pub render: RenderPolicy,
pub default: Option<Value>,
pub required: bool,
pub matches: Option<SchemaMatches>,
pub validate: Option<AttributeValidateHook>,
pub error_level: Option<ErrorLevel>,
pub description: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct SchemaSlot {
pub render: RenderPolicy,
pub required: bool,
}
#[derive(Clone, Default)]
pub struct Schema {
pub render: Option<String>,
pub children: Option<Vec<NodeType>>,
pub attributes: IndexMap<String, SchemaAttribute>,
pub slots: IndexMap<String, SchemaSlot>,
pub self_closing: bool,
pub inline: Option<bool>,
pub transform: Option<TransformHook>,
pub validate: Option<ValidateHook>,
pub description: Option<String>,
}
impl Schema {
#[must_use]
pub fn new() -> Schema {
Schema::default()
}
#[must_use]
pub fn render(mut self, element: impl Into<String>) -> Schema {
self.render = Some(element.into());
self
}
#[must_use]
pub fn attribute(mut self, name: impl Into<String>, attribute: SchemaAttribute) -> Schema {
self.attributes.insert(name.into(), attribute);
self
}
#[must_use]
pub fn slot(mut self, name: impl Into<String>, slot: SchemaSlot) -> Schema {
self.slots.insert(name.into(), slot);
self
}
}
impl std::fmt::Debug for Schema {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Schema")
.field("render", &self.render)
.field("children", &self.children)
.field("attributes", &self.attributes)
.field("slots", &self.slots)
.field("self_closing", &self.self_closing)
.field("inline", &self.inline)
.field("transform", &self.transform.is_some())
.field("validate", &self.validate.is_some())
.field("description", &self.description)
.finish()
}
}
impl std::fmt::Debug for SchemaAttribute {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SchemaAttribute")
.field("attribute_type", &self.attribute_type)
.field("render", &self.render)
.field("default", &self.default)
.field("required", &self.required)
.field("matches", &self.matches)
.field("validate", &self.validate.is_some())
.field("error_level", &self.error_level)
.field("description", &self.description)
.finish()
}
}
impl std::fmt::Debug for SchemaMatches {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SchemaMatches::Values(values) => f.debug_tuple("Values").field(values).finish(),
SchemaMatches::Pattern(pattern) => {
f.debug_tuple("Pattern").field(&pattern.display()).finish()
}
SchemaMatches::Dynamic(_) => f.write_str("Dynamic(..)"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_render_policy_has_three_states() {
assert_eq!(RenderPolicy::default().output_name("id"), Some("id"));
assert_eq!(RenderPolicy::Hidden.output_name("id"), None);
assert_eq!(
RenderPolicy::Renamed("data-id".to_string()).output_name("id"),
Some("data-id")
);
}
#[test]
fn a_hook_is_an_ordinary_closure() {
let schema = Schema {
validate: Some(Arc::new(|node: &Node<'_>, _config: &Config<'_>| {
vec![ValidationError::new(
"example",
ErrorLevel::Warning,
format!("saw {}", node.name()),
)]
})),
..Schema::new()
};
let node = Node::new(NodeType::Paragraph);
let config = Config::new();
let hook = schema.validate.expect("just set");
assert_eq!(hook(&node, &config).first().map(|e| e.id), Some("example"));
}
#[test]
fn an_error_may_quote_the_span_it_found() {
let source = String::from("# heading\n");
let lines = crate::ast::Lines::new(&source);
let mut node = Node::new(NodeType::Heading);
node.location = Some(lines.locate(0..9, None));
let hook: ValidateHook = Arc::new(|node: &Node<'_>, _config: &Config<'_>| {
let mut error = ValidationError::new("example", ErrorLevel::Error, "no");
error.location = node.location;
vec![error]
});
let config = Config::new();
let errors = hook(&node, &config);
assert_eq!(
errors.first().and_then(|e| e.location).map(|l| l.text),
Some("# heading")
);
}
#[test]
fn a_schema_builds_by_parts() {
let schema = Schema::new()
.render("aside")
.attribute(
"type",
SchemaAttribute {
required: true,
..SchemaAttribute::default()
},
)
.slot("footer", SchemaSlot::default());
assert_eq!(schema.render.as_deref(), Some("aside"));
assert!(schema.attributes["type"].required);
assert!(schema.slots.contains_key("footer"));
}
}