use crate::ir::{Primitive, TypeRef};
const FIXED_ARRAY_TUPLE_CAP: usize = 16;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BigIntStrategy {
Native,
AsString,
}
#[derive(Debug, Clone)]
pub struct Options {
pub bigint: BigIntStrategy,
pub honor_undefined: bool,
}
impl Default for Options {
fn default() -> Self {
Self {
bigint: BigIntStrategy::Native,
honor_undefined: true,
}
}
}
#[must_use]
pub fn render_type(t: &TypeRef, opts: &Options) -> String {
match t {
TypeRef::Primitive(p) => render_primitive(*p, opts).to_string(),
TypeRef::Named(name) => name.clone(),
TypeRef::Option(inner) => render_option(inner, opts),
TypeRef::Vec(inner) => render_vec_like(inner, opts),
TypeRef::Map { key, value } => render_map(key, value, opts),
TypeRef::Tuple(elems) => render_tuple(elems, opts),
TypeRef::FixedArray { elem, len } => {
render_fixed_array(elem, usize::try_from(*len).unwrap_or(usize::MAX), opts)
}
}
}
#[must_use]
#[allow(clippy::match_same_arms)] pub fn render_primitive(p: Primitive, opts: &Options) -> &'static str {
use Primitive::{
Bool, Bytes, DateTime, String, Unit, Uuid, F32, F64, I128, I16, I32, I64, I8, U128, U16,
U32, U64, U8,
};
match p {
Bool => "boolean",
U8 | U16 | U32 | I8 | I16 | I32 | F32 | F64 => "number",
U64 | I64 | U128 | I128 => match opts.bigint {
BigIntStrategy::Native => "bigint",
BigIntStrategy::AsString => "string",
},
String => "string",
Bytes => "string",
Unit => "void",
DateTime => "string",
Uuid => "string",
}
}
fn render_option(inner: &TypeRef, opts: &Options) -> String {
let mut cur = inner;
while let TypeRef::Option(next) = cur {
cur = next;
}
let rendered = render_type(cur, opts);
if needs_parens_for_union(cur) {
format!("({rendered}) | null")
} else {
format!("{rendered} | null")
}
}
fn render_vec_like(inner: &TypeRef, opts: &Options) -> String {
let rendered = render_type(inner, opts);
if needs_parens_for_array(inner) {
format!("({rendered})[]")
} else {
format!("{rendered}[]")
}
}
fn render_map(key: &TypeRef, value: &TypeRef, opts: &Options) -> String {
let v = render_type(value, opts);
if is_string_keyed(key) {
format!("Record<string, {v}>")
} else {
let k = render_type(key, opts);
format!("Array<[{k}, {v}]>")
}
}
fn render_tuple(elems: &[TypeRef], opts: &Options) -> String {
if elems.is_empty() {
return "void".to_string();
}
let inner: Vec<String> = elems.iter().map(|e| render_type(e, opts)).collect();
format!("[{}]", inner.join(", "))
}
fn render_fixed_array(elem: &TypeRef, len: usize, opts: &Options) -> String {
let rendered = render_type(elem, opts);
if len == 0 {
return "[]".to_string();
}
if len <= FIXED_ARRAY_TUPLE_CAP {
let parts: Vec<&str> = (0..len).map(|_| rendered.as_str()).collect();
return format!("[{}]", parts.join(", "));
}
if needs_parens_for_array(elem) {
format!("/* TODO: fixed-size [{rendered}; {len}] */ ({rendered})[]")
} else {
format!("/* TODO: fixed-size [{rendered}; {len}] */ {rendered}[]")
}
}
fn needs_parens_for_union(t: &TypeRef) -> bool {
matches!(t, TypeRef::Option(_))
}
fn needs_parens_for_array(t: &TypeRef) -> bool {
matches!(t, TypeRef::Option(_))
}
fn is_string_keyed(key: &TypeRef) -> bool {
matches!(
key,
TypeRef::Primitive(Primitive::String | Primitive::Uuid | Primitive::DateTime)
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ir::Primitive::*;
use crate::ir::{Primitive, TypeRef};
fn r(t: &TypeRef) -> std::string::String {
render_type(t, &Options::default())
}
fn r_opts(t: &TypeRef, opts: &Options) -> std::string::String {
render_type(t, opts)
}
fn prim(p: Primitive) -> TypeRef {
TypeRef::Primitive(p)
}
#[test]
fn primitive_bool() {
assert_eq!(r(&prim(Bool)), "boolean");
}
#[test]
fn primitive_small_ints_and_floats_are_number() {
for p in [U8, U16, U32, I8, I16, I32, F32, F64] {
assert_eq!(render_primitive(p, &Options::default()), "number", "{p:?}");
}
}
#[test]
fn primitive_big_ints_default_to_bigint() {
for p in [U64, I64, U128, I128] {
assert_eq!(render_primitive(p, &Options::default()), "bigint", "{p:?}");
}
}
#[test]
fn primitive_string_unit_bytes_datetime_uuid() {
let opts = Options::default();
assert_eq!(render_primitive(String, &opts), "string");
assert_eq!(render_primitive(Unit, &opts), "void");
assert_eq!(render_primitive(Bytes, &opts), "string");
assert_eq!(render_primitive(DateTime, &opts), "string");
assert_eq!(render_primitive(Uuid, &opts), "string");
}
#[test]
fn bigint_as_string_mode_for_64_and_128_bit_ints() {
let opts = Options {
bigint: BigIntStrategy::AsString,
honor_undefined: true,
};
for p in [U64, I64, U128, I128] {
assert_eq!(render_primitive(p, &opts), "string", "{p:?}");
}
assert_eq!(render_primitive(U32, &opts), "number");
assert_eq!(render_primitive(I32, &opts), "number");
}
#[test]
fn option_string_renders_as_string_pipe_null() {
let t = TypeRef::Option(Box::new(prim(String)));
assert_eq!(r(&t), "string | null");
}
#[test]
fn vec_u32_renders_as_number_array() {
let t = TypeRef::Vec(Box::new(prim(U32)));
assert_eq!(r(&t), "number[]");
}
#[test]
fn option_vec_string_renders_as_string_array_pipe_null() {
let t = TypeRef::Option(Box::new(TypeRef::Vec(Box::new(prim(String)))));
assert_eq!(r(&t), "string[] | null");
}
#[test]
fn vec_option_string_parenthesizes_the_union() {
let t = TypeRef::Vec(Box::new(TypeRef::Option(Box::new(prim(String)))));
assert_eq!(r(&t), "(string | null)[]");
}
#[test]
fn nested_option_collapses() {
let t = TypeRef::Option(Box::new(TypeRef::Option(Box::new(prim(String)))));
assert_eq!(r(&t), "string | null");
}
#[test]
fn hashmap_string_user_is_record() {
let t = TypeRef::Map {
key: Box::new(prim(String)),
value: Box::new(TypeRef::Named("User".into())),
};
assert_eq!(r(&t), "Record<string, User>");
}
#[test]
fn hashmap_u64_user_is_array_of_pairs() {
let t = TypeRef::Map {
key: Box::new(prim(U64)),
value: Box::new(TypeRef::Named("User".into())),
};
assert_eq!(r(&t), "Array<[bigint, User]>");
}
#[test]
fn empty_tuple_is_void() {
let t = TypeRef::Tuple(vec![]);
assert_eq!(r(&t), "void");
}
#[test]
fn tuple_i32_string() {
let t = TypeRef::Tuple(vec![prim(I32), prim(String)]);
assert_eq!(r(&t), "[number, string]");
}
#[test]
fn fixed_array_short_becomes_tuple() {
let t = TypeRef::FixedArray {
elem: Box::new(prim(U8)),
len: 3,
};
assert_eq!(r(&t), "[number, number, number]");
}
#[test]
fn fixed_array_long_falls_back_to_array_with_todo() {
let t = TypeRef::FixedArray {
elem: Box::new(prim(U8)),
len: 32,
};
let rendered = r(&t);
assert!(rendered.contains("number[]"), "got: {rendered}");
assert!(rendered.contains("TODO"), "got: {rendered}");
assert!(rendered.contains("32"), "got: {rendered}");
}
#[test]
fn named_type_passes_through_verbatim() {
let t = TypeRef::Named("User".into());
assert_eq!(r(&t), "User");
}
#[test]
fn render_type_honors_bigint_as_string_for_composites() {
let opts = Options {
bigint: BigIntStrategy::AsString,
honor_undefined: true,
};
let t = TypeRef::Vec(Box::new(prim(U64)));
assert_eq!(r_opts(&t, &opts), "string[]");
}
}