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 pub fn with_inline_codec(rust_type: impl Into<String>, codec_path: impl Into<String>) -> Self {
85 Self {
86 rust_type: rust_type.into(),
87 serde_with: Some(codec_path.into()),
88 feature: None,
89 }
90 }
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
95pub enum TypeFeature {
96 Chrono,
97 Time,
98 TimeDate,
103 TimeTime,
105 Iso8601,
106 Uuid,
107 Bytes,
108 Base64,
109 Url,
110 EmailAddress,
111}
112
113impl TypeFeature {
114 pub fn dep_requirement(self) -> DepRequirement {
116 match self {
117 Self::Chrono => DepRequirement::new("chrono", "0.4").with_features(&["serde"]),
118 Self::Time => DepRequirement::new("time", "0.3").with_features(&[
121 "serde",
122 "formatting",
123 "parsing",
124 ]),
125 Self::TimeDate | Self::TimeTime => DepRequirement::new("time", "0.3").with_features(&[
129 "serde",
130 "formatting",
131 "parsing",
132 "macros",
133 ]),
134 Self::Iso8601 => DepRequirement::new("iso8601", "0.6").with_features(&["serde"]),
135 Self::Uuid => DepRequirement::new("uuid", "1").with_features(&["serde"]),
136 Self::Bytes => DepRequirement::new("bytes", "1").with_features(&["serde"]),
137 Self::Base64 => DepRequirement::new("base64", "0.22"),
138 Self::Url => DepRequirement::new("url", "2").with_features(&["serde"]),
139 Self::EmailAddress => DepRequirement::new("email_address", "0.2"),
140 }
141 }
142}
143
144#[derive(Debug, Clone, PartialEq, Eq)]
146pub struct DepRequirement {
147 pub crate_name: &'static str,
148 pub version: &'static str,
149 pub features: Vec<&'static str>,
150 pub default_features: bool,
151 pub optional: bool,
152}
153
154impl DepRequirement {
155 pub fn new(crate_name: &'static str, version: &'static str) -> Self {
156 Self {
157 crate_name,
158 version,
159 features: Vec::new(),
160 default_features: true,
161 optional: false,
162 }
163 }
164
165 pub fn with_features(mut self, features: &[&'static str]) -> Self {
166 self.features = features.to_vec();
167 self.features.sort_unstable();
168 self.features.dedup();
169 self
170 }
171
172 pub fn without_default_features(mut self) -> Self {
173 self.default_features = false;
174 self
175 }
176
177 pub fn optional(mut self) -> Self {
178 self.optional = true;
179 self
180 }
181
182 pub fn to_toml_line(&self) -> String {
185 if self.features.is_empty() && self.default_features && !self.optional {
186 format!("{} = \"{}\"", self.crate_name, self.version)
187 } else {
188 let feats = self
189 .features
190 .iter()
191 .map(|f| format!("\"{f}\""))
192 .collect::<Vec<_>>()
193 .join(", ");
194 let mut attributes = vec![format!("version = \"{}\"", self.version)];
195 if !self.default_features {
196 attributes.push("default-features = false".to_string());
197 }
198 if !self.features.is_empty() {
199 attributes.push(format!("features = [{feats}]"));
200 }
201 if self.optional {
202 attributes.push("optional = true".to_string());
203 }
204 format!("{} = {{ {} }}", self.crate_name, attributes.join(", "))
205 }
206 }
207}
208
209pub fn render_required_deps_toml(deps: &[DepRequirement]) -> Option<String> {
214 if deps.is_empty() {
215 return None;
216 }
217 let mut out = String::new();
218 out.push_str(
219 "# Generated by openapi-to-rust.\n\
220 # Complete direct dependencies for this generated output.\n\
221 # Append this fragment to the consuming crate's Cargo.toml, or\n\
222 # merge it with existing dependency and feature sections.\n\
223 \n\
224 [dependencies]\n",
225 );
226 for dep in deps {
227 out.push_str(&dep.to_toml_line());
228 out.push('\n');
229 }
230 if deps.iter().any(|dep| dep.crate_name == "specta") {
231 out.push_str("\n[features]\nspecta = [\"dep:specta\"]\n");
232 }
233 Some(out)
234}
235
236pub fn merge_dep_requirements(
240 requirements: impl IntoIterator<Item = DepRequirement>,
241) -> Vec<DepRequirement> {
242 let mut merged: std::collections::BTreeMap<&'static str, DepRequirement> =
243 std::collections::BTreeMap::new();
244 for mut dependency in requirements {
245 dependency.features.sort_unstable();
246 dependency.features.dedup();
247 match merged.get_mut(dependency.crate_name) {
248 Some(existing) => {
249 debug_assert_eq!(existing.version, dependency.version);
250 existing.default_features |= dependency.default_features;
251 existing.optional &= dependency.optional;
252 existing.features.extend(dependency.features);
253 existing.features.sort_unstable();
254 existing.features.dedup();
255 }
256 None => {
257 merged.insert(dependency.crate_name, dependency);
258 }
259 }
260 }
261 merged.into_values().collect()
262}
263
264pub fn collect_generated_dep_requirements<'a>(
269 contents: impl IntoIterator<Item = &'a str>,
270 enable_specta: bool,
271) -> Vec<DepRequirement> {
272 let generated = contents.into_iter().collect::<Vec<_>>().join("\n");
273 let mut dependencies = Vec::new();
274 let uses = |needle: &str| generated.contains(needle);
275
276 if uses("serde::") {
277 dependencies.push(DepRequirement::new("serde", "1").with_features(&["derive"]));
278 }
279 if uses("serde_json::") {
280 dependencies.push(DepRequirement::new("serde_json", "1"));
281 }
282 if uses("serde_urlencoded::") {
283 dependencies.push(DepRequirement::new("serde_urlencoded", "0.7"));
284 }
285 if uses("chrono::") {
286 dependencies.push(TypeFeature::Chrono.dep_requirement());
287 }
288 let uses_time = uses("time::OffsetDateTime") || uses("time::Date") || uses("time::Time");
289 if uses_time {
290 let feature = if uses("time::Date") || uses("time::Time") {
291 TypeFeature::TimeDate
292 } else {
293 TypeFeature::Time
294 };
295 dependencies.push(feature.dep_requirement());
296 }
297 if uses("iso8601::") {
298 dependencies.push(TypeFeature::Iso8601.dep_requirement());
299 }
300 if uses("uuid::") {
301 dependencies.push(TypeFeature::Uuid.dep_requirement());
302 }
303 if uses("bytes::") {
304 dependencies.push(TypeFeature::Bytes.dep_requirement());
305 }
306 if uses("base64::") {
307 dependencies.push(TypeFeature::Base64.dep_requirement());
308 }
309 if uses("url::") {
310 let dependency = if uses("url::Url") {
311 TypeFeature::Url.dep_requirement()
312 } else {
313 DepRequirement::new("url", "2")
314 };
315 dependencies.push(dependency);
316 }
317 if uses("email_address::") {
318 dependencies.push(TypeFeature::EmailAddress.dep_requirement());
319 }
320
321 if uses("reqwest::") {
322 let mut features = vec!["rustls"];
323 if uses(".json(&") {
324 features.push("json");
325 }
326 if uses(".query(&") {
327 features.push("query");
328 }
329 if uses(".form(&") {
330 features.push("form");
331 }
332 if uses("reqwest::multipart") {
339 features.push("multipart");
340 }
341 if uses(".bytes_stream()") || uses(".chunk().await") {
344 features.push("stream");
345 }
346 dependencies.push(
347 DepRequirement::new("reqwest", "0.13")
348 .without_default_features()
349 .with_features(&features),
350 );
351 }
352 if uses("reqwest_middleware::") {
353 let mut features = Vec::new();
354 if uses(".json(&") {
355 features.push("json");
356 }
357 if uses(".query(&") {
358 features.push("query");
359 }
360 if uses(".form(&") {
361 features.push("form");
362 }
363 if uses(".multipart(form)") {
364 features.push("multipart");
365 }
366 dependencies
367 .push(DepRequirement::new("reqwest-middleware", "0.5").with_features(&features));
368 }
369 if uses("reqwest_retry::") {
370 let dependency = DepRequirement::new("reqwest-retry", "0.9");
371 dependencies.push(if uses("reqwest_tracing::") {
372 dependency
373 } else {
374 dependency.without_default_features()
375 });
376 }
377 if uses("reqwest_tracing::") {
378 dependencies.push(DepRequirement::new("reqwest-tracing", "0.7"));
379 }
380 if uses("thiserror::") || uses("use thiserror::") {
381 dependencies.push(DepRequirement::new("thiserror", "2"));
382 }
383 if uses("async_trait::") {
384 dependencies.push(DepRequirement::new("async-trait", "0.1"));
385 }
386 if uses("futures_util::") {
387 dependencies.push(DepRequirement::new("futures-util", "0.3"));
388 }
389 if uses("futures_timer::") {
390 dependencies.push(DepRequirement::new("futures-timer", "3"));
391 }
392 if uses("futures_core::") {
393 dependencies.push(DepRequirement::new("futures-core", "0.3"));
394 }
395 if uses("use tracing::") {
396 dependencies.push(DepRequirement::new("tracing", "0.1"));
397 }
398 if uses("axum::") {
399 let mut features = vec!["json"];
400 if uses("axum::extract::Multipart") {
401 features.push("multipart");
402 }
403 if uses("axum::response::sse::") {
404 features.push("tokio");
405 }
406 dependencies.push(
407 DepRequirement::new("axum", "0.8")
408 .without_default_features()
409 .with_features(&features),
410 );
411 }
412 if uses("jsonschema::") {
413 dependencies.push(DepRequirement::new("jsonschema", "0.49").without_default_features());
414 }
415 if uses("http_body_util::") {
416 dependencies.push(DepRequirement::new("http-body-util", "0.1"));
417 }
418 if uses("mime::") {
419 dependencies.push(DepRequirement::new("mime", "0.3"));
420 }
421 if enable_specta {
422 let mut features = vec!["derive"];
423 for (needle, feature) in [
424 ("bytes::", "bytes"),
425 ("chrono::", "chrono"),
426 ("time::OffsetDateTime", "time"),
427 ("url::Url", "url"),
428 ("uuid::", "uuid"),
429 ] {
430 if uses(needle) {
431 features.push(feature);
432 }
433 }
434 if uses_time {
435 features.push("time");
436 }
437 dependencies.push(
438 DepRequirement::new("specta", "2.0.0-rc.25")
439 .with_features(&features)
440 .optional(),
441 );
442 }
443
444 merge_dep_requirements(dependencies)
445}
446
447pub fn collect_dep_requirements(used: &UsedFeatures) -> Vec<DepRequirement> {
451 merge_dep_requirements(used.iter().map(|feature| feature.dep_requirement()))
452}
453
454#[derive(Debug, Default, Clone)]
456pub struct UsedFeatures {
457 set: BTreeSet<TypeFeature>,
458}
459
460impl UsedFeatures {
461 pub fn insert(&mut self, feature: TypeFeature) {
462 self.set.insert(feature);
463 }
464
465 pub fn contains(&self, feature: TypeFeature) -> bool {
466 self.set.contains(&feature)
467 }
468
469 pub fn iter(&self) -> impl Iterator<Item = &TypeFeature> {
470 self.set.iter()
471 }
472
473 pub fn is_empty(&self) -> bool {
474 self.set.is_empty()
475 }
476}
477
478#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
484#[serde(rename_all = "lowercase")]
485pub enum DateStrategy {
486 String,
488 #[default]
492 Chrono,
493 Time,
496}
497
498#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
500#[serde(rename_all = "lowercase")]
501pub enum DurationStrategy {
502 #[default]
509 String,
510 Chrono,
513 Iso8601,
515}
516
517#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
519#[serde(rename_all = "lowercase")]
520pub enum UuidStrategy {
521 String,
522 #[default]
524 Uuid,
525}
526
527#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
529#[serde(rename_all = "snake_case")]
530pub enum ByteStrategy {
531 String,
532 #[default]
535 Base64,
536 Base64UrlUnpadded,
540 VecU8,
542}
543
544#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
546#[serde(rename_all = "snake_case")]
547pub enum BinaryStrategy {
548 String,
549 #[default]
551 Bytes,
552 VecU8,
553}
554
555#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
557#[serde(rename_all = "lowercase")]
558pub enum IpStrategy {
559 String,
560 #[default]
562 Std,
563}
564
565#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
567#[serde(rename_all = "lowercase")]
568pub enum UriStrategy {
569 String,
570 #[default]
572 Url,
573}
574
575#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
581#[serde(rename_all = "snake_case")]
582pub enum EmailStrategy {
583 #[default]
584 String,
585 EmailAddress,
586}
587
588#[derive(Debug, Clone, Deserialize, Serialize)]
596#[serde(default, rename_all = "snake_case", deny_unknown_fields)]
597pub struct TypeMappingConfig {
598 pub date_time: DateStrategy,
599 pub date: DateStrategy,
600 pub time: DateStrategy,
601 pub duration: DurationStrategy,
602 pub uuid: UuidStrategy,
603 pub byte: ByteStrategy,
604 pub binary: BinaryStrategy,
605 pub ipv4: IpStrategy,
606 pub ipv6: IpStrategy,
607 pub uri: UriStrategy,
608 pub email: EmailStrategy,
609
610 #[serde(default = "default_true")]
615 pub unsigned: bool,
616
617 #[serde(default)]
622 pub format_aliases: BTreeMap<String, String>,
623
624 pub shape: Option<TypeShapeConfig>,
626
627 pub constraints: Option<TypeConstraintsConfig>,
629
630 pub enums: Option<TypeEnumsConfig>,
632
633 #[serde(default)]
637 pub float_precision: FloatPrecision,
638}
639
640#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
642#[serde(rename_all = "lowercase")]
643pub enum FloatPrecision {
644 #[default]
647 F64,
648 F32,
652}
653
654fn default_true() -> bool {
655 true
656}
657
658impl Default for TypeMappingConfig {
659 fn default() -> Self {
660 Self {
661 float_precision: FloatPrecision::default(),
662 date_time: DateStrategy::default(),
663 date: DateStrategy::default(),
664 time: DateStrategy::default(),
665 duration: DurationStrategy::default(),
666 uuid: UuidStrategy::default(),
667 byte: ByteStrategy::default(),
668 binary: BinaryStrategy::default(),
669 ipv4: IpStrategy::default(),
670 ipv6: IpStrategy::default(),
671 uri: UriStrategy::default(),
672 email: EmailStrategy::default(),
673 unsigned: true,
674 format_aliases: BTreeMap::new(),
675 shape: None,
676 constraints: None,
677 enums: None,
678 }
679 }
680}
681
682fn builtin_format_aliases() -> &'static [(&'static str, &'static str)] {
688 &[
689 ("uuid4", "uuid"),
690 ("uuid_v4", "uuid"),
691 ("UUID", "uuid"),
692 ("unix-time", "int64"),
693 ("unix_time", "int64"),
694 ("unixtime", "int64"),
695 ("timestamp", "int64"),
696 ]
697}
698
699pub(crate) fn normalize_builtin_format(format: &str) -> &str {
704 builtin_format_aliases()
705 .iter()
706 .find_map(|(from, to)| (*from == format).then_some(*to))
707 .unwrap_or(format)
708}
709
710impl TypeMappingConfig {
711 pub fn constraint_mode(&self) -> ConstraintMode {
716 self.constraints
717 .as_ref()
718 .and_then(|c| c.mode)
719 .unwrap_or_default()
720 }
721
722 pub fn x_enum_varnames_enabled(&self) -> bool {
725 self.enums
726 .as_ref()
727 .and_then(|e| e.x_enum_varnames)
728 .unwrap_or(true)
729 }
730
731 pub fn x_enum_descriptions_enabled(&self) -> bool {
734 self.enums
735 .as_ref()
736 .and_then(|e| e.x_enum_descriptions)
737 .unwrap_or(true)
738 }
739
740 pub fn conservative() -> Self {
745 Self {
746 float_precision: FloatPrecision::F32,
749 date_time: DateStrategy::String,
750 date: DateStrategy::String,
751 time: DateStrategy::String,
752 duration: DurationStrategy::String,
753 uuid: UuidStrategy::String,
754 byte: ByteStrategy::String,
755 binary: BinaryStrategy::String,
756 ipv4: IpStrategy::String,
757 ipv6: IpStrategy::String,
758 uri: UriStrategy::String,
759 email: EmailStrategy::String,
760 unsigned: false,
761 format_aliases: BTreeMap::new(),
762 shape: None,
763 constraints: None,
764 enums: None,
765 }
766 }
767}
768
769#[derive(Debug, Clone, Default, Deserialize, Serialize)]
770#[serde(default, rename_all = "snake_case", deny_unknown_fields)]
771pub struct TypeShapeConfig {
772 pub additional_properties_typed: Option<bool>,
773 pub unique_items_to_set: Option<bool>,
774 pub primitive_unions: Option<bool>,
775}
776
777#[derive(Debug, Clone, Default, Deserialize, Serialize)]
778#[serde(default, rename_all = "snake_case", deny_unknown_fields)]
779pub struct TypeConstraintsConfig {
780 pub mode: Option<ConstraintMode>,
784}
785
786#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
795#[serde(rename_all = "snake_case")]
796pub enum ConstraintMode {
797 Off,
799 #[default]
802 Doc,
803}
804
805#[derive(Debug, Clone, Default, Deserialize, Serialize)]
806#[serde(default, rename_all = "snake_case", deny_unknown_fields)]
807pub struct TypeEnumsConfig {
808 pub x_enum_varnames: Option<bool>,
809 pub x_enum_descriptions: Option<bool>,
810}
811
812pub struct TypeMapper {
817 config: TypeMappingConfig,
818 used: RefCell<UsedFeatures>,
819}
820
821impl Default for TypeMapper {
822 fn default() -> Self {
823 Self::new(TypeMappingConfig::default())
824 }
825}
826
827impl TypeMapper {
828 pub fn new(config: TypeMappingConfig) -> Self {
829 Self {
830 config,
831 used: RefCell::new(UsedFeatures::default()),
832 }
833 }
834
835 pub fn used_features(&self) -> UsedFeatures {
837 self.used.borrow().clone()
838 }
839
840 pub fn config(&self) -> &TypeMappingConfig {
844 &self.config
845 }
846
847 pub fn config_shape_primitive_unions(&self) -> Option<bool> {
852 self.config.shape.as_ref().and_then(|s| s.primitive_unions)
853 }
854
855 pub fn config_shape_additional_properties_typed(&self) -> Option<bool> {
859 self.config
860 .shape
861 .as_ref()
862 .and_then(|s| s.additional_properties_typed)
863 }
864
865 pub fn config_constraint_mode(&self) -> ConstraintMode {
870 self.config
871 .constraints
872 .as_ref()
873 .and_then(|c| c.mode)
874 .unwrap_or_default()
875 }
876
877 fn record(&self, feature: TypeFeature) {
878 self.used.borrow_mut().insert(feature);
879 }
880
881 pub fn string_format(&self, format: Option<&str>) -> MappedType {
889 let normalized = self.normalize_format(format);
890 match normalized.as_deref() {
891 Some("date-time") => self.map_date_time(self.config.date_time),
892 Some("date") => self.map_date(self.config.date),
893 Some("time") => self.map_time(self.config.time),
894 Some("duration") => self.map_duration(self.config.duration),
895 Some("uuid") => self.map_uuid(self.config.uuid),
896 Some("byte") => self.map_byte(self.config.byte),
897 Some("binary") => self.map_binary(self.config.binary),
898 Some("ipv4") => self.map_ipv4(self.config.ipv4),
899 Some("ipv6") => self.map_ipv6(self.config.ipv6),
900 Some("uri") | Some("url") => self.map_uri(self.config.uri),
901 Some("email") => self.map_email(self.config.email),
902 _ => MappedType::plain("String"),
905 }
906 }
907
908 fn normalize_format(&self, format: Option<&str>) -> Option<String> {
913 let raw = format?;
914 if let Some(target) = self.config.format_aliases.get(raw) {
915 return Some(target.clone());
916 }
917 Some(normalize_builtin_format(raw).to_string())
918 }
919
920 fn map_date_time(&self, strat: DateStrategy) -> MappedType {
921 match strat {
922 DateStrategy::String => MappedType::plain("String"),
923 DateStrategy::Chrono => {
924 self.record(TypeFeature::Chrono);
925 MappedType::with_feature("chrono::DateTime<chrono::Utc>", TypeFeature::Chrono)
930 }
931 DateStrategy::Time => {
932 self.record(TypeFeature::Time);
933 MappedType::with_codec(
934 "time::OffsetDateTime",
935 "time::serde::rfc3339",
936 TypeFeature::Time,
937 )
938 }
939 }
940 }
941
942 fn map_date(&self, strat: DateStrategy) -> MappedType {
943 match strat {
944 DateStrategy::String => MappedType::plain("String"),
945 DateStrategy::Chrono => {
946 self.record(TypeFeature::Chrono);
947 MappedType::with_feature("chrono::NaiveDate", TypeFeature::Chrono)
950 }
951 DateStrategy::Time => {
952 self.record(TypeFeature::TimeDate);
953 MappedType::with_codec("time::Date", "time_date_format", TypeFeature::TimeDate)
958 }
959 }
960 }
961
962 fn map_time(&self, _strat: DateStrategy) -> MappedType {
963 MappedType::plain("String")
970 }
971
972 fn map_duration(&self, strat: DurationStrategy) -> MappedType {
973 match strat {
974 DurationStrategy::String => MappedType::plain("String"),
975 DurationStrategy::Chrono => {
976 MappedType::plain("String")
983 }
984 DurationStrategy::Iso8601 => {
985 self.record(TypeFeature::Iso8601);
986 MappedType::with_feature("iso8601::Duration", TypeFeature::Iso8601)
987 }
988 }
989 }
990
991 fn map_uuid(&self, strat: UuidStrategy) -> MappedType {
992 match strat {
993 UuidStrategy::String => MappedType::plain("String"),
994 UuidStrategy::Uuid => {
995 self.record(TypeFeature::Uuid);
996 MappedType::with_feature("uuid::Uuid", TypeFeature::Uuid)
997 }
998 }
999 }
1000
1001 fn map_byte(&self, strat: ByteStrategy) -> MappedType {
1002 match strat {
1003 ByteStrategy::String => MappedType::plain("String"),
1004 ByteStrategy::VecU8 => MappedType::plain("Vec<u8>"),
1005 ByteStrategy::Base64 | ByteStrategy::Base64UrlUnpadded => {
1006 self.record(TypeFeature::Base64);
1007 MappedType::with_codec("Vec<u8>", "base64_serde", TypeFeature::Base64)
1012 }
1013 }
1014 }
1015
1016 fn map_binary(&self, strat: BinaryStrategy) -> MappedType {
1017 match strat {
1018 BinaryStrategy::String => MappedType::plain("String"),
1019 BinaryStrategy::VecU8 => MappedType::with_inline_codec("Vec<u8>", "binary_vec_serde"),
1020 BinaryStrategy::Bytes => {
1021 self.record(TypeFeature::Bytes);
1022 MappedType::with_codec("bytes::Bytes", "binary_bytes_serde", TypeFeature::Bytes)
1023 }
1024 }
1025 }
1026
1027 fn map_ipv4(&self, strat: IpStrategy) -> MappedType {
1028 match strat {
1029 IpStrategy::String => MappedType::plain("String"),
1030 IpStrategy::Std => MappedType::plain("std::net::Ipv4Addr"),
1031 }
1032 }
1033
1034 fn map_ipv6(&self, strat: IpStrategy) -> MappedType {
1035 match strat {
1036 IpStrategy::String => MappedType::plain("String"),
1037 IpStrategy::Std => MappedType::plain("std::net::Ipv6Addr"),
1038 }
1039 }
1040
1041 fn map_uri(&self, strat: UriStrategy) -> MappedType {
1042 match strat {
1043 UriStrategy::String => MappedType::plain("String"),
1044 UriStrategy::Url => {
1045 self.record(TypeFeature::Url);
1046 MappedType::with_feature("url::Url", TypeFeature::Url)
1047 }
1048 }
1049 }
1050
1051 fn map_email(&self, strat: EmailStrategy) -> MappedType {
1052 match strat {
1053 EmailStrategy::String => MappedType::plain("String"),
1054 EmailStrategy::EmailAddress => {
1055 self.record(TypeFeature::EmailAddress);
1056 MappedType::with_feature("email_address::EmailAddress", TypeFeature::EmailAddress)
1057 }
1058 }
1059 }
1060
1061 pub fn integer_format(&self, format: Option<&str>) -> MappedType {
1068 let normalized = self.normalize_format(format);
1069 match normalized.as_deref() {
1070 Some("int32") => MappedType::plain("i32"),
1071 Some("int64") => MappedType::plain("i64"),
1072 Some("uint32") if self.config.unsigned => MappedType::plain("u32"),
1073 Some("uint64") if self.config.unsigned => MappedType::plain("u64"),
1074 Some("uint") if self.config.unsigned => MappedType::plain("u64"),
1078 _ => MappedType::plain("i64"),
1079 }
1080 }
1081
1082 pub fn number_format(&self, format: Option<&str>) -> MappedType {
1095 let normalized = self.normalize_format(format);
1096 match normalized.as_deref() {
1097 Some("float") if self.config.float_precision == FloatPrecision::F32 => {
1098 MappedType::plain("f32")
1099 }
1100 Some("float") => MappedType::plain("f64"),
1101 Some("double") => MappedType::plain("f64"),
1102 _ => MappedType::plain("f64"),
1103 }
1104 }
1105
1106 pub fn boolean(&self) -> MappedType {
1107 MappedType::plain("bool")
1108 }
1109
1110 pub fn untyped_array(&self) -> MappedType {
1111 MappedType::plain("Vec<serde_json::Value>")
1112 }
1113
1114 pub fn dynamic_json(&self) -> MappedType {
1115 MappedType::plain("serde_json::Value")
1116 }
1117
1118 pub fn null_unit(&self) -> MappedType {
1119 MappedType::plain("()")
1120 }
1121
1122 pub fn map(&self, ty: OpenApiSchemaType, details: &SchemaDetails) -> MappedType {
1124 let format = details.format.as_deref();
1125 match ty {
1126 OpenApiSchemaType::String => self.string_format(format),
1127 OpenApiSchemaType::Integer => self.integer_format(format),
1128 OpenApiSchemaType::Number => self.number_format(format),
1129 OpenApiSchemaType::Boolean => self.boolean(),
1130 OpenApiSchemaType::Array => self.untyped_array(),
1131 OpenApiSchemaType::Object => self.dynamic_json(),
1132 OpenApiSchemaType::Null => self.null_unit(),
1133 }
1134 }
1135}
1136
1137#[cfg(test)]
1138mod tests {
1139 use super::*;
1140
1141 fn details_with_format(format: Option<&str>) -> SchemaDetails {
1142 SchemaDetails {
1143 format: format.map(str::to_string),
1144 ..Default::default()
1145 }
1146 }
1147
1148 #[test]
1149 fn default_mapper_emits_typed_scalars_for_common_formats() {
1150 let m = TypeMapper::default();
1151 assert_eq!(
1152 m.string_format(Some("date-time")).rust_type,
1153 "chrono::DateTime<chrono::Utc>"
1154 );
1155 assert_eq!(m.string_format(Some("date")).rust_type, "chrono::NaiveDate");
1156 assert_eq!(m.string_format(Some("time")).rust_type, "String");
1157 assert_eq!(m.string_format(Some("uuid")).rust_type, "uuid::Uuid");
1158 assert_eq!(m.string_format(Some("uri")).rust_type, "url::Url");
1159 assert_eq!(
1160 m.string_format(Some("ipv4")).rust_type,
1161 "std::net::Ipv4Addr"
1162 );
1163 assert_eq!(m.string_format(Some("byte")).rust_type, "Vec<u8>");
1164 assert_eq!(m.string_format(Some("binary")).rust_type, "bytes::Bytes");
1165 }
1166
1167 #[test]
1168 fn date_time_uses_default_chrono_serde() {
1169 let m = TypeMapper::default();
1172 let mt = m.string_format(Some("date-time"));
1173 assert_eq!(mt.rust_type, "chrono::DateTime<chrono::Utc>");
1174 assert!(mt.serde_with.is_none());
1175 assert_eq!(mt.feature, Some(TypeFeature::Chrono));
1176 }
1177
1178 #[test]
1179 fn byte_emits_base64_codec() {
1180 let m = TypeMapper::default();
1181 let mt = m.string_format(Some("byte"));
1182 assert_eq!(mt.rust_type, "Vec<u8>");
1183 assert_eq!(mt.serde_with.as_deref(), Some("base64_serde"));
1184 assert_eq!(mt.feature, Some(TypeFeature::Base64));
1185 }
1186
1187 #[test]
1188 fn byte_url_unpadded_reuses_base64_codec() {
1189 let mapper = TypeMapper::new(TypeMappingConfig {
1190 byte: ByteStrategy::Base64UrlUnpadded,
1191 ..TypeMappingConfig::default()
1192 });
1193 let mapped = mapper.string_format(Some("byte"));
1194 assert_eq!(mapped.rust_type, "Vec<u8>");
1195 assert_eq!(mapped.serde_with.as_deref(), Some("base64_serde"));
1196 assert_eq!(mapped.feature, Some(TypeFeature::Base64));
1197 }
1198
1199 #[test]
1200 fn byte_url_unpadded_parses_from_toml() {
1201 let config: TypeMappingConfig =
1202 toml::from_str(r#"byte = "base64_url_unpadded""#).expect("parse type config");
1203 assert_eq!(config.byte, ByteStrategy::Base64UrlUnpadded);
1204 }
1205
1206 #[test]
1207 fn conservative_config_collapses_everything_to_string() {
1208 let m = TypeMapper::new(TypeMappingConfig::conservative());
1209 for fmt in [
1210 Some("date-time"),
1211 Some("uuid"),
1212 Some("uri"),
1213 Some("byte"),
1214 Some("binary"),
1215 Some("ipv4"),
1216 Some("ipv6"),
1217 Some("date"),
1218 None,
1219 ] {
1220 let mt = m.string_format(fmt);
1221 assert_eq!(mt.rust_type, "String", "format = {fmt:?}");
1222 assert!(mt.serde_with.is_none(), "format = {fmt:?}");
1223 }
1224 }
1225
1226 #[test]
1227 fn unknown_formats_fall_through_to_string() {
1228 let m = TypeMapper::default();
1229 for fmt in [Some("hostname"), Some("password"), Some("idn-email")] {
1230 assert_eq!(m.string_format(fmt).rust_type, "String");
1231 }
1232 }
1233
1234 #[test]
1235 fn integer_formats_match_pre_refactor_behavior() {
1236 let m = TypeMapper::default();
1237 assert_eq!(m.integer_format(Some("int32")).rust_type, "i32");
1238 assert_eq!(m.integer_format(Some("int64")).rust_type, "i64");
1239 assert_eq!(m.integer_format(None).rust_type, "i64");
1240 }
1241
1242 #[test]
1243 fn integer_formats_default_handles_unsigned_q21() {
1244 let m = TypeMapper::default();
1245 assert_eq!(m.integer_format(Some("uint32")).rust_type, "u32");
1246 assert_eq!(m.integer_format(Some("uint64")).rust_type, "u64");
1247 assert_eq!(m.integer_format(Some("uint")).rust_type, "u64");
1249 }
1250
1251 #[test]
1252 fn unsigned_off_degrades_uint_to_i64() {
1253 let mut cfg = TypeMappingConfig::default();
1254 cfg.unsigned = false;
1255 let m = TypeMapper::new(cfg);
1256 assert_eq!(m.integer_format(Some("uint32")).rust_type, "i64");
1257 assert_eq!(m.integer_format(Some("uint64")).rust_type, "i64");
1258 }
1259
1260 #[test]
1261 fn conservative_disables_unsigned() {
1262 let m = TypeMapper::new(TypeMappingConfig::conservative());
1263 assert_eq!(m.integer_format(Some("uint64")).rust_type, "i64");
1264 }
1265
1266 #[test]
1267 fn builtin_aliases_normalize_uuid_variants_to_uuid() {
1268 let m = TypeMapper::default();
1269 for fmt in ["uuid4", "uuid_v4", "UUID"] {
1270 assert_eq!(normalize_builtin_format(fmt), "uuid", "format = {fmt}");
1271 let mt = m.string_format(Some(fmt));
1272 assert_eq!(mt.rust_type, "uuid::Uuid", "format = {fmt}");
1273 }
1274 assert_eq!(normalize_builtin_format("vendor-id"), "vendor-id");
1275 }
1276
1277 #[test]
1278 fn builtin_aliases_normalize_unix_time_to_int64() {
1279 let m = TypeMapper::default();
1280 for fmt in ["unix-time", "unix_time", "unixtime", "timestamp"] {
1281 let mt = m.integer_format(Some(fmt));
1282 assert_eq!(mt.rust_type, "i64", "format = {fmt}");
1283 }
1284 }
1285
1286 #[test]
1287 fn user_alias_overrides_builtin() {
1288 let mut cfg = TypeMappingConfig::default();
1289 cfg.format_aliases
1291 .insert("uuid4".to_string(), "hostname".to_string());
1292 let m = TypeMapper::new(cfg);
1293 assert_eq!(m.string_format(Some("uuid4")).rust_type, "String");
1295 }
1296
1297 #[test]
1298 fn used_features_records_referenced_crates() {
1299 let m = TypeMapper::default();
1300 let _ = m.string_format(Some("date-time"));
1301 let _ = m.string_format(Some("uuid"));
1302 let used = m.used_features();
1303 assert!(used.contains(TypeFeature::Chrono));
1304 assert!(used.contains(TypeFeature::Uuid));
1305 assert!(!used.contains(TypeFeature::Bytes));
1306 }
1307
1308 #[test]
1309 fn format_alias_normalizes_before_dispatch() {
1310 let mut cfg = TypeMappingConfig::default();
1311 cfg.format_aliases
1312 .insert("uuid4".to_string(), "uuid".to_string());
1313 let m = TypeMapper::new(cfg);
1314 assert_eq!(m.string_format(Some("uuid4")).rust_type, "uuid::Uuid");
1315 }
1316
1317 #[test]
1318 fn conservative_helper_round_trips() {
1319 let cfg = TypeMappingConfig::conservative();
1320 assert!(matches!(cfg.date_time, DateStrategy::String));
1321 assert!(matches!(cfg.uuid, UuidStrategy::String));
1322 }
1323
1324 #[test]
1325 fn dep_requirement_renders_features_list() {
1326 let dep = TypeFeature::Chrono.dep_requirement();
1327 assert_eq!(dep.crate_name, "chrono");
1328 assert_eq!(dep.features, vec!["serde"]);
1329 assert_eq!(
1330 dep.to_toml_line(),
1331 r#"chrono = { version = "0.4", features = ["serde"] }"#
1332 );
1333 }
1334
1335 #[test]
1336 fn dep_requirement_omits_features_when_none() {
1337 let dep = TypeFeature::Base64.dep_requirement();
1338 assert_eq!(dep.to_toml_line(), r#"base64 = "0.22""#);
1339 }
1340
1341 #[test]
1342 fn collect_dep_requirements_is_sorted_and_unique() {
1343 let mut used = UsedFeatures::default();
1344 used.insert(TypeFeature::Url);
1345 used.insert(TypeFeature::Chrono);
1346 used.insert(TypeFeature::Chrono); used.insert(TypeFeature::Uuid);
1348 let deps = collect_dep_requirements(&used);
1349 assert_eq!(
1350 deps.iter().map(|d| d.crate_name).collect::<Vec<_>>(),
1351 vec!["chrono", "url", "uuid"]
1352 );
1353 }
1354
1355 #[test]
1356 fn render_required_deps_toml_is_none_when_empty() {
1357 let deps: Vec<DepRequirement> = Vec::new();
1358 assert!(render_required_deps_toml(&deps).is_none());
1359 }
1360
1361 #[test]
1362 fn render_required_deps_toml_includes_dependencies_block() {
1363 let deps = vec![
1364 TypeFeature::Chrono.dep_requirement(),
1365 TypeFeature::Uuid.dep_requirement(),
1366 ];
1367 let toml = render_required_deps_toml(&deps).expect("non-empty");
1368 assert!(toml.contains("[dependencies]"));
1369 assert!(toml.contains("chrono = "));
1370 assert!(toml.contains("uuid = "));
1371 assert!(toml.contains("# Generated by openapi-to-rust"));
1372 }
1373
1374 #[test]
1375 fn map_dispatches_through_helpers() {
1376 let m = TypeMapper::default();
1377 assert_eq!(
1378 m.map(
1379 OpenApiSchemaType::String,
1380 &details_with_format(Some("uuid"))
1381 )
1382 .rust_type,
1383 "uuid::Uuid"
1384 );
1385 assert_eq!(
1386 m.map(
1387 OpenApiSchemaType::Integer,
1388 &details_with_format(Some("int32"))
1389 )
1390 .rust_type,
1391 "i32"
1392 );
1393 }
1394}