extern crate alloc;
use alloc::collections::BTreeSet;
use alloc::string::String;
use alloc::vec::Vec;
use core::fmt::Write;
use facet_core::{Def, Facet, Field, Shape, StructKind, Type, UserType};
pub fn to_typescript<T: Facet<'static>>() -> String {
let mut generator = TypeScriptGenerator::new();
generator.add_shape(T::SHAPE);
generator.finish()
}
pub struct TypeScriptGenerator {
output: String,
generated: BTreeSet<&'static str>,
queue: Vec<&'static Shape>,
indent: usize,
}
impl Default for TypeScriptGenerator {
fn default() -> Self {
Self::new()
}
}
impl TypeScriptGenerator {
pub const fn new() -> Self {
Self {
output: String::new(),
generated: BTreeSet::new(),
queue: Vec::new(),
indent: 0,
}
}
pub fn add_type<T: Facet<'static>>(&mut self) {
self.add_shape(T::SHAPE);
}
pub fn add_shape(&mut self, shape: &'static Shape) {
if !self.generated.contains(shape.type_identifier) {
self.queue.push(shape);
}
}
pub fn finish(mut self) -> String {
while let Some(shape) = self.queue.pop() {
if self.generated.contains(shape.type_identifier) {
continue;
}
self.generated.insert(shape.type_identifier);
self.generate_shape(shape);
}
self.output
}
fn write_indent(&mut self) {
for _ in 0..self.indent {
self.output.push_str(" ");
}
}
#[inline]
fn shape_key(shape: &'static Shape) -> &'static str {
shape.type_identifier
}
fn unwrap_to_inner_shape(shape: &'static Shape) -> (&'static Shape, bool) {
if let Def::Option(opt) = &shape.def {
let (inner, _) = Self::unwrap_to_inner_shape(opt.t);
return (inner, true);
}
if let Def::Pointer(ptr) = &shape.def
&& let Some(pointee) = ptr.pointee
{
return Self::unwrap_to_inner_shape(pointee);
}
if let Some(inner) = shape.inner {
let (inner_shape, is_optional) = Self::unwrap_to_inner_shape(inner);
return (inner_shape, is_optional);
}
if let Some(proxy_def) = shape.proxy {
return Self::unwrap_to_inner_shape(proxy_def.shape);
}
(shape, false)
}
fn format_inline_field(&mut self, field: &Field, force_optional: bool) -> String {
let field_name = field.effective_name();
let field_shape = field.shape.get();
let has_default = field.default.is_some();
if let Def::Option(opt) = &field_shape.def {
let inner_type = self.type_for_shape(opt.t);
format!("{}?: {}", field_name, inner_type)
} else if force_optional || has_default {
let field_type = self.type_for_shape(field_shape);
format!("{}?: {}", field_name, field_type)
} else {
let field_type = self.type_for_shape(field_shape);
format!("{}: {}", field_name, field_type)
}
}
fn collect_inline_fields(
&mut self,
fields: &'static [Field],
force_optional: bool,
) -> Vec<String> {
let mut flatten_stack: Vec<&'static str> = Vec::new();
self.collect_inline_fields_guarded(fields, force_optional, &mut flatten_stack)
}
fn collect_inline_fields_guarded(
&mut self,
fields: &'static [Field],
force_optional: bool,
flatten_stack: &mut Vec<&'static str>,
) -> Vec<String> {
let mut result = Vec::new();
for field in fields {
if field.should_skip_serializing_unconditional() {
continue;
}
if field.is_flattened() {
let (inner_shape, parent_is_optional) =
Self::unwrap_to_inner_shape(field.shape.get());
if let Type::User(UserType::Struct(st)) = &inner_shape.ty {
let inner_key = Self::shape_key(inner_shape);
if flatten_stack.contains(&inner_key) {
continue;
}
flatten_stack.push(inner_key);
result.extend(self.collect_inline_fields_guarded(
st.fields,
force_optional || parent_is_optional,
flatten_stack,
));
flatten_stack.pop();
continue;
}
}
result.push(self.format_inline_field(field, force_optional));
}
result
}
fn has_serializable_fields(
field_owner_shape: &'static Shape,
fields: &'static [Field],
) -> bool {
let mut flatten_stack: Vec<&'static str> = Vec::new();
flatten_stack.push(Self::shape_key(field_owner_shape));
Self::has_serializable_fields_guarded(fields, &mut flatten_stack)
}
fn has_serializable_fields_guarded(
fields: &'static [Field],
flatten_stack: &mut Vec<&'static str>,
) -> bool {
for field in fields {
if field.should_skip_serializing_unconditional() {
continue;
}
if field.is_flattened() {
let (inner_shape, _) = Self::unwrap_to_inner_shape(field.shape.get());
if let Type::User(UserType::Struct(st)) = &inner_shape.ty {
let inner_key = Self::shape_key(inner_shape);
if flatten_stack.contains(&inner_key) {
continue;
}
flatten_stack.push(inner_key);
let has_fields =
Self::has_serializable_fields_guarded(st.fields, flatten_stack);
flatten_stack.pop();
if has_fields {
return true;
}
continue;
}
}
return true;
}
false
}
fn write_struct_fields_for_shape(
&mut self,
field_owner_shape: &'static Shape,
fields: &'static [Field],
) {
let mut flatten_stack: Vec<&'static str> = Vec::new();
flatten_stack.push(Self::shape_key(field_owner_shape));
self.write_struct_fields_guarded(fields, false, &mut flatten_stack);
}
fn write_struct_fields_guarded(
&mut self,
fields: &'static [Field],
force_optional: bool,
flatten_stack: &mut Vec<&'static str>,
) {
for field in fields {
if field.should_skip_serializing_unconditional() {
continue;
}
if field.is_flattened() {
let (inner_shape, parent_is_optional) =
Self::unwrap_to_inner_shape(field.shape.get());
if let Type::User(UserType::Struct(st)) = &inner_shape.ty {
let inner_key = Self::shape_key(inner_shape);
if flatten_stack.contains(&inner_key) {
continue;
}
flatten_stack.push(inner_key);
self.write_struct_fields_guarded(
st.fields,
force_optional || parent_is_optional,
flatten_stack,
);
flatten_stack.pop();
continue;
}
}
self.write_field(field, force_optional);
}
}
fn write_field(&mut self, field: &Field, force_optional: bool) {
if !field.doc.is_empty() {
self.write_indent();
self.output.push_str("/**\n");
for line in field.doc {
self.write_indent();
self.output.push_str(" *");
self.output.push_str(line);
self.output.push('\n');
}
self.write_indent();
self.output.push_str(" */\n");
}
let field_name = field.effective_name();
let field_shape = field.shape.get();
self.write_indent();
let has_default = field.default.is_some();
if let Def::Option(opt) = &field_shape.def {
let inner_type = self.type_for_shape(opt.t);
writeln!(self.output, "{}?: {};", field_name, inner_type).unwrap();
} else if force_optional || has_default {
let field_type = self.type_for_shape(field_shape);
writeln!(self.output, "{}?: {};", field_name, field_type).unwrap();
} else {
let field_type = self.type_for_shape(field_shape);
writeln!(self.output, "{}: {};", field_name, field_type).unwrap();
}
}
fn generate_shape(&mut self, shape: &'static Shape) {
if let Some(inner) = shape.inner {
self.add_shape(inner);
let inner_type = self.type_for_shape(inner);
writeln!(
self.output,
"export type {} = {};",
shape.type_identifier, inner_type
)
.unwrap();
self.output.push('\n');
return;
}
if !shape.doc.is_empty() {
self.output.push_str("/**\n");
for line in shape.doc {
self.output.push_str(" *");
self.output.push_str(line);
self.output.push('\n');
}
self.output.push_str(" */\n");
}
if let Some(proxy_def) = shape.proxy {
let proxy_shape = proxy_def.shape;
match &proxy_shape.ty {
Type::User(UserType::Struct(st)) => {
self.generate_struct(shape, proxy_shape, st.fields, st.kind);
return;
}
Type::User(UserType::Enum(en)) => {
self.generate_enum(shape, en);
return;
}
_ => {
let proxy_type = self.type_for_shape(proxy_shape);
writeln!(
self.output,
"export type {} = {};",
shape.type_identifier, proxy_type
)
.unwrap();
self.output.push('\n');
return;
}
}
}
match &shape.ty {
Type::User(UserType::Struct(st)) => {
self.generate_struct(shape, shape, st.fields, st.kind);
}
Type::User(UserType::Enum(en)) => {
self.generate_enum(shape, en);
}
_ => {
let type_str = self.type_for_shape(shape);
writeln!(
self.output,
"export type {} = {};",
shape.type_identifier, type_str
)
.unwrap();
self.output.push('\n');
}
}
}
fn generate_struct(
&mut self,
exported_shape: &'static Shape,
field_owner_shape: &'static Shape,
fields: &'static [Field],
kind: StructKind,
) {
match kind {
StructKind::Unit => {
writeln!(
self.output,
"export type {} = null;",
exported_shape.type_identifier
)
.unwrap();
}
StructKind::TupleStruct | StructKind::Tuple => {
let types: Vec<String> = fields
.iter()
.map(|f| self.type_for_shape(f.shape.get()))
.collect();
writeln!(
self.output,
"export type {} = [{}];",
exported_shape.type_identifier,
types.join(", ")
)
.unwrap();
}
StructKind::Struct => {
if !Self::has_serializable_fields(field_owner_shape, fields) {
writeln!(
self.output,
"export type {} = object;",
exported_shape.type_identifier
)
.unwrap();
} else {
writeln!(
self.output,
"export interface {} {{",
exported_shape.type_identifier
)
.unwrap();
self.indent += 1;
self.write_struct_fields_for_shape(field_owner_shape, fields);
self.indent -= 1;
self.output.push_str("}\n");
}
}
}
self.output.push('\n');
}
fn generate_enum(&mut self, shape: &'static Shape, enum_type: &facet_core::EnumType) {
let all_unit = enum_type
.variants
.iter()
.all(|v| matches!(v.data.kind, StructKind::Unit));
let is_untagged = shape.is_untagged();
if let Some(tag_key) = shape.tag {
let mut variant_types = Vec::new();
for variant in enum_type.variants {
let variant_name = variant.effective_name();
match variant.data.kind {
StructKind::Unit => {
variant_types.push(format!("{{ {}: \"{}\" }}", tag_key, variant_name));
}
StructKind::TupleStruct if variant.data.fields.len() == 1 => {
let inner = self.type_for_shape(variant.data.fields[0].shape.get());
variant_types.push(format!(
"{{ {}: \"{}\" }} & {}",
tag_key, variant_name, inner
));
}
StructKind::TupleStruct => {
let types: Vec<String> = variant
.data
.fields
.iter()
.map(|f| self.type_for_shape(f.shape.get()))
.collect();
variant_types.push(format!(
"{{ {}: \"{}\"; _: [{}] }}",
tag_key,
variant_name,
types.join(", ")
));
}
_ => {
let field_types = self.collect_inline_fields(variant.data.fields, false);
variant_types.push(format!(
"{{ {}: \"{}\"; {} }}",
tag_key,
variant_name,
field_types.join("; ")
));
}
}
}
writeln!(
self.output,
"export type {} =\n | {};",
shape.type_identifier,
variant_types.join("\n | ")
)
.unwrap();
} else if is_untagged {
let mut variant_types = Vec::new();
for variant in enum_type.variants {
match variant.data.kind {
StructKind::Unit => {
let variant_name = variant.effective_name();
variant_types.push(format!("\"{}\"", variant_name));
}
StructKind::TupleStruct if variant.data.fields.len() == 1 => {
let inner = self.type_for_shape(variant.data.fields[0].shape.get());
variant_types.push(inner);
}
StructKind::TupleStruct => {
let types: Vec<String> = variant
.data
.fields
.iter()
.map(|f| self.type_for_shape(f.shape.get()))
.collect();
variant_types.push(format!("[{}]", types.join(", ")));
}
_ => {
let field_types = self.collect_inline_fields(variant.data.fields, false);
variant_types.push(format!("{{ {} }}", field_types.join("; ")));
}
}
}
writeln!(
self.output,
"export type {} = {};",
shape.type_identifier,
variant_types.join(" | ")
)
.unwrap();
} else if all_unit {
let variants: Vec<String> = enum_type
.variants
.iter()
.map(|v| format!("\"{}\"", v.effective_name()))
.collect();
writeln!(
self.output,
"export type {} = {};",
shape.type_identifier,
variants.join(" | ")
)
.unwrap();
} else {
let mut variant_types = Vec::new();
for variant in enum_type.variants {
let variant_name = variant.effective_name();
match variant.data.kind {
StructKind::Unit => {
variant_types.push(format!("\"{}\"", variant_name));
}
StructKind::TupleStruct if variant.data.fields.len() == 1 => {
let inner = self.type_for_shape(variant.data.fields[0].shape.get());
variant_types.push(format!("{{ {}: {} }}", variant_name, inner));
}
StructKind::TupleStruct => {
let types: Vec<String> = variant
.data
.fields
.iter()
.map(|f| self.type_for_shape(f.shape.get()))
.collect();
variant_types.push(format!(
"{{ {}: [{}] }}",
variant_name,
types.join(", ")
));
}
_ => {
let field_types = self.collect_inline_fields(variant.data.fields, false);
variant_types.push(format!(
"{{ {}: {{ {} }} }}",
variant_name,
field_types.join("; ")
));
}
}
}
writeln!(
self.output,
"export type {} =\n | {};",
shape.type_identifier,
variant_types.join("\n | ")
)
.unwrap();
}
self.output.push('\n');
}
fn type_for_shape(&mut self, shape: &'static Shape) -> String {
match &shape.def {
Def::Scalar => self.scalar_type(shape),
Def::Option(opt) => {
format!("{} | null", self.type_for_shape(opt.t))
}
Def::List(list) => {
format!("{}[]", self.type_for_shape(list.t))
}
Def::Array(arr) => {
format!("{}[]", self.type_for_shape(arr.t))
}
Def::Set(set) => {
format!("{}[]", self.type_for_shape(set.t))
}
Def::Map(map) => {
format!("Record<string, {}>", self.type_for_shape(map.v))
}
Def::Pointer(ptr) => {
if let Some(pointee) = ptr.pointee {
self.type_for_shape(pointee)
} else {
"unknown".to_string()
}
}
Def::Undefined => {
match &shape.ty {
Type::User(UserType::Struct(st)) => {
if st.kind == StructKind::Tuple {
let types: Vec<String> = st
.fields
.iter()
.map(|f| self.type_for_shape(f.shape.get()))
.collect();
format!("[{}]", types.join(", "))
} else {
self.add_shape(shape);
shape.type_identifier.to_string()
}
}
Type::User(UserType::Enum(_)) => {
self.add_shape(shape);
shape.type_identifier.to_string()
}
_ => {
if let Some(inner) = shape.inner {
self.type_for_shape(inner)
} else {
"unknown".to_string()
}
}
}
}
_ => {
if let Some(inner) = shape.inner {
self.type_for_shape(inner)
} else {
"unknown".to_string()
}
}
}
}
fn scalar_type(&self, shape: &'static Shape) -> String {
match shape.type_identifier {
"String" | "str" | "&str" | "Cow" => "string".to_string(),
"bool" => "boolean".to_string(),
"u8" | "u16" | "u32" | "u64" | "u128" | "usize" | "i8" | "i16" | "i32" | "i64"
| "i128" | "isize" | "f32" | "f64" => "number".to_string(),
"char" => "string".to_string(),
"NaiveDate"
| "NaiveDateTime"
| "NaiveTime"
| "DateTime<Utc>"
| "DateTime<FixedOffset>"
| "DateTime<Local>"
if shape.module_path == Some("chrono") =>
{
"string".to_string()
}
_ => "unknown".to_string(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::collections::BTreeMap;
use facet::Facet;
#[test]
fn test_simple_struct() {
#[derive(Facet)]
struct User {
name: String,
age: u32,
}
let ts = to_typescript::<User>();
insta::assert_snapshot!(ts);
}
#[test]
fn test_optional_field() {
#[derive(Facet)]
struct Config {
required: String,
optional: Option<String>,
}
let ts = to_typescript::<Config>();
insta::assert_snapshot!(ts);
}
#[test]
fn test_simple_enum() {
#[derive(Facet)]
#[repr(u8)]
enum Status {
Active,
Inactive,
Pending,
}
let ts = to_typescript::<Status>();
insta::assert_snapshot!(ts);
}
#[test]
fn test_vec() {
#[derive(Facet)]
struct Data {
items: Vec<String>,
}
let ts = to_typescript::<Data>();
insta::assert_snapshot!(ts);
}
#[test]
fn test_nested_types() {
#[derive(Facet)]
struct Inner {
value: i32,
}
#[derive(Facet)]
struct Outer {
inner: Inner,
name: String,
}
let ts = to_typescript::<Outer>();
insta::assert_snapshot!(ts);
}
#[test]
fn test_enum_rename_all_snake_case() {
#[derive(Facet)]
#[facet(rename_all = "snake_case")]
#[repr(u8)]
enum ValidationErrorCode {
CircularDependency,
InvalidNaming,
UnknownRequirement,
}
let ts = to_typescript::<ValidationErrorCode>();
insta::assert_snapshot!(ts);
}
#[test]
fn test_enum_rename_individual() {
#[derive(Facet)]
#[repr(u8)]
enum GitStatus {
#[facet(rename = "dirty")]
Dirty,
#[facet(rename = "staged")]
Staged,
#[facet(rename = "clean")]
Clean,
}
let ts = to_typescript::<GitStatus>();
insta::assert_snapshot!(ts);
}
#[test]
fn test_struct_rename_all_camel_case() {
#[derive(Facet)]
#[facet(rename_all = "camelCase")]
struct ApiResponse {
user_name: String,
created_at: String,
is_active: bool,
}
let ts = to_typescript::<ApiResponse>();
insta::assert_snapshot!(ts);
}
#[test]
fn test_struct_rename_individual() {
#[derive(Facet)]
struct UserProfile {
#[facet(rename = "userName")]
user_name: String,
#[facet(rename = "emailAddress")]
email: String,
}
let ts = to_typescript::<UserProfile>();
insta::assert_snapshot!(ts);
}
#[test]
fn test_enum_with_data_rename_all() {
#[derive(Facet)]
#[facet(rename_all = "snake_case")]
#[repr(C)]
#[allow(dead_code)]
enum Message {
TextMessage { content: String },
ImageUpload { url: String, width: u32 },
}
let ts = to_typescript::<Message>();
insta::assert_snapshot!(ts);
}
#[test]
fn test_tagged_enum_unit_and_data_variants() {
#[derive(Facet)]
#[facet(rename_all = "snake_case")]
#[repr(u8)]
#[allow(dead_code)]
enum ResponseStatus {
Pending,
Ok(String),
Error { message: String },
Cancelled,
}
let ts = to_typescript::<ResponseStatus>();
insta::assert_snapshot!("tagged_enum_unit_and_data_variants", ts);
}
#[test]
fn test_struct_with_tuple_field() {
#[derive(Facet)]
struct Container {
coordinates: (i32, i32),
}
let ts = to_typescript::<Container>();
insta::assert_snapshot!(ts);
}
#[test]
fn test_struct_with_single_element_tuple() {
#[derive(Facet)]
struct Wrapper {
value: (String,),
}
let ts = to_typescript::<Wrapper>();
insta::assert_snapshot!(ts);
}
#[test]
fn test_enum_with_tuple_variant() {
#[derive(Facet)]
#[repr(C)]
#[allow(dead_code)]
enum Event {
Click { x: i32, y: i32 },
Move((i32, i32)),
Resize { dimensions: (u32, u32) },
}
let ts = to_typescript::<Event>();
insta::assert_snapshot!(ts);
}
#[test]
fn test_untagged_enum() {
#[derive(Facet)]
#[facet(untagged)]
#[repr(C)]
#[allow(dead_code)]
pub enum Value {
Text(String),
Number(f64),
}
let ts = to_typescript::<Value>();
insta::assert_snapshot!(ts);
}
#[test]
fn test_untagged_enum_unit_and_struct_variants() {
#[derive(Facet)]
#[facet(untagged)]
#[repr(C)]
#[allow(dead_code)]
pub enum Event {
None,
Data { x: i32, y: i32 },
}
let ts = to_typescript::<Event>();
insta::assert_snapshot!(ts);
}
#[test]
fn test_enum_with_tuple_struct_variant() {
#[derive(Facet)]
#[allow(dead_code)]
pub struct Point {
x: f64,
y: f64,
}
#[derive(Facet)]
#[repr(u8)]
#[allow(dead_code)]
pub enum Shape {
Line(Point, Point),
}
let ts = to_typescript::<Shape>();
insta::assert_snapshot!(ts);
}
#[test]
fn test_enum_with_proxy_struct() {
#[derive(Facet)]
#[facet(proxy = PointProxy)]
#[allow(dead_code)]
pub struct Point {
xxx: f64,
yyy: f64,
}
#[derive(Facet)]
#[allow(dead_code)]
pub struct PointProxy {
x: f64,
y: f64,
}
impl From<PointProxy> for Point {
fn from(p: PointProxy) -> Self {
Self { xxx: p.x, yyy: p.y }
}
}
impl From<&Point> for PointProxy {
fn from(p: &Point) -> Self {
Self { x: p.xxx, y: p.yyy }
}
}
#[derive(Facet)]
#[repr(u8)]
#[facet(untagged)]
#[allow(dead_code)]
pub enum Shape {
Circle { center: Point, radius: f64 },
Line(Point, Point),
}
let ts = to_typescript::<Shape>();
insta::assert_snapshot!(ts);
}
#[test]
fn test_enum_with_proxy_enum() {
#[derive(Facet)]
#[repr(u8)]
#[facet(proxy = StatusProxy)]
pub enum Status {
Unknown,
}
#[derive(Facet)]
#[repr(u8)]
pub enum StatusProxy {
Active,
Inactive,
}
impl From<StatusProxy> for Status {
fn from(_: StatusProxy) -> Self {
Self::Unknown
}
}
impl From<&Status> for StatusProxy {
fn from(_: &Status) -> Self {
Self::Active
}
}
let ts = to_typescript::<Status>();
insta::assert_snapshot!(ts);
}
#[test]
fn test_proxy_to_scalar() {
#[derive(Facet)]
#[facet(proxy = String)]
#[allow(dead_code)]
pub struct UserId(u64);
impl From<String> for UserId {
fn from(s: String) -> Self {
Self(s.parse().unwrap_or(0))
}
}
impl From<&UserId> for String {
fn from(id: &UserId) -> Self {
id.0.to_string()
}
}
let ts = to_typescript::<UserId>();
insta::assert_snapshot!(ts);
}
#[test]
fn test_proxy_preserves_doc_comments() {
#[derive(Facet)]
#[facet(proxy = PointProxy)]
#[allow(dead_code)]
pub struct Point {
internal_x: f64,
internal_y: f64,
}
#[derive(Facet)]
#[allow(dead_code)]
pub struct PointProxy {
x: f64,
y: f64,
}
impl From<PointProxy> for Point {
fn from(p: PointProxy) -> Self {
Self {
internal_x: p.x,
internal_y: p.y,
}
}
}
impl From<&Point> for PointProxy {
fn from(p: &Point) -> Self {
Self {
x: p.internal_x,
y: p.internal_y,
}
}
}
let ts = to_typescript::<Point>();
insta::assert_snapshot!(ts);
}
#[test]
fn test_untagged_enum_optional_fields() {
#[derive(Facet)]
#[facet(untagged)]
#[repr(C)]
#[allow(dead_code)]
pub enum Config {
Simple {
name: String,
},
Full {
name: String,
description: Option<String>,
count: Option<u32>,
},
}
let ts = to_typescript::<Config>();
insta::assert_snapshot!(ts);
}
#[test]
fn test_flatten_variants() {
use std::sync::Arc;
#[derive(Facet)]
pub struct Coords {
pub x: i32,
pub y: i32,
#[facet(skip)]
pub internal: u8,
}
#[derive(Facet)]
pub struct FlattenDirect {
pub name: String,
#[facet(flatten)]
pub coords: Coords,
}
#[derive(Facet)]
pub struct FlattenArc {
pub name: String,
#[facet(flatten)]
pub coords: Arc<Coords>,
}
#[derive(Facet)]
pub struct FlattenBox {
pub name: String,
#[facet(flatten)]
pub coords: Box<Coords>,
}
#[derive(Facet)]
pub struct FlattenOption {
pub name: String,
#[facet(flatten)]
pub coords: Option<Coords>,
}
#[derive(Facet)]
pub struct FlattenOptionArc {
pub name: String,
#[facet(flatten)]
pub coords: Option<Arc<Coords>>,
}
#[derive(Facet)]
pub struct FlattenMap {
pub name: String,
#[facet(flatten)]
pub extra: BTreeMap<String, String>,
}
let ts_direct = to_typescript::<FlattenDirect>();
let ts_arc = to_typescript::<FlattenArc>();
let ts_box = to_typescript::<FlattenBox>();
let ts_option = to_typescript::<FlattenOption>();
let ts_option_arc = to_typescript::<FlattenOptionArc>();
let ts_map = to_typescript::<FlattenMap>();
insta::assert_snapshot!("flatten_direct", ts_direct);
insta::assert_snapshot!("flatten_arc", ts_arc);
insta::assert_snapshot!("flatten_box", ts_box);
insta::assert_snapshot!("flatten_option", ts_option);
insta::assert_snapshot!("flatten_option_arc", ts_option_arc);
insta::assert_snapshot!("flatten_map", ts_map);
}
#[test]
fn test_tagged_enum_optional_fields() {
#[derive(Facet)]
#[repr(u8)]
#[allow(dead_code)]
enum Message {
Simple {
text: String,
},
Full {
text: String,
metadata: Option<String>,
count: Option<u32>,
},
}
let ts = to_typescript::<Message>();
insta::assert_snapshot!(ts);
}
#[test]
fn test_flatten_proxy_struct() {
#[derive(Facet)]
#[facet(proxy = CoordsProxy)]
#[allow(dead_code)]
struct Coords {
internal_x: f64,
internal_y: f64,
}
#[derive(Facet)]
#[allow(dead_code)]
struct CoordsProxy {
x: f64,
y: f64,
}
impl From<CoordsProxy> for Coords {
fn from(p: CoordsProxy) -> Self {
Self {
internal_x: p.x,
internal_y: p.y,
}
}
}
impl From<&Coords> for CoordsProxy {
fn from(c: &Coords) -> Self {
Self {
x: c.internal_x,
y: c.internal_y,
}
}
}
#[derive(Facet)]
#[allow(dead_code)]
struct Shape {
name: String,
#[facet(flatten)]
coords: Coords,
}
let ts = to_typescript::<Shape>();
insta::assert_snapshot!(ts);
}
#[test]
fn test_enum_variant_skipped_field() {
#[derive(Facet)]
#[repr(u8)]
#[allow(dead_code)]
enum Event {
Data {
visible: String,
#[facet(skip)]
internal: u64,
},
}
let ts = to_typescript::<Event>();
insta::assert_snapshot!(ts);
}
#[test]
fn test_enum_variant_flatten() {
#[derive(Facet)]
#[allow(dead_code)]
struct Metadata {
author: String,
version: u32,
}
#[derive(Facet)]
#[repr(u8)]
#[allow(dead_code)]
enum Document {
Article {
title: String,
#[facet(flatten)]
meta: Metadata,
},
}
let ts = to_typescript::<Document>();
insta::assert_snapshot!(ts);
}
#[test]
fn test_nested_flatten_struct() {
#[derive(Facet)]
#[allow(dead_code)]
struct Inner {
x: i32,
y: i32,
}
#[derive(Facet)]
#[allow(dead_code)]
struct Middle {
#[facet(flatten)]
inner: Inner,
z: i32,
}
#[derive(Facet)]
#[allow(dead_code)]
struct Outer {
name: String,
#[facet(flatten)]
middle: Middle,
}
let ts = to_typescript::<Outer>();
insta::assert_snapshot!(ts);
}
#[test]
fn test_flatten_recursive_option_box() {
#[derive(Facet)]
struct Node {
value: u32,
#[facet(flatten)]
next: Option<Box<Node>>,
}
let ts = to_typescript::<Node>();
insta::assert_snapshot!("flatten_recursive_option_box", ts);
}
#[test]
fn test_skip_serializing_struct_field() {
#[derive(Facet)]
struct Data {
visible: String,
#[facet(skip_serializing)]
internal: u64,
}
let ts = to_typescript::<Data>();
insta::assert_snapshot!("skip_serializing_struct_field", ts);
}
#[test]
fn test_skip_serializing_inline_enum_variant_and_flatten_cycle_guard() {
#[derive(Facet)]
struct Node {
value: u32,
#[facet(flatten)]
next: Option<Box<Node>>,
}
#[derive(Facet)]
#[repr(u8)]
enum Wrapper {
Item {
#[facet(flatten)]
node: Node,
},
Data {
visible: String,
#[facet(skip_serializing)]
internal: u64,
},
}
let item = Wrapper::Item {
node: Node {
value: 1,
next: None,
},
};
match item {
Wrapper::Item { node } => assert_eq!(node.value, 1),
Wrapper::Data { .. } => unreachable!(),
}
let data = Wrapper::Data {
visible: String::new(),
internal: 0,
};
match data {
Wrapper::Data { visible, internal } => {
assert!(visible.is_empty());
assert_eq!(internal, 0);
}
Wrapper::Item { .. } => unreachable!(),
}
let ts = to_typescript::<Wrapper>();
insta::assert_snapshot!(
"skip_serializing_inline_enum_variant_and_flatten_cycle_guard",
ts
);
}
#[test]
fn test_empty_struct() {
#[derive(Facet)]
struct Data {
empty: Empty,
}
#[derive(Facet)]
struct Empty {}
let e = to_typescript::<Empty>();
let d = to_typescript::<Data>();
insta::assert_snapshot!("test_empty_struct", e);
insta::assert_snapshot!("test_empty_struct_wrap", d);
}
#[test]
fn test_empty_struct_with_skipped_fields() {
#[derive(Facet)]
struct EmptyAfterSkip {
#[facet(skip_serializing)]
internal: String,
}
let ts = to_typescript::<EmptyAfterSkip>();
insta::assert_snapshot!("test_empty_struct_with_skipped_fields", ts);
}
#[test]
fn test_empty_struct_multiple_references() {
#[derive(Facet)]
struct Container {
first: Empty,
second: Empty,
third: Option<Empty>,
}
#[derive(Facet)]
struct Empty {}
let ts = to_typescript::<Container>();
insta::assert_snapshot!("test_empty_struct_multiple_references", ts);
}
#[test]
fn test_flatten_empty_struct() {
#[derive(Facet)]
struct Empty {}
#[derive(Facet)]
struct Wrapper {
#[facet(flatten)]
empty: Empty,
}
let ts = to_typescript::<Wrapper>();
insta::assert_snapshot!("test_flatten_empty_struct", ts);
}
#[test]
fn test_default_not_required() {
#[derive(Facet, Default)]
struct Def {
pub a: i32,
pub b: i32,
}
#[derive(Facet)]
struct Wrapper {
pub a: String,
#[facet(default)]
pub d: Def,
}
let ts = to_typescript::<Wrapper>();
insta::assert_snapshot!("test_default_not_required", ts);
}
#[test]
fn test_default_mixed_fields() {
#[derive(Facet)]
struct MixedDefaults {
pub required: String,
pub optional: Option<String>,
#[facet(default)]
pub with_default: i32,
#[facet(default = 100)]
pub with_default_expr: i32,
#[facet(default)]
pub option_with_default: Option<String>,
}
let ts = to_typescript::<MixedDefaults>();
insta::assert_snapshot!("test_default_mixed_fields", ts);
}
#[test]
fn test_default_in_flattened_struct() {
#[derive(Facet)]
struct FlattenedInner {
pub foo: String,
#[facet(default)]
pub bar: u32,
}
#[derive(Facet)]
struct WithFlatten {
pub outer_field: String,
#[facet(flatten)]
pub inner: FlattenedInner,
}
let ts = to_typescript::<WithFlatten>();
insta::assert_snapshot!("test_default_in_flattened_struct", ts);
}
#[test]
fn test_default_in_enum_variant() {
#[derive(Facet)]
#[allow(dead_code)]
#[repr(C)]
enum Message {
Text {
content: String,
},
Data {
required: String,
#[facet(default)]
optional: i32,
},
}
let ts = to_typescript::<Message>();
insta::assert_snapshot!("test_default_in_enum_variant", ts);
}
#[test]
fn test_untagged_enum_unit_and_newtype_variants() {
#[derive(Facet, Clone, PartialEq, PartialOrd)]
#[repr(C)]
#[allow(dead_code)]
#[facet(untagged)]
pub enum Enum {
Daily,
Weekly,
Custom(f64),
}
let ts = to_typescript::<Enum>();
insta::assert_snapshot!("test_untagged_enum_unit_and_newtype_variants", ts);
}
#[test]
fn test_untagged_enum_with_tuple_variant() {
#[derive(Facet)]
#[repr(C)]
#[allow(dead_code)]
#[facet(untagged)]
pub enum Message {
Text(String),
Pair(String, i32),
Struct { x: i32, y: i32 },
}
let ts = to_typescript::<Message>();
insta::assert_snapshot!("test_untagged_enum_with_tuple_variant", ts);
}
#[test]
fn test_chrono_naive_date() {
use chrono::NaiveDate;
#[derive(Facet)]
struct WithChronoDate {
birthday: NaiveDate,
}
let ts = to_typescript::<WithChronoDate>();
insta::assert_snapshot!(ts);
}
#[test]
fn test_non_transparent_newtype_is_not_scalar_alias() {
#[derive(Facet)]
struct Envelope {
id: BacktraceId,
}
#[derive(Facet, Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct BacktraceId(u64);
let mut ts_gen = TypeScriptGenerator::new();
ts_gen.add_type::<Envelope>();
let out = ts_gen.finish();
assert!(
!out.contains("export type BacktraceId = number;"),
"bug: non-transparent tuple newtype generated scalar alias:\n{out}"
);
insta::assert_snapshot!("non_transparent_newtype", out);
}
#[test]
fn test_transparent_newtype_is_scalar_alias() {
#[derive(Facet, Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[facet(transparent)]
struct TransparentId(u64);
let ts = to_typescript::<TransparentId>();
assert!(
ts.contains("export type TransparentId = number;"),
"bug: transparent tuple newtype did not generate scalar alias:\n{ts}"
);
insta::assert_snapshot!("transparent_newtype", ts);
}
#[test]
fn test_internally_tagged_enum() {
#[derive(Facet)]
#[facet(tag = "type")]
#[repr(C)]
#[allow(dead_code)]
enum Rr {
Mat,
Sp { first_roll: u32, long_last: bool },
}
let ts = to_typescript::<Rr>();
insta::assert_snapshot!(ts);
}
#[test]
fn test_non_transparent_single_field_tuple_struct() {
#[derive(Facet)]
struct Spread(pub i32);
let ts = to_typescript::<Spread>();
assert!(
ts.contains("export type Spread = [number];"),
"bug: non-transparent single-field tuple struct should be [number], got:\n{ts}"
);
insta::assert_snapshot!(ts);
}
}