vorma 0.86.0-pre.3

Vorma framework.
Documentation
use std::collections::{BTreeMap, BTreeSet};

use serde_json::{Map, Value};

use crate::tsgen::{self, Type, TypeDef, TypePhase, TypeRef};

pub fn schema_for_type<T: Type>() -> Result<Value, String> {
	let mut registry = tsgen::TypeRegistry::default();
	T::collect_type_defs_for(TypePhase::Deserialize, &mut registry)
		.map_err(|err| err.to_string())?;
	let defs = registry
		.into_defs()
		.into_iter()
		.map(|def| (def.key().to_owned(), def))
		.collect::<BTreeMap<_, _>>();
	type_ref_to_schema(
		&T::type_ref_for(TypePhase::Deserialize),
		&defs,
		&mut BTreeSet::new(),
	)
	.ok_or_else(|| "type cannot be represented as URL search params".to_owned())
}

fn type_ref_to_schema(
	type_ref: &TypeRef,
	defs: &BTreeMap<String, TypeDef>,
	stack: &mut BTreeSet<String>,
) -> Option<Value> {
	match type_ref {
		TypeRef::Unit => Some(Value::Object(Map::new())),
		TypeRef::Bool => Some(Value::String("b".to_owned())),
		TypeRef::String | TypeRef::StringLiteral(_) => Some(Value::String("s".to_owned())),
		TypeRef::Number | TypeRef::Integer => Some(Value::String("n".to_owned())),
		TypeRef::Nullable(inner) => optional_schema(type_ref_to_schema(inner, defs, stack)?),
		TypeRef::Array(inner) => Some(Value::Array(vec![type_ref_to_scalar_schema(
			inner, defs, stack,
		)?])),
		TypeRef::Map(key, value) => {
			if !type_ref_is_string_key(key, defs, stack) {
				return None;
			}
			Some(Value::Array(vec![
				Value::String("*".to_owned()),
				type_ref_to_map_value_schema(value, defs, stack)?,
			]))
		}
		TypeRef::Named { key, .. } => named_type_to_schema(key, defs, stack),
		TypeRef::Null | TypeRef::Unknown | TypeRef::Union(_) | TypeRef::Raw(_) => None,
	}
}

fn optional_schema(schema: Value) -> Option<Value> {
	match schema {
		Value::String(value) => Some(Value::String(format!("?{value}"))),
		Value::Array(_) | Value::Object(_) => Some(schema),
		_ => None,
	}
}

fn named_type_to_schema(
	key: &str,
	defs: &BTreeMap<String, TypeDef>,
	stack: &mut BTreeSet<String>,
) -> Option<Value> {
	if !stack.insert(key.to_owned()) {
		return None;
	}
	let result = match defs.get(key)? {
		TypeDef::Alias { target, .. } => type_ref_to_schema(target, defs, stack),
		TypeDef::Record { fields, .. } => {
			let mut object = Map::new();
			for field in fields {
				let mut schema = type_ref_to_schema(field.type_ref(), defs, stack)?;
				if field.is_optional() {
					schema = optional_schema(schema)?;
				}
				object.insert(field.name().to_owned(), schema);
			}
			Some(Value::Object(object))
		}
		TypeDef::StringEnum { .. } => Some(Value::String("s".to_owned())),
		TypeDef::Raw { .. } => None,
	};
	stack.remove(key);
	result
}

fn type_ref_to_scalar_schema(
	type_ref: &TypeRef,
	defs: &BTreeMap<String, TypeDef>,
	stack: &mut BTreeSet<String>,
) -> Option<Value> {
	let schema = type_ref_to_schema(type_ref, defs, stack)?;
	if schema.is_string() {
		return Some(schema);
	}
	None
}

fn type_ref_to_map_value_schema(
	type_ref: &TypeRef,
	defs: &BTreeMap<String, TypeDef>,
	stack: &mut BTreeSet<String>,
) -> Option<Value> {
	let schema = type_ref_to_schema(type_ref, defs, stack)?;
	if schema.is_object() {
		return None;
	}
	Some(schema)
}

fn type_ref_is_string_key(
	type_ref: &TypeRef,
	defs: &BTreeMap<String, TypeDef>,
	stack: &mut BTreeSet<String>,
) -> bool {
	match type_ref {
		TypeRef::String | TypeRef::StringLiteral(_) => true,
		TypeRef::Named { key, .. } => {
			if !stack.insert(key.to_owned()) {
				return false;
			}
			let result = match defs.get(key) {
				Some(TypeDef::Alias { target, .. }) => type_ref_is_string_key(target, defs, stack),
				Some(TypeDef::StringEnum { .. }) => true,
				_ => false,
			};
			stack.remove(key);
			result
		}
		TypeRef::Unit
		| TypeRef::Null
		| TypeRef::Unknown
		| TypeRef::Bool
		| TypeRef::Number
		| TypeRef::Integer
		| TypeRef::Array(_)
		| TypeRef::Map(_, _)
		| TypeRef::Nullable(_)
		| TypeRef::Union(_)
		| TypeRef::Raw(_) => {
			let _ = stack;
			false
		}
	}
}

#[cfg(test)]
mod tests {
	use super::*;
	use crate::tsgen::{Result as TsResult, TypeRegistry};

	struct MapWithAliasKey;

	impl Type for MapWithAliasKey {
		fn type_ref() -> TypeRef {
			TypeRef::map(TypeRef::named("KeyAlias"), TypeRef::String)
		}

		fn collect_type_defs(registry: &mut TypeRegistry) -> TsResult<()> {
			registry.define(TypeDef::alias("KeyAlias", TypeRef::String));
			Ok(())
		}
	}

	struct MapWithCyclicAliasKey;

	impl Type for MapWithCyclicAliasKey {
		fn type_ref() -> TypeRef {
			TypeRef::map(TypeRef::named("KeyA"), TypeRef::String)
		}

		fn collect_type_defs(registry: &mut TypeRegistry) -> TsResult<()> {
			registry.define(TypeDef::alias("KeyA", TypeRef::named("KeyB")));
			registry.define(TypeDef::alias("KeyB", TypeRef::named("KeyA")));
			Ok(())
		}
	}

	#[test]
	fn map_key_alias_to_string_is_search_param_schema() {
		assert_eq!(
			schema_for_type::<MapWithAliasKey>().unwrap(),
			serde_json::json!(["*", "s"])
		);
	}

	#[test]
	fn map_key_alias_cycles_fail_instead_of_recursing() {
		assert_eq!(
			schema_for_type::<MapWithCyclicAliasKey>().unwrap_err(),
			"type cannot be represented as URL search params"
		);
	}
}