use std::{borrow::Cow, cell::RefCell, fmt::Write as _, iter};
use specta::{
ResolvedTypes, Types,
datatype::{
DataType, Deprecated, Enum, Fields, GenericReference, List, Map, NamedDataType,
NamedReference, OpaqueReference, Primitive, Reference, Tuple, Variant,
},
};
use crate::{
Branded, BrandedTypeExporter, Error, Exporter, Layout,
legacy::{
ExportContext, deprecated_details, escape_jsdoc_text, escape_typescript_string_literal,
is_identifier, js_doc,
},
map_keys, opaque,
};
pub fn export<'a>(
exporter: &dyn AsRef<Exporter>,
types: &ResolvedTypes,
ndts: impl Iterator<Item = &'a NamedDataType>,
indent: &str,
) -> Result<String, Error> {
let mut s = String::new();
export_internal(&mut s, exporter.as_ref(), types.as_types(), ndts, indent)?;
Ok(s)
}
pub(crate) fn export_internal<'a>(
s: &mut String,
exporter: &Exporter,
types: &Types,
ndts: impl Iterator<Item = &'a NamedDataType>,
indent: &str,
) -> Result<(), Error> {
if exporter.jsdoc {
let mut ndts = ndts.peekable();
if ndts.peek().is_none() {
return Ok(());
}
s.push_str(indent);
s.push_str("/**\n");
for (index, ndt) in ndts.enumerate() {
if index != 0 {
s.push_str(indent);
s.push_str("\t*\n");
}
append_typedef_body(s, exporter, types, ndt, indent)?;
}
s.push_str(indent);
s.push_str("\t*/\n");
return Ok(());
}
for (index, ndt) in ndts.enumerate() {
if index != 0 {
s.push('\n');
}
export_single_internal(s, exporter, types, ndt, indent)?;
}
Ok(())
}
fn export_single_internal(
s: &mut String,
exporter: &Exporter,
types: &Types,
ndt: &NamedDataType,
indent: &str,
) -> Result<(), Error> {
if exporter.jsdoc {
let mut typedef = String::new();
typedef_internal(&mut typedef, exporter, types, ndt)?;
for line in typedef.lines() {
s.push_str(indent);
s.push_str(line);
s.push('\n');
}
return Ok(());
}
let generics = (!ndt.generics().is_empty())
.then(|| {
iter::once("<")
.chain(intersperse(
ndt.generics().iter().map(|(_, g)| g.as_ref()),
", ",
))
.chain(iter::once(">"))
})
.into_iter()
.flatten();
let name = crate::legacy::sanitise_type_name(
crate::legacy::ExportContext {
cfg: exporter,
path: vec![],
},
&match exporter.layout {
Layout::ModulePrefixedName => {
let mut s = ndt.module_path().split("::").collect::<Vec<_>>().join("_");
s.push('_');
s.push_str(ndt.name());
Cow::Owned(s)
}
_ => ndt.name().clone(),
},
)?;
let mut comments = String::new();
js_doc(&mut comments, ndt.docs(), ndt.deprecated(), !exporter.jsdoc);
if !comments.is_empty() {
for line in comments.lines() {
s.push_str(indent);
s.push_str(line);
s.push('\n');
}
}
s.push_str(indent);
s.push_str("export type ");
s.push_str(&name);
for part in generics {
s.push_str(part);
}
s.push_str(" = ");
let _generic_scope = push_generic_scope(ndt.generics());
datatype(
s,
exporter,
types,
ndt.ty(),
vec![ndt.name().clone()],
Some(ndt.name()),
indent,
Default::default(),
)?;
s.push_str(";\n");
Ok(())
}
pub fn inline(
exporter: &dyn AsRef<Exporter>,
types: &ResolvedTypes,
dt: &DataType,
) -> Result<String, Error> {
let mut s = String::new();
inline_datatype(
&mut s,
exporter.as_ref(),
types.as_types(),
dt,
vec![],
None,
"",
0,
&[],
)?;
Ok(s)
}
pub(crate) fn typedef_internal(
s: &mut String,
exporter: &Exporter,
types: &Types,
dt: &NamedDataType,
) -> Result<(), Error> {
s.push_str("/**\n");
append_typedef_body(s, exporter, types, dt, "")?;
s.push_str("\t*/");
Ok(())
}
fn append_jsdoc_properties(
s: &mut String,
exporter: &Exporter,
types: &Types,
dt: &NamedDataType,
indent: &str,
) -> Result<(), Error> {
match dt.ty() {
DataType::Struct(strct) => match strct.fields() {
Fields::Unit => {}
Fields::Unnamed(unnamed) => {
for (idx, field) in unnamed.fields().iter().enumerate() {
let Some(ty) = field.ty() else {
continue;
};
let mut ty_str = String::new();
let datatype_prefix = format!("{indent}\t*\t");
datatype(
&mut ty_str,
exporter,
types,
ty,
vec![dt.name().clone(), idx.to_string().into()],
Some(dt.name()),
&datatype_prefix,
Default::default(),
)?;
push_jsdoc_property(
s,
&ty_str,
&idx.to_string(),
field.optional(),
field.docs(),
field.deprecated(),
indent,
);
}
}
Fields::Named(named) => {
for (name, field) in named.fields() {
let Some(ty) = field.ty() else {
continue;
};
let mut ty_str = String::new();
let datatype_prefix = format!("{indent}\t*\t");
datatype(
&mut ty_str,
exporter,
types,
ty,
vec![dt.name().clone(), name.clone()],
Some(dt.name()),
&datatype_prefix,
Default::default(),
)?;
push_jsdoc_property(
s,
&ty_str,
name,
field.optional(),
field.docs(),
field.deprecated(),
indent,
);
}
}
},
DataType::Enum(enm) => {
for (variant_name, variant) in enm.variants().iter().filter(|(_, v)| !v.skip()) {
let mut one_variant_enum = enm.clone();
one_variant_enum
.variants_mut()
.retain(|(name, _)| name == variant_name);
let mut variant_ty = String::new();
crate::legacy::enum_datatype(
ExportContext {
cfg: exporter,
path: vec![],
},
&one_variant_enum,
types,
&mut variant_ty,
"",
&[],
)?;
push_jsdoc_property(
s,
&variant_ty,
variant_name,
false,
variant.docs(),
variant.deprecated(),
indent,
);
}
}
_ => {}
}
Ok(())
}
fn push_jsdoc_property(
s: &mut String,
ty: &str,
name: &str,
optional: bool,
docs: &str,
deprecated: Option<&Deprecated>,
indent: &str,
) {
s.push_str(indent);
s.push_str("\t* @property {");
push_jsdoc_type(s, ty, indent);
s.push_str("} ");
s.push_str(&jsdoc_property_name(name, optional));
if let Some(description) = jsdoc_description(docs, deprecated) {
s.push_str(" - ");
s.push_str(&description);
}
s.push('\n');
}
fn push_jsdoc_type(s: &mut String, ty: &str, indent: &str) {
let mut lines = ty.lines();
if let Some(first_line) = lines.next() {
s.push_str(first_line);
}
for line in lines {
s.push('\n');
if line
.strip_prefix(indent)
.is_some_and(|rest| rest.starts_with("\t*"))
{
s.push_str(line);
} else {
s.push_str(indent);
s.push_str("\t* ");
s.push_str(line);
}
}
}
fn jsdoc_property_name(name: &str, optional: bool) -> String {
let name = if is_identifier(name) {
name.to_string()
} else {
format!("\"{}\"", escape_typescript_string_literal(name))
};
if optional { format!("[{name}]") } else { name }
}
fn append_typedef_body(
s: &mut String,
exporter: &Exporter,
types: &Types,
dt: &NamedDataType,
indent: &str,
) -> Result<(), Error> {
let generics = (!dt.generics().is_empty())
.then(|| {
iter::once("<")
.chain(intersperse(
dt.generics().iter().map(|(_, g)| g.as_ref()),
", ",
))
.chain(iter::once(">"))
})
.into_iter()
.flatten();
let name = dt.name();
let type_name = iter::empty()
.chain([name.as_ref()])
.chain(generics)
.collect::<String>();
let mut typedef_ty = String::new();
let datatype_prefix = format!("{indent}\t*\t");
let _generic_scope = push_generic_scope(dt.generics());
datatype(
&mut typedef_ty,
exporter,
types,
dt.ty(),
vec![dt.name().clone()],
Some(dt.name()),
&datatype_prefix,
Default::default(),
)?;
if !dt.docs().is_empty() {
for line in dt.docs().lines() {
s.push_str(indent);
s.push_str("\t* ");
s.push_str(&escape_jsdoc_text(line));
s.push('\n');
}
s.push_str(indent);
s.push_str("\t*\n");
}
if let Some(deprecated) = dt.deprecated() {
s.push_str(indent);
s.push_str("\t* @deprecated");
if let Some(details) = deprecated_details(deprecated) {
s.push(' ');
s.push_str(&details);
}
s.push('\n');
}
s.push_str(indent);
s.push_str("\t* @typedef {");
push_jsdoc_type(s, &typedef_ty, indent);
s.push_str("} ");
s.push_str(&type_name);
s.push('\n');
append_jsdoc_properties(s, exporter, types, dt, indent)?;
Ok(())
}
fn jsdoc_description(docs: &str, deprecated: Option<&Deprecated>) -> Option<String> {
let docs = docs
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
.map(|line| escape_jsdoc_text(line).into_owned())
.collect::<Vec<_>>()
.join(" ");
let deprecated = deprecated.map(|deprecated| {
let mut value = String::from("@deprecated");
if let Some(details) = deprecated_details(deprecated) {
value.push(' ');
value.push_str(&escape_jsdoc_text(&details));
}
value
});
match (docs.is_empty(), deprecated) {
(true, None) => None,
(true, Some(deprecated)) => Some(deprecated),
(false, None) => Some(docs),
(false, Some(deprecated)) => Some(format!("{docs} {deprecated}")),
}
}
pub fn reference(
exporter: &dyn AsRef<Exporter>,
types: &ResolvedTypes,
r: &Reference,
) -> Result<String, Error> {
let mut s = String::new();
reference_dt(
&mut s,
exporter.as_ref(),
types.as_types(),
r,
vec![],
"",
&[],
)?;
Ok(s)
}
pub(crate) fn datatype_with_inline_attr(
s: &mut String,
exporter: &Exporter,
types: &Types,
dt: &DataType,
location: Vec<Cow<'static, str>>,
parent_name: Option<&str>,
prefix: &str,
generics: &[(GenericReference, DataType)],
inline: bool,
) -> Result<(), Error> {
if inline {
return shallow_inline_datatype(
s,
exporter,
types,
dt,
location,
parent_name,
prefix,
generics,
);
}
datatype(
s,
exporter,
types,
dt,
location,
parent_name,
prefix,
generics,
)
}
fn merged_generics(
parent: &[(GenericReference, DataType)],
child: &[(GenericReference, DataType)],
) -> Vec<(GenericReference, DataType)> {
let unshadowed_parent = parent
.iter()
.filter(|(parent_generic, _)| {
!child
.iter()
.any(|(child_generic, _)| child_generic == parent_generic)
})
.cloned();
child
.iter()
.map(|(generic, dt)| (generic.clone(), resolve_generics_in_datatype(dt, parent)))
.chain(unshadowed_parent)
.collect()
}
thread_local! {
static INLINE_REFERENCE_STACK: RefCell<Vec<(Cow<'static, str>, Cow<'static, str>, Vec<(GenericReference, DataType)>)>> = const { RefCell::new(Vec::new()) };
static RESOLVING_GENERICS: RefCell<Vec<GenericReference>> = const { RefCell::new(Vec::new()) };
static GENERIC_NAME_STACK: RefCell<Vec<Vec<(GenericReference, Cow<'static, str>)>>> = const { RefCell::new(Vec::new()) };
}
struct GenericScopeGuard;
impl Drop for GenericScopeGuard {
fn drop(&mut self) {
GENERIC_NAME_STACK.with(|stack| {
stack.borrow_mut().pop();
});
}
}
fn push_generic_scope(generics: &[(GenericReference, Cow<'static, str>)]) -> GenericScopeGuard {
GENERIC_NAME_STACK.with(|stack| {
stack.borrow_mut().push(generics.to_vec());
});
GenericScopeGuard
}
fn resolve_generic_name(generic: &GenericReference) -> Option<Cow<'static, str>> {
GENERIC_NAME_STACK.with(|stack| {
stack.borrow().iter().rev().find_map(|scope| {
scope
.iter()
.find(|(candidate, _)| candidate == generic)
.map(|(_, name)| name.clone())
})
})
}
fn write_generic_reference(s: &mut String, generic: &GenericReference) -> Result<(), Error> {
let generic_name = resolve_generic_name(generic)
.ok_or_else(|| Error::unresolved_generic_reference(format!("{generic:?}")))?;
s.push_str(generic_name.as_ref());
Ok(())
}
fn shallow_inline_datatype(
s: &mut String,
exporter: &Exporter,
types: &Types,
dt: &DataType,
location: Vec<Cow<'static, str>>,
parent_name: Option<&str>,
prefix: &str,
generics: &[(GenericReference, DataType)],
) -> Result<(), Error> {
match dt {
DataType::Primitive(p) => s.push_str(primitive_dt(p, location)?),
DataType::List(list) => {
let mut inner = String::new();
shallow_inline_datatype(
&mut inner,
exporter,
types,
list.ty(),
location,
parent_name,
prefix,
generics,
)?;
let inner = if (inner.contains(' ') && !inner.ends_with('}'))
|| (inner.contains(' ') && (inner.contains('&') || inner.contains('|')))
{
format!("({inner})")
} else {
inner
};
if let Some(length) = list.length() {
s.push('[');
for i in 0..length {
if i != 0 {
s.push_str(", ");
}
s.push_str(&inner);
}
s.push(']');
} else {
write!(s, "{inner}[]")?;
}
}
DataType::Map(map) => {
let path = map_key_path(&location);
map_keys::validate_map_key(map.key_ty(), types, generics, format!("{path}.<map_key>"))?;
let rendered_key =
map_key_render_type(resolve_generics_in_datatype(map.key_ty(), generics));
fn is_exhaustive(dt: &DataType, types: &Types) -> bool {
match dt {
DataType::Enum(e) => {
e.variants().iter().filter(|(_, v)| !v.skip()).count() == 0
}
DataType::Reference(Reference::Named(r)) => r
.get(types)
.is_some_and(|ndt| is_exhaustive(ndt.ty(), types)),
DataType::Reference(Reference::Opaque(_)) => false,
_ => true,
}
}
let exhaustive = is_exhaustive(&rendered_key, types);
if !exhaustive {
s.push_str("Partial<");
}
s.push_str("{ [key in ");
shallow_inline_datatype(
s,
exporter,
types,
&rendered_key,
location.clone(),
parent_name,
prefix,
generics,
)?;
s.push_str("]: ");
shallow_inline_datatype(
s,
exporter,
types,
map.value_ty(),
location,
parent_name,
prefix,
generics,
)?;
s.push_str(" }");
if !exhaustive {
s.push('>');
}
}
DataType::Nullable(dt) => {
let mut inner = String::new();
shallow_inline_datatype(
&mut inner,
exporter,
types,
dt,
location,
parent_name,
prefix,
generics,
)?;
s.push_str(&inner);
if inner != "null" && !inner.ends_with(" | null") {
s.push_str(" | null");
}
}
DataType::Struct(st) => {
crate::legacy::struct_datatype(
crate::legacy::ExportContext {
cfg: exporter,
path: vec![],
},
parent_name,
st,
types,
s,
prefix,
generics,
)?;
}
DataType::Enum(enm) => {
crate::legacy::enum_datatype(
crate::legacy::ExportContext {
cfg: exporter,
path: vec![],
},
enm,
types,
s,
prefix,
generics,
)?;
}
DataType::Tuple(tuple) => match tuple.elements() {
[] => s.push_str("null"),
elements => {
s.push('[');
for (idx, dt) in elements.iter().enumerate() {
if idx != 0 {
s.push_str(", ");
}
shallow_inline_datatype(
s,
exporter,
types,
dt,
location.clone(),
parent_name,
prefix,
generics,
)?;
}
s.push(']');
}
},
DataType::Reference(r) => match r {
Reference::Named(r) => {
let ndt = r
.get(types)
.ok_or_else(|| Error::dangling_named_reference(format!("{r:?}")))?;
let inline_key = (
ndt.module_path().clone(),
ndt.name().clone(),
r.generics().to_vec(),
);
let already_inlining = INLINE_REFERENCE_STACK
.with(|stack| stack.borrow().iter().any(|key| key == &inline_key));
if already_inlining {
return reference_named_dt(s, exporter, types, r, location, prefix, generics);
}
INLINE_REFERENCE_STACK.with(|stack| stack.borrow_mut().push(inline_key));
let combined_generics = merged_generics(generics, r.generics());
let resolved = resolve_generics_in_datatype(ndt.ty(), &combined_generics);
let result = shallow_inline_datatype(
s,
exporter,
types,
&resolved,
location,
parent_name,
prefix,
&combined_generics,
);
INLINE_REFERENCE_STACK.with(|stack| {
stack.borrow_mut().pop();
});
result
}
Reference::Generic(g) => {
if let Some((_, resolved_dt)) = generics.iter().find(|(ge, _)| ge == g) {
if matches!(resolved_dt, DataType::Reference(Reference::Generic(inner)) if inner == g)
{
write_generic_reference(s, g)?;
} else {
let already_resolving = RESOLVING_GENERICS
.with(|stack| stack.borrow().iter().any(|seen| seen == g));
if already_resolving {
write_generic_reference(s, g)?;
} else {
RESOLVING_GENERICS.with(|stack| stack.borrow_mut().push(g.clone()));
let result = shallow_inline_datatype(
s,
exporter,
types,
resolved_dt,
location,
parent_name,
prefix,
generics,
);
RESOLVING_GENERICS.with(|stack| {
stack.borrow_mut().pop();
});
result?;
}
}
} else {
write_generic_reference(s, g)?;
}
Ok(())
}
Reference::Opaque(_) => reference_dt(s, exporter, types, r, location, prefix, generics),
}?,
}
Ok(())
}
fn resolve_generics_in_datatype(
dt: &DataType,
generics: &[(GenericReference, DataType)],
) -> DataType {
fn resolve(
dt: &DataType,
generics: &[(GenericReference, DataType)],
visiting: &mut Vec<GenericReference>,
) -> DataType {
match dt {
DataType::Primitive(_) => dt.clone(),
DataType::List(l) => {
let mut out = l.clone();
out.set_ty(resolve(l.ty(), generics, visiting));
DataType::List(out)
}
DataType::Map(m) => {
let mut out = m.clone();
out.set_key_ty(resolve(m.key_ty(), generics, visiting));
out.set_value_ty(resolve(m.value_ty(), generics, visiting));
DataType::Map(out)
}
DataType::Nullable(def) => {
DataType::Nullable(Box::new(resolve(def, generics, visiting)))
}
DataType::Struct(st) => {
let mut out = st.clone();
match out.fields_mut() {
specta::datatype::Fields::Unit => {}
specta::datatype::Fields::Unnamed(unnamed) => {
for field in unnamed.fields_mut() {
if let Some(ty) = field.ty_mut() {
*ty = resolve(ty, generics, visiting);
}
}
}
specta::datatype::Fields::Named(named) => {
for (_, field) in named.fields_mut() {
if let Some(ty) = field.ty_mut() {
*ty = resolve(ty, generics, visiting);
}
}
}
}
DataType::Struct(out)
}
DataType::Enum(en) => {
let mut out = en.clone();
for (_, variant) in out.variants_mut() {
match variant.fields_mut() {
specta::datatype::Fields::Unit => {}
specta::datatype::Fields::Unnamed(unnamed) => {
for field in unnamed.fields_mut() {
if let Some(ty) = field.ty_mut() {
*ty = resolve(ty, generics, visiting);
}
}
}
specta::datatype::Fields::Named(named) => {
for (_, field) in named.fields_mut() {
if let Some(ty) = field.ty_mut() {
*ty = resolve(ty, generics, visiting);
}
}
}
}
}
DataType::Enum(out)
}
DataType::Tuple(t) => {
let mut out = t.clone();
for element in out.elements_mut() {
*element = resolve(element, generics, visiting);
}
DataType::Tuple(out)
}
DataType::Reference(Reference::Generic(g)) => {
if visiting.iter().any(|seen| seen == g) {
return dt.clone();
}
if let Some((_, resolved_dt)) = generics.iter().find(|(ge, _)| ge == g) {
if matches!(resolved_dt, DataType::Reference(Reference::Generic(inner)) if inner == g)
{
dt.clone()
} else {
visiting.push(g.clone());
let out = resolve(resolved_dt, generics, visiting);
visiting.pop();
out
}
} else {
dt.clone()
}
}
DataType::Reference(_) => dt.clone(),
}
}
resolve(dt, generics, &mut Vec::new())
}
fn inline_datatype(
s: &mut String,
exporter: &Exporter,
types: &Types,
dt: &DataType,
location: Vec<Cow<'static, str>>,
parent_name: Option<&str>,
prefix: &str,
depth: usize,
generics: &[(GenericReference, DataType)],
) -> Result<(), Error> {
if depth == 25 {
return Err(Error::invalid_name(
location.join("."),
"Type recursion limit exceeded during inline expansion",
));
}
match dt {
DataType::Primitive(p) => s.push_str(primitive_dt(p, location)?),
DataType::List(l) => {
let mut dt_str = String::new();
crate::legacy::datatype_inner(
crate::legacy::ExportContext {
cfg: exporter,
path: vec![],
},
l.ty(),
types,
&mut dt_str,
generics,
)?;
let dt_str = if (dt_str.contains(' ') && !dt_str.ends_with('}'))
|| (dt_str.contains(' ') && (dt_str.contains('&') || dt_str.contains('|')))
{
format!("({dt_str})")
} else {
dt_str
};
if let Some(length) = l.length() {
s.push('[');
for n in 0..length {
if n != 0 {
s.push_str(", ");
}
s.push_str(&dt_str);
}
s.push(']');
} else {
write!(s, "{dt_str}[]")?;
}
}
DataType::Map(m) => map_dt(s, exporter, types, m, location, generics)?,
DataType::Nullable(def) => {
let mut inner = String::new();
inline_datatype(
&mut inner,
exporter,
types,
def,
location,
parent_name,
prefix,
depth + 1,
generics,
)?;
s.push_str(&inner);
if inner != "null" && !inner.ends_with(" | null") {
s.push_str(" | null");
}
}
DataType::Struct(st) => {
if !generics.is_empty() {
use specta::datatype::Fields;
match st.fields() {
Fields::Unit => s.push_str("null"),
Fields::Named(named) => {
s.push('{');
let mut has_field = false;
for (key, field) in named.fields() {
let Some(field_ty) = field.ty() else {
continue;
};
has_field = true;
s.push('\n');
s.push_str(prefix);
s.push('\t');
s.push_str(key);
s.push_str(": ");
inline_datatype(
s,
exporter,
types,
field_ty,
location.clone(),
parent_name,
prefix,
depth + 1,
generics,
)?;
s.push(',');
}
if has_field {
s.push('\n');
s.push_str(prefix);
}
s.push('}');
}
Fields::Unnamed(_) => {
crate::legacy::struct_datatype(
crate::legacy::ExportContext {
cfg: exporter,
path: vec![],
},
parent_name,
st,
types,
s,
prefix,
generics,
)?
}
}
} else {
crate::legacy::struct_datatype(
crate::legacy::ExportContext {
cfg: exporter,
path: vec![],
},
parent_name,
st,
types,
s,
prefix,
Default::default(),
)?
}
}
DataType::Enum(e) => enum_dt(s, exporter, types, e, location, prefix, generics)?,
DataType::Tuple(t) => tuple_dt(s, exporter, types, t, location, generics)?,
DataType::Reference(r) => {
if let Reference::Named(r) = r
&& let Some(ndt) = r.get(types)
{
let combined_generics = merged_generics(generics, r.generics());
inline_datatype(
s,
exporter,
types,
ndt.ty(),
location,
parent_name,
prefix,
depth + 1,
&combined_generics,
)?;
} else {
reference_dt(s, exporter, types, r, location, prefix, generics)?;
}
}
}
Ok(())
}
pub(crate) fn datatype(
s: &mut String,
exporter: &Exporter,
types: &Types,
dt: &DataType,
location: Vec<Cow<'static, str>>,
parent_name: Option<&str>,
prefix: &str,
generics: &[(GenericReference, DataType)],
) -> Result<(), Error> {
match dt {
DataType::Primitive(p) => s.push_str(primitive_dt(p, location)?),
DataType::List(l) => list_dt(s, exporter, types, l, location, generics)?,
DataType::Map(m) => map_dt(s, exporter, types, m, location, generics)?,
DataType::Nullable(def) => {
let mut inner = String::new();
crate::legacy::datatype_inner(
crate::legacy::ExportContext {
cfg: exporter,
path: vec![],
},
def,
types,
&mut inner,
generics,
)?;
s.push_str(&inner);
if inner != "null" && !inner.ends_with(" | null") {
s.push_str(" | null");
}
}
DataType::Struct(st) => {
crate::legacy::struct_datatype(
crate::legacy::ExportContext {
cfg: exporter,
path: vec![],
},
parent_name,
st,
types,
s,
prefix,
generics,
)?
}
DataType::Enum(e) => enum_dt(s, exporter, types, e, location, prefix, generics)?,
DataType::Tuple(t) => tuple_dt(s, exporter, types, t, location, generics)?,
DataType::Reference(r) => reference_dt(s, exporter, types, r, location, prefix, generics)?,
};
Ok(())
}
fn primitive_dt(p: &Primitive, location: Vec<Cow<'static, str>>) -> Result<&'static str, Error> {
use Primitive::*;
Ok(match p {
i8 | i16 | i32 | u8 | u16 | u32 | f16 | f32 | f64 => "number",
usize | isize | i64 | u64 | i128 | u128 | f128 => {
return Err(Error::bigint_forbidden(location.join(".")));
}
Primitive::bool => "boolean",
str | char => "string",
})
}
fn list_dt(
s: &mut String,
exporter: &Exporter,
types: &Types,
l: &List,
_location: Vec<Cow<'static, str>>,
generics: &[(GenericReference, DataType)],
) -> Result<(), Error> {
{
let mut dt = String::new();
crate::legacy::datatype_inner(
crate::legacy::ExportContext {
cfg: exporter,
path: vec![],
},
l.ty(),
types,
&mut dt,
generics,
)?;
let dt = if (dt.contains(' ') && !dt.ends_with('}'))
|| (dt.contains(' ') && (dt.contains('&') || dt.contains('|')))
{
format!("({dt})")
} else {
dt
};
if let Some(length) = l.length() {
s.push('[');
for n in 0..length {
if n != 0 {
s.push_str(", ");
}
s.push_str(&dt);
}
s.push(']');
} else {
write!(s, "{dt}[]")?;
}
}
Ok(())
}
fn map_dt(
s: &mut String,
exporter: &Exporter,
types: &Types,
m: &Map,
location: Vec<Cow<'static, str>>,
generics: &[(GenericReference, DataType)],
) -> Result<(), Error> {
let path = map_key_path(&location);
map_keys::validate_map_key(m.key_ty(), types, generics, format!("{path}.<map_key>"))?;
{
fn is_exhaustive(dt: &DataType, types: &Types) -> bool {
match dt {
DataType::Enum(e) => e.variants().iter().filter(|(_, v)| !v.skip()).count() == 0,
DataType::Reference(Reference::Named(r)) => {
if let Some(ndt) = r.get(types) {
is_exhaustive(ndt.ty(), types)
} else {
false
}
}
DataType::Reference(Reference::Opaque(_)) => false,
_ => true,
}
}
let resolved_key = map_key_render_type(resolve_generics_in_datatype(m.key_ty(), generics));
let is_exhaustive = is_exhaustive(&resolved_key, types);
if !is_exhaustive {
s.push_str("Partial<");
}
s.push_str("{ [key in ");
crate::legacy::datatype_inner(
crate::legacy::ExportContext {
cfg: exporter,
path: vec![],
},
&resolved_key,
types,
s,
generics,
)?;
s.push_str("]: ");
crate::legacy::datatype_inner(
crate::legacy::ExportContext {
cfg: exporter,
path: vec![],
},
m.value_ty(),
types,
s,
generics,
)?;
s.push_str(" }");
if !is_exhaustive {
s.push('>');
}
}
Ok(())
}
fn map_key_path(location: &[Cow<'static, str>]) -> String {
if location.is_empty() {
return "HashMap".to_string();
}
location.join(".")
}
fn map_key_render_type(dt: DataType) -> DataType {
if matches!(dt, DataType::Primitive(Primitive::bool)) {
return bool_key_literal_datatype();
}
dt
}
fn bool_key_literal_datatype() -> DataType {
let mut bool_enum = Enum::new();
bool_enum
.variants_mut()
.push((Cow::Borrowed("true"), Variant::unit()));
bool_enum
.variants_mut()
.push((Cow::Borrowed("false"), Variant::unit()));
DataType::Enum(bool_enum)
}
fn enum_dt(
s: &mut String,
exporter: &Exporter,
types: &Types,
e: &Enum,
_location: Vec<Cow<'static, str>>,
prefix: &str,
generics: &[(GenericReference, DataType)],
) -> Result<(), Error> {
{
crate::legacy::enum_datatype(
crate::legacy::ExportContext {
cfg: exporter,
path: vec![],
},
e,
types,
s,
prefix,
generics,
)?
}
Ok(())
}
fn tuple_dt(
s: &mut String,
exporter: &Exporter,
types: &Types,
t: &Tuple,
_location: Vec<Cow<'static, str>>,
generics: &[(GenericReference, DataType)],
) -> Result<(), Error> {
{
s.push_str(&crate::legacy::tuple_datatype(
crate::legacy::ExportContext {
cfg: exporter,
path: vec![],
},
t,
types,
generics,
)?);
}
Ok(())
}
fn reference_dt(
s: &mut String,
exporter: &Exporter,
types: &Types,
r: &Reference,
location: Vec<Cow<'static, str>>,
prefix: &str,
generics: &[(GenericReference, DataType)],
) -> Result<(), Error> {
match r {
Reference::Named(r) => {
reference_named_dt(s, exporter, types, r, location, prefix, generics)
}
Reference::Generic(g) => {
if let Some((_, resolved_dt)) = generics.iter().find(|(ge, _)| ge == g) {
if matches!(resolved_dt, DataType::Reference(Reference::Generic(inner)) if inner == g)
{
write_generic_reference(s, g)?;
Ok(())
} else {
datatype(
s,
exporter,
types,
resolved_dt,
location,
None,
prefix,
generics,
)
}
} else {
write_generic_reference(s, g)?;
Ok(())
}
}
Reference::Opaque(r) => reference_opaque_dt(s, exporter, types, r),
}
}
fn reference_opaque_dt(
s: &mut String,
exporter: &Exporter,
types: &Types,
r: &OpaqueReference,
) -> Result<(), Error> {
if let Some(def) = r.downcast_ref::<opaque::Define>() {
s.push_str(&def.0);
return Ok(());
} else if r.downcast_ref::<opaque::Any>().is_some() {
s.push_str("any");
return Ok(());
} else if r.downcast_ref::<opaque::Unknown>().is_some() {
s.push_str("unknown");
return Ok(());
} else if r.downcast_ref::<opaque::Never>().is_some() {
s.push_str("never");
return Ok(());
} else if let Some(def) = r.downcast_ref::<Branded>() {
let resolved_types = ResolvedTypes::from_resolved_types(types.clone());
if let Some(branded_type) = exporter
.branded_type_impl
.as_ref()
.map(|builder| {
(builder.0)(
BrandedTypeExporter {
exporter,
types: &resolved_types,
},
def,
)
})
.transpose()?
{
s.push_str(branded_type.as_ref());
return Ok(());
}
match def.ty() {
DataType::Reference(r) => reference_dt(s, exporter, types, r, vec![], "", &[])?,
ty => inline_datatype(s, exporter, types, ty, vec![], None, "", 0, &[])?,
}
s.push_str(r#" & { { readonly __brand: ""#);
s.push_str(def.brand());
s.push_str("\" }");
return Ok(());
}
Err(Error::unsupported_opaque_reference(r.clone()))
}
fn reference_named_dt(
s: &mut String,
exporter: &Exporter,
types: &Types,
r: &NamedReference,
location: Vec<Cow<'static, str>>,
prefix: &str,
generics: &[(GenericReference, DataType)],
) -> Result<(), Error> {
{
let ndt = r
.get(types)
.ok_or_else(|| Error::dangling_named_reference(format!("{r:?}")))?;
let _generic_scope = push_generic_scope(ndt.generics());
if r.inline() {
let inline_key = (
ndt.module_path().clone(),
ndt.name().clone(),
r.generics().to_vec(),
);
let already_inlining = INLINE_REFERENCE_STACK
.with(|stack| stack.borrow().iter().any(|key| key == &inline_key));
if already_inlining {
} else {
INLINE_REFERENCE_STACK.with(|stack| stack.borrow_mut().push(inline_key));
let combined_generics = merged_generics(generics, r.generics());
let resolved = resolve_generics_in_datatype(ndt.ty(), &combined_generics);
let result = datatype(
s,
exporter,
types,
&resolved,
location,
None,
prefix,
&combined_generics,
);
INLINE_REFERENCE_STACK.with(|stack| {
stack.borrow_mut().pop();
});
return result;
}
}
crate::references::track_nr(r);
let name = match exporter.layout {
Layout::ModulePrefixedName => {
let mut s = ndt.module_path().split("::").collect::<Vec<_>>().join("_");
s.push('_');
s.push_str(ndt.name());
Cow::Owned(s)
}
Layout::Namespaces => {
if ndt.module_path().is_empty() {
ndt.name().clone()
} else {
let mut path =
ndt.module_path()
.split("::")
.fold("$s$.".to_string(), |mut s, segment| {
s.push_str(segment);
s.push('.');
s
});
path.push_str(ndt.name());
Cow::Owned(path)
}
}
Layout::Files => {
let current_module_path =
crate::references::current_module_path().unwrap_or_default();
if ndt.module_path() == ¤t_module_path {
ndt.name().clone()
} else {
let mut path = crate::exporter::module_alias(ndt.module_path());
path.push('.');
path.push_str(ndt.name());
Cow::Owned(path)
}
}
_ => ndt.name().clone(),
};
let scoped_generics = generics
.iter()
.filter(|(parent_generic, _)| {
!r.generics()
.iter()
.any(|(child_generic, _)| child_generic == parent_generic)
})
.cloned()
.collect::<Vec<_>>();
s.push_str(&name);
if !r.generics().is_empty() {
s.push('<');
for (i, (_, v)) in r.generics().iter().enumerate() {
if i != 0 {
s.push_str(", ");
}
crate::legacy::datatype_inner(
crate::legacy::ExportContext {
cfg: exporter,
path: vec![],
},
v,
types,
s,
&scoped_generics,
)?;
}
s.push('>');
}
}
Ok(())
}
fn intersperse<T: Clone>(iter: impl Iterator<Item = T>, sep: T) -> impl Iterator<Item = T> {
iter.enumerate().flat_map(move |(i, item)| {
if i == 0 {
vec![item]
} else {
vec![sep.clone(), item]
}
})
}