use std::borrow::{Borrow, Cow};
use std::collections::HashSet;
use std::str::FromStr;
use kdl_schema::{
Children as ChildrenSchema, Format, Node as NodeSchema, Prop as PropSchema, Schema, Validation,
Value as ValueSchema,
};
use knuffel::ast::Literal;
use knuffel::span::Span;
use miette::Diagnostic;
use regex::Regex;
use thiserror::Error;
use url::Url;
use super::DocumentAst;
#[derive(Debug, Error, Diagnostic)]
#[allow(clippy::module_name_repetitions)]
pub enum CheckFailure {
#[error("schema error: node ref `{0}` could not be resolved")]
MissingNodeRef(String),
#[error("schema error: prop ref `{0}` could not be resolved")]
MissingPropRef(String),
#[error("schema error: value ref `{0}` could not be resolved")]
MissingValueRef(String),
#[error("schema error: children ref `{0}` could not be resolved")]
MissingChildrenRef(String),
#[error(
"wrong number of {expected_type}: expected {expected_number_range} but got {actual_number}"
)]
MinMaxViolation {
expected_type: String,
expected_number_range: String,
actual_number: usize,
},
#[error("unexpected node {name}")]
#[diagnostic()]
UnexpectedNode {
#[label]
span: Span,
name: String,
},
#[error("unexpected prop {key}")]
#[diagnostic()]
UnexpectedProp {
#[label]
span: Span,
key: String,
},
#[error("prop {key} missing")]
MissingProp { key: String },
#[error("value {actual} not in enum list {expected:?}")]
EnumViolation {
actual: String,
expected: Vec<String>,
},
#[error("value {value:?} does not have type {expected_type}")]
#[diagnostic()]
IncorrectType {
#[label]
span: Span,
value: Value, expected_type: String,
},
#[error("value {value} not of any formats {formats:?}")]
#[diagnostic()]
IncorrectStringFormat {
#[label]
span: Span,
value: String,
formats: Vec<Format>,
},
}
type Result<T = ()> = std::result::Result<T, CheckFailure>;
type Name = knuffel::ast::SpannedName<Span>;
type Node = knuffel::ast::SpannedNode<Span>;
type PropsMap = std::collections::BTreeMap<Name, Value>;
type Value = knuffel::ast::Value<Span>;
fn check_min_max(
actual: usize,
min: Option<usize>,
max: Option<usize>,
get_expected_type: impl Fn() -> String,
) -> Result {
let below_min = min.map_or(false, |min| actual < min);
let above_max = max.map_or(false, |max| actual > max);
if below_min || above_max {
let what_was_expected = get_expected_type();
let how_many_were_expected = match (min, max) {
(Some(min), Some(max)) => {
if min == max {
format!("exactly {}", min)
} else {
format!("between {} and {}", min, max)
}
}
(Some(min), None) => format!("at least {}", min),
(None, Some(max)) => format!("no more than {}", max),
(None, None) => unreachable!(),
};
Err(CheckFailure::MinMaxViolation {
expected_type: what_was_expected,
expected_number_range: how_many_were_expected,
actual_number: actual,
})
} else {
Ok(())
}
}
fn resolve_node_refs<'a>(
schema: &'a Schema,
nodes: &'a [NodeSchema],
) -> Result<Vec<&'a NodeSchema>> {
nodes
.iter()
.map(|node| match &node.ref_ {
None => Ok(node),
Some(r#ref) => schema
.resolve_node_ref(r#ref)
.ok_or_else(|| CheckFailure::MissingNodeRef(r#ref.clone())),
})
.collect()
}
fn resolve_prop_refs<'a>(
schema: &'a Schema,
props: &'a [PropSchema],
) -> Result<Vec<&'a PropSchema>> {
props
.iter()
.map(|prop| match &prop.ref_ {
None => Ok(prop),
Some(r#ref) => schema
.resolve_prop_ref(r#ref)
.ok_or_else(|| CheckFailure::MissingPropRef(r#ref.clone())),
})
.collect()
}
fn resolve_value_refs<'a>(
schema: &'a Schema,
values: &'a [ValueSchema],
) -> Result<Vec<&'a ValueSchema>> {
values
.iter()
.map(|value| match &value.ref_ {
None => Ok(value),
Some(r#ref) => schema
.resolve_value_ref(r#ref)
.ok_or_else(|| CheckFailure::MissingValueRef(r#ref.clone())),
})
.collect()
}
fn resolve_children_refs<'a>(
schema: &'a Schema,
children: &'a [ChildrenSchema],
) -> Result<Vec<&'a ChildrenSchema>> {
children
.iter()
.map(|children| match &children.ref_ {
None => Ok(children),
Some(r#ref) => schema
.resolve_children_ref(r#ref)
.ok_or_else(|| CheckFailure::MissingChildrenRef(r#ref.clone())),
})
.collect()
}
pub(crate) fn check(document: &DocumentAst, schema: &Schema) -> Result {
check_nodes(
&document.nodes,
schema,
&resolve_node_refs(schema, &schema.document.nodes)?,
)
}
fn check_nodes(nodes: &[Node], schema: &Schema, nodes_schema: &[&NodeSchema]) -> Result {
let nodes: Vec<(usize, &Node)> = nodes.iter().enumerate().collect();
let mut nodes_pending_validation: HashSet<usize> = nodes.iter().map(|(i, _)| *i).collect();
for node_schema in nodes_schema {
let applicable_nodes: Vec<(usize, &Node)> = nodes
.iter()
.filter(|(_, node)| match &node_schema.name {
Some(schema_name) => schema_name.as_str() == node.node_name.as_ref(),
None => true,
})
.copied()
.collect();
check_min_max(
applicable_nodes.len(),
node_schema.min,
node_schema.max,
|| match &node_schema.name {
Some(schema_name) => format!("`{}` nodes", schema_name),
None => "nodes".to_string(),
},
)?;
for (index, node) in applicable_nodes {
check_node(node, schema, node_schema)?;
nodes_pending_validation.remove(&index);
}
}
let invalid_node = nodes
.into_iter()
.find(|(i, _)| nodes_pending_validation.contains(i));
match invalid_node {
Some((_, node)) => Err(CheckFailure::UnexpectedNode {
span: node.span().clone(),
name: node.node_name.to_string(),
}),
None => Ok(()),
}
}
fn check_node(node: &Node, schema: &Schema, node_schema: &NodeSchema) -> Result {
let node_name = &node.node_name;
let schema_applies = match &node_schema.name {
Some(schema_name) => schema_name == node_name.as_ref(),
None => true,
};
if !schema_applies {
return Ok(());
}
check_props(
&node.properties,
&resolve_prop_refs(schema, &node_schema.props)?,
)?;
check_values(
&node.arguments,
&resolve_value_refs(schema, &node_schema.values)?,
)?;
if let Some(children) = &node.children {
check_children(
children,
schema,
&resolve_children_refs(schema, &node_schema.children)?,
)?;
}
Ok(())
}
fn check_props(props: &PropsMap, props_schema: &[&PropSchema]) -> Result {
let props: Vec<(usize, (&Name, &Value))> = props.iter().enumerate().collect();
let mut props_pending_validation: HashSet<usize> = props.iter().map(|(i, _)| *i).collect();
for prop_schema in props_schema {
let applicable_props: Vec<(usize, (&Name, &Value))> = props
.iter()
.filter(|(_, (key, _))| match &prop_schema.key {
Some(schema_key) => schema_key.as_str() == key.as_ref(),
None => true,
})
.copied()
.collect();
if prop_schema.required && applicable_props.is_empty() {
return Err(CheckFailure::MissingProp {
key: prop_schema.key.clone().unwrap(),
});
}
for (index, (_, value)) in applicable_props {
check_prop(value, prop_schema)?;
props_pending_validation.remove(&index);
}
}
let invalid_prop = props
.into_iter()
.find(|(i, _)| props_pending_validation.contains(i));
match invalid_prop {
Some((_, (name, _))) => Err(CheckFailure::UnexpectedProp {
span: name.span().clone(),
key: name.to_string(),
}),
None => Ok(()),
}
}
fn check_prop(prop_value: &Value, prop_schema: &PropSchema) -> Result {
for validation in &prop_schema.validations {
check_validation(prop_value, validation)?;
}
Ok(())
}
fn check_values(values: &[Value], values_schemas: &[&ValueSchema]) -> Result {
if values.is_empty() && values_schemas.is_empty() {
return Ok(());
}
let schemas_coherent = values_schemas.len() == 1;
assert!(schemas_coherent, "values schemas confusing");
let values_schema = values_schemas[0];
check_min_max(values.len(), values_schema.min, values_schema.max, || {
"values".to_string()
})?;
for value in values {
check_value(value, values_schema)?;
}
Ok(())
}
fn check_value(value: &Value, values_schema: &ValueSchema) -> Result {
for validation in &values_schema.validations {
check_validation(value, validation)?;
}
Ok(())
}
fn check_children(
children: &[Node],
schema: &Schema,
children_schema: &[&ChildrenSchema],
) -> Result {
let node_schemas: Vec<&NodeSchema> = children_schema
.iter()
.map(|children| resolve_node_refs(schema, &children.nodes))
.collect::<Result<Vec<Vec<&NodeSchema>>>>()?
.into_iter()
.flatten()
.collect();
check_nodes(children, schema, &node_schemas)
}
fn check_validation(value: &Value, validation: &Validation) -> Result {
match validation {
Validation::Type(r#type) if r#type == "string" => {
let _ = get_string_value(value)?;
}
Validation::Type(r#type) if r#type == "number" => match value.literal.borrow() {
Literal::Int(_) | Literal::Decimal(_) => {}
_ => {
return Err(CheckFailure::IncorrectType {
span: value.literal.span().clone(),
value: value.clone(),
expected_type: r#type.clone(),
});
}
},
Validation::Type(r#type) => todo!("validate type {}", r#type),
Validation::Enum(enum_values) => {
let search_target = match value.literal.borrow() {
Literal::Null => Cow::Borrowed("enum"),
Literal::Bool(x) => {
if *x {
Cow::Borrowed("true")
} else {
Cow::Borrowed("false")
}
}
Literal::Int(x) => match i64::try_from(x) {
Ok(x) => Cow::Owned(x.to_string()),
_ => todo!("get value from Integer"),
},
Literal::String(x) => Cow::Borrowed(x.as_ref()),
Literal::Decimal(_) => todo!("get value from Decimal"),
};
if !enum_values
.iter()
.any(|enum_value| enum_value == search_target.as_ref())
{
return Err(CheckFailure::EnumViolation {
actual: search_target.into_owned(),
expected: enum_values.clone(),
});
}
}
Validation::Format(valid_formats) => {
for format in valid_formats {
match format {
Format::Date => {
if let Ok(value) = get_string_value(value) {
if chrono::NaiveDate::from_str(value).is_ok() {
return Ok(());
}
}
}
Format::Url => {
if let Ok(value) = get_string_value(value) {
if Url::parse(value).is_ok() {
return Ok(());
}
}
}
Format::Regex => {
if let Ok(value) = get_string_value(value) {
if Regex::new(value).is_ok() {
return Ok(());
}
}
}
Format::KdlQuery => {
if let Ok(_value) = get_string_value(value) {
return Ok(());
}
}
format => todo!("validate format {:?}", format),
}
}
return match value.literal.borrow() {
Literal::String(string_value) => Err(CheckFailure::IncorrectStringFormat {
span: value.literal.span().clone(),
value: string_value.to_string(),
formats: valid_formats.clone(),
}),
value => todo!("no valid format for {:?}", value),
};
}
Validation::Pattern(regex) => {
let regex = Regex::new(&format!("^{}$", regex)).expect("invalid regex in schema");
let value = if let Literal::String(value) = value.literal.borrow() {
value
} else {
todo!("error for can't regex a non-string")
};
if !regex.is_match(value) {
todo!("error for regex failure")
}
}
}
Ok(())
}
fn get_string_value(value: &Value) -> Result<&str> {
match value.literal.borrow() {
Literal::String(x) => Ok(x),
_ => Err(CheckFailure::IncorrectType {
span: value.literal.span().clone(),
value: value.clone(),
expected_type: "string".to_string(),
}),
}
}