use std::sync::Arc;
use crate::ast::{ValidationError, Value};
use crate::renderable::Scalar;
use crate::validate::Config;
#[derive(Clone)]
#[non_exhaustive]
pub enum ValidationType {
String,
Number,
Boolean,
Object,
Array,
Custom(Arc<dyn AttributeType + Send + Sync + 'static>),
Union(Vec<ValidationType>),
}
impl ValidationType {
#[must_use]
pub fn is_same_type(&self, other: &ValidationType) -> bool {
match (self, other) {
(ValidationType::String, ValidationType::String)
| (ValidationType::Number, ValidationType::Number)
| (ValidationType::Boolean, ValidationType::Boolean)
| (ValidationType::Object, ValidationType::Object)
| (ValidationType::Array, ValidationType::Array) => true,
(ValidationType::Custom(a), ValidationType::Custom(b)) => Arc::ptr_eq(a, b),
_ => false,
}
}
#[must_use]
pub fn accepts_shape(&self, value: &Value) -> bool {
matches!(
(self, value),
(ValidationType::String, Value::String(_))
| (ValidationType::Number, Value::Number(_))
| (ValidationType::Boolean, Value::Boolean(_))
| (ValidationType::Object, Value::Hash(_))
| (ValidationType::Array, Value::Array(_))
)
}
}
#[must_use]
pub fn type_to_string(value_type: &ValidationType) -> String {
match value_type {
ValidationType::String => "String".to_string(),
ValidationType::Number => "Number".to_string(),
ValidationType::Boolean => "Boolean".to_string(),
ValidationType::Object => "Object".to_string(),
ValidationType::Array => "Array".to_string(),
ValidationType::Custom(custom) => custom.name().to_string(),
ValidationType::Union(members) => members
.iter()
.map(type_to_string)
.collect::<Vec<_>>()
.join(" | "),
}
}
pub trait AttributeType {
fn name(&self) -> &'static str;
fn validate<'a>(
&self,
value: &Value,
config: &Config<'a>,
name: &str,
) -> Option<Vec<ValidationError<'a>>> {
let _ = (value, config, name);
None
}
fn transform(&self, value: Option<&Value>, config: &Config<'_>) -> Option<Scalar> {
let _ = config;
value.and_then(Scalar::from_value)
}
}
impl std::fmt::Debug for ValidationType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&type_to_string(self))
}
}
#[cfg(test)]
mod tests {
use super::*;
use indexmap::IndexMap;
#[test]
fn built_in_types_check_the_parsed_shape() {
assert!(ValidationType::String.accepts_shape(&Value::String("x".into())));
assert!(!ValidationType::Number.accepts_shape(&Value::String("1".into())));
assert!(ValidationType::Object.accepts_shape(&Value::Hash(IndexMap::new())));
assert!(ValidationType::Array.accepts_shape(&Value::Array(Vec::new())));
assert!(!ValidationType::Boolean.accepts_shape(&Value::Null));
}
#[test]
fn a_union_prints_as_upstream_joins_it() {
let union = ValidationType::Union(vec![ValidationType::String, ValidationType::Number]);
assert_eq!(type_to_string(&union), "String | Number");
}
#[test]
fn identity_is_javascript_identity() {
assert!(ValidationType::String.is_same_type(&ValidationType::String));
assert!(!ValidationType::String.is_same_type(&ValidationType::Number));
let a = ValidationType::Union(vec![ValidationType::String]);
let b = ValidationType::Union(vec![ValidationType::String]);
assert!(!a.is_same_type(&b));
}
#[test]
fn a_custom_type_is_equal_only_to_itself() {
struct Link;
impl AttributeType for Link {
fn name(&self) -> &'static str {
"Link"
}
}
let one: Arc<dyn AttributeType + Send + Sync> = Arc::new(Link);
let same = ValidationType::Custom(Arc::clone(&one));
let other = ValidationType::Custom(Arc::new(Link));
assert!(ValidationType::Custom(one).is_same_type(&same));
assert!(!same.is_same_type(&other));
assert_eq!(type_to_string(&same), "Link");
}
#[test]
fn a_type_with_no_validate_method_accepts_nothing() {
struct Bare;
impl AttributeType for Bare {
fn name(&self) -> &'static str {
"Bare"
}
}
let config = Config::new();
assert!(
Bare.validate(&Value::String("x".into()), &config, "k")
.is_none()
);
}
}