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
35pub const WASM_TARGET_CFG: &str = "cfg(target_arch = \"wasm32\")";
38
39#[derive(Debug, Clone)]
41pub struct MappedType {
42 pub rust_type: String,
45 pub serde_with: Option<String>,
48 pub feature: Option<TypeFeature>,
50}
51
52impl MappedType {
53 pub fn plain(rust_type: impl Into<String>) -> Self {
55 Self {
56 rust_type: rust_type.into(),
57 serde_with: None,
58 feature: None,
59 }
60 }
61
62 pub fn with_feature(rust_type: impl Into<String>, feature: TypeFeature) -> Self {
67 Self {
68 rust_type: rust_type.into(),
69 serde_with: None,
70 feature: Some(feature),
71 }
72 }
73
74 pub fn with_codec(
76 rust_type: impl Into<String>,
77 codec_path: impl Into<String>,
78 feature: TypeFeature,
79 ) -> Self {
80 Self {
81 rust_type: rust_type.into(),
82 serde_with: Some(codec_path.into()),
83 feature: Some(feature),
84 }
85 }
86
87 pub fn with_inline_codec(rust_type: impl Into<String>, codec_path: impl Into<String>) -> Self {
89 Self {
90 rust_type: rust_type.into(),
91 serde_with: Some(codec_path.into()),
92 feature: None,
93 }
94 }
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
99pub enum TypeFeature {
100 Chrono,
101 Time,
102 TimeDate,
107 TimeTime,
109 Iso8601,
110 Uuid,
111 Bytes,
112 Base64,
113 Url,
114 EmailAddress,
115}
116
117impl TypeFeature {
118 pub fn dep_requirement(self) -> DepRequirement {
120 match self {
121 Self::Chrono => DepRequirement::new("chrono", "0.4").with_features(&["serde"]),
122 Self::Time => DepRequirement::new("time", "0.3").with_features(&[
125 "serde",
126 "formatting",
127 "parsing",
128 ]),
129 Self::TimeDate | Self::TimeTime => DepRequirement::new("time", "0.3").with_features(&[
133 "serde",
134 "formatting",
135 "parsing",
136 "macros",
137 ]),
138 Self::Iso8601 => DepRequirement::new("iso8601", "0.6").with_features(&["serde"]),
139 Self::Uuid => DepRequirement::new("uuid", "1").with_features(&["serde"]),
140 Self::Bytes => DepRequirement::new("bytes", "1").with_features(&["serde"]),
141 Self::Base64 => DepRequirement::new("base64", "0.22"),
142 Self::Url => DepRequirement::new("url", "2").with_features(&["serde"]),
143 Self::EmailAddress => DepRequirement::new("email_address", "0.2"),
144 }
145 }
146}
147
148#[derive(Debug, Clone, PartialEq, Eq)]
150pub struct DepRequirement {
151 pub crate_name: &'static str,
152 pub version: &'static str,
153 pub features: Vec<&'static str>,
154 pub default_features: bool,
155 pub optional: bool,
156 pub target: Option<&'static str>,
161}
162
163impl DepRequirement {
164 pub fn new(crate_name: &'static str, version: &'static str) -> Self {
165 Self {
166 crate_name,
167 version,
168 features: Vec::new(),
169 default_features: true,
170 optional: false,
171 target: None,
172 }
173 }
174
175 pub fn with_features(mut self, features: &[&'static str]) -> Self {
176 self.features = features.to_vec();
177 self.features.sort_unstable();
178 self.features.dedup();
179 self
180 }
181
182 pub fn without_default_features(mut self) -> Self {
183 self.default_features = false;
184 self
185 }
186
187 pub fn optional(mut self) -> Self {
188 self.optional = true;
189 self
190 }
191
192 pub fn for_target(mut self, cfg: &'static str) -> Self {
195 self.target = Some(cfg);
196 self
197 }
198
199 pub fn to_toml_line(&self) -> String {
202 if self.features.is_empty() && self.default_features && !self.optional {
203 format!("{} = \"{}\"", self.crate_name, self.version)
204 } else {
205 let feats = self
206 .features
207 .iter()
208 .map(|f| format!("\"{f}\""))
209 .collect::<Vec<_>>()
210 .join(", ");
211 let mut attributes = vec![format!("version = \"{}\"", self.version)];
212 if !self.default_features {
213 attributes.push("default-features = false".to_string());
214 }
215 if !self.features.is_empty() {
216 attributes.push(format!("features = [{feats}]"));
217 }
218 if self.optional {
219 attributes.push("optional = true".to_string());
220 }
221 format!("{} = {{ {} }}", self.crate_name, attributes.join(", "))
222 }
223 }
224}
225
226pub fn render_required_deps_toml(deps: &[DepRequirement]) -> Option<String> {
231 if deps.is_empty() {
232 return None;
233 }
234 let mut out = String::new();
235 out.push_str(
236 "# Generated by openapi-to-rust.\n\
237 # Complete direct dependencies for this generated output.\n\
238 # Append this fragment to the consuming crate's Cargo.toml, or\n\
239 # merge it with existing dependency and feature sections.\n\
240 \n\
241 [dependencies]\n",
242 );
243 for dep in deps.iter().filter(|dep| dep.target.is_none()) {
244 out.push_str(&dep.to_toml_line());
245 out.push('\n');
246 }
247 for target in deps
251 .iter()
252 .filter_map(|dep| dep.target)
253 .collect::<std::collections::BTreeSet<_>>()
254 {
255 out.push_str(&format!("\n[target.'{target}'.dependencies]\n"));
256 for dep in deps.iter().filter(|dep| dep.target == Some(target)) {
257 out.push_str(&dep.to_toml_line());
258 out.push('\n');
259 }
260 }
261 if deps.iter().any(|dep| dep.crate_name == "specta") {
262 out.push_str("\n[features]\nspecta = [\"dep:specta\"]\n");
263 }
264 Some(out)
265}
266
267pub fn merge_dep_requirements(
271 requirements: impl IntoIterator<Item = DepRequirement>,
272) -> Vec<DepRequirement> {
273 let mut merged: std::collections::BTreeMap<
276 (&'static str, Option<&'static str>),
277 DepRequirement,
278 > = std::collections::BTreeMap::new();
279 for mut dependency in requirements {
280 dependency.features.sort_unstable();
281 dependency.features.dedup();
282 match merged.get_mut(&(dependency.crate_name, dependency.target)) {
283 Some(existing) => {
284 debug_assert_eq!(existing.version, dependency.version);
285 existing.default_features |= dependency.default_features;
286 existing.optional &= dependency.optional;
287 existing.features.extend(dependency.features);
288 existing.features.sort_unstable();
289 existing.features.dedup();
290 }
291 None => {
292 merged.insert((dependency.crate_name, dependency.target), dependency);
293 }
294 }
295 }
296 merged.into_values().collect()
297}
298
299pub fn collect_generated_dep_requirements<'a>(
304 contents: impl IntoIterator<Item = &'a str>,
305 enable_specta: bool,
306) -> Vec<DepRequirement> {
307 let generated = contents.into_iter().collect::<Vec<_>>().join("\n");
308 let mut dependencies = Vec::new();
309 let uses = |needle: &str| generated.contains(needle);
310
311 if uses("serde::") {
312 dependencies.push(DepRequirement::new("serde", "1").with_features(&["derive"]));
313 }
314 if uses("serde_json::") {
315 dependencies.push(DepRequirement::new("serde_json", "1"));
316 }
317 if uses("serde_urlencoded::") {
318 dependencies.push(DepRequirement::new("serde_urlencoded", "0.7"));
319 }
320 if uses("chrono::") {
321 dependencies.push(TypeFeature::Chrono.dep_requirement());
322 }
323 let uses_time = uses("time::OffsetDateTime") || uses("time::Date") || uses("time::Time");
324 if uses_time {
325 let feature = if uses("time::Date") || uses("time::Time") {
326 TypeFeature::TimeDate
327 } else {
328 TypeFeature::Time
329 };
330 dependencies.push(feature.dep_requirement());
331 }
332 if uses("iso8601::") {
333 dependencies.push(TypeFeature::Iso8601.dep_requirement());
334 }
335 if uses("uuid::") {
336 dependencies.push(TypeFeature::Uuid.dep_requirement());
337 }
338 if uses("bytes::") {
339 dependencies.push(TypeFeature::Bytes.dep_requirement());
340 }
341 if uses("base64::") {
342 dependencies.push(TypeFeature::Base64.dep_requirement());
343 }
344 if uses("url::") {
345 let dependency = if uses("url::Url") {
346 TypeFeature::Url.dep_requirement()
347 } else {
348 DepRequirement::new("url", "2")
349 };
350 dependencies.push(dependency);
351 }
352 if uses("email_address::") {
353 dependencies.push(TypeFeature::EmailAddress.dep_requirement());
354 }
355
356 if uses("reqwest::") {
357 let mut features = vec!["rustls"];
358 if uses(".json(&") {
359 features.push("json");
360 }
361 if uses(".query(&") {
362 features.push("query");
363 }
364 if uses(".form(&") {
365 features.push("form");
366 }
367 if uses("reqwest::multipart") {
374 features.push("multipart");
375 }
376 if uses(".bytes_stream()") || uses(".chunk().await") {
379 features.push("stream");
380 }
381 dependencies.push(
382 DepRequirement::new("reqwest", "0.13")
383 .without_default_features()
384 .with_features(&features),
385 );
386 }
387 if uses("reqwest_middleware::") {
388 let mut features = Vec::new();
389 if uses(".json(&") {
390 features.push("json");
391 }
392 if uses(".query(&") {
393 features.push("query");
394 }
395 if uses(".form(&") {
396 features.push("form");
397 }
398 if uses(".multipart(form)") {
399 features.push("multipart");
400 }
401 dependencies
402 .push(DepRequirement::new("reqwest-middleware", "0.5").with_features(&features));
403 }
404 if uses("reqwest_retry::") {
405 let dependency = DepRequirement::new("reqwest-retry", "0.9");
406 dependencies.push(if uses("reqwest_tracing::") {
407 dependency
408 } else {
409 dependency.without_default_features()
410 });
411 dependencies.push(
416 DepRequirement::new("getrandom", "0.4")
417 .with_features(&["wasm_js"])
418 .for_target(WASM_TARGET_CFG),
419 );
420 }
421 if uses("reqwest_tracing::") {
422 dependencies.push(DepRequirement::new("reqwest-tracing", "0.7"));
423 }
424 if uses("thiserror::") || uses("use thiserror::") {
425 dependencies.push(DepRequirement::new("thiserror", "2"));
426 }
427 if uses("async_trait::") {
428 dependencies.push(DepRequirement::new("async-trait", "0.1"));
429 }
430 if uses("futures_util::") {
431 dependencies.push(DepRequirement::new("futures-util", "0.3"));
432 }
433 if uses("futures_timer::") {
434 dependencies.push(DepRequirement::new("futures-timer", "3"));
435 dependencies.push(
439 DepRequirement::new("futures-timer", "3")
440 .with_features(&["wasm-bindgen"])
441 .for_target(WASM_TARGET_CFG),
442 );
443 }
444 if uses("futures_core::") {
445 dependencies.push(DepRequirement::new("futures-core", "0.3"));
446 }
447 if uses("use tracing::") {
448 dependencies.push(DepRequirement::new("tracing", "0.1"));
449 }
450 if uses("axum::") {
451 let mut features = vec!["json"];
452 if uses("axum::extract::Multipart") {
453 features.push("multipart");
454 }
455 if uses("axum::response::sse::") {
456 features.push("tokio");
457 }
458 dependencies.push(
459 DepRequirement::new("axum", "0.8")
460 .without_default_features()
461 .with_features(&features),
462 );
463 }
464 if uses("jsonschema::") {
465 dependencies.push(DepRequirement::new("jsonschema", "0.49").without_default_features());
466 }
467 if uses("http_body_util::") {
468 dependencies.push(DepRequirement::new("http-body-util", "0.1"));
469 }
470 if uses("mime::") {
471 dependencies.push(DepRequirement::new("mime", "0.3"));
472 }
473 if enable_specta {
474 let mut features = vec!["derive"];
475 for (needle, feature) in [
476 ("bytes::", "bytes"),
477 ("chrono::", "chrono"),
478 ("time::OffsetDateTime", "time"),
479 ("url::Url", "url"),
480 ("uuid::", "uuid"),
481 ] {
482 if uses(needle) {
483 features.push(feature);
484 }
485 }
486 if uses_time {
487 features.push("time");
488 }
489 dependencies.push(
490 DepRequirement::new("specta", "2.0.0-rc.25")
491 .with_features(&features)
492 .optional(),
493 );
494 }
495
496 merge_dep_requirements(dependencies)
497}
498
499pub fn collect_dep_requirements(used: &UsedFeatures) -> Vec<DepRequirement> {
503 merge_dep_requirements(used.iter().map(|feature| feature.dep_requirement()))
504}
505
506#[derive(Debug, Default, Clone)]
508pub struct UsedFeatures {
509 set: BTreeSet<TypeFeature>,
510}
511
512impl UsedFeatures {
513 pub fn insert(&mut self, feature: TypeFeature) {
514 self.set.insert(feature);
515 }
516
517 pub fn contains(&self, feature: TypeFeature) -> bool {
518 self.set.contains(&feature)
519 }
520
521 pub fn iter(&self) -> impl Iterator<Item = &TypeFeature> {
522 self.set.iter()
523 }
524
525 pub fn is_empty(&self) -> bool {
526 self.set.is_empty()
527 }
528}
529
530#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
536#[serde(rename_all = "lowercase")]
537pub enum DateStrategy {
538 String,
540 #[default]
544 Chrono,
545 Time,
548}
549
550#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
552#[serde(rename_all = "lowercase")]
553pub enum DurationStrategy {
554 #[default]
561 String,
562 Chrono,
565 Iso8601,
567}
568
569#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
571#[serde(rename_all = "lowercase")]
572pub enum UuidStrategy {
573 String,
574 #[default]
576 Uuid,
577}
578
579#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
581#[serde(rename_all = "snake_case")]
582pub enum ByteStrategy {
583 String,
584 #[default]
587 Base64,
588 Base64UrlUnpadded,
592 VecU8,
594}
595
596#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
598#[serde(rename_all = "snake_case")]
599pub enum BinaryStrategy {
600 String,
601 #[default]
603 Bytes,
604 VecU8,
605}
606
607#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
609#[serde(rename_all = "lowercase")]
610pub enum IpStrategy {
611 String,
612 #[default]
614 Std,
615}
616
617#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
619#[serde(rename_all = "lowercase")]
620pub enum UriStrategy {
621 String,
622 #[default]
624 Url,
625}
626
627#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
633#[serde(rename_all = "snake_case")]
634pub enum EmailStrategy {
635 #[default]
636 String,
637 EmailAddress,
638}
639
640#[derive(Debug, Clone, Deserialize, Serialize)]
648#[serde(default, rename_all = "snake_case", deny_unknown_fields)]
649pub struct TypeMappingConfig {
650 pub date_time: DateStrategy,
651 pub date: DateStrategy,
652 pub time: DateStrategy,
653 pub duration: DurationStrategy,
654 pub uuid: UuidStrategy,
655 pub byte: ByteStrategy,
656 pub binary: BinaryStrategy,
657 pub ipv4: IpStrategy,
658 pub ipv6: IpStrategy,
659 pub uri: UriStrategy,
660 pub email: EmailStrategy,
661
662 #[serde(default = "default_true")]
667 pub unsigned: bool,
668
669 #[serde(default)]
674 pub format_aliases: BTreeMap<String, String>,
675
676 pub shape: Option<TypeShapeConfig>,
678
679 pub constraints: Option<TypeConstraintsConfig>,
681
682 pub enums: Option<TypeEnumsConfig>,
684
685 #[serde(default)]
689 pub float_precision: FloatPrecision,
690}
691
692#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
694#[serde(rename_all = "lowercase")]
695pub enum FloatPrecision {
696 #[default]
699 F64,
700 F32,
704}
705
706fn default_true() -> bool {
707 true
708}
709
710impl Default for TypeMappingConfig {
711 fn default() -> Self {
712 Self {
713 float_precision: FloatPrecision::default(),
714 date_time: DateStrategy::default(),
715 date: DateStrategy::default(),
716 time: DateStrategy::default(),
717 duration: DurationStrategy::default(),
718 uuid: UuidStrategy::default(),
719 byte: ByteStrategy::default(),
720 binary: BinaryStrategy::default(),
721 ipv4: IpStrategy::default(),
722 ipv6: IpStrategy::default(),
723 uri: UriStrategy::default(),
724 email: EmailStrategy::default(),
725 unsigned: true,
726 format_aliases: BTreeMap::new(),
727 shape: None,
728 constraints: None,
729 enums: None,
730 }
731 }
732}
733
734fn builtin_format_aliases() -> &'static [(&'static str, &'static str)] {
740 &[
741 ("uuid4", "uuid"),
742 ("uuid_v4", "uuid"),
743 ("UUID", "uuid"),
744 ("unix-time", "int64"),
745 ("unix_time", "int64"),
746 ("unixtime", "int64"),
747 ("timestamp", "int64"),
748 ]
749}
750
751pub(crate) fn normalize_builtin_format(format: &str) -> &str {
756 builtin_format_aliases()
757 .iter()
758 .find_map(|(from, to)| (*from == format).then_some(*to))
759 .unwrap_or(format)
760}
761
762impl TypeMappingConfig {
763 pub fn constraint_mode(&self) -> ConstraintMode {
768 self.constraints
769 .as_ref()
770 .and_then(|c| c.mode)
771 .unwrap_or_default()
772 }
773
774 pub fn x_enum_varnames_enabled(&self) -> bool {
777 self.enums
778 .as_ref()
779 .and_then(|e| e.x_enum_varnames)
780 .unwrap_or(true)
781 }
782
783 pub fn x_enum_descriptions_enabled(&self) -> bool {
786 self.enums
787 .as_ref()
788 .and_then(|e| e.x_enum_descriptions)
789 .unwrap_or(true)
790 }
791
792 pub fn conservative() -> Self {
797 Self {
798 float_precision: FloatPrecision::F32,
801 date_time: DateStrategy::String,
802 date: DateStrategy::String,
803 time: DateStrategy::String,
804 duration: DurationStrategy::String,
805 uuid: UuidStrategy::String,
806 byte: ByteStrategy::String,
807 binary: BinaryStrategy::String,
808 ipv4: IpStrategy::String,
809 ipv6: IpStrategy::String,
810 uri: UriStrategy::String,
811 email: EmailStrategy::String,
812 unsigned: false,
813 format_aliases: BTreeMap::new(),
814 shape: None,
815 constraints: None,
816 enums: None,
817 }
818 }
819}
820
821#[derive(Debug, Clone, Default, Deserialize, Serialize)]
822#[serde(default, rename_all = "snake_case", deny_unknown_fields)]
823pub struct TypeShapeConfig {
824 pub additional_properties_typed: Option<bool>,
825 pub unique_items_to_set: Option<bool>,
826 pub primitive_unions: Option<bool>,
827}
828
829#[derive(Debug, Clone, Default, Deserialize, Serialize)]
830#[serde(default, rename_all = "snake_case", deny_unknown_fields)]
831pub struct TypeConstraintsConfig {
832 pub mode: Option<ConstraintMode>,
836}
837
838#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
847#[serde(rename_all = "snake_case")]
848pub enum ConstraintMode {
849 Off,
851 #[default]
854 Doc,
855}
856
857#[derive(Debug, Clone, Default, Deserialize, Serialize)]
858#[serde(default, rename_all = "snake_case", deny_unknown_fields)]
859pub struct TypeEnumsConfig {
860 pub x_enum_varnames: Option<bool>,
861 pub x_enum_descriptions: Option<bool>,
862}
863
864pub struct TypeMapper {
869 config: TypeMappingConfig,
870 used: RefCell<UsedFeatures>,
871}
872
873impl Default for TypeMapper {
874 fn default() -> Self {
875 Self::new(TypeMappingConfig::default())
876 }
877}
878
879impl TypeMapper {
880 pub fn new(config: TypeMappingConfig) -> Self {
881 Self {
882 config,
883 used: RefCell::new(UsedFeatures::default()),
884 }
885 }
886
887 pub fn used_features(&self) -> UsedFeatures {
889 self.used.borrow().clone()
890 }
891
892 pub fn config(&self) -> &TypeMappingConfig {
896 &self.config
897 }
898
899 pub fn config_shape_primitive_unions(&self) -> Option<bool> {
904 self.config.shape.as_ref().and_then(|s| s.primitive_unions)
905 }
906
907 pub fn config_shape_additional_properties_typed(&self) -> Option<bool> {
911 self.config
912 .shape
913 .as_ref()
914 .and_then(|s| s.additional_properties_typed)
915 }
916
917 pub fn config_constraint_mode(&self) -> ConstraintMode {
922 self.config
923 .constraints
924 .as_ref()
925 .and_then(|c| c.mode)
926 .unwrap_or_default()
927 }
928
929 fn record(&self, feature: TypeFeature) {
930 self.used.borrow_mut().insert(feature);
931 }
932
933 pub fn string_format(&self, format: Option<&str>) -> MappedType {
941 let normalized = self.normalize_format(format);
942 match normalized.as_deref() {
943 Some("date-time") => self.map_date_time(self.config.date_time),
944 Some("date") => self.map_date(self.config.date),
945 Some("time") => self.map_time(self.config.time),
946 Some("duration") => self.map_duration(self.config.duration),
947 Some("uuid") => self.map_uuid(self.config.uuid),
948 Some("byte") => self.map_byte(self.config.byte),
949 Some("binary") => self.map_binary(self.config.binary),
950 Some("ipv4") => self.map_ipv4(self.config.ipv4),
951 Some("ipv6") => self.map_ipv6(self.config.ipv6),
952 Some("uri") | Some("url") => self.map_uri(self.config.uri),
953 Some("email") => self.map_email(self.config.email),
954 _ => MappedType::plain("String"),
957 }
958 }
959
960 fn normalize_format(&self, format: Option<&str>) -> Option<String> {
965 let raw = format?;
966 if let Some(target) = self.config.format_aliases.get(raw) {
967 return Some(target.clone());
968 }
969 Some(normalize_builtin_format(raw).to_string())
970 }
971
972 fn map_date_time(&self, strat: DateStrategy) -> MappedType {
973 match strat {
974 DateStrategy::String => MappedType::plain("String"),
975 DateStrategy::Chrono => {
976 self.record(TypeFeature::Chrono);
977 MappedType::with_feature("chrono::DateTime<chrono::Utc>", TypeFeature::Chrono)
982 }
983 DateStrategy::Time => {
984 self.record(TypeFeature::Time);
985 MappedType::with_codec(
986 "time::OffsetDateTime",
987 "time::serde::rfc3339",
988 TypeFeature::Time,
989 )
990 }
991 }
992 }
993
994 fn map_date(&self, strat: DateStrategy) -> MappedType {
995 match strat {
996 DateStrategy::String => MappedType::plain("String"),
997 DateStrategy::Chrono => {
998 self.record(TypeFeature::Chrono);
999 MappedType::with_feature("chrono::NaiveDate", TypeFeature::Chrono)
1002 }
1003 DateStrategy::Time => {
1004 self.record(TypeFeature::TimeDate);
1005 MappedType::with_codec("time::Date", "time_date_format", TypeFeature::TimeDate)
1010 }
1011 }
1012 }
1013
1014 fn map_time(&self, _strat: DateStrategy) -> MappedType {
1015 MappedType::plain("String")
1022 }
1023
1024 fn map_duration(&self, strat: DurationStrategy) -> MappedType {
1025 match strat {
1026 DurationStrategy::String => MappedType::plain("String"),
1027 DurationStrategy::Chrono => {
1028 MappedType::plain("String")
1035 }
1036 DurationStrategy::Iso8601 => {
1037 self.record(TypeFeature::Iso8601);
1038 MappedType::with_feature("iso8601::Duration", TypeFeature::Iso8601)
1039 }
1040 }
1041 }
1042
1043 fn map_uuid(&self, strat: UuidStrategy) -> MappedType {
1044 match strat {
1045 UuidStrategy::String => MappedType::plain("String"),
1046 UuidStrategy::Uuid => {
1047 self.record(TypeFeature::Uuid);
1048 MappedType::with_feature("uuid::Uuid", TypeFeature::Uuid)
1049 }
1050 }
1051 }
1052
1053 fn map_byte(&self, strat: ByteStrategy) -> MappedType {
1054 match strat {
1055 ByteStrategy::String => MappedType::plain("String"),
1056 ByteStrategy::VecU8 => MappedType::plain("Vec<u8>"),
1057 ByteStrategy::Base64 | ByteStrategy::Base64UrlUnpadded => {
1058 self.record(TypeFeature::Base64);
1059 MappedType::with_codec("Vec<u8>", "base64_serde", TypeFeature::Base64)
1064 }
1065 }
1066 }
1067
1068 fn map_binary(&self, strat: BinaryStrategy) -> MappedType {
1069 match strat {
1070 BinaryStrategy::String => MappedType::plain("String"),
1071 BinaryStrategy::VecU8 => MappedType::with_inline_codec("Vec<u8>", "binary_vec_serde"),
1072 BinaryStrategy::Bytes => {
1073 self.record(TypeFeature::Bytes);
1074 MappedType::with_codec("bytes::Bytes", "binary_bytes_serde", TypeFeature::Bytes)
1075 }
1076 }
1077 }
1078
1079 fn map_ipv4(&self, strat: IpStrategy) -> MappedType {
1080 match strat {
1081 IpStrategy::String => MappedType::plain("String"),
1082 IpStrategy::Std => MappedType::plain("std::net::Ipv4Addr"),
1083 }
1084 }
1085
1086 fn map_ipv6(&self, strat: IpStrategy) -> MappedType {
1087 match strat {
1088 IpStrategy::String => MappedType::plain("String"),
1089 IpStrategy::Std => MappedType::plain("std::net::Ipv6Addr"),
1090 }
1091 }
1092
1093 fn map_uri(&self, strat: UriStrategy) -> MappedType {
1094 match strat {
1095 UriStrategy::String => MappedType::plain("String"),
1096 UriStrategy::Url => {
1097 self.record(TypeFeature::Url);
1098 MappedType::with_feature("url::Url", TypeFeature::Url)
1099 }
1100 }
1101 }
1102
1103 fn map_email(&self, strat: EmailStrategy) -> MappedType {
1104 match strat {
1105 EmailStrategy::String => MappedType::plain("String"),
1106 EmailStrategy::EmailAddress => {
1107 self.record(TypeFeature::EmailAddress);
1108 MappedType::with_feature("email_address::EmailAddress", TypeFeature::EmailAddress)
1109 }
1110 }
1111 }
1112
1113 pub fn integer_format(&self, format: Option<&str>) -> MappedType {
1120 let normalized = self.normalize_format(format);
1121 match normalized.as_deref() {
1122 Some("int32") => MappedType::plain("i32"),
1123 Some("int64") => MappedType::plain("i64"),
1124 Some("uint32") if self.config.unsigned => MappedType::plain("u32"),
1125 Some("uint64") if self.config.unsigned => MappedType::plain("u64"),
1126 Some("uint") if self.config.unsigned => MappedType::plain("u64"),
1130 _ => MappedType::plain("i64"),
1131 }
1132 }
1133
1134 pub fn number_format(&self, format: Option<&str>) -> MappedType {
1147 let normalized = self.normalize_format(format);
1148 match normalized.as_deref() {
1149 Some("float") if self.config.float_precision == FloatPrecision::F32 => {
1150 MappedType::plain("f32")
1151 }
1152 Some("float") => MappedType::plain("f64"),
1153 Some("double") => MappedType::plain("f64"),
1154 _ => MappedType::plain("f64"),
1155 }
1156 }
1157
1158 pub fn boolean(&self) -> MappedType {
1159 MappedType::plain("bool")
1160 }
1161
1162 pub fn untyped_array(&self) -> MappedType {
1163 MappedType::plain("Vec<serde_json::Value>")
1164 }
1165
1166 pub fn dynamic_json(&self) -> MappedType {
1167 MappedType::plain("serde_json::Value")
1168 }
1169
1170 pub fn null_unit(&self) -> MappedType {
1171 MappedType::plain("()")
1172 }
1173
1174 pub fn map(&self, ty: OpenApiSchemaType, details: &SchemaDetails) -> MappedType {
1176 let format = details.format.as_deref();
1177 match ty {
1178 OpenApiSchemaType::String => self.string_format(format),
1179 OpenApiSchemaType::Integer => self.integer_format(format),
1180 OpenApiSchemaType::Number => self.number_format(format),
1181 OpenApiSchemaType::Boolean => self.boolean(),
1182 OpenApiSchemaType::Array => self.untyped_array(),
1183 OpenApiSchemaType::Object => self.dynamic_json(),
1184 OpenApiSchemaType::Null => self.null_unit(),
1185 }
1186 }
1187}
1188
1189#[cfg(test)]
1190mod tests {
1191 use super::*;
1192
1193 fn details_with_format(format: Option<&str>) -> SchemaDetails {
1194 SchemaDetails {
1195 format: format.map(str::to_string),
1196 ..Default::default()
1197 }
1198 }
1199
1200 #[test]
1201 fn default_mapper_emits_typed_scalars_for_common_formats() {
1202 let m = TypeMapper::default();
1203 assert_eq!(
1204 m.string_format(Some("date-time")).rust_type,
1205 "chrono::DateTime<chrono::Utc>"
1206 );
1207 assert_eq!(m.string_format(Some("date")).rust_type, "chrono::NaiveDate");
1208 assert_eq!(m.string_format(Some("time")).rust_type, "String");
1209 assert_eq!(m.string_format(Some("uuid")).rust_type, "uuid::Uuid");
1210 assert_eq!(m.string_format(Some("uri")).rust_type, "url::Url");
1211 assert_eq!(
1212 m.string_format(Some("ipv4")).rust_type,
1213 "std::net::Ipv4Addr"
1214 );
1215 assert_eq!(m.string_format(Some("byte")).rust_type, "Vec<u8>");
1216 assert_eq!(m.string_format(Some("binary")).rust_type, "bytes::Bytes");
1217 }
1218
1219 #[test]
1220 fn date_time_uses_default_chrono_serde() {
1221 let m = TypeMapper::default();
1224 let mt = m.string_format(Some("date-time"));
1225 assert_eq!(mt.rust_type, "chrono::DateTime<chrono::Utc>");
1226 assert!(mt.serde_with.is_none());
1227 assert_eq!(mt.feature, Some(TypeFeature::Chrono));
1228 }
1229
1230 #[test]
1231 fn byte_emits_base64_codec() {
1232 let m = TypeMapper::default();
1233 let mt = m.string_format(Some("byte"));
1234 assert_eq!(mt.rust_type, "Vec<u8>");
1235 assert_eq!(mt.serde_with.as_deref(), Some("base64_serde"));
1236 assert_eq!(mt.feature, Some(TypeFeature::Base64));
1237 }
1238
1239 #[test]
1240 fn byte_url_unpadded_reuses_base64_codec() {
1241 let mapper = TypeMapper::new(TypeMappingConfig {
1242 byte: ByteStrategy::Base64UrlUnpadded,
1243 ..TypeMappingConfig::default()
1244 });
1245 let mapped = mapper.string_format(Some("byte"));
1246 assert_eq!(mapped.rust_type, "Vec<u8>");
1247 assert_eq!(mapped.serde_with.as_deref(), Some("base64_serde"));
1248 assert_eq!(mapped.feature, Some(TypeFeature::Base64));
1249 }
1250
1251 #[test]
1252 fn byte_url_unpadded_parses_from_toml() {
1253 let config: TypeMappingConfig =
1254 toml::from_str(r#"byte = "base64_url_unpadded""#).expect("parse type config");
1255 assert_eq!(config.byte, ByteStrategy::Base64UrlUnpadded);
1256 }
1257
1258 #[test]
1259 fn conservative_config_collapses_everything_to_string() {
1260 let m = TypeMapper::new(TypeMappingConfig::conservative());
1261 for fmt in [
1262 Some("date-time"),
1263 Some("uuid"),
1264 Some("uri"),
1265 Some("byte"),
1266 Some("binary"),
1267 Some("ipv4"),
1268 Some("ipv6"),
1269 Some("date"),
1270 None,
1271 ] {
1272 let mt = m.string_format(fmt);
1273 assert_eq!(mt.rust_type, "String", "format = {fmt:?}");
1274 assert!(mt.serde_with.is_none(), "format = {fmt:?}");
1275 }
1276 }
1277
1278 #[test]
1279 fn unknown_formats_fall_through_to_string() {
1280 let m = TypeMapper::default();
1281 for fmt in [Some("hostname"), Some("password"), Some("idn-email")] {
1282 assert_eq!(m.string_format(fmt).rust_type, "String");
1283 }
1284 }
1285
1286 #[test]
1287 fn integer_formats_match_pre_refactor_behavior() {
1288 let m = TypeMapper::default();
1289 assert_eq!(m.integer_format(Some("int32")).rust_type, "i32");
1290 assert_eq!(m.integer_format(Some("int64")).rust_type, "i64");
1291 assert_eq!(m.integer_format(None).rust_type, "i64");
1292 }
1293
1294 #[test]
1295 fn integer_formats_default_handles_unsigned_q21() {
1296 let m = TypeMapper::default();
1297 assert_eq!(m.integer_format(Some("uint32")).rust_type, "u32");
1298 assert_eq!(m.integer_format(Some("uint64")).rust_type, "u64");
1299 assert_eq!(m.integer_format(Some("uint")).rust_type, "u64");
1301 }
1302
1303 #[test]
1304 fn unsigned_off_degrades_uint_to_i64() {
1305 let mut cfg = TypeMappingConfig::default();
1306 cfg.unsigned = false;
1307 let m = TypeMapper::new(cfg);
1308 assert_eq!(m.integer_format(Some("uint32")).rust_type, "i64");
1309 assert_eq!(m.integer_format(Some("uint64")).rust_type, "i64");
1310 }
1311
1312 #[test]
1313 fn conservative_disables_unsigned() {
1314 let m = TypeMapper::new(TypeMappingConfig::conservative());
1315 assert_eq!(m.integer_format(Some("uint64")).rust_type, "i64");
1316 }
1317
1318 #[test]
1319 fn builtin_aliases_normalize_uuid_variants_to_uuid() {
1320 let m = TypeMapper::default();
1321 for fmt in ["uuid4", "uuid_v4", "UUID"] {
1322 assert_eq!(normalize_builtin_format(fmt), "uuid", "format = {fmt}");
1323 let mt = m.string_format(Some(fmt));
1324 assert_eq!(mt.rust_type, "uuid::Uuid", "format = {fmt}");
1325 }
1326 assert_eq!(normalize_builtin_format("vendor-id"), "vendor-id");
1327 }
1328
1329 #[test]
1330 fn builtin_aliases_normalize_unix_time_to_int64() {
1331 let m = TypeMapper::default();
1332 for fmt in ["unix-time", "unix_time", "unixtime", "timestamp"] {
1333 let mt = m.integer_format(Some(fmt));
1334 assert_eq!(mt.rust_type, "i64", "format = {fmt}");
1335 }
1336 }
1337
1338 #[test]
1339 fn user_alias_overrides_builtin() {
1340 let mut cfg = TypeMappingConfig::default();
1341 cfg.format_aliases
1343 .insert("uuid4".to_string(), "hostname".to_string());
1344 let m = TypeMapper::new(cfg);
1345 assert_eq!(m.string_format(Some("uuid4")).rust_type, "String");
1347 }
1348
1349 #[test]
1350 fn used_features_records_referenced_crates() {
1351 let m = TypeMapper::default();
1352 let _ = m.string_format(Some("date-time"));
1353 let _ = m.string_format(Some("uuid"));
1354 let used = m.used_features();
1355 assert!(used.contains(TypeFeature::Chrono));
1356 assert!(used.contains(TypeFeature::Uuid));
1357 assert!(!used.contains(TypeFeature::Bytes));
1358 }
1359
1360 #[test]
1361 fn format_alias_normalizes_before_dispatch() {
1362 let mut cfg = TypeMappingConfig::default();
1363 cfg.format_aliases
1364 .insert("uuid4".to_string(), "uuid".to_string());
1365 let m = TypeMapper::new(cfg);
1366 assert_eq!(m.string_format(Some("uuid4")).rust_type, "uuid::Uuid");
1367 }
1368
1369 #[test]
1370 fn conservative_helper_round_trips() {
1371 let cfg = TypeMappingConfig::conservative();
1372 assert!(matches!(cfg.date_time, DateStrategy::String));
1373 assert!(matches!(cfg.uuid, UuidStrategy::String));
1374 }
1375
1376 #[test]
1377 fn dep_requirement_renders_features_list() {
1378 let dep = TypeFeature::Chrono.dep_requirement();
1379 assert_eq!(dep.crate_name, "chrono");
1380 assert_eq!(dep.features, vec!["serde"]);
1381 assert_eq!(
1382 dep.to_toml_line(),
1383 r#"chrono = { version = "0.4", features = ["serde"] }"#
1384 );
1385 }
1386
1387 #[test]
1388 fn dep_requirement_omits_features_when_none() {
1389 let dep = TypeFeature::Base64.dep_requirement();
1390 assert_eq!(dep.to_toml_line(), r#"base64 = "0.22""#);
1391 }
1392
1393 #[test]
1394 fn collect_dep_requirements_is_sorted_and_unique() {
1395 let mut used = UsedFeatures::default();
1396 used.insert(TypeFeature::Url);
1397 used.insert(TypeFeature::Chrono);
1398 used.insert(TypeFeature::Chrono); used.insert(TypeFeature::Uuid);
1400 let deps = collect_dep_requirements(&used);
1401 assert_eq!(
1402 deps.iter().map(|d| d.crate_name).collect::<Vec<_>>(),
1403 vec!["chrono", "url", "uuid"]
1404 );
1405 }
1406
1407 #[test]
1408 fn render_required_deps_toml_is_none_when_empty() {
1409 let deps: Vec<DepRequirement> = Vec::new();
1410 assert!(render_required_deps_toml(&deps).is_none());
1411 }
1412
1413 #[test]
1414 fn render_required_deps_toml_includes_dependencies_block() {
1415 let deps = vec![
1416 TypeFeature::Chrono.dep_requirement(),
1417 TypeFeature::Uuid.dep_requirement(),
1418 ];
1419 let toml = render_required_deps_toml(&deps).expect("non-empty");
1420 assert!(toml.contains("[dependencies]"));
1421 assert!(toml.contains("chrono = "));
1422 assert!(toml.contains("uuid = "));
1423 assert!(toml.contains("# Generated by openapi-to-rust"));
1424 }
1425
1426 #[test]
1427 fn map_dispatches_through_helpers() {
1428 let m = TypeMapper::default();
1429 assert_eq!(
1430 m.map(
1431 OpenApiSchemaType::String,
1432 &details_with_format(Some("uuid"))
1433 )
1434 .rust_type,
1435 "uuid::Uuid"
1436 );
1437 assert_eq!(
1438 m.map(
1439 OpenApiSchemaType::Integer,
1440 &details_with_format(Some("int32"))
1441 )
1442 .rust_type,
1443 "i32"
1444 );
1445 }
1446}