1use std::collections::{HashMap, HashSet};
24use std::path::{Path, PathBuf};
25
26#[doc(inline)]
28pub use csharp_rs_macros::CSharp;
29
30#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
36pub enum Serializer {
37 #[default]
39 SystemTextJson,
40 Newtonsoft,
42}
43
44#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
46pub enum CSharpVersion {
47 Unity,
49 #[default]
51 CSharp9,
52 CSharp10,
54 CSharp11,
56 CSharp12,
58}
59
60impl std::fmt::Display for CSharpVersion {
61 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 let s = match self {
63 Self::Unity => "Unity",
64 Self::CSharp9 => "9.0",
65 Self::CSharp10 => "10.0",
66 Self::CSharp11 => "11.0",
67 Self::CSharp12 => "12.0",
68 };
69 f.write_str(s)
70 }
71}
72
73impl CSharpVersion {
74 #[must_use]
76 pub fn supports_file_scoped_namespace(self) -> bool {
77 self >= Self::CSharp10
78 }
79
80 #[must_use]
82 pub fn supports_required_modifier(self) -> bool {
83 self >= Self::CSharp11
84 }
85
86 #[must_use]
88 pub fn uses_records(self) -> bool {
89 self != Self::Unity
90 }
91}
92
93#[derive(Debug, Clone, PartialEq, Eq)]
98pub struct CSharpNamespace(String);
99
100impl CSharpNamespace {
101 pub fn new(value: impl Into<String>) -> Result<Self, &'static str> {
108 let s = value.into();
109 validate_namespace(&s)?;
110 Ok(Self(s))
111 }
112}
113
114impl std::fmt::Display for CSharpNamespace {
115 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116 f.write_str(&self.0)
117 }
118}
119
120impl AsRef<str> for CSharpNamespace {
121 fn as_ref(&self) -> &str {
122 &self.0
123 }
124}
125
126impl PartialEq<&str> for CSharpNamespace {
127 fn eq(&self, other: &&str) -> bool {
128 self.0 == *other
129 }
130}
131
132fn validate_namespace(ns: &str) -> Result<(), &'static str> {
134 if ns.is_empty() {
135 return Err("namespace must not be empty");
136 }
137 for segment in ns.split('.') {
138 if segment.is_empty() {
139 return Err("namespace must not contain empty segments");
140 }
141 let mut chars = segment.chars();
142 let first = chars.next().expect("segment is non-empty");
143 if !first.is_ascii_alphabetic() && first != '_' {
144 return Err("each segment must start with a letter or underscore");
145 }
146 if !chars.all(|c| c.is_ascii_alphanumeric() || c == '_') {
147 return Err("segments must contain only letters, digits, or underscores");
148 }
149 }
150 Ok(())
151}
152
153#[derive(Debug)]
173pub struct Config {
174 namespace: CSharpNamespace,
175 serializer: Serializer,
176 target: CSharpVersion,
177 export_dir: PathBuf,
178}
179
180impl Default for Config {
181 fn default() -> Self {
182 Self {
183 namespace: CSharpNamespace::new("Generated").expect("default namespace is valid"),
184 serializer: Serializer::SystemTextJson,
185 target: CSharpVersion::CSharp9,
186 export_dir: PathBuf::from("./csharp-bindings"),
187 }
188 }
189}
190
191impl Config {
192 #[must_use]
202 pub fn from_env() -> Self {
203 let mut cfg = Self::default();
204
205 if let Ok(dir) = std::env::var("CSHARP_RS_EXPORT_DIR") {
206 cfg.export_dir = PathBuf::from(dir);
207 }
208
209 if let Ok(serializer) = std::env::var("CSHARP_RS_SERIALIZER") {
210 if serializer.as_str() == "newtonsoft" {
211 cfg.serializer = Serializer::Newtonsoft;
212 }
213 }
214
215 if let Ok(target) = std::env::var("CSHARP_RS_TARGET") {
216 match target.as_str() {
217 "unity" => cfg.target = CSharpVersion::Unity,
218 "9" => cfg.target = CSharpVersion::CSharp9,
219 "10" => cfg.target = CSharpVersion::CSharp10,
220 "11" => cfg.target = CSharpVersion::CSharp11,
221 "12" => cfg.target = CSharpVersion::CSharp12,
222 _ => {} }
224 }
225
226 if let Ok(ns) = std::env::var("CSHARP_RS_NAMESPACE") {
227 if let Ok(validated) = CSharpNamespace::new(ns) {
228 cfg.namespace = validated;
229 }
230 }
231
232 cfg
233 }
234
235 #[must_use]
242 pub fn with_namespace(mut self, ns: &str) -> Self {
243 self.namespace =
244 CSharpNamespace::new(ns).unwrap_or_else(|e| panic!("invalid namespace \"{ns}\": {e}"));
245 self
246 }
247
248 #[must_use]
250 pub fn with_validated_namespace(mut self, ns: CSharpNamespace) -> Self {
251 self.namespace = ns;
252 self
253 }
254
255 #[must_use]
257 pub fn with_serializer(mut self, serializer: Serializer) -> Self {
258 self.serializer = serializer;
259 self
260 }
261
262 #[must_use]
264 pub fn with_target(mut self, target: CSharpVersion) -> Self {
265 self.target = target;
266 self
267 }
268
269 #[must_use]
271 pub fn with_export_dir(mut self, dir: impl Into<PathBuf>) -> Self {
272 self.export_dir = dir.into();
273 self
274 }
275
276 #[must_use]
278 pub fn namespace(&self) -> &str {
279 self.namespace.as_ref()
280 }
281
282 #[must_use]
284 pub fn serializer(&self) -> Serializer {
285 self.serializer
286 }
287
288 #[must_use]
290 pub fn target(&self) -> CSharpVersion {
291 self.target
292 }
293
294 #[must_use]
296 pub fn export_dir(&self) -> &Path {
297 &self.export_dir
298 }
299}
300
301#[derive(Debug, Clone)]
303pub enum CSharpFieldInfo {
304 Property {
306 property_name: String,
308 json_name: String,
310 type_name: String,
312 is_optional: bool,
314 },
315 ExtensionData {
317 key_type_name: String,
319 value_type_name: String,
321 },
322}
323
324pub trait CSharp {
329 fn csharp_name(cfg: &Config) -> String;
331
332 fn csharp_definition(cfg: &Config) -> String;
335
336 fn dependencies(cfg: &Config) -> Vec<String>;
338
339 #[must_use]
344 fn csharp_fields(_cfg: &Config) -> Vec<CSharpFieldInfo> {
345 Vec::new()
346 }
347}
348
349pub fn export_to<T: CSharp>(cfg: &Config, path: impl AsRef<Path>) -> std::io::Result<()> {
357 let path = path.as_ref();
358 if let Some(parent) = path.parent() {
359 std::fs::create_dir_all(parent)?;
360 }
361 std::fs::write(path, T::csharp_definition(cfg))
362}
363
364macro_rules! impl_csharp_primitive {
369 ($rust_ty:ty, $csharp_name:expr) => {
370 impl CSharp for $rust_ty {
371 fn csharp_name(_cfg: &Config) -> String {
372 String::from($csharp_name)
373 }
374
375 fn csharp_definition(_cfg: &Config) -> String {
376 String::new()
378 }
379
380 fn dependencies(_cfg: &Config) -> Vec<String> {
381 Vec::new()
382 }
383 }
384 };
385}
386
387impl_csharp_primitive!(String, "string");
388impl_csharp_primitive!(bool, "bool");
389
390impl_csharp_primitive!(i8, "sbyte");
392impl_csharp_primitive!(i16, "short");
393impl_csharp_primitive!(i32, "int");
394impl_csharp_primitive!(i64, "long");
395impl_csharp_primitive!(i128, "decimal");
398
399impl_csharp_primitive!(u8, "byte");
401impl_csharp_primitive!(u16, "ushort");
402impl_csharp_primitive!(u32, "uint");
403impl_csharp_primitive!(u64, "ulong");
404impl_csharp_primitive!(u128, "decimal");
407
408impl_csharp_primitive!(f32, "float");
410impl_csharp_primitive!(f64, "double");
411
412#[cfg(feature = "uuid-impl")]
417impl_csharp_primitive!(uuid::Uuid, "Guid");
418
419#[cfg(feature = "chrono-impl")]
420mod chrono_impl;
421
422#[cfg(feature = "serde-json-impl")]
423mod serde_json_impl;
424
425impl<T: CSharp> CSharp for Option<T> {
435 fn csharp_name(cfg: &Config) -> String {
436 T::csharp_name(cfg)
437 }
438
439 fn csharp_definition(_cfg: &Config) -> String {
440 String::new()
441 }
442
443 fn dependencies(cfg: &Config) -> Vec<String> {
444 vec![T::csharp_name(cfg)]
445 }
446}
447
448impl<T: CSharp> CSharp for Vec<T> {
449 fn csharp_name(cfg: &Config) -> String {
450 format!("List<{}>", T::csharp_name(cfg))
451 }
452
453 fn csharp_definition(_cfg: &Config) -> String {
454 String::new()
455 }
456
457 fn dependencies(cfg: &Config) -> Vec<String> {
458 vec![T::csharp_name(cfg)]
459 }
460}
461
462impl<K: CSharp, V: CSharp, S: std::hash::BuildHasher> CSharp for HashMap<K, V, S> {
463 fn csharp_name(cfg: &Config) -> String {
464 format!(
465 "Dictionary<{}, {}>",
466 K::csharp_name(cfg),
467 V::csharp_name(cfg)
468 )
469 }
470
471 fn csharp_definition(_cfg: &Config) -> String {
472 String::new()
473 }
474
475 fn dependencies(cfg: &Config) -> Vec<String> {
476 vec![K::csharp_name(cfg), V::csharp_name(cfg)]
477 }
478}
479
480impl<T: CSharp, S: std::hash::BuildHasher> CSharp for HashSet<T, S> {
481 fn csharp_name(cfg: &Config) -> String {
482 format!("HashSet<{}>", T::csharp_name(cfg))
483 }
484
485 fn csharp_definition(_cfg: &Config) -> String {
486 String::new()
487 }
488
489 fn dependencies(cfg: &Config) -> Vec<String> {
490 vec![T::csharp_name(cfg)]
491 }
492}
493
494#[cfg(test)]
495mod tests {
496 use super::*;
497
498 #[test]
499 fn serializer_default_is_system_text_json() {
500 assert_eq!(Serializer::default(), Serializer::SystemTextJson);
501 }
502
503 #[test]
504 fn csharp_version_default_is_csharp9() {
505 assert_eq!(CSharpVersion::default(), CSharpVersion::CSharp9);
506 }
507
508 #[test]
509 fn csharp_version_ordering() {
510 assert!(CSharpVersion::Unity < CSharpVersion::CSharp9);
511 assert!(CSharpVersion::CSharp9 < CSharpVersion::CSharp10);
512 assert!(CSharpVersion::CSharp10 < CSharpVersion::CSharp11);
513 assert!(CSharpVersion::CSharp11 < CSharpVersion::CSharp12);
514 }
515
516 #[test]
517 fn csharp_version_display() {
518 assert_eq!(CSharpVersion::Unity.to_string(), "Unity");
519 assert_eq!(CSharpVersion::CSharp9.to_string(), "9.0");
520 assert_eq!(CSharpVersion::CSharp10.to_string(), "10.0");
521 assert_eq!(CSharpVersion::CSharp11.to_string(), "11.0");
522 assert_eq!(CSharpVersion::CSharp12.to_string(), "12.0");
523 }
524
525 #[test]
526 fn unity_does_not_support_file_scoped_namespace() {
527 assert!(!CSharpVersion::Unity.supports_file_scoped_namespace());
528 }
529
530 #[test]
531 fn unity_does_not_support_required_modifier() {
532 assert!(!CSharpVersion::Unity.supports_required_modifier());
533 }
534
535 #[test]
536 fn unity_does_not_use_records() {
537 assert!(!CSharpVersion::Unity.uses_records());
538 }
539
540 #[test]
541 fn csharp9_uses_records() {
542 assert!(CSharpVersion::CSharp9.uses_records());
543 }
544
545 #[test]
546 fn csharp10_supports_file_scoped() {
547 assert!(CSharpVersion::CSharp10.supports_file_scoped_namespace());
548 }
549
550 #[test]
551 fn csharp11_supports_all_features() {
552 assert!(CSharpVersion::CSharp11.supports_file_scoped_namespace());
553 assert!(CSharpVersion::CSharp11.supports_required_modifier());
554 assert!(CSharpVersion::CSharp11.uses_records());
555 }
556
557 #[test]
558 fn namespace_valid_single_segment() {
559 let ns = CSharpNamespace::new("MyGame").unwrap();
560 assert_eq!(ns.as_ref(), "MyGame");
561 }
562
563 #[test]
564 fn namespace_valid_multi_segment() {
565 let ns = CSharpNamespace::new("Company.Product.Module").unwrap();
566 assert_eq!(ns.as_ref(), "Company.Product.Module");
567 }
568
569 #[test]
570 fn namespace_underscore_prefix_valid() {
571 assert!(CSharpNamespace::new("_Internal").is_ok());
572 }
573
574 #[test]
575 fn namespace_invalid_empty() {
576 assert!(CSharpNamespace::new("").is_err());
577 }
578
579 #[test]
580 fn namespace_invalid_starts_with_digit() {
581 assert!(CSharpNamespace::new("1Invalid").is_err());
582 }
583
584 #[test]
585 fn namespace_invalid_special_chars() {
586 assert!(CSharpNamespace::new("My-Namespace").is_err());
587 }
588
589 #[test]
590 fn namespace_invalid_empty_segment() {
591 assert!(CSharpNamespace::new("A..B").is_err());
592 }
593
594 #[test]
595 fn namespace_display() {
596 let ns = CSharpNamespace::new("Test.Ns").unwrap();
597 assert_eq!(ns.to_string(), "Test.Ns");
598 }
599
600 #[test]
601 fn namespace_partial_eq_str() {
602 let ns = CSharpNamespace::new("Generated").unwrap();
603 assert_eq!(ns, "Generated");
604 }
605
606 #[test]
607 fn config_default_values() {
608 let cfg = Config::default();
609 assert_eq!(cfg.namespace(), "Generated");
610 assert_eq!(cfg.serializer(), Serializer::SystemTextJson);
611 assert_eq!(cfg.target(), CSharpVersion::CSharp9);
612 assert_eq!(cfg.export_dir(), Path::new("./csharp-bindings"));
613 }
614
615 #[test]
616 fn config_with_serializer() {
617 let cfg = Config::default().with_serializer(Serializer::Newtonsoft);
618 assert_eq!(cfg.serializer(), Serializer::Newtonsoft);
619 }
620
621 #[test]
622 fn config_with_target() {
623 let cfg = Config::default().with_target(CSharpVersion::CSharp12);
624 assert_eq!(cfg.target(), CSharpVersion::CSharp12);
625 }
626
627 #[test]
628 fn config_with_namespace() {
629 let cfg = Config::default().with_namespace("My.Game");
630 assert_eq!(cfg.namespace(), "My.Game");
631 }
632
633 #[test]
634 #[should_panic(expected = "each segment must start with a letter")]
635 fn config_with_namespace_panics_on_invalid() {
636 let _ = Config::default().with_namespace("1Bad");
637 }
638
639 #[test]
640 fn config_with_validated_namespace() {
641 let ns = CSharpNamespace::new("Pre.Validated").unwrap();
642 let cfg = Config::default().with_validated_namespace(ns);
643 assert_eq!(cfg.namespace(), "Pre.Validated");
644 }
645
646 #[test]
647 fn config_with_export_dir() {
648 let cfg = Config::default().with_export_dir("./output");
649 assert_eq!(cfg.export_dir(), Path::new("./output"));
650 }
651
652 #[test]
653 fn config_builder_chaining() {
654 let cfg = Config::default()
655 .with_namespace("Unity.Types")
656 .with_serializer(Serializer::Newtonsoft)
657 .with_target(CSharpVersion::CSharp11)
658 .with_export_dir("./generated");
659 assert_eq!(cfg.namespace(), "Unity.Types");
660 assert_eq!(cfg.serializer(), Serializer::Newtonsoft);
661 assert_eq!(cfg.target(), CSharpVersion::CSharp11);
662 assert_eq!(cfg.export_dir(), Path::new("./generated"));
663 }
664
665 #[test]
669 fn from_env_defaults_match_default() {
670 let cfg = Config::from_env();
671 let default = Config::default();
672 assert_eq!(cfg.namespace(), default.namespace());
673 assert_eq!(cfg.serializer(), default.serializer());
674 assert_eq!(cfg.target(), default.target());
675 assert_eq!(cfg.export_dir(), default.export_dir());
676 }
677
678 #[test]
679 fn from_env_reads_serializer() {
680 unsafe { std::env::set_var("CSHARP_RS_SERIALIZER", "newtonsoft") };
682 let cfg = Config::from_env();
683 assert_eq!(cfg.serializer(), Serializer::Newtonsoft);
684 unsafe { std::env::remove_var("CSHARP_RS_SERIALIZER") };
686 }
687
688 #[test]
689 fn from_env_reads_target_unity() {
690 unsafe { std::env::set_var("CSHARP_RS_TARGET", "unity") };
692 let cfg = Config::from_env();
693 assert_eq!(cfg.target(), CSharpVersion::Unity);
694 unsafe { std::env::remove_var("CSHARP_RS_TARGET") };
696 }
697
698 #[test]
699 fn from_env_reads_target_version() {
700 unsafe { std::env::set_var("CSHARP_RS_TARGET", "11") };
702 let cfg = Config::from_env();
703 assert_eq!(cfg.target(), CSharpVersion::CSharp11);
704 unsafe { std::env::remove_var("CSHARP_RS_TARGET") };
706 }
707
708 #[test]
709 fn from_env_reads_namespace() {
710 unsafe { std::env::set_var("CSHARP_RS_NAMESPACE", "Game.Types") };
712 let cfg = Config::from_env();
713 assert_eq!(cfg.namespace(), "Game.Types");
714 unsafe { std::env::remove_var("CSHARP_RS_NAMESPACE") };
716 }
717
718 #[test]
719 fn from_env_reads_export_dir() {
720 unsafe { std::env::set_var("CSHARP_RS_EXPORT_DIR", "/tmp/csharp-out") };
722 let cfg = Config::from_env();
723 assert_eq!(cfg.export_dir(), Path::new("/tmp/csharp-out"));
724 unsafe { std::env::remove_var("CSHARP_RS_EXPORT_DIR") };
726 }
727
728 #[test]
729 fn from_env_invalid_namespace_falls_back() {
730 unsafe { std::env::set_var("CSHARP_RS_NAMESPACE", "123invalid") };
732 let cfg = Config::from_env();
733 assert_eq!(cfg.namespace(), "Generated");
734 unsafe { std::env::remove_var("CSHARP_RS_NAMESPACE") };
736 }
737
738 #[test]
739 fn from_env_unknown_serializer_falls_back() {
740 unsafe { std::env::set_var("CSHARP_RS_SERIALIZER", "protobuf") };
742 let cfg = Config::from_env();
743 assert_eq!(cfg.serializer(), Serializer::SystemTextJson);
744 unsafe { std::env::remove_var("CSHARP_RS_SERIALIZER") };
746 }
747
748 #[test]
749 fn string_maps_to_csharp_string() {
750 let cfg = Config::default();
751 assert_eq!(String::csharp_name(&cfg), "string");
752 }
753
754 #[test]
755 fn bool_maps_to_csharp_bool() {
756 let cfg = Config::default();
757 assert_eq!(bool::csharp_name(&cfg), "bool");
758 }
759
760 #[test]
761 fn integer_type_mappings() {
762 let cfg = Config::default();
763 assert_eq!(i8::csharp_name(&cfg), "sbyte");
764 assert_eq!(i16::csharp_name(&cfg), "short");
765 assert_eq!(i32::csharp_name(&cfg), "int");
766 assert_eq!(i64::csharp_name(&cfg), "long");
767 assert_eq!(i128::csharp_name(&cfg), "decimal");
768 assert_eq!(u8::csharp_name(&cfg), "byte");
769 assert_eq!(u16::csharp_name(&cfg), "ushort");
770 assert_eq!(u32::csharp_name(&cfg), "uint");
771 assert_eq!(u64::csharp_name(&cfg), "ulong");
772 assert_eq!(u128::csharp_name(&cfg), "decimal");
773 }
774
775 #[test]
776 fn float_type_mappings() {
777 let cfg = Config::default();
778 assert_eq!(f32::csharp_name(&cfg), "float");
779 assert_eq!(f64::csharp_name(&cfg), "double");
780 }
781
782 #[test]
783 fn option_unwraps_inner_type() {
784 let cfg = Config::default();
785 assert_eq!(<Option<i32>>::csharp_name(&cfg), "int");
786 }
787
788 #[test]
789 fn vec_maps_to_list() {
790 let cfg = Config::default();
791 assert_eq!(<Vec<String>>::csharp_name(&cfg), "List<string>");
792 }
793
794 #[test]
795 fn hashmap_maps_to_dictionary() {
796 let cfg = Config::default();
797 assert_eq!(
798 <HashMap<String, i32>>::csharp_name(&cfg),
799 "Dictionary<string, int>"
800 );
801 }
802
803 #[test]
804 fn hashset_maps_to_hashset() {
805 let cfg = Config::default();
806 assert_eq!(<HashSet<String>>::csharp_name(&cfg), "HashSet<string>");
807 }
808
809 #[test]
810 fn nested_generics() {
811 let cfg = Config::default();
812 assert_eq!(<Vec<Option<i32>>>::csharp_name(&cfg), "List<int>");
813 assert_eq!(
814 <HashMap<String, Vec<f64>>>::csharp_name(&cfg),
815 "Dictionary<string, List<double>>"
816 );
817 }
818
819 #[test]
822 fn primitive_definition_is_empty() {
823 let cfg = Config::default();
824 assert!(String::csharp_definition(&cfg).is_empty());
825 assert!(bool::csharp_definition(&cfg).is_empty());
826 assert!(i32::csharp_definition(&cfg).is_empty());
827 assert!(u64::csharp_definition(&cfg).is_empty());
828 assert!(f64::csharp_definition(&cfg).is_empty());
829 }
830
831 #[test]
832 fn primitive_dependencies_is_empty() {
833 let cfg = Config::default();
834 assert!(String::dependencies(&cfg).is_empty());
835 assert!(bool::dependencies(&cfg).is_empty());
836 assert!(i32::dependencies(&cfg).is_empty());
837 assert!(u64::dependencies(&cfg).is_empty());
838 assert!(f64::dependencies(&cfg).is_empty());
839 }
840
841 #[test]
844 fn option_definition_is_empty() {
845 let cfg = Config::default();
846 assert!(<Option<i32>>::csharp_definition(&cfg).is_empty());
847 }
848
849 #[test]
850 fn option_dependencies_contains_inner() {
851 let cfg = Config::default();
852 let deps = <Option<i32>>::dependencies(&cfg);
853 assert_eq!(deps, vec!["int"]);
854 }
855
856 #[test]
857 fn vec_definition_is_empty() {
858 let cfg = Config::default();
859 assert!(<Vec<String>>::csharp_definition(&cfg).is_empty());
860 }
861
862 #[test]
863 fn vec_dependencies_contains_inner() {
864 let cfg = Config::default();
865 let deps = <Vec<String>>::dependencies(&cfg);
866 assert_eq!(deps, vec!["string"]);
867 }
868
869 #[test]
870 fn hashmap_definition_is_empty() {
871 let cfg = Config::default();
872 assert!(<HashMap<String, i32>>::csharp_definition(&cfg).is_empty());
873 }
874
875 #[test]
876 fn hashmap_dependencies_contains_key_and_value() {
877 let cfg = Config::default();
878 let deps = <HashMap<String, i32>>::dependencies(&cfg);
879 assert_eq!(deps, vec!["string", "int"]);
880 }
881
882 #[test]
883 fn hashset_definition_is_empty() {
884 let cfg = Config::default();
885 assert!(<HashSet<String>>::csharp_definition(&cfg).is_empty());
886 }
887
888 #[test]
889 fn hashset_dependencies_contains_inner() {
890 let cfg = Config::default();
891 let deps = <HashSet<String>>::dependencies(&cfg);
892 assert_eq!(deps, vec!["string"]);
893 }
894
895 #[test]
898 fn primitive_csharp_fields_is_empty() {
899 let cfg = Config::default();
900 assert!(String::csharp_fields(&cfg).is_empty());
901 assert!(i32::csharp_fields(&cfg).is_empty());
902 assert!(bool::csharp_fields(&cfg).is_empty());
903 }
904
905 #[test]
906 fn generic_csharp_fields_is_empty() {
907 let cfg = Config::default();
908 assert!(<Vec<String>>::csharp_fields(&cfg).is_empty());
909 assert!(<Option<i32>>::csharp_fields(&cfg).is_empty());
910 assert!(<HashMap<String, i32>>::csharp_fields(&cfg).is_empty());
911 assert!(<HashSet<String>>::csharp_fields(&cfg).is_empty());
912 }
913
914 #[test]
917 fn export_to_writes_file() {
918 let cfg = Config::default();
919 let dir = std::env::temp_dir().join("csharp_rs_test_export");
920 let _ = std::fs::remove_dir_all(&dir);
921 let path = dir.join("sub").join("Test.cs");
922
923 export_to::<i32>(&cfg, &path).expect("export_to should succeed");
924
925 let content = std::fs::read_to_string(&path).expect("file should exist");
926 assert!(content.is_empty());
928
929 let _ = std::fs::remove_dir_all(&dir);
931 }
932
933 #[cfg(feature = "uuid-impl")]
936 #[test]
937 fn uuid_maps_to_guid() {
938 let cfg = Config::default();
939 assert_eq!(<uuid::Uuid as CSharp>::csharp_name(&cfg), "Guid");
940 assert!(<uuid::Uuid as CSharp>::csharp_definition(&cfg).is_empty());
941 assert!(<uuid::Uuid as CSharp>::dependencies(&cfg).is_empty());
942 }
943}
944
945#[cfg(feature = "chrono-impl")]
950#[cfg(test)]
951mod chrono_tests {
952 use super::*;
953
954 #[test]
955 fn datetime_utc_maps_to_datetimeoffset() {
956 let cfg = Config::default();
957 assert_eq!(
958 <chrono::DateTime<chrono::Utc> as CSharp>::csharp_name(&cfg),
959 "DateTimeOffset"
960 );
961 }
962
963 #[test]
964 fn naive_date_maps_to_dateonly() {
965 let cfg = Config::default();
966 assert_eq!(<chrono::NaiveDate as CSharp>::csharp_name(&cfg), "DateOnly");
967 }
968
969 #[test]
970 fn naive_time_maps_to_timeonly() {
971 let cfg = Config::default();
972 assert_eq!(<chrono::NaiveTime as CSharp>::csharp_name(&cfg), "TimeOnly");
973 }
974
975 #[test]
976 fn naive_datetime_maps_to_datetime() {
977 let cfg = Config::default();
978 assert_eq!(
979 <chrono::NaiveDateTime as CSharp>::csharp_name(&cfg),
980 "DateTime"
981 );
982 }
983
984 #[test]
985 fn duration_maps_to_timespan() {
986 let cfg = Config::default();
987 assert_eq!(<chrono::Duration as CSharp>::csharp_name(&cfg), "TimeSpan");
988 }
989}
990
991#[cfg(feature = "serde-json-impl")]
992#[cfg(test)]
993mod serde_json_tests {
994 use super::*;
995
996 #[test]
997 fn serde_json_value_stj_maps_to_json_element() {
998 let cfg = Config::default();
999 assert_eq!(
1000 <serde_json::Value as CSharp>::csharp_name(&cfg),
1001 "JsonElement"
1002 );
1003 }
1004
1005 #[test]
1006 fn serde_json_value_newtonsoft_maps_to_jtoken() {
1007 let cfg = Config::default().with_serializer(Serializer::Newtonsoft);
1008 assert_eq!(<serde_json::Value as CSharp>::csharp_name(&cfg), "JToken");
1009 }
1010
1011 #[test]
1012 fn serde_json_number_maps_to_double() {
1013 let cfg = Config::default();
1014 assert_eq!(<serde_json::Number as CSharp>::csharp_name(&cfg), "double");
1015 }
1016
1017 #[test]
1018 fn serde_json_value_definition_is_empty() {
1019 let cfg = Config::default();
1020 assert!(<serde_json::Value as CSharp>::csharp_definition(&cfg).is_empty());
1021 }
1022}