1use std::cell::RefCell;
28use std::collections::BTreeMap;
29use std::collections::BTreeSet;
30
31use serde::{Deserialize, Serialize};
32
33use crate::openapi::{SchemaDetails, SchemaType as OpenApiSchemaType};
34
35#[derive(Debug, Clone)]
37pub struct MappedType {
38 pub rust_type: String,
41 pub serde_with: Option<String>,
44 pub feature: Option<TypeFeature>,
47}
48
49impl MappedType {
50 pub fn plain(rust_type: impl Into<String>) -> Self {
52 Self {
53 rust_type: rust_type.into(),
54 serde_with: None,
55 feature: None,
56 }
57 }
58
59 pub fn with_feature(rust_type: impl Into<String>, feature: TypeFeature) -> Self {
64 Self {
65 rust_type: rust_type.into(),
66 serde_with: None,
67 feature: Some(feature),
68 }
69 }
70
71 pub fn with_codec(
73 rust_type: impl Into<String>,
74 codec_path: impl Into<String>,
75 feature: TypeFeature,
76 ) -> Self {
77 Self {
78 rust_type: rust_type.into(),
79 serde_with: Some(codec_path.into()),
80 feature: Some(feature),
81 }
82 }
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
87pub enum TypeFeature {
88 Chrono,
89 Time,
90 Iso8601,
91 Uuid,
92 Bytes,
93 Base64,
94 Url,
95 EmailAddress,
96}
97
98impl TypeFeature {
99 pub fn dep_requirement(self) -> DepRequirement {
103 match self {
104 Self::Chrono => DepRequirement::new("chrono", "0.4").with_features(&["serde"]),
105 Self::Time => DepRequirement::new("time", "0.3").with_features(&["serde"]),
106 Self::Iso8601 => DepRequirement::new("iso8601", "0.6"),
107 Self::Uuid => DepRequirement::new("uuid", "1").with_features(&["serde", "v4"]),
108 Self::Bytes => DepRequirement::new("bytes", "1").with_features(&["serde"]),
109 Self::Base64 => DepRequirement::new("base64", "0.22"),
110 Self::Url => DepRequirement::new("url", "2").with_features(&["serde"]),
111 Self::EmailAddress => DepRequirement::new("email_address", "0.2"),
112 }
113 }
114}
115
116#[derive(Debug, Clone, PartialEq, Eq)]
118pub struct DepRequirement {
119 pub crate_name: &'static str,
120 pub version: &'static str,
121 pub features: Vec<&'static str>,
122}
123
124impl DepRequirement {
125 pub fn new(crate_name: &'static str, version: &'static str) -> Self {
126 Self {
127 crate_name,
128 version,
129 features: Vec::new(),
130 }
131 }
132
133 pub fn with_features(mut self, features: &[&'static str]) -> Self {
134 self.features = features.to_vec();
135 self
136 }
137
138 pub fn to_toml_line(&self) -> String {
141 if self.features.is_empty() {
142 format!("{} = \"{}\"", self.crate_name, self.version)
143 } else {
144 let feats = self
145 .features
146 .iter()
147 .map(|f| format!("\"{f}\""))
148 .collect::<Vec<_>>()
149 .join(", ");
150 format!(
151 "{} = {{ version = \"{}\", features = [{}] }}",
152 self.crate_name, self.version, feats
153 )
154 }
155 }
156}
157
158pub fn render_required_deps_toml(deps: &[DepRequirement]) -> Option<String> {
163 if deps.is_empty() {
164 return None;
165 }
166 let mut out = String::new();
167 out.push_str(
168 "# Generated by openapi-to-rust.\n\
169 # These crates are required by the typed-scalar formats used\n\
170 # in your OpenAPI spec. Copy these lines into the [dependencies]\n\
171 # section of your consuming crate's Cargo.toml.\n\
172 #\n\
173 # To opt out of typed scalars (and avoid these deps), set\n\
174 # the relevant strategies to \"string\" in [generator.types],\n\
175 # or pass --types-conservative on the CLI.\n\
176 \n\
177 [dependencies]\n",
178 );
179 for dep in deps {
180 out.push_str(&dep.to_toml_line());
181 out.push('\n');
182 }
183 Some(out)
184}
185
186pub fn collect_dep_requirements(used: &UsedFeatures) -> Vec<DepRequirement> {
190 let mut deps: Vec<DepRequirement> = used.iter().map(|f| f.dep_requirement()).collect();
191 deps.sort_by_key(|d| d.crate_name);
192 deps.dedup_by_key(|d| d.crate_name);
193 deps
194}
195
196#[derive(Debug, Default, Clone)]
198pub struct UsedFeatures {
199 set: BTreeSet<TypeFeature>,
200}
201
202impl UsedFeatures {
203 pub fn insert(&mut self, feature: TypeFeature) {
204 self.set.insert(feature);
205 }
206
207 pub fn contains(&self, feature: TypeFeature) -> bool {
208 self.set.contains(&feature)
209 }
210
211 pub fn iter(&self) -> impl Iterator<Item = &TypeFeature> {
212 self.set.iter()
213 }
214
215 pub fn is_empty(&self) -> bool {
216 self.set.is_empty()
217 }
218}
219
220#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
226#[serde(rename_all = "lowercase")]
227pub enum DateStrategy {
228 String,
230 #[default]
232 Chrono,
233 Time,
235}
236
237#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
239#[serde(rename_all = "lowercase")]
240pub enum DurationStrategy {
241 #[default]
248 String,
249 Chrono,
252 Iso8601,
254}
255
256#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
258#[serde(rename_all = "lowercase")]
259pub enum UuidStrategy {
260 String,
261 #[default]
263 Uuid,
264}
265
266#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
268#[serde(rename_all = "snake_case")]
269pub enum ByteStrategy {
270 String,
271 #[default]
274 Base64,
275 VecU8,
277}
278
279#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
281#[serde(rename_all = "snake_case")]
282pub enum BinaryStrategy {
283 String,
284 #[default]
286 Bytes,
287 VecU8,
288}
289
290#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
292#[serde(rename_all = "lowercase")]
293pub enum IpStrategy {
294 String,
295 #[default]
297 Std,
298}
299
300#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
302#[serde(rename_all = "lowercase")]
303pub enum UriStrategy {
304 String,
305 #[default]
307 Url,
308}
309
310#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
316#[serde(rename_all = "snake_case")]
317pub enum EmailStrategy {
318 #[default]
319 String,
320 EmailAddress,
321}
322
323#[derive(Debug, Clone, Deserialize, Serialize)]
331#[serde(default, rename_all = "snake_case")]
332pub struct TypeMappingConfig {
333 pub date_time: DateStrategy,
334 pub date: DateStrategy,
335 pub time: DateStrategy,
336 pub duration: DurationStrategy,
337 pub uuid: UuidStrategy,
338 pub byte: ByteStrategy,
339 pub binary: BinaryStrategy,
340 pub ipv4: IpStrategy,
341 pub ipv6: IpStrategy,
342 pub uri: UriStrategy,
343 pub email: EmailStrategy,
344
345 #[serde(default = "default_true")]
350 pub unsigned: bool,
351
352 #[serde(default)]
357 pub format_aliases: BTreeMap<String, String>,
358
359 pub shape: Option<TypeShapeConfig>,
361
362 pub constraints: Option<TypeConstraintsConfig>,
364
365 pub enums: Option<TypeEnumsConfig>,
367}
368
369fn default_true() -> bool {
370 true
371}
372
373impl Default for TypeMappingConfig {
374 fn default() -> Self {
375 Self {
376 date_time: DateStrategy::default(),
377 date: DateStrategy::default(),
378 time: DateStrategy::default(),
379 duration: DurationStrategy::default(),
380 uuid: UuidStrategy::default(),
381 byte: ByteStrategy::default(),
382 binary: BinaryStrategy::default(),
383 ipv4: IpStrategy::default(),
384 ipv6: IpStrategy::default(),
385 uri: UriStrategy::default(),
386 email: EmailStrategy::default(),
387 unsigned: true,
388 format_aliases: BTreeMap::new(),
389 shape: None,
390 constraints: None,
391 enums: None,
392 }
393 }
394}
395
396fn builtin_format_aliases() -> &'static [(&'static str, &'static str)] {
402 &[
403 ("uuid4", "uuid"),
404 ("uuid_v4", "uuid"),
405 ("UUID", "uuid"),
406 ("unix-time", "int64"),
407 ("unix_time", "int64"),
408 ("unixtime", "int64"),
409 ("timestamp", "int64"),
410 ]
411}
412
413impl TypeMappingConfig {
414 pub fn constraint_mode(&self) -> ConstraintMode {
419 self.constraints
420 .as_ref()
421 .and_then(|c| c.mode)
422 .unwrap_or_default()
423 }
424
425 pub fn x_enum_varnames_enabled(&self) -> bool {
428 self.enums
429 .as_ref()
430 .and_then(|e| e.x_enum_varnames)
431 .unwrap_or(true)
432 }
433
434 pub fn x_enum_descriptions_enabled(&self) -> bool {
437 self.enums
438 .as_ref()
439 .and_then(|e| e.x_enum_descriptions)
440 .unwrap_or(true)
441 }
442
443 pub fn conservative() -> Self {
448 Self {
449 date_time: DateStrategy::String,
450 date: DateStrategy::String,
451 time: DateStrategy::String,
452 duration: DurationStrategy::String,
453 uuid: UuidStrategy::String,
454 byte: ByteStrategy::String,
455 binary: BinaryStrategy::String,
456 ipv4: IpStrategy::String,
457 ipv6: IpStrategy::String,
458 uri: UriStrategy::String,
459 email: EmailStrategy::String,
460 unsigned: false,
461 format_aliases: BTreeMap::new(),
462 shape: None,
463 constraints: None,
464 enums: None,
465 }
466 }
467}
468
469#[derive(Debug, Clone, Default, Deserialize, Serialize)]
470#[serde(default, rename_all = "snake_case")]
471pub struct TypeShapeConfig {
472 pub additional_properties_typed: Option<bool>,
473 pub unique_items_to_set: Option<bool>,
474 pub primitive_unions: Option<bool>,
475}
476
477#[derive(Debug, Clone, Default, Deserialize, Serialize)]
478#[serde(default, rename_all = "snake_case")]
479pub struct TypeConstraintsConfig {
480 pub mode: Option<ConstraintMode>,
484}
485
486#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
495#[serde(rename_all = "snake_case")]
496pub enum ConstraintMode {
497 Off,
499 #[default]
502 Doc,
503}
504
505#[derive(Debug, Clone, Default, Deserialize, Serialize)]
506#[serde(default, rename_all = "snake_case")]
507pub struct TypeEnumsConfig {
508 pub x_enum_varnames: Option<bool>,
509 pub x_enum_descriptions: Option<bool>,
510}
511
512pub struct TypeMapper {
517 config: TypeMappingConfig,
518 used: RefCell<UsedFeatures>,
519}
520
521impl Default for TypeMapper {
522 fn default() -> Self {
523 Self::new(TypeMappingConfig::default())
524 }
525}
526
527impl TypeMapper {
528 pub fn new(config: TypeMappingConfig) -> Self {
529 Self {
530 config,
531 used: RefCell::new(UsedFeatures::default()),
532 }
533 }
534
535 pub fn used_features(&self) -> UsedFeatures {
538 self.used.borrow().clone()
539 }
540
541 pub fn config(&self) -> &TypeMappingConfig {
545 &self.config
546 }
547
548 pub fn config_shape_primitive_unions(&self) -> Option<bool> {
553 self.config.shape.as_ref().and_then(|s| s.primitive_unions)
554 }
555
556 pub fn config_shape_additional_properties_typed(&self) -> Option<bool> {
560 self.config
561 .shape
562 .as_ref()
563 .and_then(|s| s.additional_properties_typed)
564 }
565
566 pub fn config_constraint_mode(&self) -> ConstraintMode {
571 self.config
572 .constraints
573 .as_ref()
574 .and_then(|c| c.mode)
575 .unwrap_or_default()
576 }
577
578 fn record(&self, feature: TypeFeature) {
579 self.used.borrow_mut().insert(feature);
580 }
581
582 pub fn string_format(&self, format: Option<&str>) -> MappedType {
590 let normalized = self.normalize_format(format);
591 match normalized.as_deref() {
592 Some("date-time") => self.map_date_time(self.config.date_time),
593 Some("date") => self.map_date(self.config.date),
594 Some("time") => self.map_time(self.config.time),
595 Some("duration") => self.map_duration(self.config.duration),
596 Some("uuid") => self.map_uuid(self.config.uuid),
597 Some("byte") => self.map_byte(self.config.byte),
598 Some("binary") => self.map_binary(self.config.binary),
599 Some("ipv4") => self.map_ipv4(self.config.ipv4),
600 Some("ipv6") => self.map_ipv6(self.config.ipv6),
601 Some("uri") | Some("url") => self.map_uri(self.config.uri),
602 Some("email") => self.map_email(self.config.email),
603 _ => MappedType::plain("String"),
606 }
607 }
608
609 fn normalize_format(&self, format: Option<&str>) -> Option<String> {
614 let raw = format?;
615 if let Some(target) = self.config.format_aliases.get(raw) {
616 return Some(target.clone());
617 }
618 for (from, to) in builtin_format_aliases() {
619 if *from == raw {
620 return Some((*to).to_string());
621 }
622 }
623 Some(raw.to_string())
624 }
625
626 fn map_date_time(&self, strat: DateStrategy) -> MappedType {
627 match strat {
628 DateStrategy::String => MappedType::plain("String"),
629 DateStrategy::Chrono => {
630 self.record(TypeFeature::Chrono);
631 MappedType::with_feature("chrono::DateTime<chrono::Utc>", TypeFeature::Chrono)
636 }
637 DateStrategy::Time => {
638 self.record(TypeFeature::Time);
639 MappedType::with_codec(
640 "time::OffsetDateTime",
641 "time::serde::rfc3339",
642 TypeFeature::Time,
643 )
644 }
645 }
646 }
647
648 fn map_date(&self, strat: DateStrategy) -> MappedType {
649 match strat {
650 DateStrategy::String => MappedType::plain("String"),
651 DateStrategy::Chrono => {
652 self.record(TypeFeature::Chrono);
653 MappedType::with_feature("chrono::NaiveDate", TypeFeature::Chrono)
656 }
657 DateStrategy::Time => {
658 self.record(TypeFeature::Time);
659 MappedType::with_codec("time::Date", "time::serde::iso8601", TypeFeature::Time)
660 }
661 }
662 }
663
664 fn map_time(&self, strat: DateStrategy) -> MappedType {
665 match strat {
666 DateStrategy::String => MappedType::plain("String"),
667 DateStrategy::Chrono => {
668 self.record(TypeFeature::Chrono);
669 MappedType::with_feature("chrono::NaiveTime", TypeFeature::Chrono)
670 }
671 DateStrategy::Time => {
672 self.record(TypeFeature::Time);
673 MappedType::with_codec("time::Time", "time::serde::iso8601", TypeFeature::Time)
674 }
675 }
676 }
677
678 fn map_duration(&self, strat: DurationStrategy) -> MappedType {
679 match strat {
680 DurationStrategy::String => MappedType::plain("String"),
681 DurationStrategy::Chrono => {
682 MappedType::plain("String")
689 }
690 DurationStrategy::Iso8601 => {
691 self.record(TypeFeature::Iso8601);
692 MappedType::with_feature("iso8601::Duration", TypeFeature::Iso8601)
693 }
694 }
695 }
696
697 fn map_uuid(&self, strat: UuidStrategy) -> MappedType {
698 match strat {
699 UuidStrategy::String => MappedType::plain("String"),
700 UuidStrategy::Uuid => {
701 self.record(TypeFeature::Uuid);
702 MappedType::with_feature("uuid::Uuid", TypeFeature::Uuid)
703 }
704 }
705 }
706
707 fn map_byte(&self, strat: ByteStrategy) -> MappedType {
708 match strat {
709 ByteStrategy::String => MappedType::plain("String"),
710 ByteStrategy::VecU8 => MappedType::plain("Vec<u8>"),
711 ByteStrategy::Base64 => {
712 self.record(TypeFeature::Base64);
713 MappedType::with_codec("Vec<u8>", "base64_serde", TypeFeature::Base64)
717 }
718 }
719 }
720
721 fn map_binary(&self, strat: BinaryStrategy) -> MappedType {
722 match strat {
723 BinaryStrategy::String => MappedType::plain("String"),
724 BinaryStrategy::VecU8 => MappedType::plain("Vec<u8>"),
725 BinaryStrategy::Bytes => {
726 self.record(TypeFeature::Bytes);
727 MappedType::with_feature("bytes::Bytes", TypeFeature::Bytes)
728 }
729 }
730 }
731
732 fn map_ipv4(&self, strat: IpStrategy) -> MappedType {
733 match strat {
734 IpStrategy::String => MappedType::plain("String"),
735 IpStrategy::Std => MappedType::plain("std::net::Ipv4Addr"),
736 }
737 }
738
739 fn map_ipv6(&self, strat: IpStrategy) -> MappedType {
740 match strat {
741 IpStrategy::String => MappedType::plain("String"),
742 IpStrategy::Std => MappedType::plain("std::net::Ipv6Addr"),
743 }
744 }
745
746 fn map_uri(&self, strat: UriStrategy) -> MappedType {
747 match strat {
748 UriStrategy::String => MappedType::plain("String"),
749 UriStrategy::Url => {
750 self.record(TypeFeature::Url);
751 MappedType::with_feature("url::Url", TypeFeature::Url)
752 }
753 }
754 }
755
756 fn map_email(&self, strat: EmailStrategy) -> MappedType {
757 match strat {
758 EmailStrategy::String => MappedType::plain("String"),
759 EmailStrategy::EmailAddress => {
760 self.record(TypeFeature::EmailAddress);
761 MappedType::with_feature("email_address::EmailAddress", TypeFeature::EmailAddress)
762 }
763 }
764 }
765
766 pub fn integer_format(&self, format: Option<&str>) -> MappedType {
773 let normalized = self.normalize_format(format);
774 match normalized.as_deref() {
775 Some("int32") => MappedType::plain("i32"),
776 Some("int64") => MappedType::plain("i64"),
777 Some("uint32") if self.config.unsigned => MappedType::plain("u32"),
778 Some("uint64") if self.config.unsigned => MappedType::plain("u64"),
779 Some("uint") if self.config.unsigned => MappedType::plain("u64"),
783 _ => MappedType::plain("i64"),
784 }
785 }
786
787 pub fn number_format(&self, format: Option<&str>) -> MappedType {
788 let normalized = self.normalize_format(format);
789 match normalized.as_deref() {
790 Some("float") => MappedType::plain("f32"),
791 Some("double") => MappedType::plain("f64"),
792 _ => MappedType::plain("f64"),
793 }
794 }
795
796 pub fn boolean(&self) -> MappedType {
797 MappedType::plain("bool")
798 }
799
800 pub fn untyped_array(&self) -> MappedType {
801 MappedType::plain("Vec<serde_json::Value>")
802 }
803
804 pub fn dynamic_json(&self) -> MappedType {
805 MappedType::plain("serde_json::Value")
806 }
807
808 pub fn null_unit(&self) -> MappedType {
809 MappedType::plain("()")
810 }
811
812 pub fn map(&self, ty: OpenApiSchemaType, details: &SchemaDetails) -> MappedType {
814 let format = details.format.as_deref();
815 match ty {
816 OpenApiSchemaType::String => self.string_format(format),
817 OpenApiSchemaType::Integer => self.integer_format(format),
818 OpenApiSchemaType::Number => self.number_format(format),
819 OpenApiSchemaType::Boolean => self.boolean(),
820 OpenApiSchemaType::Array => self.untyped_array(),
821 OpenApiSchemaType::Object => self.dynamic_json(),
822 OpenApiSchemaType::Null => self.null_unit(),
823 }
824 }
825}
826
827#[cfg(test)]
828mod tests {
829 use super::*;
830
831 fn details_with_format(format: Option<&str>) -> SchemaDetails {
832 SchemaDetails {
833 format: format.map(str::to_string),
834 ..Default::default()
835 }
836 }
837
838 #[test]
839 fn default_mapper_emits_typed_scalars_for_common_formats() {
840 let m = TypeMapper::default();
841 assert_eq!(
842 m.string_format(Some("date-time")).rust_type,
843 "chrono::DateTime<chrono::Utc>"
844 );
845 assert_eq!(m.string_format(Some("date")).rust_type, "chrono::NaiveDate");
846 assert_eq!(m.string_format(Some("uuid")).rust_type, "uuid::Uuid");
847 assert_eq!(m.string_format(Some("uri")).rust_type, "url::Url");
848 assert_eq!(
849 m.string_format(Some("ipv4")).rust_type,
850 "std::net::Ipv4Addr"
851 );
852 assert_eq!(m.string_format(Some("byte")).rust_type, "Vec<u8>");
853 assert_eq!(m.string_format(Some("binary")).rust_type, "bytes::Bytes");
854 }
855
856 #[test]
857 fn date_time_uses_default_chrono_serde() {
858 let m = TypeMapper::default();
861 let mt = m.string_format(Some("date-time"));
862 assert_eq!(mt.rust_type, "chrono::DateTime<chrono::Utc>");
863 assert!(mt.serde_with.is_none());
864 assert_eq!(mt.feature, Some(TypeFeature::Chrono));
865 }
866
867 #[test]
868 fn byte_emits_base64_codec() {
869 let m = TypeMapper::default();
870 let mt = m.string_format(Some("byte"));
871 assert_eq!(mt.rust_type, "Vec<u8>");
872 assert_eq!(mt.serde_with.as_deref(), Some("base64_serde"));
873 assert_eq!(mt.feature, Some(TypeFeature::Base64));
874 }
875
876 #[test]
877 fn conservative_config_collapses_everything_to_string() {
878 let m = TypeMapper::new(TypeMappingConfig::conservative());
879 for fmt in [
880 Some("date-time"),
881 Some("uuid"),
882 Some("uri"),
883 Some("byte"),
884 Some("binary"),
885 Some("ipv4"),
886 Some("ipv6"),
887 Some("date"),
888 None,
889 ] {
890 let mt = m.string_format(fmt);
891 assert_eq!(mt.rust_type, "String", "format = {fmt:?}");
892 assert!(mt.serde_with.is_none(), "format = {fmt:?}");
893 }
894 }
895
896 #[test]
897 fn unknown_formats_fall_through_to_string() {
898 let m = TypeMapper::default();
899 for fmt in [Some("hostname"), Some("password"), Some("idn-email")] {
900 assert_eq!(m.string_format(fmt).rust_type, "String");
901 }
902 }
903
904 #[test]
905 fn integer_formats_match_pre_refactor_behavior() {
906 let m = TypeMapper::default();
907 assert_eq!(m.integer_format(Some("int32")).rust_type, "i32");
908 assert_eq!(m.integer_format(Some("int64")).rust_type, "i64");
909 assert_eq!(m.integer_format(None).rust_type, "i64");
910 }
911
912 #[test]
913 fn integer_formats_default_handles_unsigned_q21() {
914 let m = TypeMapper::default();
915 assert_eq!(m.integer_format(Some("uint32")).rust_type, "u32");
916 assert_eq!(m.integer_format(Some("uint64")).rust_type, "u64");
917 assert_eq!(m.integer_format(Some("uint")).rust_type, "u64");
919 }
920
921 #[test]
922 fn unsigned_off_degrades_uint_to_i64() {
923 let mut cfg = TypeMappingConfig::default();
924 cfg.unsigned = false;
925 let m = TypeMapper::new(cfg);
926 assert_eq!(m.integer_format(Some("uint32")).rust_type, "i64");
927 assert_eq!(m.integer_format(Some("uint64")).rust_type, "i64");
928 }
929
930 #[test]
931 fn conservative_disables_unsigned() {
932 let m = TypeMapper::new(TypeMappingConfig::conservative());
933 assert_eq!(m.integer_format(Some("uint64")).rust_type, "i64");
934 }
935
936 #[test]
937 fn builtin_aliases_normalize_uuid_variants_to_uuid() {
938 let m = TypeMapper::default();
939 for fmt in ["uuid4", "uuid_v4", "UUID"] {
940 let mt = m.string_format(Some(fmt));
941 assert_eq!(mt.rust_type, "uuid::Uuid", "format = {fmt}");
942 }
943 }
944
945 #[test]
946 fn builtin_aliases_normalize_unix_time_to_int64() {
947 let m = TypeMapper::default();
948 for fmt in ["unix-time", "unix_time", "unixtime", "timestamp"] {
949 let mt = m.integer_format(Some(fmt));
950 assert_eq!(mt.rust_type, "i64", "format = {fmt}");
951 }
952 }
953
954 #[test]
955 fn user_alias_overrides_builtin() {
956 let mut cfg = TypeMappingConfig::default();
957 cfg.format_aliases
959 .insert("uuid4".to_string(), "hostname".to_string());
960 let m = TypeMapper::new(cfg);
961 assert_eq!(m.string_format(Some("uuid4")).rust_type, "String");
963 }
964
965 #[test]
966 fn used_features_records_referenced_crates() {
967 let m = TypeMapper::default();
968 let _ = m.string_format(Some("date-time"));
969 let _ = m.string_format(Some("uuid"));
970 let used = m.used_features();
971 assert!(used.contains(TypeFeature::Chrono));
972 assert!(used.contains(TypeFeature::Uuid));
973 assert!(!used.contains(TypeFeature::Bytes));
974 }
975
976 #[test]
977 fn format_alias_normalizes_before_dispatch() {
978 let mut cfg = TypeMappingConfig::default();
979 cfg.format_aliases
980 .insert("uuid4".to_string(), "uuid".to_string());
981 let m = TypeMapper::new(cfg);
982 assert_eq!(m.string_format(Some("uuid4")).rust_type, "uuid::Uuid");
983 }
984
985 #[test]
986 fn conservative_helper_round_trips() {
987 let cfg = TypeMappingConfig::conservative();
988 assert!(matches!(cfg.date_time, DateStrategy::String));
989 assert!(matches!(cfg.uuid, UuidStrategy::String));
990 }
991
992 #[test]
993 fn dep_requirement_renders_features_list() {
994 let dep = TypeFeature::Chrono.dep_requirement();
995 assert_eq!(dep.crate_name, "chrono");
996 assert_eq!(dep.features, vec!["serde"]);
997 assert_eq!(
998 dep.to_toml_line(),
999 r#"chrono = { version = "0.4", features = ["serde"] }"#
1000 );
1001 }
1002
1003 #[test]
1004 fn dep_requirement_omits_features_when_none() {
1005 let dep = TypeFeature::Base64.dep_requirement();
1006 assert_eq!(dep.to_toml_line(), r#"base64 = "0.22""#);
1007 }
1008
1009 #[test]
1010 fn collect_dep_requirements_is_sorted_and_unique() {
1011 let mut used = UsedFeatures::default();
1012 used.insert(TypeFeature::Url);
1013 used.insert(TypeFeature::Chrono);
1014 used.insert(TypeFeature::Chrono); used.insert(TypeFeature::Uuid);
1016 let deps = collect_dep_requirements(&used);
1017 assert_eq!(
1018 deps.iter().map(|d| d.crate_name).collect::<Vec<_>>(),
1019 vec!["chrono", "url", "uuid"]
1020 );
1021 }
1022
1023 #[test]
1024 fn render_required_deps_toml_is_none_when_empty() {
1025 let deps: Vec<DepRequirement> = Vec::new();
1026 assert!(render_required_deps_toml(&deps).is_none());
1027 }
1028
1029 #[test]
1030 fn render_required_deps_toml_includes_dependencies_block() {
1031 let deps = vec![
1032 TypeFeature::Chrono.dep_requirement(),
1033 TypeFeature::Uuid.dep_requirement(),
1034 ];
1035 let toml = render_required_deps_toml(&deps).expect("non-empty");
1036 assert!(toml.contains("[dependencies]"));
1037 assert!(toml.contains("chrono = "));
1038 assert!(toml.contains("uuid = "));
1039 assert!(toml.contains("# Generated by openapi-to-rust"));
1040 }
1041
1042 #[test]
1043 fn map_dispatches_through_helpers() {
1044 let m = TypeMapper::default();
1045 assert_eq!(
1046 m.map(
1047 OpenApiSchemaType::String,
1048 &details_with_format(Some("uuid"))
1049 )
1050 .rust_type,
1051 "uuid::Uuid"
1052 );
1053 assert_eq!(
1054 m.map(
1055 OpenApiSchemaType::Integer,
1056 &details_with_format(Some("int32"))
1057 )
1058 .rust_type,
1059 "i32"
1060 );
1061 }
1062}