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