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 if uses("reqwest::multipart") {
324 features.push("multipart");
325 }
326 if uses(".bytes_stream()") {
329 features.push("stream");
330 }
331 dependencies.push(
332 DepRequirement::new("reqwest", "0.12")
333 .without_default_features()
334 .with_features(&features),
335 );
336 }
337 if uses("reqwest_middleware::") {
338 let dependency = if uses(".multipart(form)") {
339 DepRequirement::new("reqwest-middleware", "0.4").with_features(&["multipart"])
340 } else {
341 DepRequirement::new("reqwest-middleware", "0.4")
342 };
343 dependencies.push(dependency);
344 }
345 if uses("reqwest_retry::") {
346 let dependency = DepRequirement::new("reqwest-retry", "0.7");
347 dependencies.push(if uses("reqwest_tracing::") {
348 dependency
349 } else {
350 dependency.without_default_features()
351 });
352 }
353 if uses("reqwest_tracing::") {
354 dependencies.push(DepRequirement::new("reqwest-tracing", "0.5"));
355 }
356 if uses("reqwest_eventsource::") {
357 dependencies.push(DepRequirement::new("reqwest-eventsource", "0.6"));
358 }
359 if uses("thiserror::") || uses("use thiserror::") {
360 dependencies.push(DepRequirement::new("thiserror", "1"));
361 }
362 if uses("async_trait::") {
363 dependencies.push(DepRequirement::new("async-trait", "0.1"));
364 }
365 if uses("futures_util::") {
366 dependencies.push(DepRequirement::new("futures-util", "0.3"));
367 }
368 if uses("futures_core::") {
369 dependencies.push(DepRequirement::new("futures-core", "0.3"));
370 }
371 if uses("use tracing::") {
372 dependencies.push(DepRequirement::new("tracing", "0.1"));
373 }
374 if uses("axum::") {
375 let mut features = vec!["json"];
376 if uses("axum::response::sse::") {
377 features.push("tokio");
378 }
379 dependencies.push(
380 DepRequirement::new("axum", "0.8")
381 .without_default_features()
382 .with_features(&features),
383 );
384 }
385 if uses("jsonschema::") {
386 dependencies.push(DepRequirement::new("jsonschema", "0.49").without_default_features());
387 }
388 if uses("http_body_util::") {
389 dependencies.push(DepRequirement::new("http-body-util", "0.1"));
390 }
391 if uses("mime::") {
392 dependencies.push(DepRequirement::new("mime", "0.3"));
393 }
394 if enable_specta {
395 let mut features = vec!["derive"];
396 for (needle, feature) in [
397 ("bytes::", "bytes"),
398 ("chrono::", "chrono"),
399 ("time::OffsetDateTime", "time"),
400 ("url::Url", "url"),
401 ("uuid::", "uuid"),
402 ] {
403 if uses(needle) {
404 features.push(feature);
405 }
406 }
407 if uses_time {
408 features.push("time");
409 }
410 dependencies.push(
411 DepRequirement::new("specta", "2.0.0-rc.25")
412 .with_features(&features)
413 .optional(),
414 );
415 }
416
417 merge_dep_requirements(dependencies)
418}
419
420pub fn collect_dep_requirements(used: &UsedFeatures) -> Vec<DepRequirement> {
424 merge_dep_requirements(used.iter().map(|feature| feature.dep_requirement()))
425}
426
427#[derive(Debug, Default, Clone)]
429pub struct UsedFeatures {
430 set: BTreeSet<TypeFeature>,
431}
432
433impl UsedFeatures {
434 pub fn insert(&mut self, feature: TypeFeature) {
435 self.set.insert(feature);
436 }
437
438 pub fn contains(&self, feature: TypeFeature) -> bool {
439 self.set.contains(&feature)
440 }
441
442 pub fn iter(&self) -> impl Iterator<Item = &TypeFeature> {
443 self.set.iter()
444 }
445
446 pub fn is_empty(&self) -> bool {
447 self.set.is_empty()
448 }
449}
450
451#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
457#[serde(rename_all = "lowercase")]
458pub enum DateStrategy {
459 String,
461 #[default]
463 Chrono,
464 Time,
466}
467
468#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
470#[serde(rename_all = "lowercase")]
471pub enum DurationStrategy {
472 #[default]
479 String,
480 Chrono,
483 Iso8601,
485}
486
487#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
489#[serde(rename_all = "lowercase")]
490pub enum UuidStrategy {
491 String,
492 #[default]
494 Uuid,
495}
496
497#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
499#[serde(rename_all = "snake_case")]
500pub enum ByteStrategy {
501 String,
502 #[default]
505 Base64,
506 Base64UrlUnpadded,
510 VecU8,
512}
513
514#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
516#[serde(rename_all = "snake_case")]
517pub enum BinaryStrategy {
518 String,
519 #[default]
521 Bytes,
522 VecU8,
523}
524
525#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
527#[serde(rename_all = "lowercase")]
528pub enum IpStrategy {
529 String,
530 #[default]
532 Std,
533}
534
535#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
537#[serde(rename_all = "lowercase")]
538pub enum UriStrategy {
539 String,
540 #[default]
542 Url,
543}
544
545#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
551#[serde(rename_all = "snake_case")]
552pub enum EmailStrategy {
553 #[default]
554 String,
555 EmailAddress,
556}
557
558#[derive(Debug, Clone, Deserialize, Serialize)]
566#[serde(default, rename_all = "snake_case", deny_unknown_fields)]
567pub struct TypeMappingConfig {
568 pub date_time: DateStrategy,
569 pub date: DateStrategy,
570 pub time: DateStrategy,
571 pub duration: DurationStrategy,
572 pub uuid: UuidStrategy,
573 pub byte: ByteStrategy,
574 pub binary: BinaryStrategy,
575 pub ipv4: IpStrategy,
576 pub ipv6: IpStrategy,
577 pub uri: UriStrategy,
578 pub email: EmailStrategy,
579
580 #[serde(default = "default_true")]
585 pub unsigned: bool,
586
587 #[serde(default)]
592 pub format_aliases: BTreeMap<String, String>,
593
594 pub shape: Option<TypeShapeConfig>,
596
597 pub constraints: Option<TypeConstraintsConfig>,
599
600 pub enums: Option<TypeEnumsConfig>,
602
603 #[serde(default)]
607 pub float_precision: FloatPrecision,
608}
609
610#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
612#[serde(rename_all = "lowercase")]
613pub enum FloatPrecision {
614 #[default]
617 F64,
618 F32,
622}
623
624fn default_true() -> bool {
625 true
626}
627
628impl Default for TypeMappingConfig {
629 fn default() -> Self {
630 Self {
631 float_precision: FloatPrecision::default(),
632 date_time: DateStrategy::default(),
633 date: DateStrategy::default(),
634 time: DateStrategy::default(),
635 duration: DurationStrategy::default(),
636 uuid: UuidStrategy::default(),
637 byte: ByteStrategy::default(),
638 binary: BinaryStrategy::default(),
639 ipv4: IpStrategy::default(),
640 ipv6: IpStrategy::default(),
641 uri: UriStrategy::default(),
642 email: EmailStrategy::default(),
643 unsigned: true,
644 format_aliases: BTreeMap::new(),
645 shape: None,
646 constraints: None,
647 enums: None,
648 }
649 }
650}
651
652fn builtin_format_aliases() -> &'static [(&'static str, &'static str)] {
658 &[
659 ("uuid4", "uuid"),
660 ("uuid_v4", "uuid"),
661 ("UUID", "uuid"),
662 ("unix-time", "int64"),
663 ("unix_time", "int64"),
664 ("unixtime", "int64"),
665 ("timestamp", "int64"),
666 ]
667}
668
669impl TypeMappingConfig {
670 pub fn constraint_mode(&self) -> ConstraintMode {
675 self.constraints
676 .as_ref()
677 .and_then(|c| c.mode)
678 .unwrap_or_default()
679 }
680
681 pub fn x_enum_varnames_enabled(&self) -> bool {
684 self.enums
685 .as_ref()
686 .and_then(|e| e.x_enum_varnames)
687 .unwrap_or(true)
688 }
689
690 pub fn x_enum_descriptions_enabled(&self) -> bool {
693 self.enums
694 .as_ref()
695 .and_then(|e| e.x_enum_descriptions)
696 .unwrap_or(true)
697 }
698
699 pub fn conservative() -> Self {
704 Self {
705 float_precision: FloatPrecision::F32,
708 date_time: DateStrategy::String,
709 date: DateStrategy::String,
710 time: DateStrategy::String,
711 duration: DurationStrategy::String,
712 uuid: UuidStrategy::String,
713 byte: ByteStrategy::String,
714 binary: BinaryStrategy::String,
715 ipv4: IpStrategy::String,
716 ipv6: IpStrategy::String,
717 uri: UriStrategy::String,
718 email: EmailStrategy::String,
719 unsigned: false,
720 format_aliases: BTreeMap::new(),
721 shape: None,
722 constraints: None,
723 enums: None,
724 }
725 }
726}
727
728#[derive(Debug, Clone, Default, Deserialize, Serialize)]
729#[serde(default, rename_all = "snake_case", deny_unknown_fields)]
730pub struct TypeShapeConfig {
731 pub additional_properties_typed: Option<bool>,
732 pub unique_items_to_set: Option<bool>,
733 pub primitive_unions: Option<bool>,
734}
735
736#[derive(Debug, Clone, Default, Deserialize, Serialize)]
737#[serde(default, rename_all = "snake_case", deny_unknown_fields)]
738pub struct TypeConstraintsConfig {
739 pub mode: Option<ConstraintMode>,
743}
744
745#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
754#[serde(rename_all = "snake_case")]
755pub enum ConstraintMode {
756 Off,
758 #[default]
761 Doc,
762}
763
764#[derive(Debug, Clone, Default, Deserialize, Serialize)]
765#[serde(default, rename_all = "snake_case", deny_unknown_fields)]
766pub struct TypeEnumsConfig {
767 pub x_enum_varnames: Option<bool>,
768 pub x_enum_descriptions: Option<bool>,
769}
770
771pub struct TypeMapper {
776 config: TypeMappingConfig,
777 used: RefCell<UsedFeatures>,
778}
779
780impl Default for TypeMapper {
781 fn default() -> Self {
782 Self::new(TypeMappingConfig::default())
783 }
784}
785
786impl TypeMapper {
787 pub fn new(config: TypeMappingConfig) -> Self {
788 Self {
789 config,
790 used: RefCell::new(UsedFeatures::default()),
791 }
792 }
793
794 pub fn used_features(&self) -> UsedFeatures {
796 self.used.borrow().clone()
797 }
798
799 pub fn config(&self) -> &TypeMappingConfig {
803 &self.config
804 }
805
806 pub fn config_shape_primitive_unions(&self) -> Option<bool> {
811 self.config.shape.as_ref().and_then(|s| s.primitive_unions)
812 }
813
814 pub fn config_shape_additional_properties_typed(&self) -> Option<bool> {
818 self.config
819 .shape
820 .as_ref()
821 .and_then(|s| s.additional_properties_typed)
822 }
823
824 pub fn config_constraint_mode(&self) -> ConstraintMode {
829 self.config
830 .constraints
831 .as_ref()
832 .and_then(|c| c.mode)
833 .unwrap_or_default()
834 }
835
836 fn record(&self, feature: TypeFeature) {
837 self.used.borrow_mut().insert(feature);
838 }
839
840 pub fn string_format(&self, format: Option<&str>) -> MappedType {
848 let normalized = self.normalize_format(format);
849 match normalized.as_deref() {
850 Some("date-time") => self.map_date_time(self.config.date_time),
851 Some("date") => self.map_date(self.config.date),
852 Some("time") => self.map_time(self.config.time),
853 Some("duration") => self.map_duration(self.config.duration),
854 Some("uuid") => self.map_uuid(self.config.uuid),
855 Some("byte") => self.map_byte(self.config.byte),
856 Some("binary") => self.map_binary(self.config.binary),
857 Some("ipv4") => self.map_ipv4(self.config.ipv4),
858 Some("ipv6") => self.map_ipv6(self.config.ipv6),
859 Some("uri") | Some("url") => self.map_uri(self.config.uri),
860 Some("email") => self.map_email(self.config.email),
861 _ => MappedType::plain("String"),
864 }
865 }
866
867 fn normalize_format(&self, format: Option<&str>) -> Option<String> {
872 let raw = format?;
873 if let Some(target) = self.config.format_aliases.get(raw) {
874 return Some(target.clone());
875 }
876 for (from, to) in builtin_format_aliases() {
877 if *from == raw {
878 return Some((*to).to_string());
879 }
880 }
881 Some(raw.to_string())
882 }
883
884 fn map_date_time(&self, strat: DateStrategy) -> MappedType {
885 match strat {
886 DateStrategy::String => MappedType::plain("String"),
887 DateStrategy::Chrono => {
888 self.record(TypeFeature::Chrono);
889 MappedType::with_feature("chrono::DateTime<chrono::Utc>", TypeFeature::Chrono)
894 }
895 DateStrategy::Time => {
896 self.record(TypeFeature::Time);
897 MappedType::with_codec(
898 "time::OffsetDateTime",
899 "time::serde::rfc3339",
900 TypeFeature::Time,
901 )
902 }
903 }
904 }
905
906 fn map_date(&self, strat: DateStrategy) -> MappedType {
907 match strat {
908 DateStrategy::String => MappedType::plain("String"),
909 DateStrategy::Chrono => {
910 self.record(TypeFeature::Chrono);
911 MappedType::with_feature("chrono::NaiveDate", TypeFeature::Chrono)
914 }
915 DateStrategy::Time => {
916 self.record(TypeFeature::TimeDate);
917 MappedType::with_codec("time::Date", "time_date_format", TypeFeature::TimeDate)
922 }
923 }
924 }
925
926 fn map_time(&self, strat: DateStrategy) -> MappedType {
927 match strat {
928 DateStrategy::String => MappedType::plain("String"),
929 DateStrategy::Chrono => {
930 self.record(TypeFeature::Chrono);
931 MappedType::with_feature("chrono::NaiveTime", TypeFeature::Chrono)
932 }
933 DateStrategy::Time => {
934 self.record(TypeFeature::TimeTime);
935 MappedType::with_codec("time::Time", "time_time_format", TypeFeature::TimeTime)
938 }
939 }
940 }
941
942 fn map_duration(&self, strat: DurationStrategy) -> MappedType {
943 match strat {
944 DurationStrategy::String => MappedType::plain("String"),
945 DurationStrategy::Chrono => {
946 MappedType::plain("String")
953 }
954 DurationStrategy::Iso8601 => {
955 self.record(TypeFeature::Iso8601);
956 MappedType::with_feature("iso8601::Duration", TypeFeature::Iso8601)
957 }
958 }
959 }
960
961 fn map_uuid(&self, strat: UuidStrategy) -> MappedType {
962 match strat {
963 UuidStrategy::String => MappedType::plain("String"),
964 UuidStrategy::Uuid => {
965 self.record(TypeFeature::Uuid);
966 MappedType::with_feature("uuid::Uuid", TypeFeature::Uuid)
967 }
968 }
969 }
970
971 fn map_byte(&self, strat: ByteStrategy) -> MappedType {
972 match strat {
973 ByteStrategy::String => MappedType::plain("String"),
974 ByteStrategy::VecU8 => MappedType::plain("Vec<u8>"),
975 ByteStrategy::Base64 | ByteStrategy::Base64UrlUnpadded => {
976 self.record(TypeFeature::Base64);
977 MappedType::with_codec("Vec<u8>", "base64_serde", TypeFeature::Base64)
982 }
983 }
984 }
985
986 fn map_binary(&self, strat: BinaryStrategy) -> MappedType {
987 match strat {
988 BinaryStrategy::String => MappedType::plain("String"),
989 BinaryStrategy::VecU8 => MappedType::plain("Vec<u8>"),
990 BinaryStrategy::Bytes => {
991 self.record(TypeFeature::Bytes);
992 MappedType::with_feature("bytes::Bytes", TypeFeature::Bytes)
993 }
994 }
995 }
996
997 fn map_ipv4(&self, strat: IpStrategy) -> MappedType {
998 match strat {
999 IpStrategy::String => MappedType::plain("String"),
1000 IpStrategy::Std => MappedType::plain("std::net::Ipv4Addr"),
1001 }
1002 }
1003
1004 fn map_ipv6(&self, strat: IpStrategy) -> MappedType {
1005 match strat {
1006 IpStrategy::String => MappedType::plain("String"),
1007 IpStrategy::Std => MappedType::plain("std::net::Ipv6Addr"),
1008 }
1009 }
1010
1011 fn map_uri(&self, strat: UriStrategy) -> MappedType {
1012 match strat {
1013 UriStrategy::String => MappedType::plain("String"),
1014 UriStrategy::Url => {
1015 self.record(TypeFeature::Url);
1016 MappedType::with_feature("url::Url", TypeFeature::Url)
1017 }
1018 }
1019 }
1020
1021 fn map_email(&self, strat: EmailStrategy) -> MappedType {
1022 match strat {
1023 EmailStrategy::String => MappedType::plain("String"),
1024 EmailStrategy::EmailAddress => {
1025 self.record(TypeFeature::EmailAddress);
1026 MappedType::with_feature("email_address::EmailAddress", TypeFeature::EmailAddress)
1027 }
1028 }
1029 }
1030
1031 pub fn integer_format(&self, format: Option<&str>) -> MappedType {
1038 let normalized = self.normalize_format(format);
1039 match normalized.as_deref() {
1040 Some("int32") => MappedType::plain("i32"),
1041 Some("int64") => MappedType::plain("i64"),
1042 Some("uint32") if self.config.unsigned => MappedType::plain("u32"),
1043 Some("uint64") if self.config.unsigned => MappedType::plain("u64"),
1044 Some("uint") if self.config.unsigned => MappedType::plain("u64"),
1048 _ => MappedType::plain("i64"),
1049 }
1050 }
1051
1052 pub fn number_format(&self, format: Option<&str>) -> MappedType {
1065 let normalized = self.normalize_format(format);
1066 match normalized.as_deref() {
1067 Some("float") if self.config.float_precision == FloatPrecision::F32 => {
1068 MappedType::plain("f32")
1069 }
1070 Some("float") => MappedType::plain("f64"),
1071 Some("double") => MappedType::plain("f64"),
1072 _ => MappedType::plain("f64"),
1073 }
1074 }
1075
1076 pub fn boolean(&self) -> MappedType {
1077 MappedType::plain("bool")
1078 }
1079
1080 pub fn untyped_array(&self) -> MappedType {
1081 MappedType::plain("Vec<serde_json::Value>")
1082 }
1083
1084 pub fn dynamic_json(&self) -> MappedType {
1085 MappedType::plain("serde_json::Value")
1086 }
1087
1088 pub fn null_unit(&self) -> MappedType {
1089 MappedType::plain("()")
1090 }
1091
1092 pub fn map(&self, ty: OpenApiSchemaType, details: &SchemaDetails) -> MappedType {
1094 let format = details.format.as_deref();
1095 match ty {
1096 OpenApiSchemaType::String => self.string_format(format),
1097 OpenApiSchemaType::Integer => self.integer_format(format),
1098 OpenApiSchemaType::Number => self.number_format(format),
1099 OpenApiSchemaType::Boolean => self.boolean(),
1100 OpenApiSchemaType::Array => self.untyped_array(),
1101 OpenApiSchemaType::Object => self.dynamic_json(),
1102 OpenApiSchemaType::Null => self.null_unit(),
1103 }
1104 }
1105}
1106
1107#[cfg(test)]
1108mod tests {
1109 use super::*;
1110
1111 fn details_with_format(format: Option<&str>) -> SchemaDetails {
1112 SchemaDetails {
1113 format: format.map(str::to_string),
1114 ..Default::default()
1115 }
1116 }
1117
1118 #[test]
1119 fn default_mapper_emits_typed_scalars_for_common_formats() {
1120 let m = TypeMapper::default();
1121 assert_eq!(
1122 m.string_format(Some("date-time")).rust_type,
1123 "chrono::DateTime<chrono::Utc>"
1124 );
1125 assert_eq!(m.string_format(Some("date")).rust_type, "chrono::NaiveDate");
1126 assert_eq!(m.string_format(Some("uuid")).rust_type, "uuid::Uuid");
1127 assert_eq!(m.string_format(Some("uri")).rust_type, "url::Url");
1128 assert_eq!(
1129 m.string_format(Some("ipv4")).rust_type,
1130 "std::net::Ipv4Addr"
1131 );
1132 assert_eq!(m.string_format(Some("byte")).rust_type, "Vec<u8>");
1133 assert_eq!(m.string_format(Some("binary")).rust_type, "bytes::Bytes");
1134 }
1135
1136 #[test]
1137 fn date_time_uses_default_chrono_serde() {
1138 let m = TypeMapper::default();
1141 let mt = m.string_format(Some("date-time"));
1142 assert_eq!(mt.rust_type, "chrono::DateTime<chrono::Utc>");
1143 assert!(mt.serde_with.is_none());
1144 assert_eq!(mt.feature, Some(TypeFeature::Chrono));
1145 }
1146
1147 #[test]
1148 fn byte_emits_base64_codec() {
1149 let m = TypeMapper::default();
1150 let mt = m.string_format(Some("byte"));
1151 assert_eq!(mt.rust_type, "Vec<u8>");
1152 assert_eq!(mt.serde_with.as_deref(), Some("base64_serde"));
1153 assert_eq!(mt.feature, Some(TypeFeature::Base64));
1154 }
1155
1156 #[test]
1157 fn byte_url_unpadded_reuses_base64_codec() {
1158 let mapper = TypeMapper::new(TypeMappingConfig {
1159 byte: ByteStrategy::Base64UrlUnpadded,
1160 ..TypeMappingConfig::default()
1161 });
1162 let mapped = mapper.string_format(Some("byte"));
1163 assert_eq!(mapped.rust_type, "Vec<u8>");
1164 assert_eq!(mapped.serde_with.as_deref(), Some("base64_serde"));
1165 assert_eq!(mapped.feature, Some(TypeFeature::Base64));
1166 }
1167
1168 #[test]
1169 fn byte_url_unpadded_parses_from_toml() {
1170 let config: TypeMappingConfig =
1171 toml::from_str(r#"byte = "base64_url_unpadded""#).expect("parse type config");
1172 assert_eq!(config.byte, ByteStrategy::Base64UrlUnpadded);
1173 }
1174
1175 #[test]
1176 fn conservative_config_collapses_everything_to_string() {
1177 let m = TypeMapper::new(TypeMappingConfig::conservative());
1178 for fmt in [
1179 Some("date-time"),
1180 Some("uuid"),
1181 Some("uri"),
1182 Some("byte"),
1183 Some("binary"),
1184 Some("ipv4"),
1185 Some("ipv6"),
1186 Some("date"),
1187 None,
1188 ] {
1189 let mt = m.string_format(fmt);
1190 assert_eq!(mt.rust_type, "String", "format = {fmt:?}");
1191 assert!(mt.serde_with.is_none(), "format = {fmt:?}");
1192 }
1193 }
1194
1195 #[test]
1196 fn unknown_formats_fall_through_to_string() {
1197 let m = TypeMapper::default();
1198 for fmt in [Some("hostname"), Some("password"), Some("idn-email")] {
1199 assert_eq!(m.string_format(fmt).rust_type, "String");
1200 }
1201 }
1202
1203 #[test]
1204 fn integer_formats_match_pre_refactor_behavior() {
1205 let m = TypeMapper::default();
1206 assert_eq!(m.integer_format(Some("int32")).rust_type, "i32");
1207 assert_eq!(m.integer_format(Some("int64")).rust_type, "i64");
1208 assert_eq!(m.integer_format(None).rust_type, "i64");
1209 }
1210
1211 #[test]
1212 fn integer_formats_default_handles_unsigned_q21() {
1213 let m = TypeMapper::default();
1214 assert_eq!(m.integer_format(Some("uint32")).rust_type, "u32");
1215 assert_eq!(m.integer_format(Some("uint64")).rust_type, "u64");
1216 assert_eq!(m.integer_format(Some("uint")).rust_type, "u64");
1218 }
1219
1220 #[test]
1221 fn unsigned_off_degrades_uint_to_i64() {
1222 let mut cfg = TypeMappingConfig::default();
1223 cfg.unsigned = false;
1224 let m = TypeMapper::new(cfg);
1225 assert_eq!(m.integer_format(Some("uint32")).rust_type, "i64");
1226 assert_eq!(m.integer_format(Some("uint64")).rust_type, "i64");
1227 }
1228
1229 #[test]
1230 fn conservative_disables_unsigned() {
1231 let m = TypeMapper::new(TypeMappingConfig::conservative());
1232 assert_eq!(m.integer_format(Some("uint64")).rust_type, "i64");
1233 }
1234
1235 #[test]
1236 fn builtin_aliases_normalize_uuid_variants_to_uuid() {
1237 let m = TypeMapper::default();
1238 for fmt in ["uuid4", "uuid_v4", "UUID"] {
1239 let mt = m.string_format(Some(fmt));
1240 assert_eq!(mt.rust_type, "uuid::Uuid", "format = {fmt}");
1241 }
1242 }
1243
1244 #[test]
1245 fn builtin_aliases_normalize_unix_time_to_int64() {
1246 let m = TypeMapper::default();
1247 for fmt in ["unix-time", "unix_time", "unixtime", "timestamp"] {
1248 let mt = m.integer_format(Some(fmt));
1249 assert_eq!(mt.rust_type, "i64", "format = {fmt}");
1250 }
1251 }
1252
1253 #[test]
1254 fn user_alias_overrides_builtin() {
1255 let mut cfg = TypeMappingConfig::default();
1256 cfg.format_aliases
1258 .insert("uuid4".to_string(), "hostname".to_string());
1259 let m = TypeMapper::new(cfg);
1260 assert_eq!(m.string_format(Some("uuid4")).rust_type, "String");
1262 }
1263
1264 #[test]
1265 fn used_features_records_referenced_crates() {
1266 let m = TypeMapper::default();
1267 let _ = m.string_format(Some("date-time"));
1268 let _ = m.string_format(Some("uuid"));
1269 let used = m.used_features();
1270 assert!(used.contains(TypeFeature::Chrono));
1271 assert!(used.contains(TypeFeature::Uuid));
1272 assert!(!used.contains(TypeFeature::Bytes));
1273 }
1274
1275 #[test]
1276 fn format_alias_normalizes_before_dispatch() {
1277 let mut cfg = TypeMappingConfig::default();
1278 cfg.format_aliases
1279 .insert("uuid4".to_string(), "uuid".to_string());
1280 let m = TypeMapper::new(cfg);
1281 assert_eq!(m.string_format(Some("uuid4")).rust_type, "uuid::Uuid");
1282 }
1283
1284 #[test]
1285 fn conservative_helper_round_trips() {
1286 let cfg = TypeMappingConfig::conservative();
1287 assert!(matches!(cfg.date_time, DateStrategy::String));
1288 assert!(matches!(cfg.uuid, UuidStrategy::String));
1289 }
1290
1291 #[test]
1292 fn dep_requirement_renders_features_list() {
1293 let dep = TypeFeature::Chrono.dep_requirement();
1294 assert_eq!(dep.crate_name, "chrono");
1295 assert_eq!(dep.features, vec!["serde"]);
1296 assert_eq!(
1297 dep.to_toml_line(),
1298 r#"chrono = { version = "0.4", features = ["serde"] }"#
1299 );
1300 }
1301
1302 #[test]
1303 fn dep_requirement_omits_features_when_none() {
1304 let dep = TypeFeature::Base64.dep_requirement();
1305 assert_eq!(dep.to_toml_line(), r#"base64 = "0.22""#);
1306 }
1307
1308 #[test]
1309 fn collect_dep_requirements_is_sorted_and_unique() {
1310 let mut used = UsedFeatures::default();
1311 used.insert(TypeFeature::Url);
1312 used.insert(TypeFeature::Chrono);
1313 used.insert(TypeFeature::Chrono); used.insert(TypeFeature::Uuid);
1315 let deps = collect_dep_requirements(&used);
1316 assert_eq!(
1317 deps.iter().map(|d| d.crate_name).collect::<Vec<_>>(),
1318 vec!["chrono", "url", "uuid"]
1319 );
1320 }
1321
1322 #[test]
1323 fn render_required_deps_toml_is_none_when_empty() {
1324 let deps: Vec<DepRequirement> = Vec::new();
1325 assert!(render_required_deps_toml(&deps).is_none());
1326 }
1327
1328 #[test]
1329 fn render_required_deps_toml_includes_dependencies_block() {
1330 let deps = vec![
1331 TypeFeature::Chrono.dep_requirement(),
1332 TypeFeature::Uuid.dep_requirement(),
1333 ];
1334 let toml = render_required_deps_toml(&deps).expect("non-empty");
1335 assert!(toml.contains("[dependencies]"));
1336 assert!(toml.contains("chrono = "));
1337 assert!(toml.contains("uuid = "));
1338 assert!(toml.contains("# Generated by openapi-to-rust"));
1339 }
1340
1341 #[test]
1342 fn map_dispatches_through_helpers() {
1343 let m = TypeMapper::default();
1344 assert_eq!(
1345 m.map(
1346 OpenApiSchemaType::String,
1347 &details_with_format(Some("uuid"))
1348 )
1349 .rust_type,
1350 "uuid::Uuid"
1351 );
1352 assert_eq!(
1353 m.map(
1354 OpenApiSchemaType::Integer,
1355 &details_with_format(Some("int32"))
1356 )
1357 .rust_type,
1358 "i32"
1359 );
1360 }
1361}