1extern crate alloc;
24
25use alloc::collections::BTreeSet;
26use alloc::string::String;
27use alloc::vec::Vec;
28use core::fmt::Write;
29
30use facet_core::{Def, Facet, Field, Shape, StructKind, Type, UserType};
31
32pub fn to_typescript<T: Facet<'static>>() -> String {
36 let mut generator = TypeScriptGenerator::new();
37 generator.add_shape(T::SHAPE);
38 generator.finish()
39}
40
41pub struct TypeScriptGenerator {
45 output: String,
46 generated: BTreeSet<&'static str>,
48 queue: Vec<&'static Shape>,
50 indent: usize,
52}
53
54impl Default for TypeScriptGenerator {
55 fn default() -> Self {
56 Self::new()
57 }
58}
59
60impl TypeScriptGenerator {
61 pub const fn new() -> Self {
63 Self {
64 output: String::new(),
65 generated: BTreeSet::new(),
66 queue: Vec::new(),
67 indent: 0,
68 }
69 }
70
71 pub fn add_type<T: Facet<'static>>(&mut self) {
73 self.add_shape(T::SHAPE);
74 }
75
76 pub fn add_shape(&mut self, shape: &'static Shape) {
78 if !self.generated.contains(shape.type_identifier) {
79 self.queue.push(shape);
80 }
81 }
82
83 pub fn finish(mut self) -> String {
85 while let Some(shape) = self.queue.pop() {
87 if self.generated.contains(shape.type_identifier) {
88 continue;
89 }
90 self.generated.insert(shape.type_identifier);
91 self.generate_shape(shape);
92 }
93 self.output
94 }
95
96 fn write_indent(&mut self) {
97 for _ in 0..self.indent {
98 self.output.push_str(" ");
99 }
100 }
101
102 #[inline]
103 fn shape_key(shape: &'static Shape) -> &'static str {
104 shape.type_identifier
105 }
106
107 fn unwrap_to_inner_shape(shape: &'static Shape) -> (&'static Shape, bool) {
111 if let Def::Option(opt) = &shape.def {
113 let (inner, _) = Self::unwrap_to_inner_shape(opt.t);
114 return (inner, true);
115 }
116 if let Def::Pointer(ptr) = &shape.def
118 && let Some(pointee) = ptr.pointee
119 {
120 return Self::unwrap_to_inner_shape(pointee);
121 }
122 if let Some(inner) = shape.inner {
124 let (inner_shape, is_optional) = Self::unwrap_to_inner_shape(inner);
125 return (inner_shape, is_optional);
126 }
127 if let Some(proxy_def) = shape.proxy {
129 return Self::unwrap_to_inner_shape(proxy_def.shape);
130 }
131 (shape, false)
132 }
133
134 fn format_inline_field(&mut self, field: &Field, force_optional: bool) -> String {
137 let field_name = field.effective_name();
138 let field_shape = field.shape.get();
139 let has_default = field.default.is_some();
140
141 if let Def::Option(opt) = &field_shape.def {
142 let inner_type = self.type_for_shape(opt.t);
143 format!("{}?: {}", field_name, inner_type)
144 } else if force_optional || has_default {
145 let field_type = self.type_for_shape(field_shape);
146 format!("{}?: {}", field_name, field_type)
147 } else {
148 let field_type = self.type_for_shape(field_shape);
149 format!("{}: {}", field_name, field_type)
150 }
151 }
152
153 fn collect_inline_fields(
155 &mut self,
156 fields: &'static [Field],
157 force_optional: bool,
158 ) -> Vec<String> {
159 let mut flatten_stack: Vec<&'static str> = Vec::new();
160 self.collect_inline_fields_guarded(fields, force_optional, &mut flatten_stack)
161 }
162
163 fn collect_inline_fields_guarded(
164 &mut self,
165 fields: &'static [Field],
166 force_optional: bool,
167 flatten_stack: &mut Vec<&'static str>,
168 ) -> Vec<String> {
169 let mut result = Vec::new();
170 for field in fields {
171 if field.should_skip_serializing_unconditional() {
172 continue;
173 }
174 if field.is_flattened() {
175 let (inner_shape, parent_is_optional) =
176 Self::unwrap_to_inner_shape(field.shape.get());
177 if let Type::User(UserType::Struct(st)) = &inner_shape.ty {
178 let inner_key = Self::shape_key(inner_shape);
179 if flatten_stack.contains(&inner_key) {
180 continue;
181 }
182 flatten_stack.push(inner_key);
183 result.extend(self.collect_inline_fields_guarded(
184 st.fields,
185 force_optional || parent_is_optional,
186 flatten_stack,
187 ));
188 flatten_stack.pop();
189 continue;
190 }
191 }
192 result.push(self.format_inline_field(field, force_optional));
193 }
194 result
195 }
196
197 fn has_serializable_fields(
200 field_owner_shape: &'static Shape,
201 fields: &'static [Field],
202 ) -> bool {
203 let mut flatten_stack: Vec<&'static str> = Vec::new();
204 flatten_stack.push(Self::shape_key(field_owner_shape));
205 Self::has_serializable_fields_guarded(fields, &mut flatten_stack)
206 }
207
208 fn has_serializable_fields_guarded(
209 fields: &'static [Field],
210 flatten_stack: &mut Vec<&'static str>,
211 ) -> bool {
212 for field in fields {
213 if field.should_skip_serializing_unconditional() {
214 continue;
215 }
216 if field.is_flattened() {
217 let (inner_shape, _) = Self::unwrap_to_inner_shape(field.shape.get());
218 if let Type::User(UserType::Struct(st)) = &inner_shape.ty {
219 let inner_key = Self::shape_key(inner_shape);
220 if flatten_stack.contains(&inner_key) {
221 continue;
222 }
223 flatten_stack.push(inner_key);
224 let has_fields =
225 Self::has_serializable_fields_guarded(st.fields, flatten_stack);
226 flatten_stack.pop();
227 if has_fields {
228 return true;
229 }
230 continue;
231 }
232 }
233 return true;
235 }
236 false
237 }
238
239 fn write_struct_fields_for_shape(
241 &mut self,
242 field_owner_shape: &'static Shape,
243 fields: &'static [Field],
244 ) {
245 let mut flatten_stack: Vec<&'static str> = Vec::new();
246 flatten_stack.push(Self::shape_key(field_owner_shape));
247 self.write_struct_fields_guarded(fields, false, &mut flatten_stack);
248 }
249
250 fn write_struct_fields_guarded(
251 &mut self,
252 fields: &'static [Field],
253 force_optional: bool,
254 flatten_stack: &mut Vec<&'static str>,
255 ) {
256 for field in fields {
257 if field.should_skip_serializing_unconditional() {
258 continue;
259 }
260 if field.is_flattened() {
261 let (inner_shape, parent_is_optional) =
262 Self::unwrap_to_inner_shape(field.shape.get());
263 if let Type::User(UserType::Struct(st)) = &inner_shape.ty {
264 let inner_key = Self::shape_key(inner_shape);
265 if flatten_stack.contains(&inner_key) {
266 continue;
267 }
268 flatten_stack.push(inner_key);
269 self.write_struct_fields_guarded(
270 st.fields,
271 force_optional || parent_is_optional,
272 flatten_stack,
273 );
274 flatten_stack.pop();
275 continue;
276 }
277 }
278 self.write_field(field, force_optional);
279 }
280 }
281
282 fn write_field(&mut self, field: &Field, force_optional: bool) {
284 if !field.doc.is_empty() {
286 self.write_indent();
287 self.output.push_str("/**\n");
288 for line in field.doc {
289 self.write_indent();
290 self.output.push_str(" *");
291 self.output.push_str(line);
292 self.output.push('\n');
293 }
294 self.write_indent();
295 self.output.push_str(" */\n");
296 }
297
298 let field_name = field.effective_name();
299 let field_shape = field.shape.get();
300
301 self.write_indent();
302
303 let has_default = field.default.is_some();
305
306 if let Def::Option(opt) = &field_shape.def {
307 let inner_type = self.type_for_shape(opt.t);
308 writeln!(self.output, "{}?: {};", field_name, inner_type).unwrap();
309 } else if force_optional || has_default {
310 let field_type = self.type_for_shape(field_shape);
311 writeln!(self.output, "{}?: {};", field_name, field_type).unwrap();
312 } else {
313 let field_type = self.type_for_shape(field_shape);
314 writeln!(self.output, "{}: {};", field_name, field_type).unwrap();
315 }
316 }
317
318 fn generate_shape(&mut self, shape: &'static Shape) {
319 if let Some(inner) = shape.inner {
321 self.add_shape(inner);
322 let inner_type = self.type_for_shape(inner);
324 writeln!(
325 self.output,
326 "export type {} = {};",
327 shape.type_identifier, inner_type
328 )
329 .unwrap();
330 self.output.push('\n');
331 return;
332 }
333
334 if !shape.doc.is_empty() {
336 self.output.push_str("/**\n");
337 for line in shape.doc {
338 self.output.push_str(" *");
339 self.output.push_str(line);
340 self.output.push('\n');
341 }
342 self.output.push_str(" */\n");
343 }
344
345 if let Some(proxy_def) = shape.proxy {
348 let proxy_shape = proxy_def.shape;
349 match &proxy_shape.ty {
350 Type::User(UserType::Struct(st)) => {
351 self.generate_struct(shape, proxy_shape, st.fields, st.kind);
352 return;
353 }
354 Type::User(UserType::Enum(en)) => {
355 self.generate_enum(shape, en);
356 return;
357 }
358 _ => {
359 let proxy_type = self.type_for_shape(proxy_shape);
362 writeln!(
363 self.output,
364 "export type {} = {};",
365 shape.type_identifier, proxy_type
366 )
367 .unwrap();
368 self.output.push('\n');
369 return;
370 }
371 }
372 }
373
374 match &shape.ty {
375 Type::User(UserType::Struct(st)) => {
376 self.generate_struct(shape, shape, st.fields, st.kind);
377 }
378 Type::User(UserType::Enum(en)) => {
379 self.generate_enum(shape, en);
380 }
381 _ => {
382 let type_str = self.type_for_shape(shape);
384 writeln!(
385 self.output,
386 "export type {} = {};",
387 shape.type_identifier, type_str
388 )
389 .unwrap();
390 self.output.push('\n');
391 }
392 }
393 }
394
395 fn generate_struct(
396 &mut self,
397 exported_shape: &'static Shape,
398 field_owner_shape: &'static Shape,
399 fields: &'static [Field],
400 kind: StructKind,
401 ) {
402 match kind {
403 StructKind::Unit => {
404 writeln!(
406 self.output,
407 "export type {} = null;",
408 exported_shape.type_identifier
409 )
410 .unwrap();
411 }
412 StructKind::TupleStruct | StructKind::Tuple => {
413 let types: Vec<String> = fields
415 .iter()
416 .map(|f| self.type_for_shape(f.shape.get()))
417 .collect();
418 writeln!(
419 self.output,
420 "export type {} = [{}];",
421 exported_shape.type_identifier,
422 types.join(", ")
423 )
424 .unwrap();
425 }
426 StructKind::Struct => {
427 if !Self::has_serializable_fields(field_owner_shape, fields) {
429 writeln!(
430 self.output,
431 "export type {} = object;",
432 exported_shape.type_identifier
433 )
434 .unwrap();
435 } else {
436 writeln!(
437 self.output,
438 "export interface {} {{",
439 exported_shape.type_identifier
440 )
441 .unwrap();
442 self.indent += 1;
443
444 self.write_struct_fields_for_shape(field_owner_shape, fields);
445
446 self.indent -= 1;
447 self.output.push_str("}\n");
448 }
449 }
450 }
451 self.output.push('\n');
452 }
453
454 fn generate_enum(&mut self, shape: &'static Shape, enum_type: &facet_core::EnumType) {
455 let all_unit = enum_type
457 .variants
458 .iter()
459 .all(|v| matches!(v.data.kind, StructKind::Unit));
460
461 let is_untagged = shape.is_untagged();
463
464 if let Some(tag_key) = shape.tag {
465 let mut variant_types = Vec::new();
467
468 for variant in enum_type.variants {
469 let variant_name = variant.effective_name();
470 match variant.data.kind {
471 StructKind::Unit => {
472 variant_types.push(format!("{{ {}: \"{}\" }}", tag_key, variant_name));
473 }
474 StructKind::TupleStruct if variant.data.fields.len() == 1 => {
475 let inner = self.type_for_shape(variant.data.fields[0].shape.get());
477 variant_types.push(format!(
478 "{{ {}: \"{}\" }} & {}",
479 tag_key, variant_name, inner
480 ));
481 }
482 StructKind::TupleStruct => {
483 let types: Vec<String> = variant
485 .data
486 .fields
487 .iter()
488 .map(|f| self.type_for_shape(f.shape.get()))
489 .collect();
490 variant_types.push(format!(
491 "{{ {}: \"{}\"; _: [{}] }}",
492 tag_key,
493 variant_name,
494 types.join(", ")
495 ));
496 }
497 _ => {
498 let field_types = self.collect_inline_fields(variant.data.fields, false);
500 variant_types.push(format!(
501 "{{ {}: \"{}\"; {} }}",
502 tag_key,
503 variant_name,
504 field_types.join("; ")
505 ));
506 }
507 }
508 }
509
510 writeln!(
511 self.output,
512 "export type {} =\n | {};",
513 shape.type_identifier,
514 variant_types.join("\n | ")
515 )
516 .unwrap();
517 } else if is_untagged {
518 let mut variant_types = Vec::new();
520
521 for variant in enum_type.variants {
522 match variant.data.kind {
523 StructKind::Unit => {
524 let variant_name = variant.effective_name();
526 variant_types.push(format!("\"{}\"", variant_name));
527 }
528 StructKind::TupleStruct if variant.data.fields.len() == 1 => {
529 let inner = self.type_for_shape(variant.data.fields[0].shape.get());
531 variant_types.push(inner);
532 }
533 StructKind::TupleStruct => {
534 let types: Vec<String> = variant
536 .data
537 .fields
538 .iter()
539 .map(|f| self.type_for_shape(f.shape.get()))
540 .collect();
541 variant_types.push(format!("[{}]", types.join(", ")));
542 }
543 _ => {
544 let field_types = self.collect_inline_fields(variant.data.fields, false);
546 variant_types.push(format!("{{ {} }}", field_types.join("; ")));
547 }
548 }
549 }
550
551 writeln!(
552 self.output,
553 "export type {} = {};",
554 shape.type_identifier,
555 variant_types.join(" | ")
556 )
557 .unwrap();
558 } else if all_unit {
559 let variants: Vec<String> = enum_type
561 .variants
562 .iter()
563 .map(|v| format!("\"{}\"", v.effective_name()))
564 .collect();
565 writeln!(
566 self.output,
567 "export type {} = {};",
568 shape.type_identifier,
569 variants.join(" | ")
570 )
571 .unwrap();
572 } else {
573 let mut variant_types = Vec::new();
576
577 for variant in enum_type.variants {
578 let variant_name = variant.effective_name();
579 match variant.data.kind {
580 StructKind::Unit => {
581 variant_types.push(format!("\"{}\"", variant_name));
583 }
584 StructKind::TupleStruct if variant.data.fields.len() == 1 => {
585 let inner = self.type_for_shape(variant.data.fields[0].shape.get());
587 variant_types.push(format!("{{ {}: {} }}", variant_name, inner));
588 }
589 StructKind::TupleStruct => {
590 let types: Vec<String> = variant
592 .data
593 .fields
594 .iter()
595 .map(|f| self.type_for_shape(f.shape.get()))
596 .collect();
597 variant_types.push(format!(
598 "{{ {}: [{}] }}",
599 variant_name,
600 types.join(", ")
601 ));
602 }
603 _ => {
604 let field_types = self.collect_inline_fields(variant.data.fields, false);
606 variant_types.push(format!(
607 "{{ {}: {{ {} }} }}",
608 variant_name,
609 field_types.join("; ")
610 ));
611 }
612 }
613 }
614
615 writeln!(
616 self.output,
617 "export type {} =\n | {};",
618 shape.type_identifier,
619 variant_types.join("\n | ")
620 )
621 .unwrap();
622 }
623 self.output.push('\n');
624 }
625
626 fn type_for_shape(&mut self, shape: &'static Shape) -> String {
627 match &shape.def {
629 Def::Scalar => self.scalar_type(shape),
630 Def::Option(opt) => {
631 format!("{} | null", self.type_for_shape(opt.t))
632 }
633 Def::List(list) => {
634 format!("{}[]", self.type_for_shape(list.t))
635 }
636 Def::Array(arr) => {
637 format!("{}[]", self.type_for_shape(arr.t))
638 }
639 Def::Set(set) => {
640 format!("{}[]", self.type_for_shape(set.t))
641 }
642 Def::Map(map) => {
643 format!("Record<string, {}>", self.type_for_shape(map.v))
644 }
645 Def::Pointer(ptr) => {
646 if let Some(pointee) = ptr.pointee {
648 self.type_for_shape(pointee)
649 } else {
650 "unknown".to_string()
651 }
652 }
653 Def::Undefined => {
654 match &shape.ty {
656 Type::User(UserType::Struct(st)) => {
657 if st.kind == StructKind::Tuple {
660 let types: Vec<String> = st
661 .fields
662 .iter()
663 .map(|f| self.type_for_shape(f.shape.get()))
664 .collect();
665 format!("[{}]", types.join(", "))
666 } else {
667 self.add_shape(shape);
668 shape.type_identifier.to_string()
669 }
670 }
671 Type::User(UserType::Enum(_)) => {
672 self.add_shape(shape);
673 shape.type_identifier.to_string()
674 }
675 _ => {
676 if let Some(inner) = shape.inner {
678 self.type_for_shape(inner)
679 } else {
680 "unknown".to_string()
681 }
682 }
683 }
684 }
685 _ => {
686 if let Some(inner) = shape.inner {
688 self.type_for_shape(inner)
689 } else {
690 "unknown".to_string()
691 }
692 }
693 }
694 }
695
696 fn scalar_type(&self, shape: &'static Shape) -> String {
697 match shape.type_identifier {
698 "String" | "str" | "&str" | "Cow" => "string".to_string(),
700
701 "bool" => "boolean".to_string(),
703
704 "u8" | "u16" | "u32" | "u64" | "u128" | "usize" | "i8" | "i16" | "i32" | "i64"
706 | "i128" | "isize" | "f32" | "f64" => "number".to_string(),
707
708 "char" => "string".to_string(),
710
711 "NaiveDate"
713 | "NaiveDateTime"
714 | "NaiveTime"
715 | "DateTime<Utc>"
716 | "DateTime<FixedOffset>"
717 | "DateTime<Local>"
718 if shape.module_path == Some("chrono") =>
719 {
720 "string".to_string()
721 }
722
723 _ => "unknown".to_string(),
725 }
726 }
727}
728
729#[cfg(test)]
730mod tests {
731 use super::*;
732 use alloc::collections::BTreeMap;
733 use facet::Facet;
734
735 #[test]
736 fn test_simple_struct() {
737 #[derive(Facet)]
738 struct User {
739 name: String,
740 age: u32,
741 }
742
743 let ts = to_typescript::<User>();
744 insta::assert_snapshot!(ts);
745 }
746
747 #[test]
748 fn test_optional_field() {
749 #[derive(Facet)]
750 struct Config {
751 required: String,
752 optional: Option<String>,
753 }
754
755 let ts = to_typescript::<Config>();
756 insta::assert_snapshot!(ts);
757 }
758
759 #[test]
760 fn test_simple_enum() {
761 #[derive(Facet)]
762 #[repr(u8)]
763 enum Status {
764 Active,
765 Inactive,
766 Pending,
767 }
768
769 let ts = to_typescript::<Status>();
770 insta::assert_snapshot!(ts);
771 }
772
773 #[test]
774 fn test_vec() {
775 #[derive(Facet)]
776 struct Data {
777 items: Vec<String>,
778 }
779
780 let ts = to_typescript::<Data>();
781 insta::assert_snapshot!(ts);
782 }
783
784 #[test]
785 fn test_nested_types() {
786 #[derive(Facet)]
787 struct Inner {
788 value: i32,
789 }
790
791 #[derive(Facet)]
792 struct Outer {
793 inner: Inner,
794 name: String,
795 }
796
797 let ts = to_typescript::<Outer>();
798 insta::assert_snapshot!(ts);
799 }
800
801 #[test]
802 fn test_enum_rename_all_snake_case() {
803 #[derive(Facet)]
804 #[facet(rename_all = "snake_case")]
805 #[repr(u8)]
806 enum ValidationErrorCode {
807 CircularDependency,
808 InvalidNaming,
809 UnknownRequirement,
810 }
811
812 let ts = to_typescript::<ValidationErrorCode>();
813 insta::assert_snapshot!(ts);
814 }
815
816 #[test]
817 fn test_enum_rename_individual() {
818 #[derive(Facet)]
819 #[repr(u8)]
820 enum GitStatus {
821 #[facet(rename = "dirty")]
822 Dirty,
823 #[facet(rename = "staged")]
824 Staged,
825 #[facet(rename = "clean")]
826 Clean,
827 }
828
829 let ts = to_typescript::<GitStatus>();
830 insta::assert_snapshot!(ts);
831 }
832
833 #[test]
834 fn test_struct_rename_all_camel_case() {
835 #[derive(Facet)]
836 #[facet(rename_all = "camelCase")]
837 struct ApiResponse {
838 user_name: String,
839 created_at: String,
840 is_active: bool,
841 }
842
843 let ts = to_typescript::<ApiResponse>();
844 insta::assert_snapshot!(ts);
845 }
846
847 #[test]
848 fn test_struct_rename_individual() {
849 #[derive(Facet)]
850 struct UserProfile {
851 #[facet(rename = "userName")]
852 user_name: String,
853 #[facet(rename = "emailAddress")]
854 email: String,
855 }
856
857 let ts = to_typescript::<UserProfile>();
858 insta::assert_snapshot!(ts);
859 }
860
861 #[test]
862 fn test_enum_with_data_rename_all() {
863 #[derive(Facet)]
864 #[facet(rename_all = "snake_case")]
865 #[repr(C)]
866 #[allow(dead_code)]
867 enum Message {
868 TextMessage { content: String },
869 ImageUpload { url: String, width: u32 },
870 }
871
872 let ts = to_typescript::<Message>();
873 insta::assert_snapshot!(ts);
874 }
875
876 #[test]
877 fn test_tagged_enum_unit_and_data_variants() {
878 #[derive(Facet)]
879 #[facet(rename_all = "snake_case")]
880 #[repr(u8)]
881 #[allow(dead_code)]
882 enum ResponseStatus {
883 Pending,
884 Ok(String),
885 Error { message: String },
886 Cancelled,
887 }
888
889 let ts = to_typescript::<ResponseStatus>();
890 insta::assert_snapshot!("tagged_enum_unit_and_data_variants", ts);
891 }
892
893 #[test]
894 fn test_struct_with_tuple_field() {
895 #[derive(Facet)]
896 struct Container {
897 coordinates: (i32, i32),
898 }
899
900 let ts = to_typescript::<Container>();
901 insta::assert_snapshot!(ts);
902 }
903
904 #[test]
905 fn test_struct_with_single_element_tuple() {
906 #[derive(Facet)]
907 struct Wrapper {
908 value: (String,),
909 }
910
911 let ts = to_typescript::<Wrapper>();
912 insta::assert_snapshot!(ts);
913 }
914
915 #[test]
916 fn test_enum_with_tuple_variant() {
917 #[derive(Facet)]
918 #[repr(C)]
919 #[allow(dead_code)]
920 enum Event {
921 Click { x: i32, y: i32 },
922 Move((i32, i32)),
923 Resize { dimensions: (u32, u32) },
924 }
925
926 let ts = to_typescript::<Event>();
927 insta::assert_snapshot!(ts);
928 }
929
930 #[test]
931 fn test_untagged_enum() {
932 #[derive(Facet)]
933 #[facet(untagged)]
934 #[repr(C)]
935 #[allow(dead_code)]
936 pub enum Value {
937 Text(String),
938 Number(f64),
939 }
940
941 let ts = to_typescript::<Value>();
942 insta::assert_snapshot!(ts);
943 }
944
945 #[test]
946 fn test_untagged_enum_unit_and_struct_variants() {
947 #[derive(Facet)]
948 #[facet(untagged)]
949 #[repr(C)]
950 #[allow(dead_code)]
951 pub enum Event {
952 None,
953 Data { x: i32, y: i32 },
954 }
955
956 let ts = to_typescript::<Event>();
957 insta::assert_snapshot!(ts);
958 }
959
960 #[test]
961 fn test_enum_with_tuple_struct_variant() {
962 #[derive(Facet)]
963 #[allow(dead_code)]
964 pub struct Point {
965 x: f64,
966 y: f64,
967 }
968
969 #[derive(Facet)]
970 #[repr(u8)]
971 #[allow(dead_code)]
972 pub enum Shape {
973 Line(Point, Point),
974 }
975
976 let ts = to_typescript::<Shape>();
977 insta::assert_snapshot!(ts);
978 }
979
980 #[test]
981 fn test_enum_with_proxy_struct() {
982 #[derive(Facet)]
983 #[facet(proxy = PointProxy)]
984 #[allow(dead_code)]
985 pub struct Point {
986 xxx: f64,
987 yyy: f64,
988 }
989
990 #[derive(Facet)]
991 #[allow(dead_code)]
992 pub struct PointProxy {
993 x: f64,
994 y: f64,
995 }
996
997 impl From<PointProxy> for Point {
998 fn from(p: PointProxy) -> Self {
999 Self { xxx: p.x, yyy: p.y }
1000 }
1001 }
1002
1003 impl From<&Point> for PointProxy {
1004 fn from(p: &Point) -> Self {
1005 Self { x: p.xxx, y: p.yyy }
1006 }
1007 }
1008
1009 #[derive(Facet)]
1010 #[repr(u8)]
1011 #[facet(untagged)]
1012 #[allow(dead_code)]
1013 pub enum Shape {
1014 Circle { center: Point, radius: f64 },
1015 Line(Point, Point),
1016 }
1017
1018 let ts = to_typescript::<Shape>();
1019 insta::assert_snapshot!(ts);
1020 }
1021
1022 #[test]
1023 fn test_enum_with_proxy_enum() {
1024 #[derive(Facet)]
1025 #[repr(u8)]
1026 #[facet(proxy = StatusProxy)]
1027 pub enum Status {
1028 Unknown,
1029 }
1030
1031 #[derive(Facet)]
1032 #[repr(u8)]
1033 pub enum StatusProxy {
1034 Active,
1035 Inactive,
1036 }
1037
1038 impl From<StatusProxy> for Status {
1039 fn from(_: StatusProxy) -> Self {
1040 Self::Unknown
1041 }
1042 }
1043
1044 impl From<&Status> for StatusProxy {
1045 fn from(_: &Status) -> Self {
1046 Self::Active
1047 }
1048 }
1049
1050 let ts = to_typescript::<Status>();
1051 insta::assert_snapshot!(ts);
1052 }
1053
1054 #[test]
1055 fn test_proxy_to_scalar() {
1056 #[derive(Facet)]
1058 #[facet(proxy = String)]
1059 #[allow(dead_code)]
1060 pub struct UserId(u64);
1061
1062 impl From<String> for UserId {
1063 fn from(s: String) -> Self {
1064 Self(s.parse().unwrap_or(0))
1065 }
1066 }
1067
1068 impl From<&UserId> for String {
1069 fn from(id: &UserId) -> Self {
1070 id.0.to_string()
1071 }
1072 }
1073
1074 let ts = to_typescript::<UserId>();
1075 insta::assert_snapshot!(ts);
1076 }
1077
1078 #[test]
1079 fn test_proxy_preserves_doc_comments() {
1080 #[derive(Facet)]
1083 #[facet(proxy = PointProxy)]
1084 #[allow(dead_code)]
1085 pub struct Point {
1086 internal_x: f64,
1087 internal_y: f64,
1088 }
1089
1090 #[derive(Facet)]
1091 #[allow(dead_code)]
1092 pub struct PointProxy {
1093 x: f64,
1094 y: f64,
1095 }
1096
1097 impl From<PointProxy> for Point {
1098 fn from(p: PointProxy) -> Self {
1099 Self {
1100 internal_x: p.x,
1101 internal_y: p.y,
1102 }
1103 }
1104 }
1105
1106 impl From<&Point> for PointProxy {
1107 fn from(p: &Point) -> Self {
1108 Self {
1109 x: p.internal_x,
1110 y: p.internal_y,
1111 }
1112 }
1113 }
1114
1115 let ts = to_typescript::<Point>();
1116 insta::assert_snapshot!(ts);
1117 }
1118
1119 #[test]
1120 fn test_untagged_enum_optional_fields() {
1121 #[derive(Facet)]
1122 #[facet(untagged)]
1123 #[repr(C)]
1124 #[allow(dead_code)]
1125 pub enum Config {
1126 Simple {
1127 name: String,
1128 },
1129 Full {
1130 name: String,
1131 description: Option<String>,
1132 count: Option<u32>,
1133 },
1134 }
1135
1136 let ts = to_typescript::<Config>();
1137 insta::assert_snapshot!(ts);
1138 }
1139
1140 #[test]
1141 fn test_flatten_variants() {
1142 use std::sync::Arc;
1143
1144 #[derive(Facet)]
1146 pub struct Coords {
1147 pub x: i32,
1148 pub y: i32,
1149 #[facet(skip)]
1150 pub internal: u8,
1151 }
1152
1153 #[derive(Facet)]
1155 pub struct FlattenDirect {
1156 pub name: String,
1157 #[facet(flatten)]
1158 pub coords: Coords,
1159 }
1160
1161 #[derive(Facet)]
1163 pub struct FlattenArc {
1164 pub name: String,
1165 #[facet(flatten)]
1166 pub coords: Arc<Coords>,
1167 }
1168
1169 #[derive(Facet)]
1171 pub struct FlattenBox {
1172 pub name: String,
1173 #[facet(flatten)]
1174 pub coords: Box<Coords>,
1175 }
1176
1177 #[derive(Facet)]
1179 pub struct FlattenOption {
1180 pub name: String,
1181 #[facet(flatten)]
1182 pub coords: Option<Coords>,
1183 }
1184
1185 #[derive(Facet)]
1187 pub struct FlattenOptionArc {
1188 pub name: String,
1189 #[facet(flatten)]
1190 pub coords: Option<Arc<Coords>>,
1191 }
1192
1193 #[derive(Facet)]
1195 pub struct FlattenMap {
1196 pub name: String,
1197 #[facet(flatten)]
1198 pub extra: BTreeMap<String, String>,
1199 }
1200
1201 let ts_direct = to_typescript::<FlattenDirect>();
1202 let ts_arc = to_typescript::<FlattenArc>();
1203 let ts_box = to_typescript::<FlattenBox>();
1204 let ts_option = to_typescript::<FlattenOption>();
1205 let ts_option_arc = to_typescript::<FlattenOptionArc>();
1206 let ts_map = to_typescript::<FlattenMap>();
1207
1208 insta::assert_snapshot!("flatten_direct", ts_direct);
1209 insta::assert_snapshot!("flatten_arc", ts_arc);
1210 insta::assert_snapshot!("flatten_box", ts_box);
1211 insta::assert_snapshot!("flatten_option", ts_option);
1212 insta::assert_snapshot!("flatten_option_arc", ts_option_arc);
1213 insta::assert_snapshot!("flatten_map", ts_map);
1214 }
1215
1216 #[test]
1217 fn test_tagged_enum_optional_fields() {
1218 #[derive(Facet)]
1219 #[repr(u8)]
1220 #[allow(dead_code)]
1221 enum Message {
1222 Simple {
1223 text: String,
1224 },
1225 Full {
1226 text: String,
1227 metadata: Option<String>,
1228 count: Option<u32>,
1229 },
1230 }
1231
1232 let ts = to_typescript::<Message>();
1233 insta::assert_snapshot!(ts);
1234 }
1235
1236 #[test]
1237 fn test_flatten_proxy_struct() {
1238 #[derive(Facet)]
1239 #[facet(proxy = CoordsProxy)]
1240 #[allow(dead_code)]
1241 struct Coords {
1242 internal_x: f64,
1243 internal_y: f64,
1244 }
1245
1246 #[derive(Facet)]
1247 #[allow(dead_code)]
1248 struct CoordsProxy {
1249 x: f64,
1250 y: f64,
1251 }
1252
1253 impl From<CoordsProxy> for Coords {
1254 fn from(p: CoordsProxy) -> Self {
1255 Self {
1256 internal_x: p.x,
1257 internal_y: p.y,
1258 }
1259 }
1260 }
1261
1262 impl From<&Coords> for CoordsProxy {
1263 fn from(c: &Coords) -> Self {
1264 Self {
1265 x: c.internal_x,
1266 y: c.internal_y,
1267 }
1268 }
1269 }
1270
1271 #[derive(Facet)]
1272 #[allow(dead_code)]
1273 struct Shape {
1274 name: String,
1275 #[facet(flatten)]
1276 coords: Coords,
1277 }
1278
1279 let ts = to_typescript::<Shape>();
1280 insta::assert_snapshot!(ts);
1281 }
1282
1283 #[test]
1284 fn test_enum_variant_skipped_field() {
1285 #[derive(Facet)]
1286 #[repr(u8)]
1287 #[allow(dead_code)]
1288 enum Event {
1289 Data {
1290 visible: String,
1291 #[facet(skip)]
1292 internal: u64,
1293 },
1294 }
1295
1296 let ts = to_typescript::<Event>();
1297 insta::assert_snapshot!(ts);
1298 }
1299
1300 #[test]
1301 fn test_enum_variant_flatten() {
1302 #[derive(Facet)]
1304 #[allow(dead_code)]
1305 struct Metadata {
1306 author: String,
1307 version: u32,
1308 }
1309
1310 #[derive(Facet)]
1311 #[repr(u8)]
1312 #[allow(dead_code)]
1313 enum Document {
1314 Article {
1315 title: String,
1316 #[facet(flatten)]
1317 meta: Metadata,
1318 },
1319 }
1320
1321 let ts = to_typescript::<Document>();
1322 insta::assert_snapshot!(ts);
1323 }
1324
1325 #[test]
1326 fn test_nested_flatten_struct() {
1327 #[derive(Facet)]
1328 #[allow(dead_code)]
1329 struct Inner {
1330 x: i32,
1331 y: i32,
1332 }
1333
1334 #[derive(Facet)]
1335 #[allow(dead_code)]
1336 struct Middle {
1337 #[facet(flatten)]
1338 inner: Inner,
1339 z: i32,
1340 }
1341
1342 #[derive(Facet)]
1343 #[allow(dead_code)]
1344 struct Outer {
1345 name: String,
1346 #[facet(flatten)]
1347 middle: Middle,
1348 }
1349
1350 let ts = to_typescript::<Outer>();
1351 insta::assert_snapshot!(ts);
1352 }
1353
1354 #[test]
1355 fn test_flatten_recursive_option_box() {
1356 #[derive(Facet)]
1357 struct Node {
1358 value: u32,
1359 #[facet(flatten)]
1360 next: Option<Box<Node>>,
1361 }
1362
1363 let ts = to_typescript::<Node>();
1364 insta::assert_snapshot!("flatten_recursive_option_box", ts);
1365 }
1366
1367 #[test]
1368 fn test_skip_serializing_struct_field() {
1369 #[derive(Facet)]
1370 struct Data {
1371 visible: String,
1372 #[facet(skip_serializing)]
1373 internal: u64,
1374 }
1375
1376 let ts = to_typescript::<Data>();
1377 insta::assert_snapshot!("skip_serializing_struct_field", ts);
1378 }
1379
1380 #[test]
1381 fn test_skip_serializing_inline_enum_variant_and_flatten_cycle_guard() {
1382 #[derive(Facet)]
1383 struct Node {
1384 value: u32,
1385 #[facet(flatten)]
1386 next: Option<Box<Node>>,
1387 }
1388
1389 #[derive(Facet)]
1390 #[repr(u8)]
1391 enum Wrapper {
1392 Item {
1393 #[facet(flatten)]
1394 node: Node,
1395 },
1396 Data {
1397 visible: String,
1398 #[facet(skip_serializing)]
1399 internal: u64,
1400 },
1401 }
1402
1403 let item = Wrapper::Item {
1404 node: Node {
1405 value: 1,
1406 next: None,
1407 },
1408 };
1409 match item {
1410 Wrapper::Item { node } => assert_eq!(node.value, 1),
1411 Wrapper::Data { .. } => unreachable!(),
1412 }
1413
1414 let data = Wrapper::Data {
1415 visible: String::new(),
1416 internal: 0,
1417 };
1418 match data {
1419 Wrapper::Data { visible, internal } => {
1420 assert!(visible.is_empty());
1421 assert_eq!(internal, 0);
1422 }
1423 Wrapper::Item { .. } => unreachable!(),
1424 }
1425
1426 let ts = to_typescript::<Wrapper>();
1427 insta::assert_snapshot!(
1428 "skip_serializing_inline_enum_variant_and_flatten_cycle_guard",
1429 ts
1430 );
1431 }
1432
1433 #[test]
1434 fn test_empty_struct() {
1435 #[derive(Facet)]
1436 struct Data {
1437 empty: Empty,
1438 }
1439
1440 #[derive(Facet)]
1441 struct Empty {}
1442
1443 let e = to_typescript::<Empty>();
1444 let d = to_typescript::<Data>();
1445 insta::assert_snapshot!("test_empty_struct", e);
1446 insta::assert_snapshot!("test_empty_struct_wrap", d);
1447 }
1448
1449 #[test]
1450 fn test_empty_struct_with_skipped_fields() {
1451 #[derive(Facet)]
1452 struct EmptyAfterSkip {
1453 #[facet(skip_serializing)]
1454 internal: String,
1455 }
1456
1457 let ts = to_typescript::<EmptyAfterSkip>();
1458 insta::assert_snapshot!("test_empty_struct_with_skipped_fields", ts);
1459 }
1460
1461 #[test]
1462 fn test_empty_struct_multiple_references() {
1463 #[derive(Facet)]
1464 struct Container {
1465 first: Empty,
1466 second: Empty,
1467 third: Option<Empty>,
1468 }
1469
1470 #[derive(Facet)]
1471 struct Empty {}
1472
1473 let ts = to_typescript::<Container>();
1474 insta::assert_snapshot!("test_empty_struct_multiple_references", ts);
1475 }
1476
1477 #[test]
1478 fn test_flatten_empty_struct() {
1479 #[derive(Facet)]
1480 struct Empty {}
1481
1482 #[derive(Facet)]
1483 struct Wrapper {
1484 #[facet(flatten)]
1485 empty: Empty,
1486 }
1487
1488 let ts = to_typescript::<Wrapper>();
1489 insta::assert_snapshot!("test_flatten_empty_struct", ts);
1490 }
1491
1492 #[test]
1493 fn test_default_not_required() {
1494 #[derive(Facet, Default)]
1495 struct Def {
1496 pub a: i32,
1497 pub b: i32,
1498 }
1499
1500 #[derive(Facet)]
1501 struct Wrapper {
1502 pub a: String,
1503 #[facet(default)]
1504 pub d: Def,
1505 }
1506
1507 let ts = to_typescript::<Wrapper>();
1508 insta::assert_snapshot!("test_default_not_required", ts);
1509 }
1510
1511 #[test]
1512 fn test_default_mixed_fields() {
1513 #[derive(Facet)]
1514 struct MixedDefaults {
1515 pub required: String,
1516 pub optional: Option<String>,
1517 #[facet(default)]
1518 pub with_default: i32,
1519 #[facet(default = 100)]
1520 pub with_default_expr: i32,
1521 #[facet(default)]
1522 pub option_with_default: Option<String>,
1523 }
1524
1525 let ts = to_typescript::<MixedDefaults>();
1526 insta::assert_snapshot!("test_default_mixed_fields", ts);
1527 }
1528
1529 #[test]
1530 fn test_default_in_flattened_struct() {
1531 #[derive(Facet)]
1532 struct FlattenedInner {
1533 pub foo: String,
1534 #[facet(default)]
1535 pub bar: u32,
1536 }
1537
1538 #[derive(Facet)]
1539 struct WithFlatten {
1540 pub outer_field: String,
1541 #[facet(flatten)]
1542 pub inner: FlattenedInner,
1543 }
1544
1545 let ts = to_typescript::<WithFlatten>();
1546 insta::assert_snapshot!("test_default_in_flattened_struct", ts);
1547 }
1548
1549 #[test]
1550 fn test_default_in_enum_variant() {
1551 #[derive(Facet)]
1552 #[allow(dead_code)]
1553 #[repr(C)]
1554 enum Message {
1555 Text {
1556 content: String,
1557 },
1558 Data {
1559 required: String,
1560 #[facet(default)]
1561 optional: i32,
1562 },
1563 }
1564
1565 let ts = to_typescript::<Message>();
1566 insta::assert_snapshot!("test_default_in_enum_variant", ts);
1567 }
1568
1569 #[test]
1570 fn test_untagged_enum_unit_and_newtype_variants() {
1571 #[derive(Facet, Clone, PartialEq, PartialOrd)]
1572 #[repr(C)]
1573 #[allow(dead_code)]
1574 #[facet(untagged)]
1575 pub enum Enum {
1576 Daily,
1577 Weekly,
1578 Custom(f64),
1579 }
1580
1581 let ts = to_typescript::<Enum>();
1582 insta::assert_snapshot!("test_untagged_enum_unit_and_newtype_variants", ts);
1583 }
1584
1585 #[test]
1586 fn test_untagged_enum_with_tuple_variant() {
1587 #[derive(Facet)]
1588 #[repr(C)]
1589 #[allow(dead_code)]
1590 #[facet(untagged)]
1591 pub enum Message {
1592 Text(String),
1593 Pair(String, i32),
1594 Struct { x: i32, y: i32 },
1595 }
1596
1597 let ts = to_typescript::<Message>();
1598 insta::assert_snapshot!("test_untagged_enum_with_tuple_variant", ts);
1599 }
1600 #[test]
1601 fn test_chrono_naive_date() {
1602 use chrono::NaiveDate;
1603
1604 #[derive(Facet)]
1605 struct WithChronoDate {
1606 birthday: NaiveDate,
1607 }
1608
1609 let ts = to_typescript::<WithChronoDate>();
1610 insta::assert_snapshot!(ts);
1611 }
1612
1613 #[test]
1614 fn test_non_transparent_newtype_is_not_scalar_alias() {
1615 #[derive(Facet)]
1616 struct Envelope {
1617 id: BacktraceId,
1618 }
1619
1620 #[derive(Facet, Debug, Clone, Copy, PartialEq, Eq, Hash)]
1621 struct BacktraceId(u64);
1622
1623 let mut ts_gen = TypeScriptGenerator::new();
1624 ts_gen.add_type::<Envelope>();
1625 let out = ts_gen.finish();
1626
1627 assert!(
1628 !out.contains("export type BacktraceId = number;"),
1629 "bug: non-transparent tuple newtype generated scalar alias:\n{out}"
1630 );
1631 insta::assert_snapshot!("non_transparent_newtype", out);
1632 }
1633
1634 #[test]
1635 fn test_transparent_newtype_is_scalar_alias() {
1636 #[derive(Facet, Debug, Clone, Copy, PartialEq, Eq, Hash)]
1637 #[facet(transparent)]
1638 struct TransparentId(u64);
1639
1640 let ts = to_typescript::<TransparentId>();
1641 assert!(
1642 ts.contains("export type TransparentId = number;"),
1643 "bug: transparent tuple newtype did not generate scalar alias:\n{ts}"
1644 );
1645 insta::assert_snapshot!("transparent_newtype", ts);
1646 }
1647
1648 #[test]
1649 fn test_internally_tagged_enum() {
1650 #[derive(Facet)]
1651 #[facet(tag = "type")]
1652 #[repr(C)]
1653 #[allow(dead_code)]
1654 enum Rr {
1655 Mat,
1656 Sp { first_roll: u32, long_last: bool },
1657 }
1658
1659 let ts = to_typescript::<Rr>();
1660 insta::assert_snapshot!(ts);
1661 }
1662
1663 #[test]
1666 fn test_non_transparent_single_field_tuple_struct() {
1667 #[derive(Facet)]
1668 struct Spread(pub i32);
1669
1670 let ts = to_typescript::<Spread>();
1671 assert!(
1672 ts.contains("export type Spread = [number];"),
1673 "bug: non-transparent single-field tuple struct should be [number], got:\n{ts}"
1674 );
1675 insta::assert_snapshot!(ts);
1676 }
1677}