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 TimeDate,
95 TimeTime,
97 Iso8601,
98 Uuid,
99 Bytes,
100 Base64,
101 Url,
102 EmailAddress,
103}
104
105impl TypeFeature {
106 pub fn dep_requirement(self) -> DepRequirement {
110 match self {
111 Self::Chrono => DepRequirement::new("chrono", "0.4").with_features(&["serde"]),
112 Self::Time => DepRequirement::new("time", "0.3").with_features(&[
115 "serde",
116 "formatting",
117 "parsing",
118 ]),
119 Self::TimeDate | Self::TimeTime => DepRequirement::new("time", "0.3").with_features(&[
123 "serde",
124 "formatting",
125 "parsing",
126 "macros",
127 ]),
128 Self::Iso8601 => DepRequirement::new("iso8601", "0.6"),
129 Self::Uuid => DepRequirement::new("uuid", "1").with_features(&["serde", "v4"]),
130 Self::Bytes => DepRequirement::new("bytes", "1").with_features(&["serde"]),
131 Self::Base64 => DepRequirement::new("base64", "0.22"),
132 Self::Url => DepRequirement::new("url", "2").with_features(&["serde"]),
133 Self::EmailAddress => DepRequirement::new("email_address", "0.2"),
134 }
135 }
136}
137
138#[derive(Debug, Clone, PartialEq, Eq)]
140pub struct DepRequirement {
141 pub crate_name: &'static str,
142 pub version: &'static str,
143 pub features: Vec<&'static str>,
144}
145
146impl DepRequirement {
147 pub fn new(crate_name: &'static str, version: &'static str) -> Self {
148 Self {
149 crate_name,
150 version,
151 features: Vec::new(),
152 }
153 }
154
155 pub fn with_features(mut self, features: &[&'static str]) -> Self {
156 self.features = features.to_vec();
157 self
158 }
159
160 pub fn to_toml_line(&self) -> String {
163 if self.features.is_empty() {
164 format!("{} = \"{}\"", self.crate_name, self.version)
165 } else {
166 let feats = self
167 .features
168 .iter()
169 .map(|f| format!("\"{f}\""))
170 .collect::<Vec<_>>()
171 .join(", ");
172 format!(
173 "{} = {{ version = \"{}\", features = [{}] }}",
174 self.crate_name, self.version, feats
175 )
176 }
177 }
178}
179
180pub fn render_required_deps_toml(deps: &[DepRequirement]) -> Option<String> {
185 if deps.is_empty() {
186 return None;
187 }
188 let mut out = String::new();
189 out.push_str(
190 "# Generated by openapi-to-rust.\n\
191 # These crates are required by the typed-scalar formats used\n\
192 # in your OpenAPI spec. Copy these lines into the [dependencies]\n\
193 # section of your consuming crate's Cargo.toml.\n\
194 #\n\
195 # To opt out of typed scalars (and avoid these deps), set\n\
196 # the relevant strategies to \"string\" in [generator.types],\n\
197 # or pass --types-conservative on the CLI.\n\
198 \n\
199 [dependencies]\n",
200 );
201 for dep in deps {
202 out.push_str(&dep.to_toml_line());
203 out.push('\n');
204 }
205 Some(out)
206}
207
208pub fn collect_dep_requirements(used: &UsedFeatures) -> Vec<DepRequirement> {
212 let mut deps: Vec<DepRequirement> = used.iter().map(|f| f.dep_requirement()).collect();
213 deps.sort_by_key(|d| d.crate_name);
214 let mut merged: Vec<DepRequirement> = Vec::new();
218 for dep in deps {
219 match merged.last_mut() {
220 Some(last) if last.crate_name == dep.crate_name => {
221 for feat in dep.features {
222 if !last.features.contains(&feat) {
223 last.features.push(feat);
224 }
225 }
226 }
227 _ => merged.push(dep),
228 }
229 }
230 merged
231}
232
233#[derive(Debug, Default, Clone)]
235pub struct UsedFeatures {
236 set: BTreeSet<TypeFeature>,
237}
238
239impl UsedFeatures {
240 pub fn insert(&mut self, feature: TypeFeature) {
241 self.set.insert(feature);
242 }
243
244 pub fn contains(&self, feature: TypeFeature) -> bool {
245 self.set.contains(&feature)
246 }
247
248 pub fn iter(&self) -> impl Iterator<Item = &TypeFeature> {
249 self.set.iter()
250 }
251
252 pub fn is_empty(&self) -> bool {
253 self.set.is_empty()
254 }
255}
256
257#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
263#[serde(rename_all = "lowercase")]
264pub enum DateStrategy {
265 String,
267 #[default]
269 Chrono,
270 Time,
272}
273
274#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
276#[serde(rename_all = "lowercase")]
277pub enum DurationStrategy {
278 #[default]
285 String,
286 Chrono,
289 Iso8601,
291}
292
293#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
295#[serde(rename_all = "lowercase")]
296pub enum UuidStrategy {
297 String,
298 #[default]
300 Uuid,
301}
302
303#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
305#[serde(rename_all = "snake_case")]
306pub enum ByteStrategy {
307 String,
308 #[default]
311 Base64,
312 VecU8,
314}
315
316#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
318#[serde(rename_all = "snake_case")]
319pub enum BinaryStrategy {
320 String,
321 #[default]
323 Bytes,
324 VecU8,
325}
326
327#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
329#[serde(rename_all = "lowercase")]
330pub enum IpStrategy {
331 String,
332 #[default]
334 Std,
335}
336
337#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
339#[serde(rename_all = "lowercase")]
340pub enum UriStrategy {
341 String,
342 #[default]
344 Url,
345}
346
347#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
353#[serde(rename_all = "snake_case")]
354pub enum EmailStrategy {
355 #[default]
356 String,
357 EmailAddress,
358}
359
360#[derive(Debug, Clone, Deserialize, Serialize)]
368#[serde(default, rename_all = "snake_case")]
369pub struct TypeMappingConfig {
370 pub date_time: DateStrategy,
371 pub date: DateStrategy,
372 pub time: DateStrategy,
373 pub duration: DurationStrategy,
374 pub uuid: UuidStrategy,
375 pub byte: ByteStrategy,
376 pub binary: BinaryStrategy,
377 pub ipv4: IpStrategy,
378 pub ipv6: IpStrategy,
379 pub uri: UriStrategy,
380 pub email: EmailStrategy,
381
382 #[serde(default = "default_true")]
387 pub unsigned: bool,
388
389 #[serde(default)]
394 pub format_aliases: BTreeMap<String, String>,
395
396 pub shape: Option<TypeShapeConfig>,
398
399 pub constraints: Option<TypeConstraintsConfig>,
401
402 pub enums: Option<TypeEnumsConfig>,
404}
405
406fn default_true() -> bool {
407 true
408}
409
410impl Default for TypeMappingConfig {
411 fn default() -> Self {
412 Self {
413 date_time: DateStrategy::default(),
414 date: DateStrategy::default(),
415 time: DateStrategy::default(),
416 duration: DurationStrategy::default(),
417 uuid: UuidStrategy::default(),
418 byte: ByteStrategy::default(),
419 binary: BinaryStrategy::default(),
420 ipv4: IpStrategy::default(),
421 ipv6: IpStrategy::default(),
422 uri: UriStrategy::default(),
423 email: EmailStrategy::default(),
424 unsigned: true,
425 format_aliases: BTreeMap::new(),
426 shape: None,
427 constraints: None,
428 enums: None,
429 }
430 }
431}
432
433fn builtin_format_aliases() -> &'static [(&'static str, &'static str)] {
439 &[
440 ("uuid4", "uuid"),
441 ("uuid_v4", "uuid"),
442 ("UUID", "uuid"),
443 ("unix-time", "int64"),
444 ("unix_time", "int64"),
445 ("unixtime", "int64"),
446 ("timestamp", "int64"),
447 ]
448}
449
450impl TypeMappingConfig {
451 pub fn constraint_mode(&self) -> ConstraintMode {
456 self.constraints
457 .as_ref()
458 .and_then(|c| c.mode)
459 .unwrap_or_default()
460 }
461
462 pub fn x_enum_varnames_enabled(&self) -> bool {
465 self.enums
466 .as_ref()
467 .and_then(|e| e.x_enum_varnames)
468 .unwrap_or(true)
469 }
470
471 pub fn x_enum_descriptions_enabled(&self) -> bool {
474 self.enums
475 .as_ref()
476 .and_then(|e| e.x_enum_descriptions)
477 .unwrap_or(true)
478 }
479
480 pub fn conservative() -> Self {
485 Self {
486 date_time: DateStrategy::String,
487 date: DateStrategy::String,
488 time: DateStrategy::String,
489 duration: DurationStrategy::String,
490 uuid: UuidStrategy::String,
491 byte: ByteStrategy::String,
492 binary: BinaryStrategy::String,
493 ipv4: IpStrategy::String,
494 ipv6: IpStrategy::String,
495 uri: UriStrategy::String,
496 email: EmailStrategy::String,
497 unsigned: false,
498 format_aliases: BTreeMap::new(),
499 shape: None,
500 constraints: None,
501 enums: None,
502 }
503 }
504}
505
506#[derive(Debug, Clone, Default, Deserialize, Serialize)]
507#[serde(default, rename_all = "snake_case")]
508pub struct TypeShapeConfig {
509 pub additional_properties_typed: Option<bool>,
510 pub unique_items_to_set: Option<bool>,
511 pub primitive_unions: Option<bool>,
512}
513
514#[derive(Debug, Clone, Default, Deserialize, Serialize)]
515#[serde(default, rename_all = "snake_case")]
516pub struct TypeConstraintsConfig {
517 pub mode: Option<ConstraintMode>,
521}
522
523#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
532#[serde(rename_all = "snake_case")]
533pub enum ConstraintMode {
534 Off,
536 #[default]
539 Doc,
540}
541
542#[derive(Debug, Clone, Default, Deserialize, Serialize)]
543#[serde(default, rename_all = "snake_case")]
544pub struct TypeEnumsConfig {
545 pub x_enum_varnames: Option<bool>,
546 pub x_enum_descriptions: Option<bool>,
547}
548
549pub struct TypeMapper {
554 config: TypeMappingConfig,
555 used: RefCell<UsedFeatures>,
556}
557
558impl Default for TypeMapper {
559 fn default() -> Self {
560 Self::new(TypeMappingConfig::default())
561 }
562}
563
564impl TypeMapper {
565 pub fn new(config: TypeMappingConfig) -> Self {
566 Self {
567 config,
568 used: RefCell::new(UsedFeatures::default()),
569 }
570 }
571
572 pub fn used_features(&self) -> UsedFeatures {
575 self.used.borrow().clone()
576 }
577
578 pub fn config(&self) -> &TypeMappingConfig {
582 &self.config
583 }
584
585 pub fn config_shape_primitive_unions(&self) -> Option<bool> {
590 self.config.shape.as_ref().and_then(|s| s.primitive_unions)
591 }
592
593 pub fn config_shape_additional_properties_typed(&self) -> Option<bool> {
597 self.config
598 .shape
599 .as_ref()
600 .and_then(|s| s.additional_properties_typed)
601 }
602
603 pub fn config_constraint_mode(&self) -> ConstraintMode {
608 self.config
609 .constraints
610 .as_ref()
611 .and_then(|c| c.mode)
612 .unwrap_or_default()
613 }
614
615 fn record(&self, feature: TypeFeature) {
616 self.used.borrow_mut().insert(feature);
617 }
618
619 pub fn string_format(&self, format: Option<&str>) -> MappedType {
627 let normalized = self.normalize_format(format);
628 match normalized.as_deref() {
629 Some("date-time") => self.map_date_time(self.config.date_time),
630 Some("date") => self.map_date(self.config.date),
631 Some("time") => self.map_time(self.config.time),
632 Some("duration") => self.map_duration(self.config.duration),
633 Some("uuid") => self.map_uuid(self.config.uuid),
634 Some("byte") => self.map_byte(self.config.byte),
635 Some("binary") => self.map_binary(self.config.binary),
636 Some("ipv4") => self.map_ipv4(self.config.ipv4),
637 Some("ipv6") => self.map_ipv6(self.config.ipv6),
638 Some("uri") | Some("url") => self.map_uri(self.config.uri),
639 Some("email") => self.map_email(self.config.email),
640 _ => MappedType::plain("String"),
643 }
644 }
645
646 fn normalize_format(&self, format: Option<&str>) -> Option<String> {
651 let raw = format?;
652 if let Some(target) = self.config.format_aliases.get(raw) {
653 return Some(target.clone());
654 }
655 for (from, to) in builtin_format_aliases() {
656 if *from == raw {
657 return Some((*to).to_string());
658 }
659 }
660 Some(raw.to_string())
661 }
662
663 fn map_date_time(&self, strat: DateStrategy) -> MappedType {
664 match strat {
665 DateStrategy::String => MappedType::plain("String"),
666 DateStrategy::Chrono => {
667 self.record(TypeFeature::Chrono);
668 MappedType::with_feature("chrono::DateTime<chrono::Utc>", TypeFeature::Chrono)
673 }
674 DateStrategy::Time => {
675 self.record(TypeFeature::Time);
676 MappedType::with_codec(
677 "time::OffsetDateTime",
678 "time::serde::rfc3339",
679 TypeFeature::Time,
680 )
681 }
682 }
683 }
684
685 fn map_date(&self, strat: DateStrategy) -> MappedType {
686 match strat {
687 DateStrategy::String => MappedType::plain("String"),
688 DateStrategy::Chrono => {
689 self.record(TypeFeature::Chrono);
690 MappedType::with_feature("chrono::NaiveDate", TypeFeature::Chrono)
693 }
694 DateStrategy::Time => {
695 self.record(TypeFeature::TimeDate);
696 MappedType::with_codec("time::Date", "time_date_format", TypeFeature::TimeDate)
701 }
702 }
703 }
704
705 fn map_time(&self, strat: DateStrategy) -> MappedType {
706 match strat {
707 DateStrategy::String => MappedType::plain("String"),
708 DateStrategy::Chrono => {
709 self.record(TypeFeature::Chrono);
710 MappedType::with_feature("chrono::NaiveTime", TypeFeature::Chrono)
711 }
712 DateStrategy::Time => {
713 self.record(TypeFeature::TimeTime);
714 MappedType::with_codec("time::Time", "time_time_format", TypeFeature::TimeTime)
717 }
718 }
719 }
720
721 fn map_duration(&self, strat: DurationStrategy) -> MappedType {
722 match strat {
723 DurationStrategy::String => MappedType::plain("String"),
724 DurationStrategy::Chrono => {
725 MappedType::plain("String")
732 }
733 DurationStrategy::Iso8601 => {
734 self.record(TypeFeature::Iso8601);
735 MappedType::with_feature("iso8601::Duration", TypeFeature::Iso8601)
736 }
737 }
738 }
739
740 fn map_uuid(&self, strat: UuidStrategy) -> MappedType {
741 match strat {
742 UuidStrategy::String => MappedType::plain("String"),
743 UuidStrategy::Uuid => {
744 self.record(TypeFeature::Uuid);
745 MappedType::with_feature("uuid::Uuid", TypeFeature::Uuid)
746 }
747 }
748 }
749
750 fn map_byte(&self, strat: ByteStrategy) -> MappedType {
751 match strat {
752 ByteStrategy::String => MappedType::plain("String"),
753 ByteStrategy::VecU8 => MappedType::plain("Vec<u8>"),
754 ByteStrategy::Base64 => {
755 self.record(TypeFeature::Base64);
756 MappedType::with_codec("Vec<u8>", "base64_serde", TypeFeature::Base64)
760 }
761 }
762 }
763
764 fn map_binary(&self, strat: BinaryStrategy) -> MappedType {
765 match strat {
766 BinaryStrategy::String => MappedType::plain("String"),
767 BinaryStrategy::VecU8 => MappedType::plain("Vec<u8>"),
768 BinaryStrategy::Bytes => {
769 self.record(TypeFeature::Bytes);
770 MappedType::with_feature("bytes::Bytes", TypeFeature::Bytes)
771 }
772 }
773 }
774
775 fn map_ipv4(&self, strat: IpStrategy) -> MappedType {
776 match strat {
777 IpStrategy::String => MappedType::plain("String"),
778 IpStrategy::Std => MappedType::plain("std::net::Ipv4Addr"),
779 }
780 }
781
782 fn map_ipv6(&self, strat: IpStrategy) -> MappedType {
783 match strat {
784 IpStrategy::String => MappedType::plain("String"),
785 IpStrategy::Std => MappedType::plain("std::net::Ipv6Addr"),
786 }
787 }
788
789 fn map_uri(&self, strat: UriStrategy) -> MappedType {
790 match strat {
791 UriStrategy::String => MappedType::plain("String"),
792 UriStrategy::Url => {
793 self.record(TypeFeature::Url);
794 MappedType::with_feature("url::Url", TypeFeature::Url)
795 }
796 }
797 }
798
799 fn map_email(&self, strat: EmailStrategy) -> MappedType {
800 match strat {
801 EmailStrategy::String => MappedType::plain("String"),
802 EmailStrategy::EmailAddress => {
803 self.record(TypeFeature::EmailAddress);
804 MappedType::with_feature("email_address::EmailAddress", TypeFeature::EmailAddress)
805 }
806 }
807 }
808
809 pub fn integer_format(&self, format: Option<&str>) -> MappedType {
816 let normalized = self.normalize_format(format);
817 match normalized.as_deref() {
818 Some("int32") => MappedType::plain("i32"),
819 Some("int64") => MappedType::plain("i64"),
820 Some("uint32") if self.config.unsigned => MappedType::plain("u32"),
821 Some("uint64") if self.config.unsigned => MappedType::plain("u64"),
822 Some("uint") if self.config.unsigned => MappedType::plain("u64"),
826 _ => MappedType::plain("i64"),
827 }
828 }
829
830 pub fn number_format(&self, format: Option<&str>) -> MappedType {
831 let normalized = self.normalize_format(format);
832 match normalized.as_deref() {
833 Some("float") => MappedType::plain("f32"),
834 Some("double") => MappedType::plain("f64"),
835 _ => MappedType::plain("f64"),
836 }
837 }
838
839 pub fn boolean(&self) -> MappedType {
840 MappedType::plain("bool")
841 }
842
843 pub fn untyped_array(&self) -> MappedType {
844 MappedType::plain("Vec<serde_json::Value>")
845 }
846
847 pub fn dynamic_json(&self) -> MappedType {
848 MappedType::plain("serde_json::Value")
849 }
850
851 pub fn null_unit(&self) -> MappedType {
852 MappedType::plain("()")
853 }
854
855 pub fn map(&self, ty: OpenApiSchemaType, details: &SchemaDetails) -> MappedType {
857 let format = details.format.as_deref();
858 match ty {
859 OpenApiSchemaType::String => self.string_format(format),
860 OpenApiSchemaType::Integer => self.integer_format(format),
861 OpenApiSchemaType::Number => self.number_format(format),
862 OpenApiSchemaType::Boolean => self.boolean(),
863 OpenApiSchemaType::Array => self.untyped_array(),
864 OpenApiSchemaType::Object => self.dynamic_json(),
865 OpenApiSchemaType::Null => self.null_unit(),
866 }
867 }
868}
869
870#[cfg(test)]
871mod tests {
872 use super::*;
873
874 fn details_with_format(format: Option<&str>) -> SchemaDetails {
875 SchemaDetails {
876 format: format.map(str::to_string),
877 ..Default::default()
878 }
879 }
880
881 #[test]
882 fn default_mapper_emits_typed_scalars_for_common_formats() {
883 let m = TypeMapper::default();
884 assert_eq!(
885 m.string_format(Some("date-time")).rust_type,
886 "chrono::DateTime<chrono::Utc>"
887 );
888 assert_eq!(m.string_format(Some("date")).rust_type, "chrono::NaiveDate");
889 assert_eq!(m.string_format(Some("uuid")).rust_type, "uuid::Uuid");
890 assert_eq!(m.string_format(Some("uri")).rust_type, "url::Url");
891 assert_eq!(
892 m.string_format(Some("ipv4")).rust_type,
893 "std::net::Ipv4Addr"
894 );
895 assert_eq!(m.string_format(Some("byte")).rust_type, "Vec<u8>");
896 assert_eq!(m.string_format(Some("binary")).rust_type, "bytes::Bytes");
897 }
898
899 #[test]
900 fn date_time_uses_default_chrono_serde() {
901 let m = TypeMapper::default();
904 let mt = m.string_format(Some("date-time"));
905 assert_eq!(mt.rust_type, "chrono::DateTime<chrono::Utc>");
906 assert!(mt.serde_with.is_none());
907 assert_eq!(mt.feature, Some(TypeFeature::Chrono));
908 }
909
910 #[test]
911 fn byte_emits_base64_codec() {
912 let m = TypeMapper::default();
913 let mt = m.string_format(Some("byte"));
914 assert_eq!(mt.rust_type, "Vec<u8>");
915 assert_eq!(mt.serde_with.as_deref(), Some("base64_serde"));
916 assert_eq!(mt.feature, Some(TypeFeature::Base64));
917 }
918
919 #[test]
920 fn conservative_config_collapses_everything_to_string() {
921 let m = TypeMapper::new(TypeMappingConfig::conservative());
922 for fmt in [
923 Some("date-time"),
924 Some("uuid"),
925 Some("uri"),
926 Some("byte"),
927 Some("binary"),
928 Some("ipv4"),
929 Some("ipv6"),
930 Some("date"),
931 None,
932 ] {
933 let mt = m.string_format(fmt);
934 assert_eq!(mt.rust_type, "String", "format = {fmt:?}");
935 assert!(mt.serde_with.is_none(), "format = {fmt:?}");
936 }
937 }
938
939 #[test]
940 fn unknown_formats_fall_through_to_string() {
941 let m = TypeMapper::default();
942 for fmt in [Some("hostname"), Some("password"), Some("idn-email")] {
943 assert_eq!(m.string_format(fmt).rust_type, "String");
944 }
945 }
946
947 #[test]
948 fn integer_formats_match_pre_refactor_behavior() {
949 let m = TypeMapper::default();
950 assert_eq!(m.integer_format(Some("int32")).rust_type, "i32");
951 assert_eq!(m.integer_format(Some("int64")).rust_type, "i64");
952 assert_eq!(m.integer_format(None).rust_type, "i64");
953 }
954
955 #[test]
956 fn integer_formats_default_handles_unsigned_q21() {
957 let m = TypeMapper::default();
958 assert_eq!(m.integer_format(Some("uint32")).rust_type, "u32");
959 assert_eq!(m.integer_format(Some("uint64")).rust_type, "u64");
960 assert_eq!(m.integer_format(Some("uint")).rust_type, "u64");
962 }
963
964 #[test]
965 fn unsigned_off_degrades_uint_to_i64() {
966 let mut cfg = TypeMappingConfig::default();
967 cfg.unsigned = false;
968 let m = TypeMapper::new(cfg);
969 assert_eq!(m.integer_format(Some("uint32")).rust_type, "i64");
970 assert_eq!(m.integer_format(Some("uint64")).rust_type, "i64");
971 }
972
973 #[test]
974 fn conservative_disables_unsigned() {
975 let m = TypeMapper::new(TypeMappingConfig::conservative());
976 assert_eq!(m.integer_format(Some("uint64")).rust_type, "i64");
977 }
978
979 #[test]
980 fn builtin_aliases_normalize_uuid_variants_to_uuid() {
981 let m = TypeMapper::default();
982 for fmt in ["uuid4", "uuid_v4", "UUID"] {
983 let mt = m.string_format(Some(fmt));
984 assert_eq!(mt.rust_type, "uuid::Uuid", "format = {fmt}");
985 }
986 }
987
988 #[test]
989 fn builtin_aliases_normalize_unix_time_to_int64() {
990 let m = TypeMapper::default();
991 for fmt in ["unix-time", "unix_time", "unixtime", "timestamp"] {
992 let mt = m.integer_format(Some(fmt));
993 assert_eq!(mt.rust_type, "i64", "format = {fmt}");
994 }
995 }
996
997 #[test]
998 fn user_alias_overrides_builtin() {
999 let mut cfg = TypeMappingConfig::default();
1000 cfg.format_aliases
1002 .insert("uuid4".to_string(), "hostname".to_string());
1003 let m = TypeMapper::new(cfg);
1004 assert_eq!(m.string_format(Some("uuid4")).rust_type, "String");
1006 }
1007
1008 #[test]
1009 fn used_features_records_referenced_crates() {
1010 let m = TypeMapper::default();
1011 let _ = m.string_format(Some("date-time"));
1012 let _ = m.string_format(Some("uuid"));
1013 let used = m.used_features();
1014 assert!(used.contains(TypeFeature::Chrono));
1015 assert!(used.contains(TypeFeature::Uuid));
1016 assert!(!used.contains(TypeFeature::Bytes));
1017 }
1018
1019 #[test]
1020 fn format_alias_normalizes_before_dispatch() {
1021 let mut cfg = TypeMappingConfig::default();
1022 cfg.format_aliases
1023 .insert("uuid4".to_string(), "uuid".to_string());
1024 let m = TypeMapper::new(cfg);
1025 assert_eq!(m.string_format(Some("uuid4")).rust_type, "uuid::Uuid");
1026 }
1027
1028 #[test]
1029 fn conservative_helper_round_trips() {
1030 let cfg = TypeMappingConfig::conservative();
1031 assert!(matches!(cfg.date_time, DateStrategy::String));
1032 assert!(matches!(cfg.uuid, UuidStrategy::String));
1033 }
1034
1035 #[test]
1036 fn dep_requirement_renders_features_list() {
1037 let dep = TypeFeature::Chrono.dep_requirement();
1038 assert_eq!(dep.crate_name, "chrono");
1039 assert_eq!(dep.features, vec!["serde"]);
1040 assert_eq!(
1041 dep.to_toml_line(),
1042 r#"chrono = { version = "0.4", features = ["serde"] }"#
1043 );
1044 }
1045
1046 #[test]
1047 fn dep_requirement_omits_features_when_none() {
1048 let dep = TypeFeature::Base64.dep_requirement();
1049 assert_eq!(dep.to_toml_line(), r#"base64 = "0.22""#);
1050 }
1051
1052 #[test]
1053 fn collect_dep_requirements_is_sorted_and_unique() {
1054 let mut used = UsedFeatures::default();
1055 used.insert(TypeFeature::Url);
1056 used.insert(TypeFeature::Chrono);
1057 used.insert(TypeFeature::Chrono); used.insert(TypeFeature::Uuid);
1059 let deps = collect_dep_requirements(&used);
1060 assert_eq!(
1061 deps.iter().map(|d| d.crate_name).collect::<Vec<_>>(),
1062 vec!["chrono", "url", "uuid"]
1063 );
1064 }
1065
1066 #[test]
1067 fn render_required_deps_toml_is_none_when_empty() {
1068 let deps: Vec<DepRequirement> = Vec::new();
1069 assert!(render_required_deps_toml(&deps).is_none());
1070 }
1071
1072 #[test]
1073 fn render_required_deps_toml_includes_dependencies_block() {
1074 let deps = vec![
1075 TypeFeature::Chrono.dep_requirement(),
1076 TypeFeature::Uuid.dep_requirement(),
1077 ];
1078 let toml = render_required_deps_toml(&deps).expect("non-empty");
1079 assert!(toml.contains("[dependencies]"));
1080 assert!(toml.contains("chrono = "));
1081 assert!(toml.contains("uuid = "));
1082 assert!(toml.contains("# Generated by openapi-to-rust"));
1083 }
1084
1085 #[test]
1086 fn map_dispatches_through_helpers() {
1087 let m = TypeMapper::default();
1088 assert_eq!(
1089 m.map(
1090 OpenApiSchemaType::String,
1091 &details_with_format(Some("uuid"))
1092 )
1093 .rust_type,
1094 "uuid::Uuid"
1095 );
1096 assert_eq!(
1097 m.map(
1098 OpenApiSchemaType::Integer,
1099 &details_with_format(Some("int32"))
1100 )
1101 .rust_type,
1102 "i32"
1103 );
1104 }
1105}