vorma 0.86.0-pre.6

Rust-first full-stack web framework: typed views and resources, generated TypeScript contracts, and a Vite-integrated frontend.
Documentation
//! The check that a JSON body has an object wherever its input type declares
//! one.
//!
//! serde's derived `Deserialize` for a struct also accepts a JSON array and
//! assigns its elements to the fields in declaration order. Vorma's generated
//! client never sends that form, and a client that sent it would silently
//! change meaning when the struct's field order changed, so an API endpoint
//! rejects a JSON body that has an array where its input type declares an
//! object.

use std::collections::HashMap;

use primitives_rs::tsgen::{FieldPresence, TsGen, TypePhase, TypeRef, TypeRegistry};
use serde_json::Value;

/// The positions at which an input type's JSON form is an object, derived once
/// from the type's TypeScript declarations.
pub(crate) struct JsonObjectPositions {
	defs: HashMap<String, TypeRef>,
	root: TypeRef,
}

impl JsonObjectPositions {
	/// The object positions of `T`'s deserialized form, or `None` when `T`
	/// declares no object, so that no body of `T` needs the check. `None` also
	/// stands for a type whose declarations cannot be collected, which fails the
	/// app's loading before any request arrives.
	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)
	}

	/// Whether `value` has no array where the type declares an object. Every
	/// other mismatch is left to the type's own `Deserialize`.
	///
	/// At a union, such as an enum, the check looks only at the variants that
	/// `value` has the shape of (see [`Self::fits`]), and it admits `value` when
	/// one of them admits it or when none fits, so that the type's own
	/// `Deserialize` reports the mismatch. A tag selects one variant of a tagged
	/// enum. The empty object of a flattened `Option` counts only when no other
	/// variant fits. In an untagged enum whose variants fit the same value, a
	/// body that one variant admits is admitted, although serde decodes the
	/// first variant that it can, which may be another one.
	///
	/// The check of one body visits each pair of a named type and a value node
	/// once, so its cost grows with the size of the body whatever the shape of
	/// the type's unions.
	pub(crate) fn admits(&self, value: &Value) -> bool {
		BodyCheck {
			positions: self,
			named_verdicts: HashMap::new(),
		}
		.admits(&self.root, value)
	}

	/// Whether `value` has the JSON shape that `type_ref` decodes: the same kind
	/// of JSON value and, for an object, every field that the type requires,
	/// with the value of each field whose type is a string literal, such as an
	/// enum's tag. An array also has the shape of an object, because serde
	/// decodes a struct from an array. `named` holds the keys of the named types
	/// that the walk entered at this value, so that a cycle of named types ends.
	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::Record(_, _) => 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::Undefined | TypeRef::Null => value.is_null(),
			TypeRef::Unknown | TypeRef::FormData | TypeRef::Blob | TypeRef::Raw(_) => true,
		}
	}

	/// Whether `type_ref` reaches an object. `named` holds the keys of the named
	/// types that enclose the current position.
	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::Record(_, inner) => {
				self.declares_object(inner, named)
			}
			TypeRef::Undefined
			| TypeRef::Null
			| TypeRef::Unknown
			| TypeRef::Bool
			| TypeRef::String
			| TypeRef::Number
			| TypeRef::Integer
			| TypeRef::WideInteger { .. }
			| TypeRef::FormData
			| TypeRef::Blob
			| TypeRef::StringLiteral(_)
			| TypeRef::Raw(_) => false,
		}
	}
}

/// The check of one body.
struct BodyCheck<'a> {
	positions: &'a JsonObjectPositions,
	/// The verdict for each pair of a named type's definition and a value node,
	/// by their addresses. A pair whose check is still running counts as
	/// admitted, which ends a cycle of named types that does not move into a
	/// nested value.
	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();
				// An empty object is the form of a flattened `None`, which serde
				// decodes only when the other variants fail.
				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::Record(_, item) => match value {
				Value::Object(entries) => entries.values().all(|entry| self.admits(item, entry)),
				_ => true,
			},
			TypeRef::Undefined
			| 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())
}