use indexmap::IndexMap;
use crate::ast::{Node, NodeType};
use crate::validate::schema::Schema;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SchemaKey<'a> {
Tag(&'a str),
Node(NodeType),
}
impl<'a> SchemaKey<'a> {
#[must_use]
pub fn for_node(node: &'a Node<'_>) -> SchemaKey<'a> {
match &node.tag {
Some(tag) => SchemaKey::Tag(tag.as_str()),
None => SchemaKey::Node(node.node_type),
}
}
}
pub trait SchemaSource {
fn find(&self, key: SchemaKey<'_>) -> Option<&Schema>;
fn tag_names(&self) -> Option<Vec<&str>> {
None
}
fn node_types(&self) -> Option<Vec<NodeType>> {
None
}
}
#[derive(Clone, Default)]
pub struct MapSchemaSource {
nodes: IndexMap<NodeType, Schema>,
tags: IndexMap<String, Schema>,
}
impl MapSchemaSource {
#[must_use]
pub fn new() -> MapSchemaSource {
MapSchemaSource::default()
}
#[must_use]
pub fn builtin() -> MapSchemaSource {
MapSchemaSource {
nodes: crate::validate::nodes::builtin(),
tags: crate::tags::builtin(),
}
}
pub fn insert_tag(&mut self, name: impl Into<String>, schema: Schema) -> &mut MapSchemaSource {
self.tags.insert(name.into(), schema);
self
}
pub fn insert_node(&mut self, node_type: NodeType, schema: Schema) -> &mut MapSchemaSource {
self.nodes.insert(node_type, schema);
self
}
#[must_use]
pub fn tags(&self) -> &IndexMap<String, Schema> {
&self.tags
}
pub fn tags_mut(&mut self) -> &mut IndexMap<String, Schema> {
&mut self.tags
}
#[must_use]
pub fn nodes(&self) -> &IndexMap<NodeType, Schema> {
&self.nodes
}
pub fn nodes_mut(&mut self) -> &mut IndexMap<NodeType, Schema> {
&mut self.nodes
}
}
impl SchemaSource for MapSchemaSource {
fn find(&self, key: SchemaKey<'_>) -> Option<&Schema> {
match key {
SchemaKey::Tag(name) => self.tags.get(name),
SchemaKey::Node(node_type) => self.nodes.get(&node_type),
}
}
fn tag_names(&self) -> Option<Vec<&str>> {
Some(self.tags.keys().map(String::as_str).collect())
}
fn node_types(&self) -> Option<Vec<NodeType>> {
Some(self.nodes.keys().copied().collect())
}
}
impl std::fmt::Debug for MapSchemaSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MapSchemaSource")
.field("nodes", &self.node_types())
.field("tags", &self.tag_names())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_tag_node_is_keyed_by_name_and_a_plain_node_by_type() {
let mut tag = Node::new(NodeType::Tag);
tag.tag = Some("callout".to_string());
assert_eq!(SchemaKey::for_node(&tag), SchemaKey::Tag("callout"));
assert_eq!(
SchemaKey::for_node(&Node::new(NodeType::Heading)),
SchemaKey::Node(NodeType::Heading)
);
}
#[test]
fn the_map_source_answers_from_its_maps() {
let mut schemas = MapSchemaSource::new();
schemas
.insert_tag("callout", Schema::new())
.insert_node(NodeType::Heading, Schema::new());
assert!(schemas.find(SchemaKey::Tag("callout")).is_some());
assert!(schemas.find(SchemaKey::Node(NodeType::Heading)).is_some());
assert!(schemas.find(SchemaKey::Tag("nope")).is_none());
assert!(schemas.find(SchemaKey::Node(NodeType::Paragraph)).is_none());
assert!(schemas.find(SchemaKey::Node(NodeType::Tag)).is_none());
}
#[test]
fn the_map_source_can_enumerate_and_the_default_cannot() {
struct Opaque;
impl SchemaSource for Opaque {
fn find(&self, _key: SchemaKey<'_>) -> Option<&Schema> {
None
}
}
let mut schemas = MapSchemaSource::new();
schemas
.insert_tag("b", Schema::new())
.insert_tag("a", Schema::new());
assert_eq!(schemas.tag_names(), Some(vec!["b", "a"]));
assert_eq!(schemas.node_types(), Some(vec![]));
assert_eq!(Opaque.tag_names(), None);
assert_eq!(Opaque.node_types(), None);
}
#[test]
fn debug_reads_the_same_names_the_trait_reports() {
let mut schemas = MapSchemaSource::new();
schemas.insert_tag("callout", Schema::new());
let debug = format!("{schemas:?}");
assert!(debug.contains(r#"tags: Some(["callout"])"#), "{debug}");
assert!(debug.contains("nodes: Some([])"), "{debug}");
}
#[test]
fn builtin_is_markdocs_vocabulary() {
let schemas = MapSchemaSource::builtin();
assert!(schemas.find(SchemaKey::Tag("if")).is_some());
assert!(schemas.find(SchemaKey::Tag("partial")).is_some());
assert!(schemas.find(SchemaKey::Node(NodeType::Heading)).is_some());
assert!(schemas.find(SchemaKey::Tag("callout")).is_none());
}
}