vorma 0.86.0-pre.3

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

use super::identifier::validate_ts_identifier;
use super::{Error, RawTsPart, Result, TypeDef, TypeRef};
use crate::tsgen::render_type_ref;

#[doc(hidden)]
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct TypeResolver {
	roots: Vec<TypeRef>,
	defs: Vec<TypeDef>,
}

impl TypeResolver {
	#[doc(hidden)]
	pub fn add_root(&mut self, root: TypeRef) {
		self.roots.push(root);
	}

	#[doc(hidden)]
	pub fn add_defs(&mut self, defs: impl IntoIterator<Item = TypeDef>) {
		self.defs.extend(defs);
	}

	#[doc(hidden)]
	pub fn resolve(self) -> Result<ResolvedTypes> {
		let defs_by_key = defs_by_key(self.defs)?;
		let mut retained = BTreeSet::new();

		for root in &self.roots {
			retain_type_ref(root, &defs_by_key, &mut retained)?;
		}

		let retained_defs = retained
			.into_iter()
			.filter_map(|key| defs_by_key.get(&key).cloned())
			.collect::<Vec<_>>();
		let canonical = canonicalize_defs(&retained_defs);
		let names_by_canonical_key = assign_export_names(&canonical.defs)?;
		let names_by_key = canonical
			.key_to_canonical_key
			.into_iter()
			.map(|(key, canonical_key)| {
				let name = names_by_canonical_key
					.get(&canonical_key)
					.expect("canonical type should have assigned TypeScript name")
					.clone();
				(key, name)
			})
			.collect::<BTreeMap<_, _>>();
		let mut defs = retained_defs
			.into_iter()
			.filter(|def| canonical.canonical_keys.contains(def.key()))
			.map(|def| {
				let key = def.key().to_owned();
				let name = names_by_key
					.get(&key)
					.expect("retained type should have assigned TypeScript name")
					.clone();
				ResolvedTypeDef { name, def }
			})
			.collect::<Vec<_>>();
		defs.sort_by(|a, b| a.name.cmp(&b.name));

		Ok(ResolvedTypes { defs, names_by_key })
	}
}

#[doc(hidden)]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ResolvedTypes {
	defs: Vec<ResolvedTypeDef>,
	names_by_key: BTreeMap<String, String>,
}

impl ResolvedTypes {
	#[doc(hidden)]
	pub fn defs(&self) -> &[ResolvedTypeDef] {
		&self.defs
	}

	#[doc(hidden)]
	pub fn render_type_ref(&self, type_ref: &TypeRef) -> String {
		render_type_ref(type_ref, &self.names_by_key)
	}

	pub(crate) fn names_by_key(&self) -> &BTreeMap<String, String> {
		&self.names_by_key
	}
}

#[doc(hidden)]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ResolvedTypeDef {
	name: String,
	def: TypeDef,
}

impl ResolvedTypeDef {
	#[doc(hidden)]
	pub fn name(&self) -> &str {
		&self.name
	}

	#[doc(hidden)]
	pub fn def(&self) -> &TypeDef {
		&self.def
	}
}

struct CanonicalDefs {
	defs: Vec<TypeDef>,
	canonical_keys: BTreeSet<String>,
	key_to_canonical_key: BTreeMap<String, String>,
}

fn canonicalize_defs(defs: &[TypeDef]) -> CanonicalDefs {
	let mut canonical_defs = Vec::<TypeDef>::new();
	let mut canonical_keys = BTreeSet::<String>::new();
	let mut key_to_canonical_key = BTreeMap::<String, String>::new();

	for def in defs {
		let key = def.key().to_owned();
		let canonical_key = canonical_defs
			.iter()
			.find(|candidate| candidate.name() == def.name() && candidate.has_same_body_as(def))
			.map(|candidate| candidate.key().to_owned());

		match canonical_key {
			Some(canonical_key) => {
				key_to_canonical_key.insert(key, canonical_key);
			}
			None => {
				canonical_keys.insert(key.clone());
				key_to_canonical_key.insert(key.clone(), key);
				canonical_defs.push(def.clone());
			}
		}
	}

	CanonicalDefs {
		defs: canonical_defs,
		canonical_keys,
		key_to_canonical_key,
	}
}

fn defs_by_key(defs: Vec<TypeDef>) -> Result<BTreeMap<String, TypeDef>> {
	let mut by_key = BTreeMap::new();
	for def in defs {
		match by_key.get(def.key()) {
			Some(existing) if existing == &def => {}
			Some(_) => {
				return Err(Error::new(format!(
					"conflicting TypeScript type definitions for {}",
					def.name()
				)));
			}
			None => {
				by_key.insert(def.key().to_owned(), def);
			}
		}
	}
	Ok(by_key)
}

fn assign_export_names(defs: &[TypeDef]) -> Result<BTreeMap<String, String>> {
	let mut keys_by_name = BTreeMap::<String, Vec<String>>::new();
	for def in defs {
		keys_by_name
			.entry(def.name().to_owned())
			.or_default()
			.push(def.key().to_owned());
	}

	let mut names_by_key = BTreeMap::new();
	for (name, mut keys) in keys_by_name {
		keys.sort();
		for (index, key) in keys.into_iter().enumerate() {
			let resolved_name = if index == 0 {
				name.clone()
			} else {
				format!("{name}_{}", index + 1)
			};
			validate_export_type_name(&resolved_name)?;
			names_by_key.insert(key, resolved_name);
		}
	}
	Ok(names_by_key)
}

fn validate_export_type_name(name: &str) -> Result<()> {
	validate_ts_identifier(name, "TypeScript export type name")
}

fn retain_type_ref(
	type_ref: &TypeRef,
	defs_by_key: &BTreeMap<String, TypeDef>,
	retained: &mut BTreeSet<String>,
) -> Result<()> {
	match type_ref {
		TypeRef::Named { key, name } => retain_named_type(key, name, defs_by_key, retained),
		TypeRef::Array(inner) | TypeRef::Nullable(inner) => {
			retain_type_ref(inner, defs_by_key, retained)
		}
		TypeRef::Map(key, value) => {
			retain_type_ref(key, defs_by_key, retained)?;
			retain_type_ref(value, defs_by_key, retained)
		}
		TypeRef::Union(types) => {
			for type_ref in types {
				retain_type_ref(type_ref, defs_by_key, retained)?;
			}
			Ok(())
		}
		TypeRef::Raw(parts) => {
			for part in parts {
				if let RawTsPart::TypeRef(type_ref) = part {
					retain_type_ref(type_ref, defs_by_key, retained)?;
				}
			}
			Ok(())
		}
		TypeRef::Unit
		| TypeRef::Null
		| TypeRef::Unknown
		| TypeRef::Bool
		| TypeRef::String
		| TypeRef::Number
		| TypeRef::Integer
		| TypeRef::StringLiteral(_) => Ok(()),
	}
}

fn retain_named_type(
	key: &str,
	name: &str,
	defs_by_key: &BTreeMap<String, TypeDef>,
	retained: &mut BTreeSet<String>,
) -> Result<()> {
	let Some(def) = defs_by_key.get(key) else {
		return Err(Error::new(format!(
			"unresolved TypeScript type reference {name}"
		)));
	};

	if !retained.insert(key.to_owned()) {
		return Ok(());
	}

	match def {
		TypeDef::Alias { target, .. } => retain_type_ref(target, defs_by_key, retained),
		TypeDef::Record { fields, .. } => {
			for field in fields {
				retain_type_ref(field.type_ref(), defs_by_key, retained)?;
			}
			Ok(())
		}
		TypeDef::StringEnum { .. } => Ok(()),
		TypeDef::Raw { body, .. } => {
			for part in body {
				if let RawTsPart::TypeRef(type_ref) = part {
					retain_type_ref(type_ref, defs_by_key, retained)?;
				}
			}
			Ok(())
		}
	}
}

#[cfg(test)]
mod tests {
	use super::*;
	use crate::tsgen::{FieldDef, TypeDef, TypeRef, render_types};

	#[test]
	fn resolver_rejects_invalid_export_type_names() {
		let mut resolver = TypeResolver::default();
		resolver.add_root(TypeRef::named_with_key(
			"bad",
			"Bad; export type Evil = string",
		));
		resolver.add_defs([TypeDef::alias_with_key(
			"bad",
			"Bad; export type Evil = string",
			TypeRef::String,
		)]);

		let err = resolver.resolve().unwrap_err();

		assert!(
			err.to_string()
				.contains("invalid TypeScript export type name")
		);
	}

	#[test]
	fn resolver_rejects_reserved_export_type_names() {
		let mut resolver = TypeResolver::default();
		resolver.add_root(TypeRef::named_with_key("bad", "type"));
		resolver.add_defs([TypeDef::alias_with_key("bad", "type", TypeRef::String)]);

		let err = resolver.resolve().unwrap_err();

		assert!(
			err.to_string()
				.contains("invalid TypeScript export type name")
		);
	}

	#[test]
	fn resolver_collapses_phase_specific_defs_when_bodies_match() {
		let mut resolver = TypeResolver::default();
		resolver.add_root(TypeRef::named_with_key("user::deserialize", "User"));
		resolver.add_root(TypeRef::named_with_key("user::serialize", "User"));
		resolver.add_defs([
			TypeDef::record_with_key(
				"user::deserialize",
				"User",
				vec![FieldDef::required::<String>("name")],
			),
			TypeDef::record_with_key(
				"user::serialize",
				"User",
				vec![FieldDef::required::<String>("name")],
			),
		]);

		let resolved = resolver.resolve().unwrap();

		assert_eq!(
			resolved.render_type_ref(&TypeRef::named_with_key("user::deserialize", "User")),
			"User"
		);
		assert_eq!(
			resolved.render_type_ref(&TypeRef::named_with_key("user::serialize", "User")),
			"User"
		);
		assert_eq!(
			render_types(&resolved),
			"export type User = {\n\tname: string;\n};\n"
		);
	}

	#[test]
	fn resolver_keeps_phase_specific_defs_when_bodies_differ() {
		let mut resolver = TypeResolver::default();
		resolver.add_root(TypeRef::named_with_key("user::deserialize", "User"));
		resolver.add_root(TypeRef::named_with_key("user::serialize", "User"));
		resolver.add_defs([
			TypeDef::record_with_key(
				"user::deserialize",
				"User",
				vec![FieldDef::optional::<String>("name")],
			),
			TypeDef::record_with_key(
				"user::serialize",
				"User",
				vec![FieldDef::required::<String>("name")],
			),
		]);

		let resolved = resolver.resolve().unwrap();

		assert_eq!(
			resolved.render_type_ref(&TypeRef::named_with_key("user::deserialize", "User")),
			"User"
		);
		assert_eq!(
			resolved.render_type_ref(&TypeRef::named_with_key("user::serialize", "User")),
			"User_2"
		);
		assert_eq!(
			render_types(&resolved),
			"export type User = {\n\tname?: string;\n};\n\nexport type User_2 = {\n\tname: string;\n};\n"
		);
	}
}