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>,
46}
47
48impl MappedType {
49 pub fn plain(rust_type: impl Into<String>) -> Self {
51 Self {
52 rust_type: rust_type.into(),
53 serde_with: None,
54 feature: None,
55 }
56 }
57
58 pub fn with_feature(rust_type: impl Into<String>, feature: TypeFeature) -> Self {
63 Self {
64 rust_type: rust_type.into(),
65 serde_with: None,
66 feature: Some(feature),
67 }
68 }
69
70 pub fn with_codec(
72 rust_type: impl Into<String>,
73 codec_path: impl Into<String>,
74 feature: TypeFeature,
75 ) -> Self {
76 Self {
77 rust_type: rust_type.into(),
78 serde_with: Some(codec_path.into()),
79 feature: Some(feature),
80 }
81 }
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
86pub enum TypeFeature {
87 Chrono,
88 Time,
89 TimeDate,
94 TimeTime,
96 Iso8601,
97 Uuid,
98 Bytes,
99 Base64,
100 Url,
101 EmailAddress,
102}
103
104impl TypeFeature {
105 pub fn dep_requirement(self) -> DepRequirement {
107 match self {
108 Self::Chrono => DepRequirement::new("chrono", "0.4").with_features(&["serde"]),
109 Self::Time => DepRequirement::new("time", "0.3").with_features(&[
112 "serde",
113 "formatting",
114 "parsing",
115 ]),
116 Self::TimeDate | Self::TimeTime => DepRequirement::new("time", "0.3").with_features(&[
120 "serde",
121 "formatting",
122 "parsing",
123 "macros",
124 ]),
125 Self::Iso8601 => DepRequirement::new("iso8601", "0.6").with_features(&["serde"]),
126 Self::Uuid => DepRequirement::new("uuid", "1").with_features(&["serde"]),
127 Self::Bytes => DepRequirement::new("bytes", "1").with_features(&["serde"]),
128 Self::Base64 => DepRequirement::new("base64", "0.22"),
129 Self::Url => DepRequirement::new("url", "2").with_features(&["serde"]),
130 Self::EmailAddress => DepRequirement::new("email_address", "0.2"),
131 }
132 }
133}
134
135#[derive(Debug, Clone, PartialEq, Eq)]
137pub struct DepRequirement {
138 pub crate_name: &'static str,
139 pub version: &'static str,
140 pub features: Vec<&'static str>,
141 pub default_features: bool,
142 pub optional: bool,
143}
144
145impl DepRequirement {
146 pub fn new(crate_name: &'static str, version: &'static str) -> Self {
147 Self {
148 crate_name,
149 version,
150 features: Vec::new(),
151 default_features: true,
152 optional: false,
153 }
154 }
155
156 pub fn with_features(mut self, features: &[&'static str]) -> Self {
157 self.features = features.to_vec();
158 self.features.sort_unstable();
159 self.features.dedup();
160 self
161 }
162
163 pub fn without_default_features(mut self) -> Self {
164 self.default_features = false;
165 self
166 }
167
168 pub fn optional(mut self) -> Self {
169 self.optional = true;
170 self
171 }
172
173 pub fn to_toml_line(&self) -> String {
176 if self.features.is_empty() && self.default_features && !self.optional {
177 format!("{} = \"{}\"", self.crate_name, self.version)
178 } else {
179 let feats = self
180 .features
181 .iter()
182 .map(|f| format!("\"{f}\""))
183 .collect::<Vec<_>>()
184 .join(", ");
185 let mut attributes = vec![format!("version = \"{}\"", self.version)];
186 if !self.default_features {
187 attributes.push("default-features = false".to_string());
188 }
189 if !self.features.is_empty() {
190 attributes.push(format!("features = [{feats}]"));
191 }
192 if self.optional {
193 attributes.push("optional = true".to_string());
194 }
195 format!("{} = {{ {} }}", self.crate_name, attributes.join(", "))
196 }
197 }
198}
199
200pub fn render_required_deps_toml(deps: &[DepRequirement]) -> Option<String> {
205 if deps.is_empty() {
206 return None;
207 }
208 let mut out = String::new();
209 out.push_str(
210 "# Generated by openapi-to-rust.\n\
211 # Complete direct dependencies for this generated output.\n\
212 # Append this fragment to the consuming crate's Cargo.toml, or\n\
213 # merge it with existing dependency and feature sections.\n\
214 \n\
215 [dependencies]\n",
216 );
217 for dep in deps {
218 out.push_str(&dep.to_toml_line());
219 out.push('\n');
220 }
221 if deps.iter().any(|dep| dep.crate_name == "specta") {
222 out.push_str("\n[features]\nspecta = [\"dep:specta\"]\n");
223 }
224 Some(out)
225}
226
227pub fn merge_dep_requirements(
231 requirements: impl IntoIterator<Item = DepRequirement>,
232) -> Vec<DepRequirement> {
233 let mut merged: std::collections::BTreeMap<&'static str, DepRequirement> =
234 std::collections::BTreeMap::new();
235 for mut dependency in requirements {
236 dependency.features.sort_unstable();
237 dependency.features.dedup();
238 match merged.get_mut(dependency.crate_name) {
239 Some(existing) => {
240 debug_assert_eq!(existing.version, dependency.version);
241 existing.default_features |= dependency.default_features;
242 existing.optional &= dependency.optional;
243 existing.features.extend(dependency.features);
244 existing.features.sort_unstable();
245 existing.features.dedup();
246 }
247 None => {
248 merged.insert(dependency.crate_name, dependency);
249 }
250 }
251 }
252 merged.into_values().collect()
253}
254
255pub fn collect_generated_dep_requirements<'a>(
260 contents: impl IntoIterator<Item = &'a str>,
261 enable_specta: bool,
262) -> Vec<DepRequirement> {
263 let generated = contents.into_iter().collect::<Vec<_>>().join("\n");
264 let mut dependencies = Vec::new();
265 let uses = |needle: &str| generated.contains(needle);
266
267 if uses("serde::") {
268 dependencies.push(DepRequirement::new("serde", "1").with_features(&["derive"]));
269 }
270 if uses("serde_json::") {
271 dependencies.push(DepRequirement::new("serde_json", "1"));
272 }
273 if uses("serde_urlencoded::") {
274 dependencies.push(DepRequirement::new("serde_urlencoded", "0.7"));
275 }
276 if uses("chrono::") {
277 dependencies.push(TypeFeature::Chrono.dep_requirement());
278 }
279 let uses_time = uses("time::OffsetDateTime") || uses("time::Date") || uses("time::Time");
280 if uses_time {
281 let feature = if uses("time::Date") || uses("time::Time") {
282 TypeFeature::TimeDate
283 } else {
284 TypeFeature::Time
285 };
286 dependencies.push(feature.dep_requirement());
287 }
288 if uses("iso8601::") {
289 dependencies.push(TypeFeature::Iso8601.dep_requirement());
290 }
291 if uses("uuid::") {
292 dependencies.push(TypeFeature::Uuid.dep_requirement());
293 }
294 if uses("bytes::") {
295 dependencies.push(TypeFeature::Bytes.dep_requirement());
296 }
297 if uses("base64::") {
298 dependencies.push(TypeFeature::Base64.dep_requirement());
299 }
300 if uses("url::") {
301 let dependency = if uses("url::Url") {
302 TypeFeature::Url.dep_requirement()
303 } else {
304 DepRequirement::new("url", "2")
305 };
306 dependencies.push(dependency);
307 }
308 if uses("email_address::") {
309 dependencies.push(TypeFeature::EmailAddress.dep_requirement());
310 }
311
312 if uses("reqwest::") {
313 let mut features = vec!["rustls-tls"];
314 if uses(".json(&") {
315 features.push("json");
316 }
317 dependencies.push(
318 DepRequirement::new("reqwest", "0.12")
319 .without_default_features()
320 .with_features(&features),
321 );
322 }
323 if uses("reqwest_middleware::") {
324 let dependency = if uses(".multipart(form)") {
325 DepRequirement::new("reqwest-middleware", "0.4").with_features(&["multipart"])
326 } else {
327 DepRequirement::new("reqwest-middleware", "0.4")
328 };
329 dependencies.push(dependency);
330 }
331 if uses("reqwest_retry::") {
332 let dependency = DepRequirement::new("reqwest-retry", "0.7");
333 dependencies.push(if uses("reqwest_tracing::") {
334 dependency
335 } else {
336 dependency.without_default_features()
337 });
338 }
339 if uses("reqwest_tracing::") {
340 dependencies.push(DepRequirement::new("reqwest-tracing", "0.5"));
341 }
342 if uses("reqwest_eventsource::") {
343 dependencies.push(DepRequirement::new("reqwest-eventsource", "0.6"));
344 }
345 if uses("thiserror::") || uses("use thiserror::") {
346 dependencies.push(DepRequirement::new("thiserror", "1"));
347 }
348 if uses("async_trait::") {
349 dependencies.push(DepRequirement::new("async-trait", "0.1"));
350 }
351 if uses("futures_util::") {
352 dependencies.push(DepRequirement::new("futures-util", "0.3"));
353 }
354 if uses("futures_core::") {
355 dependencies.push(DepRequirement::new("futures-core", "0.3"));
356 }
357 if uses("use tracing::") {
358 dependencies.push(DepRequirement::new("tracing", "0.1"));
359 }
360 if uses("axum::") {
361 let mut features = vec!["json"];
362 if uses("axum::response::sse::") {
363 features.push("tokio");
364 }
365 dependencies.push(
366 DepRequirement::new("axum", "0.7")
367 .without_default_features()
368 .with_features(&features),
369 );
370 }
371 if enable_specta {
372 let mut features = vec!["derive"];
373 for (needle, feature) in [
374 ("bytes::", "bytes"),
375 ("chrono::", "chrono"),
376 ("time::OffsetDateTime", "time"),
377 ("url::Url", "url"),
378 ("uuid::", "uuid"),
379 ] {
380 if uses(needle) {
381 features.push(feature);
382 }
383 }
384 if uses_time {
385 features.push("time");
386 }
387 dependencies.push(
388 DepRequirement::new("specta", "2.0.0-rc.25")
389 .with_features(&features)
390 .optional(),
391 );
392 }
393
394 merge_dep_requirements(dependencies)
395}
396
397pub fn collect_dep_requirements(used: &UsedFeatures) -> Vec<DepRequirement> {
401 merge_dep_requirements(used.iter().map(|feature| feature.dep_requirement()))
402}
403
404#[derive(Debug, Default, Clone)]
406pub struct UsedFeatures {
407 set: BTreeSet<TypeFeature>,
408}
409
410impl UsedFeatures {
411 pub fn insert(&mut self, feature: TypeFeature) {
412 self.set.insert(feature);
413 }
414
415 pub fn contains(&self, feature: TypeFeature) -> bool {
416 self.set.contains(&feature)
417 }
418
419 pub fn iter(&self) -> impl Iterator<Item = &TypeFeature> {
420 self.set.iter()
421 }
422
423 pub fn is_empty(&self) -> bool {
424 self.set.is_empty()
425 }
426}
427
428#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
434#[serde(rename_all = "lowercase")]
435pub enum DateStrategy {
436 String,
438 #[default]
440 Chrono,
441 Time,
443}
444
445#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
447#[serde(rename_all = "lowercase")]
448pub enum DurationStrategy {
449 #[default]
456 String,
457 Chrono,
460 Iso8601,
462}
463
464#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
466#[serde(rename_all = "lowercase")]
467pub enum UuidStrategy {
468 String,
469 #[default]
471 Uuid,
472}
473
474#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
476#[serde(rename_all = "snake_case")]
477pub enum ByteStrategy {
478 String,
479 #[default]
482 Base64,
483 Base64UrlUnpadded,
487 VecU8,
489}
490
491#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
493#[serde(rename_all = "snake_case")]
494pub enum BinaryStrategy {
495 String,
496 #[default]
498 Bytes,
499 VecU8,
500}
501
502#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
504#[serde(rename_all = "lowercase")]
505pub enum IpStrategy {
506 String,
507 #[default]
509 Std,
510}
511
512#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
514#[serde(rename_all = "lowercase")]
515pub enum UriStrategy {
516 String,
517 #[default]
519 Url,
520}
521
522#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
528#[serde(rename_all = "snake_case")]
529pub enum EmailStrategy {
530 #[default]
531 String,
532 EmailAddress,
533}
534
535#[derive(Debug, Clone, Deserialize, Serialize)]
543#[serde(default, rename_all = "snake_case", deny_unknown_fields)]
544pub struct TypeMappingConfig {
545 pub date_time: DateStrategy,
546 pub date: DateStrategy,
547 pub time: DateStrategy,
548 pub duration: DurationStrategy,
549 pub uuid: UuidStrategy,
550 pub byte: ByteStrategy,
551 pub binary: BinaryStrategy,
552 pub ipv4: IpStrategy,
553 pub ipv6: IpStrategy,
554 pub uri: UriStrategy,
555 pub email: EmailStrategy,
556
557 #[serde(default = "default_true")]
562 pub unsigned: bool,
563
564 #[serde(default)]
569 pub format_aliases: BTreeMap<String, String>,
570
571 pub shape: Option<TypeShapeConfig>,
573
574 pub constraints: Option<TypeConstraintsConfig>,
576
577 pub enums: Option<TypeEnumsConfig>,
579}
580
581fn default_true() -> bool {
582 true
583}
584
585impl Default for TypeMappingConfig {
586 fn default() -> Self {
587 Self {
588 date_time: DateStrategy::default(),
589 date: DateStrategy::default(),
590 time: DateStrategy::default(),
591 duration: DurationStrategy::default(),
592 uuid: UuidStrategy::default(),
593 byte: ByteStrategy::default(),
594 binary: BinaryStrategy::default(),
595 ipv4: IpStrategy::default(),
596 ipv6: IpStrategy::default(),
597 uri: UriStrategy::default(),
598 email: EmailStrategy::default(),
599 unsigned: true,
600 format_aliases: BTreeMap::new(),
601 shape: None,
602 constraints: None,
603 enums: None,
604 }
605 }
606}
607
608fn builtin_format_aliases() -> &'static [(&'static str, &'static str)] {
614 &[
615 ("uuid4", "uuid"),
616 ("uuid_v4", "uuid"),
617 ("UUID", "uuid"),
618 ("unix-time", "int64"),
619 ("unix_time", "int64"),
620 ("unixtime", "int64"),
621 ("timestamp", "int64"),
622 ]
623}
624
625impl TypeMappingConfig {
626 pub fn constraint_mode(&self) -> ConstraintMode {
631 self.constraints
632 .as_ref()
633 .and_then(|c| c.mode)
634 .unwrap_or_default()
635 }
636
637 pub fn x_enum_varnames_enabled(&self) -> bool {
640 self.enums
641 .as_ref()
642 .and_then(|e| e.x_enum_varnames)
643 .unwrap_or(true)
644 }
645
646 pub fn x_enum_descriptions_enabled(&self) -> bool {
649 self.enums
650 .as_ref()
651 .and_then(|e| e.x_enum_descriptions)
652 .unwrap_or(true)
653 }
654
655 pub fn conservative() -> Self {
660 Self {
661 date_time: DateStrategy::String,
662 date: DateStrategy::String,
663 time: DateStrategy::String,
664 duration: DurationStrategy::String,
665 uuid: UuidStrategy::String,
666 byte: ByteStrategy::String,
667 binary: BinaryStrategy::String,
668 ipv4: IpStrategy::String,
669 ipv6: IpStrategy::String,
670 uri: UriStrategy::String,
671 email: EmailStrategy::String,
672 unsigned: false,
673 format_aliases: BTreeMap::new(),
674 shape: None,
675 constraints: None,
676 enums: None,
677 }
678 }
679}
680
681#[derive(Debug, Clone, Default, Deserialize, Serialize)]
682#[serde(default, rename_all = "snake_case", deny_unknown_fields)]
683pub struct TypeShapeConfig {
684 pub additional_properties_typed: Option<bool>,
685 pub unique_items_to_set: Option<bool>,
686 pub primitive_unions: Option<bool>,
687}
688
689#[derive(Debug, Clone, Default, Deserialize, Serialize)]
690#[serde(default, rename_all = "snake_case", deny_unknown_fields)]
691pub struct TypeConstraintsConfig {
692 pub mode: Option<ConstraintMode>,
696}
697
698#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
707#[serde(rename_all = "snake_case")]
708pub enum ConstraintMode {
709 Off,
711 #[default]
714 Doc,
715}
716
717#[derive(Debug, Clone, Default, Deserialize, Serialize)]
718#[serde(default, rename_all = "snake_case", deny_unknown_fields)]
719pub struct TypeEnumsConfig {
720 pub x_enum_varnames: Option<bool>,
721 pub x_enum_descriptions: Option<bool>,
722}
723
724pub struct TypeMapper {
729 config: TypeMappingConfig,
730 used: RefCell<UsedFeatures>,
731}
732
733impl Default for TypeMapper {
734 fn default() -> Self {
735 Self::new(TypeMappingConfig::default())
736 }
737}
738
739impl TypeMapper {
740 pub fn new(config: TypeMappingConfig) -> Self {
741 Self {
742 config,
743 used: RefCell::new(UsedFeatures::default()),
744 }
745 }
746
747 pub fn used_features(&self) -> UsedFeatures {
749 self.used.borrow().clone()
750 }
751
752 pub fn config(&self) -> &TypeMappingConfig {
756 &self.config
757 }
758
759 pub fn config_shape_primitive_unions(&self) -> Option<bool> {
764 self.config.shape.as_ref().and_then(|s| s.primitive_unions)
765 }
766
767 pub fn config_shape_additional_properties_typed(&self) -> Option<bool> {
771 self.config
772 .shape
773 .as_ref()
774 .and_then(|s| s.additional_properties_typed)
775 }
776
777 pub fn config_constraint_mode(&self) -> ConstraintMode {
782 self.config
783 .constraints
784 .as_ref()
785 .and_then(|c| c.mode)
786 .unwrap_or_default()
787 }
788
789 fn record(&self, feature: TypeFeature) {
790 self.used.borrow_mut().insert(feature);
791 }
792
793 pub fn string_format(&self, format: Option<&str>) -> MappedType {
801 let normalized = self.normalize_format(format);
802 match normalized.as_deref() {
803 Some("date-time") => self.map_date_time(self.config.date_time),
804 Some("date") => self.map_date(self.config.date),
805 Some("time") => self.map_time(self.config.time),
806 Some("duration") => self.map_duration(self.config.duration),
807 Some("uuid") => self.map_uuid(self.config.uuid),
808 Some("byte") => self.map_byte(self.config.byte),
809 Some("binary") => self.map_binary(self.config.binary),
810 Some("ipv4") => self.map_ipv4(self.config.ipv4),
811 Some("ipv6") => self.map_ipv6(self.config.ipv6),
812 Some("uri") | Some("url") => self.map_uri(self.config.uri),
813 Some("email") => self.map_email(self.config.email),
814 _ => MappedType::plain("String"),
817 }
818 }
819
820 fn normalize_format(&self, format: Option<&str>) -> Option<String> {
825 let raw = format?;
826 if let Some(target) = self.config.format_aliases.get(raw) {
827 return Some(target.clone());
828 }
829 for (from, to) in builtin_format_aliases() {
830 if *from == raw {
831 return Some((*to).to_string());
832 }
833 }
834 Some(raw.to_string())
835 }
836
837 fn map_date_time(&self, strat: DateStrategy) -> MappedType {
838 match strat {
839 DateStrategy::String => MappedType::plain("String"),
840 DateStrategy::Chrono => {
841 self.record(TypeFeature::Chrono);
842 MappedType::with_feature("chrono::DateTime<chrono::Utc>", TypeFeature::Chrono)
847 }
848 DateStrategy::Time => {
849 self.record(TypeFeature::Time);
850 MappedType::with_codec(
851 "time::OffsetDateTime",
852 "time::serde::rfc3339",
853 TypeFeature::Time,
854 )
855 }
856 }
857 }
858
859 fn map_date(&self, strat: DateStrategy) -> MappedType {
860 match strat {
861 DateStrategy::String => MappedType::plain("String"),
862 DateStrategy::Chrono => {
863 self.record(TypeFeature::Chrono);
864 MappedType::with_feature("chrono::NaiveDate", TypeFeature::Chrono)
867 }
868 DateStrategy::Time => {
869 self.record(TypeFeature::TimeDate);
870 MappedType::with_codec("time::Date", "time_date_format", TypeFeature::TimeDate)
875 }
876 }
877 }
878
879 fn map_time(&self, strat: DateStrategy) -> MappedType {
880 match strat {
881 DateStrategy::String => MappedType::plain("String"),
882 DateStrategy::Chrono => {
883 self.record(TypeFeature::Chrono);
884 MappedType::with_feature("chrono::NaiveTime", TypeFeature::Chrono)
885 }
886 DateStrategy::Time => {
887 self.record(TypeFeature::TimeTime);
888 MappedType::with_codec("time::Time", "time_time_format", TypeFeature::TimeTime)
891 }
892 }
893 }
894
895 fn map_duration(&self, strat: DurationStrategy) -> MappedType {
896 match strat {
897 DurationStrategy::String => MappedType::plain("String"),
898 DurationStrategy::Chrono => {
899 MappedType::plain("String")
906 }
907 DurationStrategy::Iso8601 => {
908 self.record(TypeFeature::Iso8601);
909 MappedType::with_feature("iso8601::Duration", TypeFeature::Iso8601)
910 }
911 }
912 }
913
914 fn map_uuid(&self, strat: UuidStrategy) -> MappedType {
915 match strat {
916 UuidStrategy::String => MappedType::plain("String"),
917 UuidStrategy::Uuid => {
918 self.record(TypeFeature::Uuid);
919 MappedType::with_feature("uuid::Uuid", TypeFeature::Uuid)
920 }
921 }
922 }
923
924 fn map_byte(&self, strat: ByteStrategy) -> MappedType {
925 match strat {
926 ByteStrategy::String => MappedType::plain("String"),
927 ByteStrategy::VecU8 => MappedType::plain("Vec<u8>"),
928 ByteStrategy::Base64 | ByteStrategy::Base64UrlUnpadded => {
929 self.record(TypeFeature::Base64);
930 MappedType::with_codec("Vec<u8>", "base64_serde", TypeFeature::Base64)
935 }
936 }
937 }
938
939 fn map_binary(&self, strat: BinaryStrategy) -> MappedType {
940 match strat {
941 BinaryStrategy::String => MappedType::plain("String"),
942 BinaryStrategy::VecU8 => MappedType::plain("Vec<u8>"),
943 BinaryStrategy::Bytes => {
944 self.record(TypeFeature::Bytes);
945 MappedType::with_feature("bytes::Bytes", TypeFeature::Bytes)
946 }
947 }
948 }
949
950 fn map_ipv4(&self, strat: IpStrategy) -> MappedType {
951 match strat {
952 IpStrategy::String => MappedType::plain("String"),
953 IpStrategy::Std => MappedType::plain("std::net::Ipv4Addr"),
954 }
955 }
956
957 fn map_ipv6(&self, strat: IpStrategy) -> MappedType {
958 match strat {
959 IpStrategy::String => MappedType::plain("String"),
960 IpStrategy::Std => MappedType::plain("std::net::Ipv6Addr"),
961 }
962 }
963
964 fn map_uri(&self, strat: UriStrategy) -> MappedType {
965 match strat {
966 UriStrategy::String => MappedType::plain("String"),
967 UriStrategy::Url => {
968 self.record(TypeFeature::Url);
969 MappedType::with_feature("url::Url", TypeFeature::Url)
970 }
971 }
972 }
973
974 fn map_email(&self, strat: EmailStrategy) -> MappedType {
975 match strat {
976 EmailStrategy::String => MappedType::plain("String"),
977 EmailStrategy::EmailAddress => {
978 self.record(TypeFeature::EmailAddress);
979 MappedType::with_feature("email_address::EmailAddress", TypeFeature::EmailAddress)
980 }
981 }
982 }
983
984 pub fn integer_format(&self, format: Option<&str>) -> MappedType {
991 let normalized = self.normalize_format(format);
992 match normalized.as_deref() {
993 Some("int32") => MappedType::plain("i32"),
994 Some("int64") => MappedType::plain("i64"),
995 Some("uint32") if self.config.unsigned => MappedType::plain("u32"),
996 Some("uint64") if self.config.unsigned => MappedType::plain("u64"),
997 Some("uint") if self.config.unsigned => MappedType::plain("u64"),
1001 _ => MappedType::plain("i64"),
1002 }
1003 }
1004
1005 pub fn number_format(&self, format: Option<&str>) -> MappedType {
1006 let normalized = self.normalize_format(format);
1007 match normalized.as_deref() {
1008 Some("float") => MappedType::plain("f32"),
1009 Some("double") => MappedType::plain("f64"),
1010 _ => MappedType::plain("f64"),
1011 }
1012 }
1013
1014 pub fn boolean(&self) -> MappedType {
1015 MappedType::plain("bool")
1016 }
1017
1018 pub fn untyped_array(&self) -> MappedType {
1019 MappedType::plain("Vec<serde_json::Value>")
1020 }
1021
1022 pub fn dynamic_json(&self) -> MappedType {
1023 MappedType::plain("serde_json::Value")
1024 }
1025
1026 pub fn null_unit(&self) -> MappedType {
1027 MappedType::plain("()")
1028 }
1029
1030 pub fn map(&self, ty: OpenApiSchemaType, details: &SchemaDetails) -> MappedType {
1032 let format = details.format.as_deref();
1033 match ty {
1034 OpenApiSchemaType::String => self.string_format(format),
1035 OpenApiSchemaType::Integer => self.integer_format(format),
1036 OpenApiSchemaType::Number => self.number_format(format),
1037 OpenApiSchemaType::Boolean => self.boolean(),
1038 OpenApiSchemaType::Array => self.untyped_array(),
1039 OpenApiSchemaType::Object => self.dynamic_json(),
1040 OpenApiSchemaType::Null => self.null_unit(),
1041 }
1042 }
1043}
1044
1045#[cfg(test)]
1046mod tests {
1047 use super::*;
1048
1049 fn details_with_format(format: Option<&str>) -> SchemaDetails {
1050 SchemaDetails {
1051 format: format.map(str::to_string),
1052 ..Default::default()
1053 }
1054 }
1055
1056 #[test]
1057 fn default_mapper_emits_typed_scalars_for_common_formats() {
1058 let m = TypeMapper::default();
1059 assert_eq!(
1060 m.string_format(Some("date-time")).rust_type,
1061 "chrono::DateTime<chrono::Utc>"
1062 );
1063 assert_eq!(m.string_format(Some("date")).rust_type, "chrono::NaiveDate");
1064 assert_eq!(m.string_format(Some("uuid")).rust_type, "uuid::Uuid");
1065 assert_eq!(m.string_format(Some("uri")).rust_type, "url::Url");
1066 assert_eq!(
1067 m.string_format(Some("ipv4")).rust_type,
1068 "std::net::Ipv4Addr"
1069 );
1070 assert_eq!(m.string_format(Some("byte")).rust_type, "Vec<u8>");
1071 assert_eq!(m.string_format(Some("binary")).rust_type, "bytes::Bytes");
1072 }
1073
1074 #[test]
1075 fn date_time_uses_default_chrono_serde() {
1076 let m = TypeMapper::default();
1079 let mt = m.string_format(Some("date-time"));
1080 assert_eq!(mt.rust_type, "chrono::DateTime<chrono::Utc>");
1081 assert!(mt.serde_with.is_none());
1082 assert_eq!(mt.feature, Some(TypeFeature::Chrono));
1083 }
1084
1085 #[test]
1086 fn byte_emits_base64_codec() {
1087 let m = TypeMapper::default();
1088 let mt = m.string_format(Some("byte"));
1089 assert_eq!(mt.rust_type, "Vec<u8>");
1090 assert_eq!(mt.serde_with.as_deref(), Some("base64_serde"));
1091 assert_eq!(mt.feature, Some(TypeFeature::Base64));
1092 }
1093
1094 #[test]
1095 fn byte_url_unpadded_reuses_base64_codec() {
1096 let mapper = TypeMapper::new(TypeMappingConfig {
1097 byte: ByteStrategy::Base64UrlUnpadded,
1098 ..TypeMappingConfig::default()
1099 });
1100 let mapped = mapper.string_format(Some("byte"));
1101 assert_eq!(mapped.rust_type, "Vec<u8>");
1102 assert_eq!(mapped.serde_with.as_deref(), Some("base64_serde"));
1103 assert_eq!(mapped.feature, Some(TypeFeature::Base64));
1104 }
1105
1106 #[test]
1107 fn byte_url_unpadded_parses_from_toml() {
1108 let config: TypeMappingConfig =
1109 toml::from_str(r#"byte = "base64_url_unpadded""#).expect("parse type config");
1110 assert_eq!(config.byte, ByteStrategy::Base64UrlUnpadded);
1111 }
1112
1113 #[test]
1114 fn conservative_config_collapses_everything_to_string() {
1115 let m = TypeMapper::new(TypeMappingConfig::conservative());
1116 for fmt in [
1117 Some("date-time"),
1118 Some("uuid"),
1119 Some("uri"),
1120 Some("byte"),
1121 Some("binary"),
1122 Some("ipv4"),
1123 Some("ipv6"),
1124 Some("date"),
1125 None,
1126 ] {
1127 let mt = m.string_format(fmt);
1128 assert_eq!(mt.rust_type, "String", "format = {fmt:?}");
1129 assert!(mt.serde_with.is_none(), "format = {fmt:?}");
1130 }
1131 }
1132
1133 #[test]
1134 fn unknown_formats_fall_through_to_string() {
1135 let m = TypeMapper::default();
1136 for fmt in [Some("hostname"), Some("password"), Some("idn-email")] {
1137 assert_eq!(m.string_format(fmt).rust_type, "String");
1138 }
1139 }
1140
1141 #[test]
1142 fn integer_formats_match_pre_refactor_behavior() {
1143 let m = TypeMapper::default();
1144 assert_eq!(m.integer_format(Some("int32")).rust_type, "i32");
1145 assert_eq!(m.integer_format(Some("int64")).rust_type, "i64");
1146 assert_eq!(m.integer_format(None).rust_type, "i64");
1147 }
1148
1149 #[test]
1150 fn integer_formats_default_handles_unsigned_q21() {
1151 let m = TypeMapper::default();
1152 assert_eq!(m.integer_format(Some("uint32")).rust_type, "u32");
1153 assert_eq!(m.integer_format(Some("uint64")).rust_type, "u64");
1154 assert_eq!(m.integer_format(Some("uint")).rust_type, "u64");
1156 }
1157
1158 #[test]
1159 fn unsigned_off_degrades_uint_to_i64() {
1160 let mut cfg = TypeMappingConfig::default();
1161 cfg.unsigned = false;
1162 let m = TypeMapper::new(cfg);
1163 assert_eq!(m.integer_format(Some("uint32")).rust_type, "i64");
1164 assert_eq!(m.integer_format(Some("uint64")).rust_type, "i64");
1165 }
1166
1167 #[test]
1168 fn conservative_disables_unsigned() {
1169 let m = TypeMapper::new(TypeMappingConfig::conservative());
1170 assert_eq!(m.integer_format(Some("uint64")).rust_type, "i64");
1171 }
1172
1173 #[test]
1174 fn builtin_aliases_normalize_uuid_variants_to_uuid() {
1175 let m = TypeMapper::default();
1176 for fmt in ["uuid4", "uuid_v4", "UUID"] {
1177 let mt = m.string_format(Some(fmt));
1178 assert_eq!(mt.rust_type, "uuid::Uuid", "format = {fmt}");
1179 }
1180 }
1181
1182 #[test]
1183 fn builtin_aliases_normalize_unix_time_to_int64() {
1184 let m = TypeMapper::default();
1185 for fmt in ["unix-time", "unix_time", "unixtime", "timestamp"] {
1186 let mt = m.integer_format(Some(fmt));
1187 assert_eq!(mt.rust_type, "i64", "format = {fmt}");
1188 }
1189 }
1190
1191 #[test]
1192 fn user_alias_overrides_builtin() {
1193 let mut cfg = TypeMappingConfig::default();
1194 cfg.format_aliases
1196 .insert("uuid4".to_string(), "hostname".to_string());
1197 let m = TypeMapper::new(cfg);
1198 assert_eq!(m.string_format(Some("uuid4")).rust_type, "String");
1200 }
1201
1202 #[test]
1203 fn used_features_records_referenced_crates() {
1204 let m = TypeMapper::default();
1205 let _ = m.string_format(Some("date-time"));
1206 let _ = m.string_format(Some("uuid"));
1207 let used = m.used_features();
1208 assert!(used.contains(TypeFeature::Chrono));
1209 assert!(used.contains(TypeFeature::Uuid));
1210 assert!(!used.contains(TypeFeature::Bytes));
1211 }
1212
1213 #[test]
1214 fn format_alias_normalizes_before_dispatch() {
1215 let mut cfg = TypeMappingConfig::default();
1216 cfg.format_aliases
1217 .insert("uuid4".to_string(), "uuid".to_string());
1218 let m = TypeMapper::new(cfg);
1219 assert_eq!(m.string_format(Some("uuid4")).rust_type, "uuid::Uuid");
1220 }
1221
1222 #[test]
1223 fn conservative_helper_round_trips() {
1224 let cfg = TypeMappingConfig::conservative();
1225 assert!(matches!(cfg.date_time, DateStrategy::String));
1226 assert!(matches!(cfg.uuid, UuidStrategy::String));
1227 }
1228
1229 #[test]
1230 fn dep_requirement_renders_features_list() {
1231 let dep = TypeFeature::Chrono.dep_requirement();
1232 assert_eq!(dep.crate_name, "chrono");
1233 assert_eq!(dep.features, vec!["serde"]);
1234 assert_eq!(
1235 dep.to_toml_line(),
1236 r#"chrono = { version = "0.4", features = ["serde"] }"#
1237 );
1238 }
1239
1240 #[test]
1241 fn dep_requirement_omits_features_when_none() {
1242 let dep = TypeFeature::Base64.dep_requirement();
1243 assert_eq!(dep.to_toml_line(), r#"base64 = "0.22""#);
1244 }
1245
1246 #[test]
1247 fn collect_dep_requirements_is_sorted_and_unique() {
1248 let mut used = UsedFeatures::default();
1249 used.insert(TypeFeature::Url);
1250 used.insert(TypeFeature::Chrono);
1251 used.insert(TypeFeature::Chrono); used.insert(TypeFeature::Uuid);
1253 let deps = collect_dep_requirements(&used);
1254 assert_eq!(
1255 deps.iter().map(|d| d.crate_name).collect::<Vec<_>>(),
1256 vec!["chrono", "url", "uuid"]
1257 );
1258 }
1259
1260 #[test]
1261 fn render_required_deps_toml_is_none_when_empty() {
1262 let deps: Vec<DepRequirement> = Vec::new();
1263 assert!(render_required_deps_toml(&deps).is_none());
1264 }
1265
1266 #[test]
1267 fn render_required_deps_toml_includes_dependencies_block() {
1268 let deps = vec![
1269 TypeFeature::Chrono.dep_requirement(),
1270 TypeFeature::Uuid.dep_requirement(),
1271 ];
1272 let toml = render_required_deps_toml(&deps).expect("non-empty");
1273 assert!(toml.contains("[dependencies]"));
1274 assert!(toml.contains("chrono = "));
1275 assert!(toml.contains("uuid = "));
1276 assert!(toml.contains("# Generated by openapi-to-rust"));
1277 }
1278
1279 #[test]
1280 fn map_dispatches_through_helpers() {
1281 let m = TypeMapper::default();
1282 assert_eq!(
1283 m.map(
1284 OpenApiSchemaType::String,
1285 &details_with_format(Some("uuid"))
1286 )
1287 .rust_type,
1288 "uuid::Uuid"
1289 );
1290 assert_eq!(
1291 m.map(
1292 OpenApiSchemaType::Integer,
1293 &details_with_format(Some("int32"))
1294 )
1295 .rust_type,
1296 "i32"
1297 );
1298 }
1299}