use std::collections::HashMap;
use primitives_rs::tsgen::{FieldPresence, TsGen, TypePhase, TypeRef, TypeRegistry};
use serde_json::Value;
pub(crate) struct JsonObjectPositions {
defs: HashMap<String, TypeRef>,
root: TypeRef,
}
impl JsonObjectPositions {
pub(crate) fn of<T: TsGen>() -> Option<Self> {
let mut registry = TypeRegistry::default();
T::collect_type_defs_for(TypePhase::Deserialize, &mut registry).ok()?;
let positions = Self {
defs: registry
.into_defs()
.into_iter()
.map(|def| (def.key().to_owned(), def.type_ref().clone()))
.collect(),
root: T::type_ref_for(TypePhase::Deserialize),
};
positions
.declares_object(&positions.root, &mut Vec::new())
.then_some(positions)
}
pub(crate) fn admits(&self, value: &Value) -> bool {
BodyCheck {
positions: self,
named_verdicts: HashMap::new(),
}
.admits(&self.root, value)
}
fn fits<'a>(&'a self, type_ref: &'a TypeRef, value: &Value, named: &mut Vec<&'a str>) -> bool {
match type_ref {
TypeRef::Named { key, .. } => {
if named.contains(&key.as_str()) {
return true;
}
let Some(def) = self.defs.get(key) else {
return true;
};
named.push(key);
let fits = self.fits(def, value, named);
named.pop();
fits
}
TypeRef::Object(fields) => match value {
Value::Array(_) => true,
Value::Object(entries) => fields.iter().all(|field| {
match (
field.presence(),
field.type_ref(),
entries.get(field.name()),
) {
(FieldPresence::Optional, _, _) => true,
(FieldPresence::Required, _, None) => false,
(FieldPresence::Required, TypeRef::StringLiteral(literal), Some(entry)) => {
entry.as_str() == Some(literal.as_str())
}
(FieldPresence::Required, _, Some(_)) => true,
}
}),
_ => false,
},
TypeRef::Intersection(parts) => parts.iter().all(|part| self.fits(part, value, named)),
TypeRef::Union(variants) => variants
.iter()
.any(|variant| self.fits(variant, value, named)),
TypeRef::Nullable(inner) => value.is_null() || self.fits(inner, value, named),
TypeRef::Array(_) | TypeRef::Tuple(_) => value.is_array(),
TypeRef::Map(_, _) => value.is_object(),
TypeRef::StringLiteral(literal) => value.as_str() == Some(literal.as_str()),
TypeRef::String => value.is_string(),
TypeRef::Number | TypeRef::Integer | TypeRef::WideInteger { .. } => value.is_number(),
TypeRef::Bool => value.is_boolean(),
TypeRef::Unit | TypeRef::Null => value.is_null(),
TypeRef::Unknown | TypeRef::FormData | TypeRef::Blob | TypeRef::Raw(_) => true,
}
}
fn declares_object<'a>(&'a self, type_ref: &'a TypeRef, named: &mut Vec<&'a str>) -> bool {
match type_ref {
TypeRef::Named { key, .. } => {
if named.contains(&key.as_str()) {
return false;
}
let Some(def) = self.defs.get(key) else {
return false;
};
named.push(key);
let declares = self.declares_object(def, named);
named.pop();
declares
}
TypeRef::Object(_) => true,
TypeRef::Intersection(parts) | TypeRef::Union(parts) | TypeRef::Tuple(parts) => {
parts.iter().any(|part| self.declares_object(part, named))
}
TypeRef::Nullable(inner) | TypeRef::Array(inner) | TypeRef::Map(_, inner) => {
self.declares_object(inner, named)
}
TypeRef::Unit
| TypeRef::Null
| TypeRef::Unknown
| TypeRef::Bool
| TypeRef::String
| TypeRef::Number
| TypeRef::Integer
| TypeRef::WideInteger { .. }
| TypeRef::FormData
| TypeRef::Blob
| TypeRef::StringLiteral(_)
| TypeRef::Raw(_) => false,
}
}
}
struct BodyCheck<'a> {
positions: &'a JsonObjectPositions,
named_verdicts: HashMap<(usize, usize), bool>,
}
impl<'a> BodyCheck<'a> {
fn admits(&mut self, type_ref: &'a TypeRef, value: &Value) -> bool {
match type_ref {
TypeRef::Named { key, .. } => {
let Some(def) = self.positions.defs.get(key) else {
return true;
};
let pair = (
std::ptr::from_ref(def).addr(),
std::ptr::from_ref(value).addr(),
);
if let Some(verdict) = self.named_verdicts.get(&pair) {
return *verdict;
}
self.named_verdicts.insert(pair, true);
let verdict = self.admits(def, value);
self.named_verdicts.insert(pair, verdict);
verdict
}
TypeRef::Object(fields) => match value {
Value::Array(_) => false,
Value::Object(entries) => fields.iter().all(|field| {
entries
.get(field.name())
.is_none_or(|entry| self.admits(field.type_ref(), entry))
}),
_ => true,
},
TypeRef::Intersection(parts) => parts.iter().all(|part| self.admits(part, value)),
TypeRef::Union(variants) => {
let mut fitting: Vec<&'a TypeRef> = variants
.iter()
.filter(|variant| self.positions.fits(variant, value, &mut Vec::new()))
.collect();
if fitting.iter().any(|variant| !is_empty_object(variant)) {
fitting.retain(|variant| !is_empty_object(variant));
}
fitting.is_empty()
|| fitting
.into_iter()
.any(|variant| self.admits(variant, value))
}
TypeRef::Nullable(inner) => value.is_null() || self.admits(inner, value),
TypeRef::Array(item) => match value {
Value::Array(items) => items.iter().all(|entry| self.admits(item, entry)),
_ => true,
},
TypeRef::Tuple(items) => match value {
Value::Array(entries) => items
.iter()
.zip(entries)
.all(|(item, entry)| self.admits(item, entry)),
_ => true,
},
TypeRef::Map(_, item) => match value {
Value::Object(entries) => entries.values().all(|entry| self.admits(item, entry)),
_ => true,
},
TypeRef::Unit
| TypeRef::Null
| TypeRef::Unknown
| TypeRef::Bool
| TypeRef::String
| TypeRef::Number
| TypeRef::Integer
| TypeRef::WideInteger { .. }
| TypeRef::FormData
| TypeRef::Blob
| TypeRef::StringLiteral(_)
| TypeRef::Raw(_) => true,
}
}
}
fn is_empty_object(type_ref: &TypeRef) -> bool {
matches!(type_ref, TypeRef::Object(fields) if fields.is_empty())
}