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
140 if let Def::Option(opt) = &field_shape.def {
141 let inner_type = self.type_for_shape(opt.t);
142 format!("{}?: {}", field_name, inner_type)
143 } else if force_optional {
144 let field_type = self.type_for_shape(field_shape);
145 format!("{}?: {}", field_name, field_type)
146 } else {
147 let field_type = self.type_for_shape(field_shape);
148 format!("{}: {}", field_name, field_type)
149 }
150 }
151
152 fn collect_inline_fields(
154 &mut self,
155 fields: &'static [Field],
156 force_optional: bool,
157 ) -> Vec<String> {
158 let mut flatten_stack: Vec<&'static str> = Vec::new();
159 self.collect_inline_fields_guarded(fields, force_optional, &mut flatten_stack)
160 }
161
162 fn collect_inline_fields_guarded(
163 &mut self,
164 fields: &'static [Field],
165 force_optional: bool,
166 flatten_stack: &mut Vec<&'static str>,
167 ) -> Vec<String> {
168 let mut result = Vec::new();
169 for field in fields {
170 if field.should_skip_serializing_unconditional() {
171 continue;
172 }
173 if field.is_flattened() {
174 let (inner_shape, parent_is_optional) =
175 Self::unwrap_to_inner_shape(field.shape.get());
176 if let Type::User(UserType::Struct(st)) = &inner_shape.ty {
177 let inner_key = Self::shape_key(inner_shape);
178 if flatten_stack.contains(&inner_key) {
179 continue;
180 }
181 flatten_stack.push(inner_key);
182 result.extend(self.collect_inline_fields_guarded(
183 st.fields,
184 force_optional || parent_is_optional,
185 flatten_stack,
186 ));
187 flatten_stack.pop();
188 continue;
189 }
190 }
191 result.push(self.format_inline_field(field, force_optional));
192 }
193 result
194 }
195
196 fn write_struct_fields_for_shape(
198 &mut self,
199 field_owner_shape: &'static Shape,
200 fields: &'static [Field],
201 ) {
202 let mut flatten_stack: Vec<&'static str> = Vec::new();
203 flatten_stack.push(Self::shape_key(field_owner_shape));
204 self.write_struct_fields_guarded(fields, false, &mut flatten_stack);
205 }
206
207 fn write_struct_fields_guarded(
208 &mut self,
209 fields: &'static [Field],
210 force_optional: bool,
211 flatten_stack: &mut Vec<&'static str>,
212 ) {
213 for field in fields {
214 if field.should_skip_serializing_unconditional() {
215 continue;
216 }
217 if field.is_flattened() {
218 let (inner_shape, parent_is_optional) =
219 Self::unwrap_to_inner_shape(field.shape.get());
220 if let Type::User(UserType::Struct(st)) = &inner_shape.ty {
221 let inner_key = Self::shape_key(inner_shape);
222 if flatten_stack.contains(&inner_key) {
223 continue;
224 }
225 flatten_stack.push(inner_key);
226 self.write_struct_fields_guarded(
227 st.fields,
228 force_optional || parent_is_optional,
229 flatten_stack,
230 );
231 flatten_stack.pop();
232 continue;
233 }
234 }
235 self.write_field(field, force_optional);
236 }
237 }
238
239 fn write_field(&mut self, field: &Field, force_optional: bool) {
241 if !field.doc.is_empty() {
243 self.write_indent();
244 self.output.push_str("/**\n");
245 for line in field.doc {
246 self.write_indent();
247 self.output.push_str(" *");
248 self.output.push_str(line);
249 self.output.push('\n');
250 }
251 self.write_indent();
252 self.output.push_str(" */\n");
253 }
254
255 let field_name = field.effective_name();
256 let field_shape = field.shape.get();
257
258 self.write_indent();
259
260 if let Def::Option(opt) = &field_shape.def {
262 let inner_type = self.type_for_shape(opt.t);
263 writeln!(self.output, "{}?: {};", field_name, inner_type).unwrap();
264 } else if force_optional {
265 let field_type = self.type_for_shape(field_shape);
266 writeln!(self.output, "{}?: {};", field_name, field_type).unwrap();
267 } else {
268 let field_type = self.type_for_shape(field_shape);
269 writeln!(self.output, "{}: {};", field_name, field_type).unwrap();
270 }
271 }
272
273 fn generate_shape(&mut self, shape: &'static Shape) {
274 if let Some(inner) = shape.inner {
276 self.add_shape(inner);
277 let inner_type = self.type_for_shape(inner);
279 writeln!(
280 self.output,
281 "export type {} = {};",
282 shape.type_identifier, inner_type
283 )
284 .unwrap();
285 self.output.push('\n');
286 return;
287 }
288
289 if !shape.doc.is_empty() {
291 self.output.push_str("/**\n");
292 for line in shape.doc {
293 self.output.push_str(" *");
294 self.output.push_str(line);
295 self.output.push('\n');
296 }
297 self.output.push_str(" */\n");
298 }
299
300 if let Some(proxy_def) = shape.proxy {
303 let proxy_shape = proxy_def.shape;
304 match &proxy_shape.ty {
305 Type::User(UserType::Struct(st)) => {
306 self.generate_struct(shape, proxy_shape, st.fields, st.kind);
307 return;
308 }
309 Type::User(UserType::Enum(en)) => {
310 self.generate_enum(shape, en);
311 return;
312 }
313 _ => {
314 let proxy_type = self.type_for_shape(proxy_shape);
317 writeln!(
318 self.output,
319 "export type {} = {};",
320 shape.type_identifier, proxy_type
321 )
322 .unwrap();
323 self.output.push('\n');
324 return;
325 }
326 }
327 }
328
329 match &shape.ty {
330 Type::User(UserType::Struct(st)) => {
331 self.generate_struct(shape, shape, st.fields, st.kind);
332 }
333 Type::User(UserType::Enum(en)) => {
334 self.generate_enum(shape, en);
335 }
336 _ => {
337 let type_str = self.type_for_shape(shape);
339 writeln!(
340 self.output,
341 "export type {} = {};",
342 shape.type_identifier, type_str
343 )
344 .unwrap();
345 self.output.push('\n');
346 }
347 }
348 }
349
350 fn generate_struct(
351 &mut self,
352 exported_shape: &'static Shape,
353 field_owner_shape: &'static Shape,
354 fields: &'static [Field],
355 kind: StructKind,
356 ) {
357 match kind {
358 StructKind::Unit => {
359 writeln!(
361 self.output,
362 "export type {} = null;",
363 exported_shape.type_identifier
364 )
365 .unwrap();
366 }
367 StructKind::TupleStruct if fields.len() == 1 => {
368 let inner_type = self.type_for_shape(fields[0].shape.get());
370 writeln!(
371 self.output,
372 "export type {} = {};",
373 exported_shape.type_identifier, inner_type
374 )
375 .unwrap();
376 }
377 StructKind::TupleStruct | StructKind::Tuple => {
378 let types: Vec<String> = fields
380 .iter()
381 .map(|f| self.type_for_shape(f.shape.get()))
382 .collect();
383 writeln!(
384 self.output,
385 "export type {} = [{}];",
386 exported_shape.type_identifier,
387 types.join(", ")
388 )
389 .unwrap();
390 }
391 StructKind::Struct => {
392 writeln!(
393 self.output,
394 "export interface {} {{",
395 exported_shape.type_identifier
396 )
397 .unwrap();
398 self.indent += 1;
399
400 self.write_struct_fields_for_shape(field_owner_shape, fields);
401
402 self.indent -= 1;
403 self.output.push_str("}\n");
404 }
405 }
406 self.output.push('\n');
407 }
408
409 fn generate_enum(&mut self, shape: &'static Shape, enum_type: &facet_core::EnumType) {
410 let all_unit = enum_type
412 .variants
413 .iter()
414 .all(|v| matches!(v.data.kind, StructKind::Unit));
415
416 let is_untagged = shape.is_untagged();
418
419 if is_untagged {
420 let mut variant_types = Vec::new();
422
423 for variant in enum_type.variants {
424 match variant.data.kind {
425 StructKind::Unit => {
426 variant_types.push("null".to_string());
428 }
429 StructKind::TupleStruct if variant.data.fields.len() == 1 => {
430 let inner = self.type_for_shape(variant.data.fields[0].shape.get());
432 variant_types.push(inner);
433 }
434 StructKind::TupleStruct => {
435 let types: Vec<String> = variant
437 .data
438 .fields
439 .iter()
440 .map(|f| self.type_for_shape(f.shape.get()))
441 .collect();
442 variant_types.push(format!("[{}]", types.join(", ")));
443 }
444 _ => {
445 let field_types = self.collect_inline_fields(variant.data.fields, false);
447 variant_types.push(format!("{{ {} }}", field_types.join("; ")));
448 }
449 }
450 }
451
452 writeln!(
453 self.output,
454 "export type {} = {};",
455 shape.type_identifier,
456 variant_types.join(" | ")
457 )
458 .unwrap();
459 } else if all_unit {
460 let variants: Vec<String> = enum_type
462 .variants
463 .iter()
464 .map(|v| format!("\"{}\"", v.effective_name()))
465 .collect();
466 writeln!(
467 self.output,
468 "export type {} = {};",
469 shape.type_identifier,
470 variants.join(" | ")
471 )
472 .unwrap();
473 } else {
474 let mut variant_types = Vec::new();
477
478 for variant in enum_type.variants {
479 let variant_name = variant.effective_name();
480 match variant.data.kind {
481 StructKind::Unit => {
482 variant_types.push(format!("{{ {}: \"{}\" }}", variant_name, 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!("{{ {}: {} }}", variant_name, inner));
489 }
490 StructKind::TupleStruct => {
491 let types: Vec<String> = variant
493 .data
494 .fields
495 .iter()
496 .map(|f| self.type_for_shape(f.shape.get()))
497 .collect();
498 variant_types.push(format!(
499 "{{ {}: [{}] }}",
500 variant_name,
501 types.join(", ")
502 ));
503 }
504 _ => {
505 let field_types = self.collect_inline_fields(variant.data.fields, false);
507 variant_types.push(format!(
508 "{{ {}: {{ {} }} }}",
509 variant_name,
510 field_types.join("; ")
511 ));
512 }
513 }
514 }
515
516 writeln!(
517 self.output,
518 "export type {} =\n | {};",
519 shape.type_identifier,
520 variant_types.join("\n | ")
521 )
522 .unwrap();
523 }
524 self.output.push('\n');
525 }
526
527 fn type_for_shape(&mut self, shape: &'static Shape) -> String {
528 match &shape.def {
530 Def::Scalar => self.scalar_type(shape),
531 Def::Option(opt) => {
532 format!("{} | null", self.type_for_shape(opt.t))
533 }
534 Def::List(list) => {
535 format!("{}[]", self.type_for_shape(list.t))
536 }
537 Def::Array(arr) => {
538 format!("{}[]", self.type_for_shape(arr.t))
539 }
540 Def::Set(set) => {
541 format!("{}[]", self.type_for_shape(set.t))
542 }
543 Def::Map(map) => {
544 format!("Record<string, {}>", self.type_for_shape(map.v))
545 }
546 Def::Pointer(ptr) => {
547 if let Some(pointee) = ptr.pointee {
549 self.type_for_shape(pointee)
550 } else {
551 "unknown".to_string()
552 }
553 }
554 Def::Undefined => {
555 match &shape.ty {
557 Type::User(UserType::Struct(st)) => {
558 if st.kind == StructKind::Tuple {
561 let types: Vec<String> = st
562 .fields
563 .iter()
564 .map(|f| self.type_for_shape(f.shape.get()))
565 .collect();
566 format!("[{}]", types.join(", "))
567 } else {
568 self.add_shape(shape);
569 shape.type_identifier.to_string()
570 }
571 }
572 Type::User(UserType::Enum(_)) => {
573 self.add_shape(shape);
574 shape.type_identifier.to_string()
575 }
576 _ => {
577 if let Some(inner) = shape.inner {
579 self.type_for_shape(inner)
580 } else {
581 "unknown".to_string()
582 }
583 }
584 }
585 }
586 _ => {
587 if let Some(inner) = shape.inner {
589 self.type_for_shape(inner)
590 } else {
591 "unknown".to_string()
592 }
593 }
594 }
595 }
596
597 fn scalar_type(&self, shape: &'static Shape) -> String {
598 match shape.type_identifier {
599 "String" | "str" | "&str" | "Cow" => "string".to_string(),
601
602 "bool" => "boolean".to_string(),
604
605 "u8" | "u16" | "u32" | "u64" | "u128" | "usize" | "i8" | "i16" | "i32" | "i64"
607 | "i128" | "isize" | "f32" | "f64" => "number".to_string(),
608
609 "char" => "string".to_string(),
611
612 _ => "unknown".to_string(),
614 }
615 }
616}
617
618#[cfg(test)]
619mod tests {
620 use super::*;
621 use alloc::collections::BTreeMap;
622 use facet::Facet;
623
624 #[test]
625 fn test_simple_struct() {
626 #[derive(Facet)]
627 struct User {
628 name: String,
629 age: u32,
630 }
631
632 let ts = to_typescript::<User>();
633 insta::assert_snapshot!(ts);
634 }
635
636 #[test]
637 fn test_optional_field() {
638 #[derive(Facet)]
639 struct Config {
640 required: String,
641 optional: Option<String>,
642 }
643
644 let ts = to_typescript::<Config>();
645 insta::assert_snapshot!(ts);
646 }
647
648 #[test]
649 fn test_simple_enum() {
650 #[derive(Facet)]
651 #[repr(u8)]
652 enum Status {
653 Active,
654 Inactive,
655 Pending,
656 }
657
658 let ts = to_typescript::<Status>();
659 insta::assert_snapshot!(ts);
660 }
661
662 #[test]
663 fn test_vec() {
664 #[derive(Facet)]
665 struct Data {
666 items: Vec<String>,
667 }
668
669 let ts = to_typescript::<Data>();
670 insta::assert_snapshot!(ts);
671 }
672
673 #[test]
674 fn test_nested_types() {
675 #[derive(Facet)]
676 struct Inner {
677 value: i32,
678 }
679
680 #[derive(Facet)]
681 struct Outer {
682 inner: Inner,
683 name: String,
684 }
685
686 let ts = to_typescript::<Outer>();
687 insta::assert_snapshot!(ts);
688 }
689
690 #[test]
691 fn test_enum_rename_all_snake_case() {
692 #[derive(Facet)]
693 #[facet(rename_all = "snake_case")]
694 #[repr(u8)]
695 enum ValidationErrorCode {
696 CircularDependency,
697 InvalidNaming,
698 UnknownRequirement,
699 }
700
701 let ts = to_typescript::<ValidationErrorCode>();
702 insta::assert_snapshot!(ts);
703 }
704
705 #[test]
706 fn test_enum_rename_individual() {
707 #[derive(Facet)]
708 #[repr(u8)]
709 enum GitStatus {
710 #[facet(rename = "dirty")]
711 Dirty,
712 #[facet(rename = "staged")]
713 Staged,
714 #[facet(rename = "clean")]
715 Clean,
716 }
717
718 let ts = to_typescript::<GitStatus>();
719 insta::assert_snapshot!(ts);
720 }
721
722 #[test]
723 fn test_struct_rename_all_camel_case() {
724 #[derive(Facet)]
725 #[facet(rename_all = "camelCase")]
726 struct ApiResponse {
727 user_name: String,
728 created_at: String,
729 is_active: bool,
730 }
731
732 let ts = to_typescript::<ApiResponse>();
733 insta::assert_snapshot!(ts);
734 }
735
736 #[test]
737 fn test_struct_rename_individual() {
738 #[derive(Facet)]
739 struct UserProfile {
740 #[facet(rename = "userName")]
741 user_name: String,
742 #[facet(rename = "emailAddress")]
743 email: String,
744 }
745
746 let ts = to_typescript::<UserProfile>();
747 insta::assert_snapshot!(ts);
748 }
749
750 #[test]
751 fn test_enum_with_data_rename_all() {
752 #[derive(Facet)]
753 #[facet(rename_all = "snake_case")]
754 #[repr(C)]
755 #[allow(dead_code)]
756 enum Message {
757 TextMessage { content: String },
758 ImageUpload { url: String, width: u32 },
759 }
760
761 let ts = to_typescript::<Message>();
762 insta::assert_snapshot!(ts);
763 }
764
765 #[test]
766 fn test_struct_with_tuple_field() {
767 #[derive(Facet)]
768 struct Container {
769 coordinates: (i32, i32),
770 }
771
772 let ts = to_typescript::<Container>();
773 insta::assert_snapshot!(ts);
774 }
775
776 #[test]
777 fn test_struct_with_single_element_tuple() {
778 #[derive(Facet)]
779 struct Wrapper {
780 value: (String,),
781 }
782
783 let ts = to_typescript::<Wrapper>();
784 insta::assert_snapshot!(ts);
785 }
786
787 #[test]
788 fn test_enum_with_tuple_variant() {
789 #[derive(Facet)]
790 #[repr(C)]
791 #[allow(dead_code)]
792 enum Event {
793 Click { x: i32, y: i32 },
794 Move((i32, i32)),
795 Resize { dimensions: (u32, u32) },
796 }
797
798 let ts = to_typescript::<Event>();
799 insta::assert_snapshot!(ts);
800 }
801
802 #[test]
803 fn test_untagged_enum() {
804 #[derive(Facet)]
805 #[facet(untagged)]
806 #[repr(C)]
807 #[allow(dead_code)]
808 pub enum Value {
809 Text(String),
810 Number(f64),
811 }
812
813 let ts = to_typescript::<Value>();
814 insta::assert_snapshot!(ts);
815 }
816
817 #[test]
818 fn test_untagged_enum_unit_and_struct_variants() {
819 #[derive(Facet)]
820 #[facet(untagged)]
821 #[repr(C)]
822 #[allow(dead_code)]
823 pub enum Event {
824 None,
825 Data { x: i32, y: i32 },
826 }
827
828 let ts = to_typescript::<Event>();
829 insta::assert_snapshot!(ts);
830 }
831
832 #[test]
833 fn test_enum_with_tuple_struct_variant() {
834 #[derive(Facet)]
835 #[allow(dead_code)]
836 pub struct Point {
837 x: f64,
838 y: f64,
839 }
840
841 #[derive(Facet)]
842 #[repr(u8)]
843 #[allow(dead_code)]
844 pub enum Shape {
845 Line(Point, Point),
846 }
847
848 let ts = to_typescript::<Shape>();
849 insta::assert_snapshot!(ts);
850 }
851
852 #[test]
853 fn test_enum_with_proxy_struct() {
854 #[derive(Facet)]
855 #[facet(proxy = PointProxy)]
856 #[allow(dead_code)]
857 pub struct Point {
858 xxx: f64,
859 yyy: f64,
860 }
861
862 #[derive(Facet)]
863 #[allow(dead_code)]
864 pub struct PointProxy {
865 x: f64,
866 y: f64,
867 }
868
869 impl From<PointProxy> for Point {
870 fn from(p: PointProxy) -> Self {
871 Self { xxx: p.x, yyy: p.y }
872 }
873 }
874
875 impl From<&Point> for PointProxy {
876 fn from(p: &Point) -> Self {
877 Self { x: p.xxx, y: p.yyy }
878 }
879 }
880
881 #[derive(Facet)]
882 #[repr(u8)]
883 #[facet(untagged)]
884 #[allow(dead_code)]
885 pub enum Shape {
886 Circle { center: Point, radius: f64 },
887 Line(Point, Point),
888 }
889
890 let ts = to_typescript::<Shape>();
891 insta::assert_snapshot!(ts);
892 }
893
894 #[test]
895 fn test_enum_with_proxy_enum() {
896 #[derive(Facet)]
897 #[repr(u8)]
898 #[facet(proxy = StatusProxy)]
899 pub enum Status {
900 Unknown,
901 }
902
903 #[derive(Facet)]
904 #[repr(u8)]
905 pub enum StatusProxy {
906 Active,
907 Inactive,
908 }
909
910 impl From<StatusProxy> for Status {
911 fn from(_: StatusProxy) -> Self {
912 Self::Unknown
913 }
914 }
915
916 impl From<&Status> for StatusProxy {
917 fn from(_: &Status) -> Self {
918 Self::Active
919 }
920 }
921
922 let ts = to_typescript::<Status>();
923 insta::assert_snapshot!(ts);
924 }
925
926 #[test]
927 fn test_proxy_to_scalar() {
928 #[derive(Facet)]
930 #[facet(proxy = String)]
931 #[allow(dead_code)]
932 pub struct UserId(u64);
933
934 impl From<String> for UserId {
935 fn from(s: String) -> Self {
936 Self(s.parse().unwrap_or(0))
937 }
938 }
939
940 impl From<&UserId> for String {
941 fn from(id: &UserId) -> Self {
942 id.0.to_string()
943 }
944 }
945
946 let ts = to_typescript::<UserId>();
947 insta::assert_snapshot!(ts);
948 }
949
950 #[test]
951 fn test_proxy_preserves_doc_comments() {
952 #[derive(Facet)]
955 #[facet(proxy = PointProxy)]
956 #[allow(dead_code)]
957 pub struct Point {
958 internal_x: f64,
959 internal_y: f64,
960 }
961
962 #[derive(Facet)]
963 #[allow(dead_code)]
964 pub struct PointProxy {
965 x: f64,
966 y: f64,
967 }
968
969 impl From<PointProxy> for Point {
970 fn from(p: PointProxy) -> Self {
971 Self {
972 internal_x: p.x,
973 internal_y: p.y,
974 }
975 }
976 }
977
978 impl From<&Point> for PointProxy {
979 fn from(p: &Point) -> Self {
980 Self {
981 x: p.internal_x,
982 y: p.internal_y,
983 }
984 }
985 }
986
987 let ts = to_typescript::<Point>();
988 insta::assert_snapshot!(ts);
989 }
990
991 #[test]
992 fn test_untagged_enum_optional_fields() {
993 #[derive(Facet)]
994 #[facet(untagged)]
995 #[repr(C)]
996 #[allow(dead_code)]
997 pub enum Config {
998 Simple {
999 name: String,
1000 },
1001 Full {
1002 name: String,
1003 description: Option<String>,
1004 count: Option<u32>,
1005 },
1006 }
1007
1008 let ts = to_typescript::<Config>();
1009 insta::assert_snapshot!(ts);
1010 }
1011
1012 #[test]
1013 fn test_flatten_variants() {
1014 use std::sync::Arc;
1015
1016 #[derive(Facet)]
1018 pub struct Coords {
1019 pub x: i32,
1020 pub y: i32,
1021 #[facet(skip)]
1022 pub internal: u8,
1023 }
1024
1025 #[derive(Facet)]
1027 pub struct FlattenDirect {
1028 pub name: String,
1029 #[facet(flatten)]
1030 pub coords: Coords,
1031 }
1032
1033 #[derive(Facet)]
1035 pub struct FlattenArc {
1036 pub name: String,
1037 #[facet(flatten)]
1038 pub coords: Arc<Coords>,
1039 }
1040
1041 #[derive(Facet)]
1043 pub struct FlattenBox {
1044 pub name: String,
1045 #[facet(flatten)]
1046 pub coords: Box<Coords>,
1047 }
1048
1049 #[derive(Facet)]
1051 pub struct FlattenOption {
1052 pub name: String,
1053 #[facet(flatten)]
1054 pub coords: Option<Coords>,
1055 }
1056
1057 #[derive(Facet)]
1059 pub struct FlattenOptionArc {
1060 pub name: String,
1061 #[facet(flatten)]
1062 pub coords: Option<Arc<Coords>>,
1063 }
1064
1065 #[derive(Facet)]
1067 pub struct FlattenMap {
1068 pub name: String,
1069 #[facet(flatten)]
1070 pub extra: BTreeMap<String, String>,
1071 }
1072
1073 let ts_direct = to_typescript::<FlattenDirect>();
1074 let ts_arc = to_typescript::<FlattenArc>();
1075 let ts_box = to_typescript::<FlattenBox>();
1076 let ts_option = to_typescript::<FlattenOption>();
1077 let ts_option_arc = to_typescript::<FlattenOptionArc>();
1078 let ts_map = to_typescript::<FlattenMap>();
1079
1080 insta::assert_snapshot!("flatten_direct", ts_direct);
1081 insta::assert_snapshot!("flatten_arc", ts_arc);
1082 insta::assert_snapshot!("flatten_box", ts_box);
1083 insta::assert_snapshot!("flatten_option", ts_option);
1084 insta::assert_snapshot!("flatten_option_arc", ts_option_arc);
1085 insta::assert_snapshot!("flatten_map", ts_map);
1086 }
1087
1088 #[test]
1089 fn test_tagged_enum_optional_fields() {
1090 #[derive(Facet)]
1091 #[repr(u8)]
1092 #[allow(dead_code)]
1093 enum Message {
1094 Simple {
1095 text: String,
1096 },
1097 Full {
1098 text: String,
1099 metadata: Option<String>,
1100 count: Option<u32>,
1101 },
1102 }
1103
1104 let ts = to_typescript::<Message>();
1105 insta::assert_snapshot!(ts);
1106 }
1107
1108 #[test]
1109 fn test_flatten_proxy_struct() {
1110 #[derive(Facet)]
1111 #[facet(proxy = CoordsProxy)]
1112 #[allow(dead_code)]
1113 struct Coords {
1114 internal_x: f64,
1115 internal_y: f64,
1116 }
1117
1118 #[derive(Facet)]
1119 #[allow(dead_code)]
1120 struct CoordsProxy {
1121 x: f64,
1122 y: f64,
1123 }
1124
1125 impl From<CoordsProxy> for Coords {
1126 fn from(p: CoordsProxy) -> Self {
1127 Self {
1128 internal_x: p.x,
1129 internal_y: p.y,
1130 }
1131 }
1132 }
1133
1134 impl From<&Coords> for CoordsProxy {
1135 fn from(c: &Coords) -> Self {
1136 Self {
1137 x: c.internal_x,
1138 y: c.internal_y,
1139 }
1140 }
1141 }
1142
1143 #[derive(Facet)]
1144 #[allow(dead_code)]
1145 struct Shape {
1146 name: String,
1147 #[facet(flatten)]
1148 coords: Coords,
1149 }
1150
1151 let ts = to_typescript::<Shape>();
1152 insta::assert_snapshot!(ts);
1153 }
1154
1155 #[test]
1156 fn test_enum_variant_skipped_field() {
1157 #[derive(Facet)]
1158 #[repr(u8)]
1159 #[allow(dead_code)]
1160 enum Event {
1161 Data {
1162 visible: String,
1163 #[facet(skip)]
1164 internal: u64,
1165 },
1166 }
1167
1168 let ts = to_typescript::<Event>();
1169 insta::assert_snapshot!(ts);
1170 }
1171
1172 #[test]
1173 fn test_enum_variant_flatten() {
1174 #[derive(Facet)]
1176 #[allow(dead_code)]
1177 struct Metadata {
1178 author: String,
1179 version: u32,
1180 }
1181
1182 #[derive(Facet)]
1183 #[repr(u8)]
1184 #[allow(dead_code)]
1185 enum Document {
1186 Article {
1187 title: String,
1188 #[facet(flatten)]
1189 meta: Metadata,
1190 },
1191 }
1192
1193 let ts = to_typescript::<Document>();
1194 insta::assert_snapshot!(ts);
1195 }
1196
1197 #[test]
1198 fn test_nested_flatten_struct() {
1199 #[derive(Facet)]
1200 #[allow(dead_code)]
1201 struct Inner {
1202 x: i32,
1203 y: i32,
1204 }
1205
1206 #[derive(Facet)]
1207 #[allow(dead_code)]
1208 struct Middle {
1209 #[facet(flatten)]
1210 inner: Inner,
1211 z: i32,
1212 }
1213
1214 #[derive(Facet)]
1215 #[allow(dead_code)]
1216 struct Outer {
1217 name: String,
1218 #[facet(flatten)]
1219 middle: Middle,
1220 }
1221
1222 let ts = to_typescript::<Outer>();
1223 insta::assert_snapshot!(ts);
1224 }
1225
1226 #[test]
1227 fn test_flatten_recursive_option_box() {
1228 #[derive(Facet)]
1229 struct Node {
1230 value: u32,
1231 #[facet(flatten)]
1232 next: Option<Box<Node>>,
1233 }
1234
1235 let ts = to_typescript::<Node>();
1236 insta::assert_snapshot!("flatten_recursive_option_box", ts);
1237 }
1238
1239 #[test]
1240 fn test_skip_serializing_struct_field() {
1241 #[derive(Facet)]
1242 struct Data {
1243 visible: String,
1244 #[facet(skip_serializing)]
1245 internal: u64,
1246 }
1247
1248 let ts = to_typescript::<Data>();
1249 insta::assert_snapshot!("skip_serializing_struct_field", ts);
1250 }
1251
1252 #[test]
1253 fn test_skip_serializing_inline_enum_variant_and_flatten_cycle_guard() {
1254 #[derive(Facet)]
1255 struct Node {
1256 value: u32,
1257 #[facet(flatten)]
1258 next: Option<Box<Node>>,
1259 }
1260
1261 #[derive(Facet)]
1262 #[repr(u8)]
1263 enum Wrapper {
1264 Item {
1265 #[facet(flatten)]
1266 node: Node,
1267 },
1268 Data {
1269 visible: String,
1270 #[facet(skip_serializing)]
1271 internal: u64,
1272 },
1273 }
1274
1275 let item = Wrapper::Item {
1276 node: Node {
1277 value: 1,
1278 next: None,
1279 },
1280 };
1281 match item {
1282 Wrapper::Item { node } => assert_eq!(node.value, 1),
1283 Wrapper::Data { .. } => unreachable!(),
1284 }
1285
1286 let data = Wrapper::Data {
1287 visible: String::new(),
1288 internal: 0,
1289 };
1290 match data {
1291 Wrapper::Data { visible, internal } => {
1292 assert!(visible.is_empty());
1293 assert_eq!(internal, 0);
1294 }
1295 Wrapper::Item { .. } => unreachable!(),
1296 }
1297
1298 let ts = to_typescript::<Wrapper>();
1299 insta::assert_snapshot!(
1300 "skip_serializing_inline_enum_variant_and_flatten_cycle_guard",
1301 ts
1302 );
1303 }
1304}