use std::sync::Arc;
use indexmap::IndexMap;
use crate::ast::{Node, Value};
use crate::validate::schema::{FunctionTransformHook, FunctionValidateHook, Schema};
use crate::validate::source::{MapSchemaSource, SchemaKey, SchemaSource};
use crate::validate::{SchemaAttribute, ValidationType};
pub type Variables = IndexMap<String, Value>;
#[derive(Clone)]
pub struct Config<'a> {
pub schemas: Arc<dyn SchemaSource + Send + Sync>,
pub variables: Option<Variables>,
pub functions: Arc<IndexMap<String, ConfigFunction>>,
pub partials: Arc<IndexMap<String, Node<'a>>>,
pub validation: ValidationOptions<'a>,
}
impl Default for Config<'_> {
fn default() -> Self {
Config {
schemas: Arc::new(MapSchemaSource::new()),
variables: None,
functions: Arc::default(),
partials: Arc::default(),
validation: ValidationOptions::default(),
}
}
}
impl<'a> Config<'a> {
#[must_use]
pub fn new() -> Config<'a> {
Config::default()
}
#[must_use]
pub fn with_schemas(mut self, schemas: Arc<dyn SchemaSource + Send + Sync>) -> Config<'a> {
self.schemas = schemas;
self
}
pub fn functions_mut(&mut self) -> &mut IndexMap<String, ConfigFunction> {
Arc::make_mut(&mut self.functions)
}
pub fn partials_mut(&mut self) -> &mut IndexMap<String, Node<'a>> {
Arc::make_mut(&mut self.partials)
}
#[must_use]
pub fn find_schema(&self, node: &Node<'_>) -> Option<&Schema> {
self.schemas.find(SchemaKey::for_node(node))
}
}
#[derive(Clone, Debug, Default)]
pub struct ValidationOptions<'a> {
pub parents: Vec<&'a Node<'a>>,
pub validate_functions: bool,
pub environment: Option<String>,
}
#[derive(Clone, Default)]
pub struct ConfigFunction {
pub returns: Option<ValidationType>,
pub parameters: Option<IndexMap<String, SchemaAttribute>>,
pub transform: Option<FunctionTransformHook>,
pub validate: Option<FunctionValidateHook>,
}
impl std::fmt::Debug for Config<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Config")
.field("nodes", &self.schemas.node_types())
.field("tags", &self.schemas.tag_names())
.field("variables", &self.variables)
.field("functions", &self.functions.keys().collect::<Vec<_>>())
.field("partials", &self.partials.keys().collect::<Vec<_>>())
.field("validation", &self.validation)
.finish()
}
}
impl std::fmt::Debug for ConfigFunction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ConfigFunction")
.field("returns", &self.returns)
.field("parameters", &self.parameters)
.field("transform", &self.transform.is_some())
.field("validate", &self.validate.is_some())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::{Node, NodeType};
#[test]
fn a_tag_is_looked_up_by_name_and_a_node_by_type() {
let mut schemas = MapSchemaSource::new();
schemas
.insert_tag("callout", Schema::default())
.insert_node(NodeType::Heading, Schema::default());
let config = Config::new().with_schemas(Arc::new(schemas));
let mut tag = Node::new(NodeType::Tag);
tag.tag = Some("callout".to_string());
assert!(config.find_schema(&tag).is_some());
assert!(config.find_schema(&Node::new(NodeType::Heading)).is_some());
assert!(
config
.find_schema(&Node::new(NodeType::Paragraph))
.is_none()
);
let mut unknown = Node::new(NodeType::Tag);
unknown.tag = Some("nope".to_string());
assert!(config.find_schema(&unknown).is_none());
}
#[test]
fn a_source_that_is_not_a_map_resolves_through_the_config() {
struct Aside(Schema);
impl SchemaSource for Aside {
fn find(&self, key: SchemaKey<'_>) -> Option<&Schema> {
match key {
SchemaKey::Tag("callout") => Some(&self.0),
_ => None,
}
}
}
let config = Config::new().with_schemas(Arc::new(Aside(Schema::new().render("aside"))));
let mut callout = Node::new(NodeType::Tag);
callout.tag = Some("callout".to_string());
assert_eq!(
config
.find_schema(&callout)
.and_then(|s| s.render.as_deref()),
Some("aside")
);
assert!(config.find_schema(&Node::new(NodeType::Heading)).is_none());
}
#[test]
fn debug_names_what_a_map_holds_and_admits_what_it_cannot_see() {
struct Opaque;
impl SchemaSource for Opaque {
fn find(&self, _key: SchemaKey<'_>) -> Option<&Schema> {
None
}
}
let mut schemas = MapSchemaSource::new();
schemas.insert_tag("callout", Schema::new());
let named = format!("{:?}", Config::new().with_schemas(Arc::new(schemas)));
assert!(named.contains(r#"tags: Some(["callout"])"#), "{named}");
assert!(named.contains("nodes: Some([])"), "{named}");
let opaque = format!("{:?}", Config::new().with_schemas(Arc::new(Opaque)));
assert!(opaque.contains("tags: None"), "{opaque}");
assert!(opaque.contains("nodes: None"), "{opaque}");
}
#[test]
fn a_config_with_nothing_borrowed_outlives_any_document() {
let config: Config<'static> = Config::new();
let source = String::from("# hi\n");
let node = Node::new(NodeType::Document);
let _ = source;
assert!(config.find_schema(&node).is_none());
}
}