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 is_untagged {
465 let mut variant_types = Vec::new();
467
468 for variant in enum_type.variants {
469 match variant.data.kind {
470 StructKind::Unit => {
471 let variant_name = variant.effective_name();
473 variant_types.push(format!("\"{}\"", variant_name));
474 }
475 StructKind::TupleStruct if variant.data.fields.len() == 1 => {
476 let inner = self.type_for_shape(variant.data.fields[0].shape.get());
478 variant_types.push(inner);
479 }
480 StructKind::TupleStruct => {
481 let types: Vec<String> = variant
483 .data
484 .fields
485 .iter()
486 .map(|f| self.type_for_shape(f.shape.get()))
487 .collect();
488 variant_types.push(format!("[{}]", types.join(", ")));
489 }
490 _ => {
491 let field_types = self.collect_inline_fields(variant.data.fields, false);
493 variant_types.push(format!("{{ {} }}", field_types.join("; ")));
494 }
495 }
496 }
497
498 writeln!(
499 self.output,
500 "export type {} = {};",
501 shape.type_identifier,
502 variant_types.join(" | ")
503 )
504 .unwrap();
505 } else if all_unit {
506 let variants: Vec<String> = enum_type
508 .variants
509 .iter()
510 .map(|v| format!("\"{}\"", v.effective_name()))
511 .collect();
512 writeln!(
513 self.output,
514 "export type {} = {};",
515 shape.type_identifier,
516 variants.join(" | ")
517 )
518 .unwrap();
519 } else {
520 let mut variant_types = Vec::new();
523
524 for variant in enum_type.variants {
525 let variant_name = variant.effective_name();
526 match variant.data.kind {
527 StructKind::Unit => {
528 variant_types.push(format!("\"{}\"", variant_name));
530 }
531 StructKind::TupleStruct if variant.data.fields.len() == 1 => {
532 let inner = self.type_for_shape(variant.data.fields[0].shape.get());
534 variant_types.push(format!("{{ {}: {} }}", variant_name, inner));
535 }
536 StructKind::TupleStruct => {
537 let types: Vec<String> = variant
539 .data
540 .fields
541 .iter()
542 .map(|f| self.type_for_shape(f.shape.get()))
543 .collect();
544 variant_types.push(format!(
545 "{{ {}: [{}] }}",
546 variant_name,
547 types.join(", ")
548 ));
549 }
550 _ => {
551 let field_types = self.collect_inline_fields(variant.data.fields, false);
553 variant_types.push(format!(
554 "{{ {}: {{ {} }} }}",
555 variant_name,
556 field_types.join("; ")
557 ));
558 }
559 }
560 }
561
562 writeln!(
563 self.output,
564 "export type {} =\n | {};",
565 shape.type_identifier,
566 variant_types.join("\n | ")
567 )
568 .unwrap();
569 }
570 self.output.push('\n');
571 }
572
573 fn type_for_shape(&mut self, shape: &'static Shape) -> String {
574 match &shape.def {
576 Def::Scalar => self.scalar_type(shape),
577 Def::Option(opt) => {
578 format!("{} | null", self.type_for_shape(opt.t))
579 }
580 Def::List(list) => {
581 format!("{}[]", self.type_for_shape(list.t))
582 }
583 Def::Array(arr) => {
584 format!("{}[]", self.type_for_shape(arr.t))
585 }
586 Def::Set(set) => {
587 format!("{}[]", self.type_for_shape(set.t))
588 }
589 Def::Map(map) => {
590 format!("Record<string, {}>", self.type_for_shape(map.v))
591 }
592 Def::Pointer(ptr) => {
593 if let Some(pointee) = ptr.pointee {
595 self.type_for_shape(pointee)
596 } else {
597 "unknown".to_string()
598 }
599 }
600 Def::Undefined => {
601 match &shape.ty {
603 Type::User(UserType::Struct(st)) => {
604 if st.kind == StructKind::Tuple {
607 let types: Vec<String> = st
608 .fields
609 .iter()
610 .map(|f| self.type_for_shape(f.shape.get()))
611 .collect();
612 format!("[{}]", types.join(", "))
613 } else {
614 self.add_shape(shape);
615 shape.type_identifier.to_string()
616 }
617 }
618 Type::User(UserType::Enum(_)) => {
619 self.add_shape(shape);
620 shape.type_identifier.to_string()
621 }
622 _ => {
623 if let Some(inner) = shape.inner {
625 self.type_for_shape(inner)
626 } else {
627 "unknown".to_string()
628 }
629 }
630 }
631 }
632 _ => {
633 if let Some(inner) = shape.inner {
635 self.type_for_shape(inner)
636 } else {
637 "unknown".to_string()
638 }
639 }
640 }
641 }
642
643 fn scalar_type(&self, shape: &'static Shape) -> String {
644 match shape.type_identifier {
645 "String" | "str" | "&str" | "Cow" => "string".to_string(),
647
648 "bool" => "boolean".to_string(),
650
651 "u8" | "u16" | "u32" | "u64" | "u128" | "usize" | "i8" | "i16" | "i32" | "i64"
653 | "i128" | "isize" | "f32" | "f64" => "number".to_string(),
654
655 "char" => "string".to_string(),
657
658 "NaiveDate"
660 | "NaiveDateTime"
661 | "NaiveTime"
662 | "DateTime<Utc>"
663 | "DateTime<FixedOffset>"
664 | "DateTime<Local>"
665 if shape.module_path == Some("chrono") =>
666 {
667 "string".to_string()
668 }
669
670 _ => "unknown".to_string(),
672 }
673 }
674}
675
676#[cfg(test)]
677mod tests {
678 use super::*;
679 use alloc::collections::BTreeMap;
680 use facet::Facet;
681
682 #[test]
683 fn test_simple_struct() {
684 #[derive(Facet)]
685 struct User {
686 name: String,
687 age: u32,
688 }
689
690 let ts = to_typescript::<User>();
691 insta::assert_snapshot!(ts);
692 }
693
694 #[test]
695 fn test_optional_field() {
696 #[derive(Facet)]
697 struct Config {
698 required: String,
699 optional: Option<String>,
700 }
701
702 let ts = to_typescript::<Config>();
703 insta::assert_snapshot!(ts);
704 }
705
706 #[test]
707 fn test_simple_enum() {
708 #[derive(Facet)]
709 #[repr(u8)]
710 enum Status {
711 Active,
712 Inactive,
713 Pending,
714 }
715
716 let ts = to_typescript::<Status>();
717 insta::assert_snapshot!(ts);
718 }
719
720 #[test]
721 fn test_vec() {
722 #[derive(Facet)]
723 struct Data {
724 items: Vec<String>,
725 }
726
727 let ts = to_typescript::<Data>();
728 insta::assert_snapshot!(ts);
729 }
730
731 #[test]
732 fn test_nested_types() {
733 #[derive(Facet)]
734 struct Inner {
735 value: i32,
736 }
737
738 #[derive(Facet)]
739 struct Outer {
740 inner: Inner,
741 name: String,
742 }
743
744 let ts = to_typescript::<Outer>();
745 insta::assert_snapshot!(ts);
746 }
747
748 #[test]
749 fn test_enum_rename_all_snake_case() {
750 #[derive(Facet)]
751 #[facet(rename_all = "snake_case")]
752 #[repr(u8)]
753 enum ValidationErrorCode {
754 CircularDependency,
755 InvalidNaming,
756 UnknownRequirement,
757 }
758
759 let ts = to_typescript::<ValidationErrorCode>();
760 insta::assert_snapshot!(ts);
761 }
762
763 #[test]
764 fn test_enum_rename_individual() {
765 #[derive(Facet)]
766 #[repr(u8)]
767 enum GitStatus {
768 #[facet(rename = "dirty")]
769 Dirty,
770 #[facet(rename = "staged")]
771 Staged,
772 #[facet(rename = "clean")]
773 Clean,
774 }
775
776 let ts = to_typescript::<GitStatus>();
777 insta::assert_snapshot!(ts);
778 }
779
780 #[test]
781 fn test_struct_rename_all_camel_case() {
782 #[derive(Facet)]
783 #[facet(rename_all = "camelCase")]
784 struct ApiResponse {
785 user_name: String,
786 created_at: String,
787 is_active: bool,
788 }
789
790 let ts = to_typescript::<ApiResponse>();
791 insta::assert_snapshot!(ts);
792 }
793
794 #[test]
795 fn test_struct_rename_individual() {
796 #[derive(Facet)]
797 struct UserProfile {
798 #[facet(rename = "userName")]
799 user_name: String,
800 #[facet(rename = "emailAddress")]
801 email: String,
802 }
803
804 let ts = to_typescript::<UserProfile>();
805 insta::assert_snapshot!(ts);
806 }
807
808 #[test]
809 fn test_enum_with_data_rename_all() {
810 #[derive(Facet)]
811 #[facet(rename_all = "snake_case")]
812 #[repr(C)]
813 #[allow(dead_code)]
814 enum Message {
815 TextMessage { content: String },
816 ImageUpload { url: String, width: u32 },
817 }
818
819 let ts = to_typescript::<Message>();
820 insta::assert_snapshot!(ts);
821 }
822
823 #[test]
824 fn test_tagged_enum_unit_and_data_variants() {
825 #[derive(Facet)]
826 #[facet(rename_all = "snake_case")]
827 #[repr(u8)]
828 #[allow(dead_code)]
829 enum ResponseStatus {
830 Pending,
831 Ok(String),
832 Error { message: String },
833 Cancelled,
834 }
835
836 let ts = to_typescript::<ResponseStatus>();
837 insta::assert_snapshot!("tagged_enum_unit_and_data_variants", ts);
838 }
839
840 #[test]
841 fn test_struct_with_tuple_field() {
842 #[derive(Facet)]
843 struct Container {
844 coordinates: (i32, i32),
845 }
846
847 let ts = to_typescript::<Container>();
848 insta::assert_snapshot!(ts);
849 }
850
851 #[test]
852 fn test_struct_with_single_element_tuple() {
853 #[derive(Facet)]
854 struct Wrapper {
855 value: (String,),
856 }
857
858 let ts = to_typescript::<Wrapper>();
859 insta::assert_snapshot!(ts);
860 }
861
862 #[test]
863 fn test_enum_with_tuple_variant() {
864 #[derive(Facet)]
865 #[repr(C)]
866 #[allow(dead_code)]
867 enum Event {
868 Click { x: i32, y: i32 },
869 Move((i32, i32)),
870 Resize { dimensions: (u32, u32) },
871 }
872
873 let ts = to_typescript::<Event>();
874 insta::assert_snapshot!(ts);
875 }
876
877 #[test]
878 fn test_untagged_enum() {
879 #[derive(Facet)]
880 #[facet(untagged)]
881 #[repr(C)]
882 #[allow(dead_code)]
883 pub enum Value {
884 Text(String),
885 Number(f64),
886 }
887
888 let ts = to_typescript::<Value>();
889 insta::assert_snapshot!(ts);
890 }
891
892 #[test]
893 fn test_untagged_enum_unit_and_struct_variants() {
894 #[derive(Facet)]
895 #[facet(untagged)]
896 #[repr(C)]
897 #[allow(dead_code)]
898 pub enum Event {
899 None,
900 Data { x: i32, y: i32 },
901 }
902
903 let ts = to_typescript::<Event>();
904 insta::assert_snapshot!(ts);
905 }
906
907 #[test]
908 fn test_enum_with_tuple_struct_variant() {
909 #[derive(Facet)]
910 #[allow(dead_code)]
911 pub struct Point {
912 x: f64,
913 y: f64,
914 }
915
916 #[derive(Facet)]
917 #[repr(u8)]
918 #[allow(dead_code)]
919 pub enum Shape {
920 Line(Point, Point),
921 }
922
923 let ts = to_typescript::<Shape>();
924 insta::assert_snapshot!(ts);
925 }
926
927 #[test]
928 fn test_enum_with_proxy_struct() {
929 #[derive(Facet)]
930 #[facet(proxy = PointProxy)]
931 #[allow(dead_code)]
932 pub struct Point {
933 xxx: f64,
934 yyy: f64,
935 }
936
937 #[derive(Facet)]
938 #[allow(dead_code)]
939 pub struct PointProxy {
940 x: f64,
941 y: f64,
942 }
943
944 impl From<PointProxy> for Point {
945 fn from(p: PointProxy) -> Self {
946 Self { xxx: p.x, yyy: p.y }
947 }
948 }
949
950 impl From<&Point> for PointProxy {
951 fn from(p: &Point) -> Self {
952 Self { x: p.xxx, y: p.yyy }
953 }
954 }
955
956 #[derive(Facet)]
957 #[repr(u8)]
958 #[facet(untagged)]
959 #[allow(dead_code)]
960 pub enum Shape {
961 Circle { center: Point, radius: f64 },
962 Line(Point, Point),
963 }
964
965 let ts = to_typescript::<Shape>();
966 insta::assert_snapshot!(ts);
967 }
968
969 #[test]
970 fn test_enum_with_proxy_enum() {
971 #[derive(Facet)]
972 #[repr(u8)]
973 #[facet(proxy = StatusProxy)]
974 pub enum Status {
975 Unknown,
976 }
977
978 #[derive(Facet)]
979 #[repr(u8)]
980 pub enum StatusProxy {
981 Active,
982 Inactive,
983 }
984
985 impl From<StatusProxy> for Status {
986 fn from(_: StatusProxy) -> Self {
987 Self::Unknown
988 }
989 }
990
991 impl From<&Status> for StatusProxy {
992 fn from(_: &Status) -> Self {
993 Self::Active
994 }
995 }
996
997 let ts = to_typescript::<Status>();
998 insta::assert_snapshot!(ts);
999 }
1000
1001 #[test]
1002 fn test_proxy_to_scalar() {
1003 #[derive(Facet)]
1005 #[facet(proxy = String)]
1006 #[allow(dead_code)]
1007 pub struct UserId(u64);
1008
1009 impl From<String> for UserId {
1010 fn from(s: String) -> Self {
1011 Self(s.parse().unwrap_or(0))
1012 }
1013 }
1014
1015 impl From<&UserId> for String {
1016 fn from(id: &UserId) -> Self {
1017 id.0.to_string()
1018 }
1019 }
1020
1021 let ts = to_typescript::<UserId>();
1022 insta::assert_snapshot!(ts);
1023 }
1024
1025 #[test]
1026 fn test_proxy_preserves_doc_comments() {
1027 #[derive(Facet)]
1030 #[facet(proxy = PointProxy)]
1031 #[allow(dead_code)]
1032 pub struct Point {
1033 internal_x: f64,
1034 internal_y: f64,
1035 }
1036
1037 #[derive(Facet)]
1038 #[allow(dead_code)]
1039 pub struct PointProxy {
1040 x: f64,
1041 y: f64,
1042 }
1043
1044 impl From<PointProxy> for Point {
1045 fn from(p: PointProxy) -> Self {
1046 Self {
1047 internal_x: p.x,
1048 internal_y: p.y,
1049 }
1050 }
1051 }
1052
1053 impl From<&Point> for PointProxy {
1054 fn from(p: &Point) -> Self {
1055 Self {
1056 x: p.internal_x,
1057 y: p.internal_y,
1058 }
1059 }
1060 }
1061
1062 let ts = to_typescript::<Point>();
1063 insta::assert_snapshot!(ts);
1064 }
1065
1066 #[test]
1067 fn test_untagged_enum_optional_fields() {
1068 #[derive(Facet)]
1069 #[facet(untagged)]
1070 #[repr(C)]
1071 #[allow(dead_code)]
1072 pub enum Config {
1073 Simple {
1074 name: String,
1075 },
1076 Full {
1077 name: String,
1078 description: Option<String>,
1079 count: Option<u32>,
1080 },
1081 }
1082
1083 let ts = to_typescript::<Config>();
1084 insta::assert_snapshot!(ts);
1085 }
1086
1087 #[test]
1088 fn test_flatten_variants() {
1089 use std::sync::Arc;
1090
1091 #[derive(Facet)]
1093 pub struct Coords {
1094 pub x: i32,
1095 pub y: i32,
1096 #[facet(skip)]
1097 pub internal: u8,
1098 }
1099
1100 #[derive(Facet)]
1102 pub struct FlattenDirect {
1103 pub name: String,
1104 #[facet(flatten)]
1105 pub coords: Coords,
1106 }
1107
1108 #[derive(Facet)]
1110 pub struct FlattenArc {
1111 pub name: String,
1112 #[facet(flatten)]
1113 pub coords: Arc<Coords>,
1114 }
1115
1116 #[derive(Facet)]
1118 pub struct FlattenBox {
1119 pub name: String,
1120 #[facet(flatten)]
1121 pub coords: Box<Coords>,
1122 }
1123
1124 #[derive(Facet)]
1126 pub struct FlattenOption {
1127 pub name: String,
1128 #[facet(flatten)]
1129 pub coords: Option<Coords>,
1130 }
1131
1132 #[derive(Facet)]
1134 pub struct FlattenOptionArc {
1135 pub name: String,
1136 #[facet(flatten)]
1137 pub coords: Option<Arc<Coords>>,
1138 }
1139
1140 #[derive(Facet)]
1142 pub struct FlattenMap {
1143 pub name: String,
1144 #[facet(flatten)]
1145 pub extra: BTreeMap<String, String>,
1146 }
1147
1148 let ts_direct = to_typescript::<FlattenDirect>();
1149 let ts_arc = to_typescript::<FlattenArc>();
1150 let ts_box = to_typescript::<FlattenBox>();
1151 let ts_option = to_typescript::<FlattenOption>();
1152 let ts_option_arc = to_typescript::<FlattenOptionArc>();
1153 let ts_map = to_typescript::<FlattenMap>();
1154
1155 insta::assert_snapshot!("flatten_direct", ts_direct);
1156 insta::assert_snapshot!("flatten_arc", ts_arc);
1157 insta::assert_snapshot!("flatten_box", ts_box);
1158 insta::assert_snapshot!("flatten_option", ts_option);
1159 insta::assert_snapshot!("flatten_option_arc", ts_option_arc);
1160 insta::assert_snapshot!("flatten_map", ts_map);
1161 }
1162
1163 #[test]
1164 fn test_tagged_enum_optional_fields() {
1165 #[derive(Facet)]
1166 #[repr(u8)]
1167 #[allow(dead_code)]
1168 enum Message {
1169 Simple {
1170 text: String,
1171 },
1172 Full {
1173 text: String,
1174 metadata: Option<String>,
1175 count: Option<u32>,
1176 },
1177 }
1178
1179 let ts = to_typescript::<Message>();
1180 insta::assert_snapshot!(ts);
1181 }
1182
1183 #[test]
1184 fn test_flatten_proxy_struct() {
1185 #[derive(Facet)]
1186 #[facet(proxy = CoordsProxy)]
1187 #[allow(dead_code)]
1188 struct Coords {
1189 internal_x: f64,
1190 internal_y: f64,
1191 }
1192
1193 #[derive(Facet)]
1194 #[allow(dead_code)]
1195 struct CoordsProxy {
1196 x: f64,
1197 y: f64,
1198 }
1199
1200 impl From<CoordsProxy> for Coords {
1201 fn from(p: CoordsProxy) -> Self {
1202 Self {
1203 internal_x: p.x,
1204 internal_y: p.y,
1205 }
1206 }
1207 }
1208
1209 impl From<&Coords> for CoordsProxy {
1210 fn from(c: &Coords) -> Self {
1211 Self {
1212 x: c.internal_x,
1213 y: c.internal_y,
1214 }
1215 }
1216 }
1217
1218 #[derive(Facet)]
1219 #[allow(dead_code)]
1220 struct Shape {
1221 name: String,
1222 #[facet(flatten)]
1223 coords: Coords,
1224 }
1225
1226 let ts = to_typescript::<Shape>();
1227 insta::assert_snapshot!(ts);
1228 }
1229
1230 #[test]
1231 fn test_enum_variant_skipped_field() {
1232 #[derive(Facet)]
1233 #[repr(u8)]
1234 #[allow(dead_code)]
1235 enum Event {
1236 Data {
1237 visible: String,
1238 #[facet(skip)]
1239 internal: u64,
1240 },
1241 }
1242
1243 let ts = to_typescript::<Event>();
1244 insta::assert_snapshot!(ts);
1245 }
1246
1247 #[test]
1248 fn test_enum_variant_flatten() {
1249 #[derive(Facet)]
1251 #[allow(dead_code)]
1252 struct Metadata {
1253 author: String,
1254 version: u32,
1255 }
1256
1257 #[derive(Facet)]
1258 #[repr(u8)]
1259 #[allow(dead_code)]
1260 enum Document {
1261 Article {
1262 title: String,
1263 #[facet(flatten)]
1264 meta: Metadata,
1265 },
1266 }
1267
1268 let ts = to_typescript::<Document>();
1269 insta::assert_snapshot!(ts);
1270 }
1271
1272 #[test]
1273 fn test_nested_flatten_struct() {
1274 #[derive(Facet)]
1275 #[allow(dead_code)]
1276 struct Inner {
1277 x: i32,
1278 y: i32,
1279 }
1280
1281 #[derive(Facet)]
1282 #[allow(dead_code)]
1283 struct Middle {
1284 #[facet(flatten)]
1285 inner: Inner,
1286 z: i32,
1287 }
1288
1289 #[derive(Facet)]
1290 #[allow(dead_code)]
1291 struct Outer {
1292 name: String,
1293 #[facet(flatten)]
1294 middle: Middle,
1295 }
1296
1297 let ts = to_typescript::<Outer>();
1298 insta::assert_snapshot!(ts);
1299 }
1300
1301 #[test]
1302 fn test_flatten_recursive_option_box() {
1303 #[derive(Facet)]
1304 struct Node {
1305 value: u32,
1306 #[facet(flatten)]
1307 next: Option<Box<Node>>,
1308 }
1309
1310 let ts = to_typescript::<Node>();
1311 insta::assert_snapshot!("flatten_recursive_option_box", ts);
1312 }
1313
1314 #[test]
1315 fn test_skip_serializing_struct_field() {
1316 #[derive(Facet)]
1317 struct Data {
1318 visible: String,
1319 #[facet(skip_serializing)]
1320 internal: u64,
1321 }
1322
1323 let ts = to_typescript::<Data>();
1324 insta::assert_snapshot!("skip_serializing_struct_field", ts);
1325 }
1326
1327 #[test]
1328 fn test_skip_serializing_inline_enum_variant_and_flatten_cycle_guard() {
1329 #[derive(Facet)]
1330 struct Node {
1331 value: u32,
1332 #[facet(flatten)]
1333 next: Option<Box<Node>>,
1334 }
1335
1336 #[derive(Facet)]
1337 #[repr(u8)]
1338 enum Wrapper {
1339 Item {
1340 #[facet(flatten)]
1341 node: Node,
1342 },
1343 Data {
1344 visible: String,
1345 #[facet(skip_serializing)]
1346 internal: u64,
1347 },
1348 }
1349
1350 let item = Wrapper::Item {
1351 node: Node {
1352 value: 1,
1353 next: None,
1354 },
1355 };
1356 match item {
1357 Wrapper::Item { node } => assert_eq!(node.value, 1),
1358 Wrapper::Data { .. } => unreachable!(),
1359 }
1360
1361 let data = Wrapper::Data {
1362 visible: String::new(),
1363 internal: 0,
1364 };
1365 match data {
1366 Wrapper::Data { visible, internal } => {
1367 assert!(visible.is_empty());
1368 assert_eq!(internal, 0);
1369 }
1370 Wrapper::Item { .. } => unreachable!(),
1371 }
1372
1373 let ts = to_typescript::<Wrapper>();
1374 insta::assert_snapshot!(
1375 "skip_serializing_inline_enum_variant_and_flatten_cycle_guard",
1376 ts
1377 );
1378 }
1379
1380 #[test]
1381 fn test_empty_struct() {
1382 #[derive(Facet)]
1383 struct Data {
1384 empty: Empty,
1385 }
1386
1387 #[derive(Facet)]
1388 struct Empty {}
1389
1390 let e = to_typescript::<Empty>();
1391 let d = to_typescript::<Data>();
1392 insta::assert_snapshot!("test_empty_struct", e);
1393 insta::assert_snapshot!("test_empty_struct_wrap", d);
1394 }
1395
1396 #[test]
1397 fn test_empty_struct_with_skipped_fields() {
1398 #[derive(Facet)]
1399 struct EmptyAfterSkip {
1400 #[facet(skip_serializing)]
1401 internal: String,
1402 }
1403
1404 let ts = to_typescript::<EmptyAfterSkip>();
1405 insta::assert_snapshot!("test_empty_struct_with_skipped_fields", ts);
1406 }
1407
1408 #[test]
1409 fn test_empty_struct_multiple_references() {
1410 #[derive(Facet)]
1411 struct Container {
1412 first: Empty,
1413 second: Empty,
1414 third: Option<Empty>,
1415 }
1416
1417 #[derive(Facet)]
1418 struct Empty {}
1419
1420 let ts = to_typescript::<Container>();
1421 insta::assert_snapshot!("test_empty_struct_multiple_references", ts);
1422 }
1423
1424 #[test]
1425 fn test_flatten_empty_struct() {
1426 #[derive(Facet)]
1427 struct Empty {}
1428
1429 #[derive(Facet)]
1430 struct Wrapper {
1431 #[facet(flatten)]
1432 empty: Empty,
1433 }
1434
1435 let ts = to_typescript::<Wrapper>();
1436 insta::assert_snapshot!("test_flatten_empty_struct", ts);
1437 }
1438
1439 #[test]
1440 fn test_default_not_required() {
1441 #[derive(Facet, Default)]
1442 struct Def {
1443 pub a: i32,
1444 pub b: i32,
1445 }
1446
1447 #[derive(Facet)]
1448 struct Wrapper {
1449 pub a: String,
1450 #[facet(default)]
1451 pub d: Def,
1452 }
1453
1454 let ts = to_typescript::<Wrapper>();
1455 insta::assert_snapshot!("test_default_not_required", ts);
1456 }
1457
1458 #[test]
1459 fn test_default_mixed_fields() {
1460 #[derive(Facet)]
1461 struct MixedDefaults {
1462 pub required: String,
1463 pub optional: Option<String>,
1464 #[facet(default)]
1465 pub with_default: i32,
1466 #[facet(default = 100)]
1467 pub with_default_expr: i32,
1468 #[facet(default)]
1469 pub option_with_default: Option<String>,
1470 }
1471
1472 let ts = to_typescript::<MixedDefaults>();
1473 insta::assert_snapshot!("test_default_mixed_fields", ts);
1474 }
1475
1476 #[test]
1477 fn test_default_in_flattened_struct() {
1478 #[derive(Facet)]
1479 struct FlattenedInner {
1480 pub foo: String,
1481 #[facet(default)]
1482 pub bar: u32,
1483 }
1484
1485 #[derive(Facet)]
1486 struct WithFlatten {
1487 pub outer_field: String,
1488 #[facet(flatten)]
1489 pub inner: FlattenedInner,
1490 }
1491
1492 let ts = to_typescript::<WithFlatten>();
1493 insta::assert_snapshot!("test_default_in_flattened_struct", ts);
1494 }
1495
1496 #[test]
1497 fn test_default_in_enum_variant() {
1498 #[derive(Facet)]
1499 #[allow(dead_code)]
1500 #[repr(C)]
1501 enum Message {
1502 Text {
1503 content: String,
1504 },
1505 Data {
1506 required: String,
1507 #[facet(default)]
1508 optional: i32,
1509 },
1510 }
1511
1512 let ts = to_typescript::<Message>();
1513 insta::assert_snapshot!("test_default_in_enum_variant", ts);
1514 }
1515
1516 #[test]
1517 fn test_untagged_enum_unit_and_newtype_variants() {
1518 #[derive(Facet, Clone, PartialEq, PartialOrd)]
1519 #[repr(C)]
1520 #[allow(dead_code)]
1521 #[facet(untagged)]
1522 pub enum Enum {
1523 Daily,
1524 Weekly,
1525 Custom(f64),
1526 }
1527
1528 let ts = to_typescript::<Enum>();
1529 insta::assert_snapshot!("test_untagged_enum_unit_and_newtype_variants", ts);
1530 }
1531
1532 #[test]
1533 fn test_untagged_enum_with_tuple_variant() {
1534 #[derive(Facet)]
1535 #[repr(C)]
1536 #[allow(dead_code)]
1537 #[facet(untagged)]
1538 pub enum Message {
1539 Text(String),
1540 Pair(String, i32),
1541 Struct { x: i32, y: i32 },
1542 }
1543
1544 let ts = to_typescript::<Message>();
1545 insta::assert_snapshot!("test_untagged_enum_with_tuple_variant", ts);
1546 }
1547 #[test]
1548 fn test_chrono_naive_date() {
1549 use chrono::NaiveDate;
1550
1551 #[derive(Facet)]
1552 struct WithChronoDate {
1553 birthday: NaiveDate,
1554 }
1555
1556 let ts = to_typescript::<WithChronoDate>();
1557 insta::assert_snapshot!(ts);
1558 }
1559
1560 #[test]
1561 fn test_non_transparent_newtype_is_not_scalar_alias() {
1562 #[derive(Facet)]
1563 struct Envelope {
1564 id: BacktraceId,
1565 }
1566
1567 #[derive(Facet, Debug, Clone, Copy, PartialEq, Eq, Hash)]
1568 struct BacktraceId(u64);
1569
1570 let mut ts_gen = TypeScriptGenerator::new();
1571 ts_gen.add_type::<Envelope>();
1572 let out = ts_gen.finish();
1573
1574 assert!(
1575 !out.contains("export type BacktraceId = number;"),
1576 "bug: non-transparent tuple newtype generated scalar alias:\n{out}"
1577 );
1578 insta::assert_snapshot!("non_transparent_newtype", out);
1579 }
1580
1581 #[test]
1582 fn test_transparent_newtype_is_scalar_alias() {
1583 #[derive(Facet, Debug, Clone, Copy, PartialEq, Eq, Hash)]
1584 #[facet(transparent)]
1585 struct TransparentId(u64);
1586
1587 let ts = to_typescript::<TransparentId>();
1588 assert!(
1589 ts.contains("export type TransparentId = number;"),
1590 "bug: transparent tuple newtype did not generate scalar alias:\n{ts}"
1591 );
1592 insta::assert_snapshot!("transparent_newtype", ts);
1593 }
1594}