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