pub use ferro_type_derive::TS;
#[deprecated(since = "0.2.0", note = "use `TS` instead")]
#[allow(deprecated)]
pub use ferro_type_derive::TypeScript;
pub use linkme;
use std::collections::HashMap;
#[linkme::distributed_slice]
pub static TYPESCRIPT_TYPES: [fn() -> TypeDef];
pub trait TS {
fn typescript() -> TypeDef;
}
#[deprecated(since = "0.2.0", note = "use `TS` instead")]
pub trait TypeScript: TS {}
#[derive(Debug, Clone, PartialEq)]
pub enum TypeDef {
Primitive(Primitive),
Array(Box<TypeDef>),
Tuple(Vec<TypeDef>),
Object(Vec<Field>),
Union(Vec<TypeDef>),
Intersection(Vec<TypeDef>),
Record {
key: Box<TypeDef>,
value: Box<TypeDef>,
},
Named {
namespace: Vec<String>,
name: String,
def: Box<TypeDef>,
module: Option<String>,
wrapper: Option<String>,
},
Ref(String),
Literal(Literal),
Function {
params: Vec<Field>,
return_type: Box<TypeDef>,
},
Generic {
base: String,
args: Vec<TypeDef>,
},
IndexedAccess {
base: String,
key: String,
},
TemplateLiteral {
strings: Vec<String>,
types: Vec<Box<TypeDef>>,
},
GenericDef {
name: String,
type_params: Vec<TypeParam>,
def: Box<TypeDef>,
},
TypeParamRef(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Primitive {
String,
Number,
Boolean,
Null,
Undefined,
Void,
Never,
Any,
Unknown,
BigInt,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Field {
pub name: String,
pub ty: TypeDef,
pub optional: bool,
pub readonly: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct TypeParam {
pub name: String,
pub constraint: Option<Box<TypeDef>>,
pub default: Option<Box<TypeDef>>,
}
impl TypeParam {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
constraint: None,
default: None,
}
}
pub fn with_constraint(mut self, constraint: TypeDef) -> Self {
self.constraint = Some(Box::new(constraint));
self
}
pub fn with_default(mut self, default: TypeDef) -> Self {
self.default = Some(Box::new(default));
self
}
pub fn render(&self) -> String {
let mut result = self.name.clone();
if let Some(ref constraint) = self.constraint {
result.push_str(" extends ");
result.push_str(&constraint.render());
}
if let Some(ref default) = self.default {
result.push_str(" = ");
result.push_str(&default.render());
}
result
}
}
impl Field {
pub fn new(name: impl Into<String>, ty: TypeDef) -> Self {
Self {
name: name.into(),
ty,
optional: false,
readonly: false,
}
}
pub fn optional(name: impl Into<String>, ty: TypeDef) -> Self {
Self {
name: name.into(),
ty,
optional: true,
readonly: false,
}
}
pub fn readonly(mut self) -> Self {
self.readonly = true;
self
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Literal {
String(String),
Number(f64),
Boolean(bool),
}
impl TypeDef {
pub fn render(&self) -> String {
match self {
TypeDef::Primitive(p) => p.render().to_string(),
TypeDef::Array(inner) => {
let inner_str = inner.render();
if matches!(inner.as_ref(), TypeDef::Union(_)) {
format!("({})[]", inner_str)
} else {
format!("{}[]", inner_str)
}
}
TypeDef::Tuple(items) => {
let items_str: Vec<_> = items.iter().map(|t| t.render()).collect();
format!("[{}]", items_str.join(", "))
}
TypeDef::Object(fields) => {
if fields.is_empty() {
"{}".to_string()
} else {
let fields_str: Vec<_> = fields
.iter()
.map(|f| {
let readonly = if f.readonly { "readonly " } else { "" };
let opt = if f.optional { "?" } else { "" };
format!("{}{}{}: {}", readonly, f.name, opt, f.ty.render())
})
.collect();
format!("{{ {} }}", fields_str.join("; "))
}
}
TypeDef::Union(variants) => {
let variants_str: Vec<_> = variants.iter().map(|t| t.render()).collect();
variants_str.join(" | ")
}
TypeDef::Intersection(types) => {
let types_str: Vec<_> = types.iter().map(|t| t.render()).collect();
types_str.join(" & ")
}
TypeDef::Record { key, value } => {
format!("Record<{}, {}>", key.render(), value.render())
}
TypeDef::Named { namespace, name, .. } => {
if namespace.is_empty() {
name.clone()
} else {
format!("{}.{}", namespace.join("."), name)
}
}
TypeDef::Ref(name) => name.clone(),
TypeDef::Literal(lit) => lit.render(),
TypeDef::Function {
params,
return_type,
} => {
let params_str: Vec<_> = params
.iter()
.map(|p| format!("{}: {}", p.name, p.ty.render()))
.collect();
format!("({}) => {}", params_str.join(", "), return_type.render())
}
TypeDef::Generic { base, args } => {
let args_str: Vec<_> = args.iter().map(|t| t.render()).collect();
format!("{}<{}>", base, args_str.join(", "))
}
TypeDef::IndexedAccess { base, key } => {
format!("{}[\"{}\"]", base, key.replace('\\', "\\\\").replace('"', "\\\""))
}
TypeDef::TemplateLiteral { strings, types } => {
let mut result = String::from("`");
for (i, s) in strings.iter().enumerate() {
let escaped = s.replace('\\', "\\\\").replace('`', "\\`");
result.push_str(&escaped);
if i < types.len() {
result.push_str("${");
result.push_str(&types[i].render());
result.push('}');
}
}
result.push('`');
result
}
TypeDef::GenericDef { name, .. } => {
name.clone()
}
TypeDef::TypeParamRef(name) => {
name.clone()
}
}
}
pub fn render_declaration(&self) -> String {
match self {
TypeDef::Named { namespace, name, def, wrapper, .. } => {
let def_rendered = def.render();
let wrapped = match wrapper {
Some(w) => Self::apply_wrapper(w, &def_rendered),
None => def_rendered,
};
let inner = format!("type {} = {};", name, wrapped);
Self::wrap_in_namespace(namespace, &inner)
}
TypeDef::GenericDef {
name,
type_params,
def,
} => {
let params_str: Vec<_> = type_params.iter().map(|p| p.render()).collect();
format!("type {}<{}> = {};", name, params_str.join(", "), def.render())
}
_ => self.render(),
}
}
fn apply_wrapper(wrapper: &str, definition: &str) -> String {
let open_count = wrapper.chars().filter(|c| *c == '<').count();
let close_count = wrapper.chars().filter(|c| *c == '>').count();
let needed_closes = open_count.saturating_sub(close_count);
if open_count == 0 {
format!("{}<{}>", wrapper, definition)
} else {
format!("{}{}{}", wrapper, definition, ">".repeat(needed_closes))
}
}
fn wrap_in_namespace(namespace: &[String], inner: &str) -> String {
if namespace.is_empty() {
return inner.to_string();
}
let mut result = String::new();
let indent = " ";
for (i, ns) in namespace.iter().enumerate() {
for _ in 0..i {
result.push_str(indent);
}
result.push_str("namespace ");
result.push_str(ns);
result.push_str(" {\n");
}
let depth = namespace.len();
for _ in 0..depth {
result.push_str(indent);
}
result.push_str(inner);
result.push('\n');
for i in (0..depth).rev() {
for _ in 0..i {
result.push_str(indent);
}
result.push('}');
if i > 0 {
result.push('\n');
}
}
result
}
}
impl Primitive {
pub fn render(&self) -> &'static str {
match self {
Primitive::String => "string",
Primitive::Number => "number",
Primitive::Boolean => "boolean",
Primitive::Null => "null",
Primitive::Undefined => "undefined",
Primitive::Void => "void",
Primitive::Never => "never",
Primitive::Any => "any",
Primitive::Unknown => "unknown",
Primitive::BigInt => "bigint",
}
}
}
impl Literal {
pub fn render(&self) -> String {
match self {
Literal::String(s) => format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\"")),
Literal::Number(n) => {
if n.fract() == 0.0 {
format!("{}", *n as i64)
} else {
format!("{}", n)
}
}
Literal::Boolean(b) => b.to_string(),
}
}
}
pub fn extract_object_fields(typedef: &TypeDef) -> Vec<Field> {
match typedef {
TypeDef::Object(fields) => fields.clone(),
TypeDef::Named { def, .. } => extract_object_fields(def),
other => panic!(
"#[ts(flatten)] can only be used on fields with object types, got: {:?}",
other
),
}
}
pub fn inline_typedef(typedef: TypeDef) -> TypeDef {
match typedef {
TypeDef::Named { def, .. } => *def,
other => other,
}
}
use std::collections::{HashSet, VecDeque};
#[derive(Debug, Default)]
pub struct TypeRegistry {
types: HashMap<String, TypeDef>,
registration_order: Vec<String>,
}
impl TypeRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn from_distributed() -> Self {
let mut registry = Self::new();
for type_fn in TYPESCRIPT_TYPES {
let typedef = type_fn();
registry.add_typedef(typedef);
}
registry
}
pub fn collect_all(&mut self) {
for type_fn in TYPESCRIPT_TYPES {
let typedef = type_fn();
self.add_typedef(typedef);
}
}
pub fn register<T: TS>(&mut self) {
let typedef = T::typescript();
self.add_typedef(typedef);
}
pub fn add_typedef(&mut self, typedef: TypeDef) {
self.extract_named_types(&typedef);
}
fn extract_named_types(&mut self, typedef: &TypeDef) {
match typedef {
TypeDef::Named { namespace, name, def, .. } => {
let qualified_name = if namespace.is_empty() {
name.clone()
} else {
format!("{}.{}", namespace.join("."), name)
};
if !self.types.contains_key(&qualified_name) {
self.types.insert(qualified_name.clone(), typedef.clone());
self.registration_order.push(qualified_name);
self.extract_named_types(def);
}
}
TypeDef::Array(inner) => self.extract_named_types(inner),
TypeDef::Tuple(items) => {
for item in items {
self.extract_named_types(item);
}
}
TypeDef::Object(fields) => {
for field in fields {
self.extract_named_types(&field.ty);
}
}
TypeDef::Union(items) | TypeDef::Intersection(items) => {
for item in items {
self.extract_named_types(item);
}
}
TypeDef::Record { key, value } => {
self.extract_named_types(key);
self.extract_named_types(value);
}
TypeDef::Function { params, return_type } => {
for param in params {
self.extract_named_types(¶m.ty);
}
self.extract_named_types(return_type);
}
TypeDef::Generic { args, .. } => {
for arg in args {
self.extract_named_types(arg);
}
}
TypeDef::TemplateLiteral { types, .. } => {
for ty in types {
self.extract_named_types(ty);
}
}
TypeDef::GenericDef { name, type_params, def } => {
if !self.types.contains_key(name) {
self.types.insert(name.clone(), typedef.clone());
self.registration_order.push(name.clone());
for param in type_params {
if let Some(ref constraint) = param.constraint {
self.extract_named_types(constraint);
}
if let Some(ref default) = param.default {
self.extract_named_types(default);
}
}
self.extract_named_types(def);
}
}
TypeDef::Primitive(_) | TypeDef::Ref(_) | TypeDef::Literal(_) | TypeDef::IndexedAccess { .. } | TypeDef::TypeParamRef(_) => {}
}
}
pub fn len(&self) -> usize {
self.types.len()
}
pub fn is_empty(&self) -> bool {
self.types.is_empty()
}
pub fn type_names(&self) -> impl Iterator<Item = &str> {
self.types.keys().map(|s| s.as_str())
}
pub fn get(&self, name: &str) -> Option<&TypeDef> {
self.types.get(name)
}
fn get_dependencies(&self, typedef: &TypeDef) -> HashSet<String> {
let mut deps = HashSet::new();
self.collect_dependencies(typedef, &mut deps);
deps
}
fn collect_dependencies(&self, typedef: &TypeDef, deps: &mut HashSet<String>) {
match typedef {
TypeDef::Named { def, .. } => {
self.collect_dependencies(def, deps);
}
TypeDef::Ref(name) => {
if self.types.contains_key(name) {
deps.insert(name.clone());
}
}
TypeDef::Array(inner) => self.collect_dependencies(inner, deps),
TypeDef::Tuple(items) => {
for item in items {
self.collect_dependencies(item, deps);
}
}
TypeDef::Object(fields) => {
for field in fields {
self.collect_dependencies(&field.ty, deps);
}
}
TypeDef::Union(variants) => {
for v in variants {
self.collect_dependencies(v, deps);
}
}
TypeDef::Intersection(types) => {
for t in types {
self.collect_dependencies(t, deps);
}
}
TypeDef::Record { key, value } => {
self.collect_dependencies(key, deps);
self.collect_dependencies(value, deps);
}
TypeDef::Function { params, return_type } => {
for param in params {
self.collect_dependencies(¶m.ty, deps);
}
self.collect_dependencies(return_type, deps);
}
TypeDef::Generic { base, args } => {
if self.types.contains_key(base) {
deps.insert(base.clone());
}
for arg in args {
self.collect_dependencies(arg, deps);
}
}
TypeDef::TemplateLiteral { types, .. } => {
for ty in types {
self.collect_dependencies(ty, deps);
}
}
TypeDef::IndexedAccess { base, .. } => {
if self.types.contains_key(base) {
deps.insert(base.clone());
}
}
TypeDef::GenericDef { type_params, def, .. } => {
for param in type_params {
if let Some(ref constraint) = param.constraint {
self.collect_dependencies(constraint, deps);
}
if let Some(ref default) = param.default {
self.collect_dependencies(default, deps);
}
}
self.collect_dependencies(def, deps);
}
TypeDef::Primitive(_) | TypeDef::Literal(_) | TypeDef::TypeParamRef(_) => {}
}
}
pub fn sorted_types(&self) -> Vec<&str> {
let mut in_degree: HashMap<&str, usize> = HashMap::new();
let mut dependents: HashMap<&str, Vec<&str>> = HashMap::new();
for name in self.types.keys() {
in_degree.insert(name.as_str(), 0);
dependents.insert(name.as_str(), Vec::new());
}
for (name, typedef) in &self.types {
let deps = self.get_dependencies(typedef);
for dep in deps {
if let Some(dep_name) = self.types.get_key_value(&dep) {
*in_degree.get_mut(name.as_str()).unwrap() += 1;
dependents.get_mut(dep_name.0.as_str()).unwrap().push(name.as_str());
}
}
}
let mut queue: VecDeque<&str> = VecDeque::new();
let mut result: Vec<&str> = Vec::new();
for (name, °ree) in &in_degree {
if degree == 0 {
queue.push_back(name);
}
}
let mut initial: Vec<_> = queue.drain(..).collect();
initial.sort_by_key(|name| {
self.registration_order.iter().position(|n| n == *name).unwrap_or(usize::MAX)
});
queue.extend(initial);
while let Some(name) = queue.pop_front() {
result.push(name);
let mut deps: Vec<_> = dependents.get(name).map(|v| v.as_slice()).unwrap_or(&[]).to_vec();
deps.sort_by_key(|n| {
self.registration_order.iter().position(|name| name == *n).unwrap_or(usize::MAX)
});
for dependent in deps {
let degree = in_degree.get_mut(dependent).unwrap();
*degree -= 1;
if *degree == 0 {
queue.push_back(dependent);
}
}
}
if result.len() < self.types.len() {
for name in &self.registration_order {
if !result.contains(&name.as_str()) {
result.push(name.as_str());
}
}
}
result
}
pub fn render(&self) -> String {
let sorted = self.sorted_types();
let mut output = String::new();
output.push_str("// Generated by ferrotype\n");
output.push_str("// Do not edit manually\n\n");
for name in sorted {
if let Some(typedef) = self.types.get(name) {
output.push_str(&typedef.render_declaration());
output.push_str("\n\n");
}
}
output.trim_end().to_string() + "\n"
}
pub fn render_exported(&self) -> String {
let sorted = self.sorted_types();
let mut output = String::new();
output.push_str("// Generated by ferrotype\n");
output.push_str("// Do not edit manually\n\n");
for name in sorted {
if let Some(typedef) = self.types.get(name) {
match typedef {
TypeDef::Named { namespace, name, def, wrapper, .. } => {
let def_rendered = def.render();
let wrapped = match wrapper {
Some(w) => TypeDef::apply_wrapper(w, &def_rendered),
None => def_rendered,
};
let inner = format!("export type {} = {};", name, wrapped);
if namespace.is_empty() {
output.push_str(&inner);
} else {
output.push_str(&Self::wrap_in_export_namespace(namespace, &inner));
}
output.push_str("\n\n");
}
TypeDef::GenericDef { name, type_params, def } => {
let params_str: Vec<_> = type_params.iter().map(|p| p.render()).collect();
output.push_str(&format!(
"export type {}<{}> = {};\n\n",
name,
params_str.join(", "),
def.render()
));
}
_ => {}
}
}
}
output.trim_end().to_string() + "\n"
}
fn wrap_in_export_namespace(namespace: &[String], inner: &str) -> String {
if namespace.is_empty() {
return inner.to_string();
}
let mut result = String::new();
let indent = " ";
for (i, ns) in namespace.iter().enumerate() {
for _ in 0..i {
result.push_str(indent);
}
result.push_str("export namespace ");
result.push_str(ns);
result.push_str(" {\n");
}
let depth = namespace.len();
for _ in 0..depth {
result.push_str(indent);
}
result.push_str(inner);
result.push('\n');
for i in (0..depth).rev() {
for _ in 0..i {
result.push_str(indent);
}
result.push('}');
if i > 0 {
result.push('\n');
}
}
result
}
pub fn clear(&mut self) {
self.types.clear();
self.registration_order.clear();
}
}
impl TS for () {
fn typescript() -> TypeDef {
TypeDef::Primitive(Primitive::Void)
}
}
impl TS for bool {
fn typescript() -> TypeDef {
TypeDef::Primitive(Primitive::Boolean)
}
}
impl TS for String {
fn typescript() -> TypeDef {
TypeDef::Primitive(Primitive::String)
}
}
impl TS for &str {
fn typescript() -> TypeDef {
TypeDef::Primitive(Primitive::String)
}
}
impl TS for char {
fn typescript() -> TypeDef {
TypeDef::Primitive(Primitive::String)
}
}
macro_rules! impl_ts_number {
($($t:ty),*) => {
$(
impl TS for $t {
fn typescript() -> TypeDef {
TypeDef::Primitive(Primitive::Number)
}
}
)*
};
}
impl_ts_number!(i8, i16, i32, i64, isize, u8, u16, u32, u64, usize, f32, f64);
impl TS for i128 {
fn typescript() -> TypeDef {
TypeDef::Primitive(Primitive::BigInt)
}
}
impl TS for u128 {
fn typescript() -> TypeDef {
TypeDef::Primitive(Primitive::BigInt)
}
}
impl<T: TS> TS for Option<T> {
fn typescript() -> TypeDef {
TypeDef::Union(vec![T::typescript(), TypeDef::Primitive(Primitive::Null)])
}
}
impl<T: TS> TS for Vec<T> {
fn typescript() -> TypeDef {
TypeDef::Array(Box::new(T::typescript()))
}
}
impl<T: TS> TS for Box<T> {
fn typescript() -> TypeDef {
T::typescript()
}
}
impl<T: TS> TS for std::rc::Rc<T> {
fn typescript() -> TypeDef {
T::typescript()
}
}
impl<T: TS> TS for std::sync::Arc<T> {
fn typescript() -> TypeDef {
T::typescript()
}
}
impl<T: TS> TS for std::cell::RefCell<T> {
fn typescript() -> TypeDef {
T::typescript()
}
}
impl<T: TS> TS for std::cell::Cell<T> {
fn typescript() -> TypeDef {
T::typescript()
}
}
impl<K: TS, V: TS> TS for HashMap<K, V> {
fn typescript() -> TypeDef {
TypeDef::Record {
key: Box::new(K::typescript()),
value: Box::new(V::typescript()),
}
}
}
impl<K: TS, V: TS> TS for std::collections::BTreeMap<K, V> {
fn typescript() -> TypeDef {
TypeDef::Record {
key: Box::new(K::typescript()),
value: Box::new(V::typescript()),
}
}
}
impl<T: TS, E: TS> TS for Result<T, E> {
fn typescript() -> TypeDef {
TypeDef::Union(vec![
TypeDef::Object(vec![
Field::new("ok", TypeDef::Literal(Literal::Boolean(true))),
Field::new("value", T::typescript()),
]),
TypeDef::Object(vec![
Field::new("ok", TypeDef::Literal(Literal::Boolean(false))),
Field::new("error", E::typescript()),
]),
])
}
}
impl<A: TS> TS for (A,) {
fn typescript() -> TypeDef {
TypeDef::Tuple(vec![A::typescript()])
}
}
impl<A: TS, B: TS> TS for (A, B) {
fn typescript() -> TypeDef {
TypeDef::Tuple(vec![A::typescript(), B::typescript()])
}
}
impl<A: TS, B: TS, C: TS> TS for (A, B, C) {
fn typescript() -> TypeDef {
TypeDef::Tuple(vec![A::typescript(), B::typescript(), C::typescript()])
}
}
impl<A: TS, B: TS, C: TS, D: TS> TS for (A, B, C, D) {
fn typescript() -> TypeDef {
TypeDef::Tuple(vec![
A::typescript(),
B::typescript(),
C::typescript(),
D::typescript(),
])
}
}
impl<A: TS, B: TS, C: TS, D: TS, E: TS> TS
for (A, B, C, D, E)
{
fn typescript() -> TypeDef {
TypeDef::Tuple(vec![
A::typescript(),
B::typescript(),
C::typescript(),
D::typescript(),
E::typescript(),
])
}
}
impl<A: TS, B: TS, C: TS, D: TS, E: TS, F: TS>
TS for (A, B, C, D, E, F)
{
fn typescript() -> TypeDef {
TypeDef::Tuple(vec![
A::typescript(),
B::typescript(),
C::typescript(),
D::typescript(),
E::typescript(),
F::typescript(),
])
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_typedef_primitive_render() {
assert_eq!(TypeDef::Primitive(Primitive::String).render(), "string");
assert_eq!(TypeDef::Primitive(Primitive::Number).render(), "number");
assert_eq!(TypeDef::Primitive(Primitive::Boolean).render(), "boolean");
assert_eq!(TypeDef::Primitive(Primitive::Null).render(), "null");
assert_eq!(TypeDef::Primitive(Primitive::Undefined).render(), "undefined");
assert_eq!(TypeDef::Primitive(Primitive::Void).render(), "void");
assert_eq!(TypeDef::Primitive(Primitive::Never).render(), "never");
assert_eq!(TypeDef::Primitive(Primitive::Any).render(), "any");
assert_eq!(TypeDef::Primitive(Primitive::Unknown).render(), "unknown");
assert_eq!(TypeDef::Primitive(Primitive::BigInt).render(), "bigint");
}
#[test]
fn test_typedef_array_render() {
let arr = TypeDef::Array(Box::new(TypeDef::Primitive(Primitive::String)));
assert_eq!(arr.render(), "string[]");
let union_arr = TypeDef::Array(Box::new(TypeDef::Union(vec![
TypeDef::Primitive(Primitive::String),
TypeDef::Primitive(Primitive::Number),
])));
assert_eq!(union_arr.render(), "(string | number)[]");
}
#[test]
fn test_typedef_tuple_render() {
let tuple = TypeDef::Tuple(vec![
TypeDef::Primitive(Primitive::String),
TypeDef::Primitive(Primitive::Number),
]);
assert_eq!(tuple.render(), "[string, number]");
}
#[test]
fn test_typedef_object_render() {
let obj = TypeDef::Object(vec![
Field::new("name", TypeDef::Primitive(Primitive::String)),
Field::optional("age", TypeDef::Primitive(Primitive::Number)),
]);
assert_eq!(obj.render(), "{ name: string; age?: number }");
let empty_obj = TypeDef::Object(vec![]);
assert_eq!(empty_obj.render(), "{}");
}
#[test]
fn test_typedef_object_readonly_field() {
let obj = TypeDef::Object(vec![
Field::new("id", TypeDef::Primitive(Primitive::String)).readonly(),
]);
assert_eq!(obj.render(), "{ readonly id: string }");
}
#[test]
fn test_typedef_union_render() {
let union = TypeDef::Union(vec![
TypeDef::Primitive(Primitive::String),
TypeDef::Primitive(Primitive::Number),
TypeDef::Primitive(Primitive::Null),
]);
assert_eq!(union.render(), "string | number | null");
}
#[test]
fn test_typedef_intersection_render() {
let intersection = TypeDef::Intersection(vec![
TypeDef::Ref("Base".into()),
TypeDef::Object(vec![
Field::new("extra", TypeDef::Primitive(Primitive::String)),
]),
]);
assert_eq!(intersection.render(), "Base & { extra: string }");
}
#[test]
fn test_typedef_record_render() {
let record = TypeDef::Record {
key: Box::new(TypeDef::Primitive(Primitive::String)),
value: Box::new(TypeDef::Primitive(Primitive::Number)),
};
assert_eq!(record.render(), "Record<string, number>");
}
#[test]
fn test_typedef_named_render() {
let named = TypeDef::Named {
namespace: vec![],
name: "UserId".into(),
def: Box::new(TypeDef::Primitive(Primitive::String)),
module: None,
wrapper: None,
};
assert_eq!(named.render(), "UserId");
assert_eq!(named.render_declaration(), "type UserId = string;");
}
#[test]
fn test_typedef_namespaced_render() {
let namespaced = TypeDef::Named {
namespace: vec!["VM".into(), "Git".into()],
name: "State".into(),
def: Box::new(TypeDef::Union(vec![
TypeDef::Literal(Literal::String("clean".into())),
TypeDef::Literal(Literal::String("dirty".into())),
TypeDef::Literal(Literal::String("unknown".into())),
])),
module: None,
wrapper: None,
};
assert_eq!(namespaced.render(), "VM.Git.State");
let decl = namespaced.render_declaration();
assert!(decl.contains("namespace VM {"));
assert!(decl.contains("namespace Git {"));
assert!(decl.contains(r#"type State = "clean" | "dirty" | "unknown";"#));
}
#[test]
fn test_typedef_single_namespace_render() {
let namespaced = TypeDef::Named {
namespace: vec!["API".into()],
name: "Response".into(),
def: Box::new(TypeDef::Primitive(Primitive::String)),
module: None,
wrapper: None,
};
assert_eq!(namespaced.render(), "API.Response");
let decl = namespaced.render_declaration();
assert!(decl.contains("namespace API {"));
assert!(decl.contains("type Response = string;"));
}
#[test]
fn test_typedef_wrapper_simple() {
let wrapped = TypeDef::Named {
namespace: vec![],
name: "User".into(),
def: Box::new(TypeDef::Object(vec![
Field::new("id", TypeDef::Primitive(Primitive::String)),
Field::new("name", TypeDef::Primitive(Primitive::String)),
])),
module: None,
wrapper: Some("Prettify".to_string()),
};
let decl = wrapped.render_declaration();
assert_eq!(decl, "type User = Prettify<{ id: string; name: string }>;");
}
#[test]
fn test_typedef_wrapper_chained() {
let wrapped = TypeDef::Named {
namespace: vec![],
name: "Config".into(),
def: Box::new(TypeDef::Object(vec![
Field::new("theme", TypeDef::Primitive(Primitive::String)),
])),
module: None,
wrapper: Some("Prettify<Required<".to_string()),
};
let decl = wrapped.render_declaration();
assert_eq!(decl, "type Config = Prettify<Required<{ theme: string }>>;");
}
#[test]
fn test_typedef_wrapper_with_namespace() {
let wrapped = TypeDef::Named {
namespace: vec!["API".into()],
name: "Response".into(),
def: Box::new(TypeDef::Primitive(Primitive::String)),
module: None,
wrapper: Some("Prettify".to_string()),
};
let decl = wrapped.render_declaration();
assert!(decl.contains("namespace API {"));
assert!(decl.contains("type Response = Prettify<string>;"));
}
#[test]
fn test_apply_wrapper_simple() {
let result = TypeDef::apply_wrapper("Prettify", "{ id: string }");
assert_eq!(result, "Prettify<{ id: string }>");
}
#[test]
fn test_apply_wrapper_chained() {
let result = TypeDef::apply_wrapper("Prettify<Required<", "{ id: string }");
assert_eq!(result, "Prettify<Required<{ id: string }>>");
}
#[test]
fn test_apply_wrapper_triple_chained() {
let result = TypeDef::apply_wrapper("A<B<C<", "T");
assert_eq!(result, "A<B<C<T>>>");
}
#[test]
fn test_typedef_ref_render() {
let ref_type = TypeDef::Ref("User".into());
assert_eq!(ref_type.render(), "User");
}
#[test]
fn test_typedef_literal_render() {
assert_eq!(TypeDef::Literal(Literal::String("foo".into())).render(), "\"foo\"");
assert_eq!(TypeDef::Literal(Literal::Number(42.0)).render(), "42");
assert_eq!(TypeDef::Literal(Literal::Number(3.14)).render(), "3.14");
assert_eq!(TypeDef::Literal(Literal::Boolean(true)).render(), "true");
assert_eq!(TypeDef::Literal(Literal::Boolean(false)).render(), "false");
}
#[test]
fn test_typedef_literal_escaping() {
let lit = Literal::String("say \"hello\"".into());
assert_eq!(lit.render(), "\"say \\\"hello\\\"\"");
}
#[test]
fn test_typedef_function_render() {
let func = TypeDef::Function {
params: vec![
Field::new("name", TypeDef::Primitive(Primitive::String)),
Field::new("age", TypeDef::Primitive(Primitive::Number)),
],
return_type: Box::new(TypeDef::Primitive(Primitive::Void)),
};
assert_eq!(func.render(), "(name: string, age: number) => void");
}
#[test]
fn test_typedef_generic_render() {
let generic = TypeDef::Generic {
base: "Promise".into(),
args: vec![TypeDef::Primitive(Primitive::String)],
};
assert_eq!(generic.render(), "Promise<string>");
let multi_generic = TypeDef::Generic {
base: "Map".into(),
args: vec![
TypeDef::Primitive(Primitive::String),
TypeDef::Primitive(Primitive::Number),
],
};
assert_eq!(multi_generic.render(), "Map<string, number>");
}
#[test]
fn test_typedef_template_literal_render() {
let vm_id = TypeDef::TemplateLiteral {
strings: vec!["vm-".into(), "".into()],
types: vec![Box::new(TypeDef::Primitive(Primitive::String))],
};
assert_eq!(vm_id.render(), "`vm-${string}`");
let semver = TypeDef::TemplateLiteral {
strings: vec!["v".into(), ".".into(), ".".into(), "".into()],
types: vec![
Box::new(TypeDef::Primitive(Primitive::Number)),
Box::new(TypeDef::Primitive(Primitive::Number)),
Box::new(TypeDef::Primitive(Primitive::Number)),
],
};
assert_eq!(semver.render(), "`v${number}.${number}.${number}`");
let api_route = TypeDef::TemplateLiteral {
strings: vec!["/api/".into(), "".into()],
types: vec![Box::new(TypeDef::Primitive(Primitive::String))],
};
assert_eq!(api_route.render(), "`/api/${string}`");
let complex_id = TypeDef::TemplateLiteral {
strings: vec!["user-".into(), "-id".into()],
types: vec![Box::new(TypeDef::Primitive(Primitive::String))],
};
assert_eq!(complex_id.render(), "`user-${string}-id`");
let static_str = TypeDef::TemplateLiteral {
strings: vec!["static-value".into()],
types: vec![],
};
assert_eq!(static_str.render(), "`static-value`");
}
#[test]
fn test_typedef_template_literal_escaping() {
let with_backtick = TypeDef::TemplateLiteral {
strings: vec!["code: `".into(), "`".into()],
types: vec![Box::new(TypeDef::Primitive(Primitive::String))],
};
assert_eq!(with_backtick.render(), "`code: \\`${string}\\``");
let with_backslash = TypeDef::TemplateLiteral {
strings: vec!["path\\to\\".into(), "".into()],
types: vec![Box::new(TypeDef::Primitive(Primitive::String))],
};
assert_eq!(with_backslash.render(), "`path\\\\to\\\\${string}`");
}
#[test]
fn test_typedef_template_literal_with_refs() {
let typed_id = TypeDef::TemplateLiteral {
strings: vec!["vm-".into(), "".into()],
types: vec![Box::new(TypeDef::Ref("VmIdSuffix".into()))],
};
assert_eq!(typed_id.render(), "`vm-${VmIdSuffix}`");
}
#[test]
fn test_typedef_indexed_access_render() {
let indexed = TypeDef::IndexedAccess {
base: "Profile".into(),
key: "login".into(),
};
assert_eq!(indexed.render(), "Profile[\"login\"]");
let nested = TypeDef::IndexedAccess {
base: "User.Settings".into(),
key: "theme".into(),
};
assert_eq!(nested.render(), "User.Settings[\"theme\"]");
}
#[test]
fn test_typedef_indexed_access_escaping() {
let with_quotes = TypeDef::IndexedAccess {
base: "Config".into(),
key: "key\"with\"quotes".into(),
};
assert_eq!(with_quotes.render(), "Config[\"key\\\"with\\\"quotes\"]");
let with_backslash = TypeDef::IndexedAccess {
base: "Config".into(),
key: "path\\to\\key".into(),
};
assert_eq!(with_backslash.render(), "Config[\"path\\\\to\\\\key\"]");
}
#[test]
fn test_typescript_trait_primitives() {
assert_eq!(<()>::typescript().render(), "void");
assert_eq!(bool::typescript().render(), "boolean");
assert_eq!(String::typescript().render(), "string");
assert_eq!(i32::typescript().render(), "number");
assert_eq!(f64::typescript().render(), "number");
assert_eq!(i128::typescript().render(), "bigint");
assert_eq!(u128::typescript().render(), "bigint");
}
#[test]
fn test_typescript_trait_option() {
let opt = <Option<String>>::typescript();
assert_eq!(opt.render(), "string | null");
}
#[test]
fn test_typescript_trait_vec() {
let vec_type = <Vec<i32>>::typescript();
assert_eq!(vec_type.render(), "number[]");
}
#[test]
fn test_typescript_trait_hashmap() {
let map = <HashMap<String, i32>>::typescript();
assert_eq!(map.render(), "Record<string, number>");
}
#[test]
fn test_typescript_trait_result() {
let result = <Result<String, String>>::typescript();
assert_eq!(
result.render(),
"{ ok: true; value: string } | { ok: false; error: string }"
);
}
#[test]
fn test_typescript_trait_tuples() {
assert_eq!(<(String,)>::typescript().render(), "[string]");
assert_eq!(<(String, i32)>::typescript().render(), "[string, number]");
assert_eq!(
<(String, i32, bool)>::typescript().render(),
"[string, number, boolean]"
);
}
#[test]
fn test_typescript_trait_box() {
assert_eq!(<Box<String>>::typescript().render(), "string");
}
#[test]
fn test_typedef_equality() {
let a = TypeDef::Primitive(Primitive::String);
let b = TypeDef::Primitive(Primitive::String);
let c = TypeDef::Primitive(Primitive::Number);
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn test_field_builder() {
let field = Field::new("name", TypeDef::Primitive(Primitive::String));
assert!(!field.optional);
assert!(!field.readonly);
let opt_field = Field::optional("name", TypeDef::Primitive(Primitive::String));
assert!(opt_field.optional);
let readonly_field = Field::new("id", TypeDef::Primitive(Primitive::String)).readonly();
assert!(readonly_field.readonly);
}
#[test]
fn test_registry_new() {
let registry = TypeRegistry::new();
assert!(registry.is_empty());
assert_eq!(registry.len(), 0);
}
#[test]
fn test_registry_add_typedef() {
let mut registry = TypeRegistry::new();
let user_type = TypeDef::Named {
namespace: vec![],
name: "User".to_string(),
def: Box::new(TypeDef::Object(vec![
Field::new("id", TypeDef::Primitive(Primitive::String)),
Field::new("name", TypeDef::Primitive(Primitive::String)),
])),
module: None,
wrapper: None,
};
registry.add_typedef(user_type);
assert_eq!(registry.len(), 1);
assert!(registry.get("User").is_some());
}
#[test]
fn test_registry_deduplication() {
let mut registry = TypeRegistry::new();
let user_type = TypeDef::Named {
namespace: vec![],
name: "User".to_string(),
def: Box::new(TypeDef::Primitive(Primitive::String)),
module: None,
wrapper: None,
};
registry.add_typedef(user_type.clone());
registry.add_typedef(user_type);
assert_eq!(registry.len(), 1);
}
#[test]
fn test_registry_extracts_nested_types() {
let mut registry = TypeRegistry::new();
let user_id = TypeDef::Named {
namespace: vec![],
name: "UserId".to_string(),
def: Box::new(TypeDef::Primitive(Primitive::String)),
module: None,
wrapper: None,
};
let user = TypeDef::Named {
namespace: vec![],
name: "User".to_string(),
def: Box::new(TypeDef::Object(vec![
Field::new("id", TypeDef::Ref("UserId".to_string())),
Field::new("name", TypeDef::Primitive(Primitive::String)),
])),
module: None,
wrapper: None,
};
let post_type = TypeDef::Named {
namespace: vec![],
name: "Post".to_string(),
def: Box::new(TypeDef::Object(vec![
Field::new("title", TypeDef::Primitive(Primitive::String)),
Field::new("author", user),
])),
module: None,
wrapper: None,
};
registry.add_typedef(post_type);
registry.add_typedef(user_id);
assert_eq!(registry.len(), 3);
assert!(registry.get("Post").is_some());
assert!(registry.get("User").is_some());
assert!(registry.get("UserId").is_some());
}
#[test]
fn test_registry_render() {
let mut registry = TypeRegistry::new();
let user_type = TypeDef::Named {
namespace: vec![],
name: "User".to_string(),
def: Box::new(TypeDef::Object(vec![
Field::new("id", TypeDef::Primitive(Primitive::String)),
Field::new("name", TypeDef::Primitive(Primitive::String)),
])),
module: None,
wrapper: None,
};
registry.add_typedef(user_type);
let output = registry.render();
assert!(output.contains("// Generated by ferrotype"));
assert!(output.contains("type User = { id: string; name: string };"));
}
#[test]
fn test_registry_render_exported() {
let mut registry = TypeRegistry::new();
let user_type = TypeDef::Named {
namespace: vec![],
name: "User".to_string(),
def: Box::new(TypeDef::Primitive(Primitive::String)),
module: None,
wrapper: None,
};
registry.add_typedef(user_type);
let output = registry.render_exported();
assert!(output.contains("export type User = string;"));
}
#[test]
fn test_registry_dependency_order() {
let mut registry = TypeRegistry::new();
let user_id = TypeDef::Named {
namespace: vec![],
name: "UserId".to_string(),
def: Box::new(TypeDef::Primitive(Primitive::String)),
module: None,
wrapper: None,
};
let user = TypeDef::Named {
namespace: vec![],
name: "User".to_string(),
def: Box::new(TypeDef::Object(vec![
Field::new("id", TypeDef::Ref("UserId".to_string())),
Field::new("name", TypeDef::Primitive(Primitive::String)),
])),
module: None,
wrapper: None,
};
registry.add_typedef(user);
registry.add_typedef(user_id);
let sorted = registry.sorted_types();
let user_id_pos = sorted.iter().position(|&n| n == "UserId").unwrap();
let user_pos = sorted.iter().position(|&n| n == "User").unwrap();
assert!(user_id_pos < user_pos, "UserId should come before User");
}
#[test]
fn test_registry_clear() {
let mut registry = TypeRegistry::new();
let user_type = TypeDef::Named {
namespace: vec![],
name: "User".to_string(),
def: Box::new(TypeDef::Primitive(Primitive::String)),
module: None,
wrapper: None,
};
registry.add_typedef(user_type);
assert_eq!(registry.len(), 1);
registry.clear();
assert!(registry.is_empty());
}
#[test]
fn test_registry_type_names() {
let mut registry = TypeRegistry::new();
registry.add_typedef(TypeDef::Named {
namespace: vec![],
name: "Alpha".to_string(),
def: Box::new(TypeDef::Primitive(Primitive::String)),
module: None,
wrapper: None,
});
registry.add_typedef(TypeDef::Named {
namespace: vec![],
name: "Beta".to_string(),
def: Box::new(TypeDef::Primitive(Primitive::Number)),
module: None,
wrapper: None,
});
let names: Vec<_> = registry.type_names().collect();
assert_eq!(names.len(), 2);
assert!(names.contains(&"Alpha"));
assert!(names.contains(&"Beta"));
}
#[test]
fn test_registry_complex_dependencies() {
let mut registry = TypeRegistry::new();
let c = TypeDef::Named {
namespace: vec![],
name: "C".to_string(),
def: Box::new(TypeDef::Primitive(Primitive::String)),
module: None,
wrapper: None,
};
let b = TypeDef::Named {
namespace: vec![],
name: "B".to_string(),
def: Box::new(TypeDef::Object(vec![
Field::new("c", TypeDef::Ref("C".to_string())),
])),
module: None,
wrapper: None,
};
let a = TypeDef::Named {
namespace: vec![],
name: "A".to_string(),
def: Box::new(TypeDef::Object(vec![
Field::new("b", TypeDef::Ref("B".to_string())),
])),
module: None,
wrapper: None,
};
registry.add_typedef(a);
registry.add_typedef(b);
registry.add_typedef(c);
let sorted = registry.sorted_types();
let c_pos = sorted.iter().position(|&n| n == "C").unwrap();
let b_pos = sorted.iter().position(|&n| n == "B").unwrap();
let a_pos = sorted.iter().position(|&n| n == "A").unwrap();
assert!(c_pos < b_pos, "C should come before B");
assert!(b_pos < a_pos, "B should come before A");
}
#[test]
fn test_registry_indexed_access_dependency() {
let mut registry = TypeRegistry::new();
let profile = TypeDef::Named {
namespace: vec![],
name: "Profile".to_string(),
def: Box::new(TypeDef::Object(vec![
Field::new("login", TypeDef::Primitive(Primitive::String)),
Field::new("email", TypeDef::Primitive(Primitive::String)),
])),
module: None,
wrapper: None,
};
let user_login = TypeDef::Named {
namespace: vec![],
name: "UserLogin".to_string(),
def: Box::new(TypeDef::IndexedAccess {
base: "Profile".to_string(),
key: "login".to_string(),
}),
module: None,
wrapper: None,
};
registry.add_typedef(user_login);
registry.add_typedef(profile);
let sorted = registry.sorted_types();
let profile_pos = sorted.iter().position(|&n| n == "Profile").unwrap();
let user_login_pos = sorted.iter().position(|&n| n == "UserLogin").unwrap();
assert!(profile_pos < user_login_pos, "Profile should come before UserLogin");
}
#[derive(Debug)]
struct AutoRegTestUser {
name: String,
age: u32,
}
impl TS for AutoRegTestUser {
fn typescript() -> TypeDef {
TypeDef::Named {
namespace: vec![],
name: "AutoRegTestUser".to_string(),
def: Box::new(TypeDef::Object(vec![
Field::new("name", TypeDef::Primitive(Primitive::String)),
Field::new("age", TypeDef::Primitive(Primitive::Number)),
])),
module: None,
wrapper: None,
}
}
}
#[linkme::distributed_slice(TYPESCRIPT_TYPES)]
static __TEST_REGISTER_USER: fn() -> TypeDef = || AutoRegTestUser::typescript();
#[test]
fn test_from_distributed_collects_types() {
let registry = TypeRegistry::from_distributed();
assert!(registry.get("AutoRegTestUser").is_some(),
"Registry should contain AutoRegTestUser");
}
#[test]
fn test_collect_all_adds_to_existing() {
let mut registry = TypeRegistry::new();
let manual_type = TypeDef::Named {
namespace: vec![],
name: "ManualType".to_string(),
def: Box::new(TypeDef::Primitive(Primitive::String)),
module: None,
wrapper: None,
};
registry.add_typedef(manual_type);
registry.collect_all();
assert!(registry.get("ManualType").is_some(),
"Registry should contain ManualType");
assert!(registry.get("AutoRegTestUser").is_some(),
"Registry should contain AutoRegTestUser from distributed slice");
}
#[test]
fn test_distributed_slice_is_accessible() {
let count = TYPESCRIPT_TYPES.len();
assert!(count >= 1, "TYPESCRIPT_TYPES should have at least 1 entry");
}
#[test]
fn test_type_param_simple() {
let param = TypeParam::new("T");
assert_eq!(param.render(), "T");
}
#[test]
fn test_type_param_with_constraint() {
let param = TypeParam::new("T").with_constraint(TypeDef::Primitive(Primitive::String));
assert_eq!(param.render(), "T extends string");
}
#[test]
fn test_type_param_with_object_constraint() {
let param = TypeParam::new("T").with_constraint(TypeDef::Object(vec![
Field::new("type", TypeDef::Primitive(Primitive::String)),
]));
assert_eq!(param.render(), "T extends { type: string }");
}
#[test]
fn test_type_param_with_default() {
let param = TypeParam::new("T").with_default(TypeDef::Primitive(Primitive::Never));
assert_eq!(param.render(), "T = never");
}
#[test]
fn test_type_param_with_constraint_and_default() {
let param = TypeParam::new("T")
.with_constraint(TypeDef::Primitive(Primitive::String))
.with_default(TypeDef::Literal(Literal::String("default".into())));
assert_eq!(param.render(), "T extends string = \"default\"");
}
#[test]
fn test_type_param_ref_render() {
let param_ref = TypeDef::TypeParamRef("T".into());
assert_eq!(param_ref.render(), "T");
}
#[test]
fn test_generic_def_simple() {
let generic_def = TypeDef::GenericDef {
name: "Identity".into(),
type_params: vec![TypeParam::new("T")],
def: Box::new(TypeDef::TypeParamRef("T".into())),
};
assert_eq!(generic_def.render(), "Identity");
assert_eq!(generic_def.render_declaration(), "type Identity<T> = T;");
}
#[test]
fn test_generic_def_with_constraint() {
let generic_def = TypeDef::GenericDef {
name: "Wrapper".into(),
type_params: vec![TypeParam::new("T").with_constraint(TypeDef::Object(vec![
Field::new("type", TypeDef::Primitive(Primitive::String)),
]))],
def: Box::new(TypeDef::Object(vec![
Field::new("data", TypeDef::TypeParamRef("T".into())),
])),
};
assert_eq!(
generic_def.render_declaration(),
"type Wrapper<T extends { type: string }> = { data: T };"
);
}
#[test]
fn test_generic_def_core_pattern() {
let core_def = TypeDef::GenericDef {
name: "Core".into(),
type_params: vec![TypeParam::new("T").with_constraint(TypeDef::Object(vec![
Field::new("type", TypeDef::Primitive(Primitive::String)),
]))],
def: Box::new(TypeDef::Object(vec![
Field::new("id", TypeDef::Primitive(Primitive::String)),
Field::new("timestamp", TypeDef::Primitive(Primitive::Number)),
Field::new("data", TypeDef::TypeParamRef("T".into())),
])),
};
assert_eq!(
core_def.render_declaration(),
"type Core<T extends { type: string }> = { id: string; timestamp: number; data: T };"
);
}
#[test]
fn test_generic_def_multiple_params() {
let pair_def = TypeDef::GenericDef {
name: "Pair".into(),
type_params: vec![TypeParam::new("K"), TypeParam::new("V")],
def: Box::new(TypeDef::Object(vec![
Field::new("key", TypeDef::TypeParamRef("K".into())),
Field::new("value", TypeDef::TypeParamRef("V".into())),
])),
};
assert_eq!(
pair_def.render_declaration(),
"type Pair<K, V> = { key: K; value: V };"
);
}
#[test]
fn test_generic_def_with_default_params() {
let result_def = TypeDef::GenericDef {
name: "Result".into(),
type_params: vec![
TypeParam::new("T"),
TypeParam::new("E").with_default(TypeDef::Ref("Error".into())),
],
def: Box::new(TypeDef::Union(vec![
TypeDef::Object(vec![
Field::new("ok", TypeDef::Literal(Literal::Boolean(true))),
Field::new("value", TypeDef::TypeParamRef("T".into())),
]),
TypeDef::Object(vec![
Field::new("ok", TypeDef::Literal(Literal::Boolean(false))),
Field::new("error", TypeDef::TypeParamRef("E".into())),
]),
])),
};
assert_eq!(
result_def.render_declaration(),
"type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };"
);
}
#[test]
fn test_generic_application_with_def() {
let application = TypeDef::Generic {
base: "Core".into(),
args: vec![TypeDef::Object(vec![
Field::new("type", TypeDef::Literal(Literal::String("text".into()))),
Field::new("content", TypeDef::Primitive(Primitive::String)),
])],
};
assert_eq!(
application.render(),
"Core<{ type: \"text\"; content: string }>"
);
}
#[test]
fn test_registry_with_generic_def() {
let mut registry = TypeRegistry::new();
let core_def = TypeDef::GenericDef {
name: "Core".into(),
type_params: vec![TypeParam::new("T")],
def: Box::new(TypeDef::Object(vec![
Field::new("id", TypeDef::Primitive(Primitive::String)),
Field::new("data", TypeDef::TypeParamRef("T".into())),
])),
};
registry.add_typedef(core_def);
assert_eq!(registry.len(), 1);
assert!(registry.get("Core").is_some());
let output = registry.render();
assert!(output.contains("type Core<T> = { id: string; data: T };"));
}
#[test]
fn test_registry_exported_with_generic_def() {
let mut registry = TypeRegistry::new();
let core_def = TypeDef::GenericDef {
name: "Core".into(),
type_params: vec![TypeParam::new("T").with_constraint(TypeDef::Object(vec![
Field::new("type", TypeDef::Primitive(Primitive::String)),
]))],
def: Box::new(TypeDef::Object(vec![
Field::new("data", TypeDef::TypeParamRef("T".into())),
])),
};
registry.add_typedef(core_def);
let output = registry.render_exported();
assert!(output.contains("export type Core<T extends { type: string }> = { data: T };"));
}
#[test]
fn test_registry_generic_depends_on_constraint() {
let mut registry = TypeRegistry::new();
let discriminant = TypeDef::Named {
namespace: vec![],
name: "Discriminant".into(),
def: Box::new(TypeDef::Object(vec![Field::new(
"type",
TypeDef::Primitive(Primitive::String),
)])),
module: None,
wrapper: None,
};
let core_def = TypeDef::GenericDef {
name: "Core".into(),
type_params: vec![TypeParam::new("T").with_constraint(TypeDef::Ref("Discriminant".into()))],
def: Box::new(TypeDef::Object(vec![
Field::new("data", TypeDef::TypeParamRef("T".into())),
])),
};
registry.add_typedef(core_def);
registry.add_typedef(discriminant);
let sorted = registry.sorted_types();
let discrim_pos = sorted.iter().position(|&n| n == "Discriminant").unwrap();
let core_pos = sorted.iter().position(|&n| n == "Core").unwrap();
assert!(discrim_pos < core_pos, "Discriminant should come before Core");
}
#[test]
fn test_full_core_pattern_example() {
let mut registry = TypeRegistry::new();
let core_def = TypeDef::GenericDef {
name: "Core".into(),
type_params: vec![TypeParam::new("T").with_constraint(TypeDef::Object(vec![
Field::new("type", TypeDef::Primitive(Primitive::String)),
]))],
def: Box::new(TypeDef::Object(vec![
Field::new("id", TypeDef::Primitive(Primitive::String)),
Field::new("timestamp", TypeDef::Primitive(Primitive::Number)),
Field::new("data", TypeDef::TypeParamRef("T".into())),
])),
};
let text_data = TypeDef::Named {
namespace: vec![],
name: "TextData".into(),
def: Box::new(TypeDef::Object(vec![
Field::new("type", TypeDef::Literal(Literal::String("text".into()))),
Field::new("content", TypeDef::Primitive(Primitive::String)),
])),
module: None,
wrapper: None,
};
let image_data = TypeDef::Named {
namespace: vec![],
name: "ImageData".into(),
def: Box::new(TypeDef::Object(vec![
Field::new("type", TypeDef::Literal(Literal::String("image".into()))),
Field::new("url", TypeDef::Primitive(Primitive::String)),
])),
module: None,
wrapper: None,
};
let text_message = TypeDef::Named {
namespace: vec![],
name: "TextMessage".into(),
def: Box::new(TypeDef::Generic {
base: "Core".into(),
args: vec![TypeDef::Ref("TextData".into())],
}),
module: None,
wrapper: None,
};
let image_message = TypeDef::Named {
namespace: vec![],
name: "ImageMessage".into(),
def: Box::new(TypeDef::Generic {
base: "Core".into(),
args: vec![TypeDef::Ref("ImageData".into())],
}),
module: None,
wrapper: None,
};
let message = TypeDef::Named {
namespace: vec![],
name: "Message".into(),
def: Box::new(TypeDef::Union(vec![
TypeDef::Ref("TextMessage".into()),
TypeDef::Ref("ImageMessage".into()),
])),
module: None,
wrapper: None,
};
registry.add_typedef(core_def);
registry.add_typedef(text_data);
registry.add_typedef(image_data);
registry.add_typedef(text_message);
registry.add_typedef(image_message);
registry.add_typedef(message);
let output = registry.render_exported();
assert!(output.contains("export type Core<T extends { type: string }>"));
assert!(output.contains("export type TextData ="));
assert!(output.contains("export type ImageData ="));
assert!(output.contains("export type TextMessage = Core<TextData>"));
assert!(output.contains("export type ImageMessage = Core<ImageData>"));
assert!(output.contains("export type Message = TextMessage | ImageMessage"));
}
}