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"];
314 if uses(".json(&") {
315 features.push("json");
316 }
317 if uses(".query(&") {
318 features.push("query");
319 }
320 if uses(".form(&") {
321 features.push("form");
322 }
323 if uses("reqwest::multipart") {
330 features.push("multipart");
331 }
332 if uses(".bytes_stream()") || uses(".chunk().await") {
335 features.push("stream");
336 }
337 dependencies.push(
338 DepRequirement::new("reqwest", "0.13")
339 .without_default_features()
340 .with_features(&features),
341 );
342 }
343 if uses("reqwest_middleware::") {
344 let mut features = Vec::new();
345 if uses(".json(&") {
346 features.push("json");
347 }
348 if uses(".query(&") {
349 features.push("query");
350 }
351 if uses(".form(&") {
352 features.push("form");
353 }
354 if uses(".multipart(form)") {
355 features.push("multipart");
356 }
357 dependencies
358 .push(DepRequirement::new("reqwest-middleware", "0.5").with_features(&features));
359 }
360 if uses("reqwest_retry::") {
361 let dependency = DepRequirement::new("reqwest-retry", "0.9");
362 dependencies.push(if uses("reqwest_tracing::") {
363 dependency
364 } else {
365 dependency.without_default_features()
366 });
367 }
368 if uses("reqwest_tracing::") {
369 dependencies.push(DepRequirement::new("reqwest-tracing", "0.7"));
370 }
371 if uses("thiserror::") || uses("use thiserror::") {
372 dependencies.push(DepRequirement::new("thiserror", "2"));
373 }
374 if uses("async_trait::") {
375 dependencies.push(DepRequirement::new("async-trait", "0.1"));
376 }
377 if uses("futures_util::") {
378 dependencies.push(DepRequirement::new("futures-util", "0.3"));
379 }
380 if uses("futures_timer::") {
381 dependencies.push(DepRequirement::new("futures-timer", "3"));
382 }
383 if uses("futures_core::") {
384 dependencies.push(DepRequirement::new("futures-core", "0.3"));
385 }
386 if uses("use tracing::") {
387 dependencies.push(DepRequirement::new("tracing", "0.1"));
388 }
389 if uses("axum::") {
390 let mut features = vec!["json"];
391 if uses("axum::extract::Multipart") {
392 features.push("multipart");
393 }
394 if uses("axum::response::sse::") {
395 features.push("tokio");
396 }
397 dependencies.push(
398 DepRequirement::new("axum", "0.8")
399 .without_default_features()
400 .with_features(&features),
401 );
402 }
403 if uses("jsonschema::") {
404 dependencies.push(DepRequirement::new("jsonschema", "0.49").without_default_features());
405 }
406 if uses("http_body_util::") {
407 dependencies.push(DepRequirement::new("http-body-util", "0.1"));
408 }
409 if uses("mime::") {
410 dependencies.push(DepRequirement::new("mime", "0.3"));
411 }
412 if enable_specta {
413 let mut features = vec!["derive"];
414 for (needle, feature) in [
415 ("bytes::", "bytes"),
416 ("chrono::", "chrono"),
417 ("time::OffsetDateTime", "time"),
418 ("url::Url", "url"),
419 ("uuid::", "uuid"),
420 ] {
421 if uses(needle) {
422 features.push(feature);
423 }
424 }
425 if uses_time {
426 features.push("time");
427 }
428 dependencies.push(
429 DepRequirement::new("specta", "2.0.0-rc.25")
430 .with_features(&features)
431 .optional(),
432 );
433 }
434
435 merge_dep_requirements(dependencies)
436}
437
438pub fn collect_dep_requirements(used: &UsedFeatures) -> Vec<DepRequirement> {
442 merge_dep_requirements(used.iter().map(|feature| feature.dep_requirement()))
443}
444
445#[derive(Debug, Default, Clone)]
447pub struct UsedFeatures {
448 set: BTreeSet<TypeFeature>,
449}
450
451impl UsedFeatures {
452 pub fn insert(&mut self, feature: TypeFeature) {
453 self.set.insert(feature);
454 }
455
456 pub fn contains(&self, feature: TypeFeature) -> bool {
457 self.set.contains(&feature)
458 }
459
460 pub fn iter(&self) -> impl Iterator<Item = &TypeFeature> {
461 self.set.iter()
462 }
463
464 pub fn is_empty(&self) -> bool {
465 self.set.is_empty()
466 }
467}
468
469#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
475#[serde(rename_all = "lowercase")]
476pub enum DateStrategy {
477 String,
479 #[default]
481 Chrono,
482 Time,
484}
485
486#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
488#[serde(rename_all = "lowercase")]
489pub enum DurationStrategy {
490 #[default]
497 String,
498 Chrono,
501 Iso8601,
503}
504
505#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
507#[serde(rename_all = "lowercase")]
508pub enum UuidStrategy {
509 String,
510 #[default]
512 Uuid,
513}
514
515#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
517#[serde(rename_all = "snake_case")]
518pub enum ByteStrategy {
519 String,
520 #[default]
523 Base64,
524 Base64UrlUnpadded,
528 VecU8,
530}
531
532#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
534#[serde(rename_all = "snake_case")]
535pub enum BinaryStrategy {
536 String,
537 #[default]
539 Bytes,
540 VecU8,
541}
542
543#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
545#[serde(rename_all = "lowercase")]
546pub enum IpStrategy {
547 String,
548 #[default]
550 Std,
551}
552
553#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
555#[serde(rename_all = "lowercase")]
556pub enum UriStrategy {
557 String,
558 #[default]
560 Url,
561}
562
563#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
569#[serde(rename_all = "snake_case")]
570pub enum EmailStrategy {
571 #[default]
572 String,
573 EmailAddress,
574}
575
576#[derive(Debug, Clone, Deserialize, Serialize)]
584#[serde(default, rename_all = "snake_case", deny_unknown_fields)]
585pub struct TypeMappingConfig {
586 pub date_time: DateStrategy,
587 pub date: DateStrategy,
588 pub time: DateStrategy,
589 pub duration: DurationStrategy,
590 pub uuid: UuidStrategy,
591 pub byte: ByteStrategy,
592 pub binary: BinaryStrategy,
593 pub ipv4: IpStrategy,
594 pub ipv6: IpStrategy,
595 pub uri: UriStrategy,
596 pub email: EmailStrategy,
597
598 #[serde(default = "default_true")]
603 pub unsigned: bool,
604
605 #[serde(default)]
610 pub format_aliases: BTreeMap<String, String>,
611
612 pub shape: Option<TypeShapeConfig>,
614
615 pub constraints: Option<TypeConstraintsConfig>,
617
618 pub enums: Option<TypeEnumsConfig>,
620
621 #[serde(default)]
625 pub float_precision: FloatPrecision,
626}
627
628#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
630#[serde(rename_all = "lowercase")]
631pub enum FloatPrecision {
632 #[default]
635 F64,
636 F32,
640}
641
642fn default_true() -> bool {
643 true
644}
645
646impl Default for TypeMappingConfig {
647 fn default() -> Self {
648 Self {
649 float_precision: FloatPrecision::default(),
650 date_time: DateStrategy::default(),
651 date: DateStrategy::default(),
652 time: DateStrategy::default(),
653 duration: DurationStrategy::default(),
654 uuid: UuidStrategy::default(),
655 byte: ByteStrategy::default(),
656 binary: BinaryStrategy::default(),
657 ipv4: IpStrategy::default(),
658 ipv6: IpStrategy::default(),
659 uri: UriStrategy::default(),
660 email: EmailStrategy::default(),
661 unsigned: true,
662 format_aliases: BTreeMap::new(),
663 shape: None,
664 constraints: None,
665 enums: None,
666 }
667 }
668}
669
670fn builtin_format_aliases() -> &'static [(&'static str, &'static str)] {
676 &[
677 ("uuid4", "uuid"),
678 ("uuid_v4", "uuid"),
679 ("UUID", "uuid"),
680 ("unix-time", "int64"),
681 ("unix_time", "int64"),
682 ("unixtime", "int64"),
683 ("timestamp", "int64"),
684 ]
685}
686
687impl TypeMappingConfig {
688 pub fn constraint_mode(&self) -> ConstraintMode {
693 self.constraints
694 .as_ref()
695 .and_then(|c| c.mode)
696 .unwrap_or_default()
697 }
698
699 pub fn x_enum_varnames_enabled(&self) -> bool {
702 self.enums
703 .as_ref()
704 .and_then(|e| e.x_enum_varnames)
705 .unwrap_or(true)
706 }
707
708 pub fn x_enum_descriptions_enabled(&self) -> bool {
711 self.enums
712 .as_ref()
713 .and_then(|e| e.x_enum_descriptions)
714 .unwrap_or(true)
715 }
716
717 pub fn conservative() -> Self {
722 Self {
723 float_precision: FloatPrecision::F32,
726 date_time: DateStrategy::String,
727 date: DateStrategy::String,
728 time: DateStrategy::String,
729 duration: DurationStrategy::String,
730 uuid: UuidStrategy::String,
731 byte: ByteStrategy::String,
732 binary: BinaryStrategy::String,
733 ipv4: IpStrategy::String,
734 ipv6: IpStrategy::String,
735 uri: UriStrategy::String,
736 email: EmailStrategy::String,
737 unsigned: false,
738 format_aliases: BTreeMap::new(),
739 shape: None,
740 constraints: None,
741 enums: None,
742 }
743 }
744}
745
746#[derive(Debug, Clone, Default, Deserialize, Serialize)]
747#[serde(default, rename_all = "snake_case", deny_unknown_fields)]
748pub struct TypeShapeConfig {
749 pub additional_properties_typed: Option<bool>,
750 pub unique_items_to_set: Option<bool>,
751 pub primitive_unions: Option<bool>,
752}
753
754#[derive(Debug, Clone, Default, Deserialize, Serialize)]
755#[serde(default, rename_all = "snake_case", deny_unknown_fields)]
756pub struct TypeConstraintsConfig {
757 pub mode: Option<ConstraintMode>,
761}
762
763#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
772#[serde(rename_all = "snake_case")]
773pub enum ConstraintMode {
774 Off,
776 #[default]
779 Doc,
780}
781
782#[derive(Debug, Clone, Default, Deserialize, Serialize)]
783#[serde(default, rename_all = "snake_case", deny_unknown_fields)]
784pub struct TypeEnumsConfig {
785 pub x_enum_varnames: Option<bool>,
786 pub x_enum_descriptions: Option<bool>,
787}
788
789pub struct TypeMapper {
794 config: TypeMappingConfig,
795 used: RefCell<UsedFeatures>,
796}
797
798impl Default for TypeMapper {
799 fn default() -> Self {
800 Self::new(TypeMappingConfig::default())
801 }
802}
803
804impl TypeMapper {
805 pub fn new(config: TypeMappingConfig) -> Self {
806 Self {
807 config,
808 used: RefCell::new(UsedFeatures::default()),
809 }
810 }
811
812 pub fn used_features(&self) -> UsedFeatures {
814 self.used.borrow().clone()
815 }
816
817 pub fn config(&self) -> &TypeMappingConfig {
821 &self.config
822 }
823
824 pub fn config_shape_primitive_unions(&self) -> Option<bool> {
829 self.config.shape.as_ref().and_then(|s| s.primitive_unions)
830 }
831
832 pub fn config_shape_additional_properties_typed(&self) -> Option<bool> {
836 self.config
837 .shape
838 .as_ref()
839 .and_then(|s| s.additional_properties_typed)
840 }
841
842 pub fn config_constraint_mode(&self) -> ConstraintMode {
847 self.config
848 .constraints
849 .as_ref()
850 .and_then(|c| c.mode)
851 .unwrap_or_default()
852 }
853
854 fn record(&self, feature: TypeFeature) {
855 self.used.borrow_mut().insert(feature);
856 }
857
858 pub fn string_format(&self, format: Option<&str>) -> MappedType {
866 let normalized = self.normalize_format(format);
867 match normalized.as_deref() {
868 Some("date-time") => self.map_date_time(self.config.date_time),
869 Some("date") => self.map_date(self.config.date),
870 Some("time") => self.map_time(self.config.time),
871 Some("duration") => self.map_duration(self.config.duration),
872 Some("uuid") => self.map_uuid(self.config.uuid),
873 Some("byte") => self.map_byte(self.config.byte),
874 Some("binary") => self.map_binary(self.config.binary),
875 Some("ipv4") => self.map_ipv4(self.config.ipv4),
876 Some("ipv6") => self.map_ipv6(self.config.ipv6),
877 Some("uri") | Some("url") => self.map_uri(self.config.uri),
878 Some("email") => self.map_email(self.config.email),
879 _ => MappedType::plain("String"),
882 }
883 }
884
885 fn normalize_format(&self, format: Option<&str>) -> Option<String> {
890 let raw = format?;
891 if let Some(target) = self.config.format_aliases.get(raw) {
892 return Some(target.clone());
893 }
894 for (from, to) in builtin_format_aliases() {
895 if *from == raw {
896 return Some((*to).to_string());
897 }
898 }
899 Some(raw.to_string())
900 }
901
902 fn map_date_time(&self, strat: DateStrategy) -> MappedType {
903 match strat {
904 DateStrategy::String => MappedType::plain("String"),
905 DateStrategy::Chrono => {
906 self.record(TypeFeature::Chrono);
907 MappedType::with_feature("chrono::DateTime<chrono::Utc>", TypeFeature::Chrono)
912 }
913 DateStrategy::Time => {
914 self.record(TypeFeature::Time);
915 MappedType::with_codec(
916 "time::OffsetDateTime",
917 "time::serde::rfc3339",
918 TypeFeature::Time,
919 )
920 }
921 }
922 }
923
924 fn map_date(&self, strat: DateStrategy) -> MappedType {
925 match strat {
926 DateStrategy::String => MappedType::plain("String"),
927 DateStrategy::Chrono => {
928 self.record(TypeFeature::Chrono);
929 MappedType::with_feature("chrono::NaiveDate", TypeFeature::Chrono)
932 }
933 DateStrategy::Time => {
934 self.record(TypeFeature::TimeDate);
935 MappedType::with_codec("time::Date", "time_date_format", TypeFeature::TimeDate)
940 }
941 }
942 }
943
944 fn map_time(&self, strat: DateStrategy) -> MappedType {
945 match strat {
946 DateStrategy::String => MappedType::plain("String"),
947 DateStrategy::Chrono => {
948 self.record(TypeFeature::Chrono);
949 MappedType::with_feature("chrono::NaiveTime", TypeFeature::Chrono)
950 }
951 DateStrategy::Time => {
952 self.record(TypeFeature::TimeTime);
953 MappedType::with_codec("time::Time", "time_time_format", TypeFeature::TimeTime)
956 }
957 }
958 }
959
960 fn map_duration(&self, strat: DurationStrategy) -> MappedType {
961 match strat {
962 DurationStrategy::String => MappedType::plain("String"),
963 DurationStrategy::Chrono => {
964 MappedType::plain("String")
971 }
972 DurationStrategy::Iso8601 => {
973 self.record(TypeFeature::Iso8601);
974 MappedType::with_feature("iso8601::Duration", TypeFeature::Iso8601)
975 }
976 }
977 }
978
979 fn map_uuid(&self, strat: UuidStrategy) -> MappedType {
980 match strat {
981 UuidStrategy::String => MappedType::plain("String"),
982 UuidStrategy::Uuid => {
983 self.record(TypeFeature::Uuid);
984 MappedType::with_feature("uuid::Uuid", TypeFeature::Uuid)
985 }
986 }
987 }
988
989 fn map_byte(&self, strat: ByteStrategy) -> MappedType {
990 match strat {
991 ByteStrategy::String => MappedType::plain("String"),
992 ByteStrategy::VecU8 => MappedType::plain("Vec<u8>"),
993 ByteStrategy::Base64 | ByteStrategy::Base64UrlUnpadded => {
994 self.record(TypeFeature::Base64);
995 MappedType::with_codec("Vec<u8>", "base64_serde", TypeFeature::Base64)
1000 }
1001 }
1002 }
1003
1004 fn map_binary(&self, strat: BinaryStrategy) -> MappedType {
1005 match strat {
1006 BinaryStrategy::String => MappedType::plain("String"),
1007 BinaryStrategy::VecU8 => MappedType::plain("Vec<u8>"),
1008 BinaryStrategy::Bytes => {
1009 self.record(TypeFeature::Bytes);
1010 MappedType::with_feature("bytes::Bytes", TypeFeature::Bytes)
1011 }
1012 }
1013 }
1014
1015 fn map_ipv4(&self, strat: IpStrategy) -> MappedType {
1016 match strat {
1017 IpStrategy::String => MappedType::plain("String"),
1018 IpStrategy::Std => MappedType::plain("std::net::Ipv4Addr"),
1019 }
1020 }
1021
1022 fn map_ipv6(&self, strat: IpStrategy) -> MappedType {
1023 match strat {
1024 IpStrategy::String => MappedType::plain("String"),
1025 IpStrategy::Std => MappedType::plain("std::net::Ipv6Addr"),
1026 }
1027 }
1028
1029 fn map_uri(&self, strat: UriStrategy) -> MappedType {
1030 match strat {
1031 UriStrategy::String => MappedType::plain("String"),
1032 UriStrategy::Url => {
1033 self.record(TypeFeature::Url);
1034 MappedType::with_feature("url::Url", TypeFeature::Url)
1035 }
1036 }
1037 }
1038
1039 fn map_email(&self, strat: EmailStrategy) -> MappedType {
1040 match strat {
1041 EmailStrategy::String => MappedType::plain("String"),
1042 EmailStrategy::EmailAddress => {
1043 self.record(TypeFeature::EmailAddress);
1044 MappedType::with_feature("email_address::EmailAddress", TypeFeature::EmailAddress)
1045 }
1046 }
1047 }
1048
1049 pub fn integer_format(&self, format: Option<&str>) -> MappedType {
1056 let normalized = self.normalize_format(format);
1057 match normalized.as_deref() {
1058 Some("int32") => MappedType::plain("i32"),
1059 Some("int64") => MappedType::plain("i64"),
1060 Some("uint32") if self.config.unsigned => MappedType::plain("u32"),
1061 Some("uint64") if self.config.unsigned => MappedType::plain("u64"),
1062 Some("uint") if self.config.unsigned => MappedType::plain("u64"),
1066 _ => MappedType::plain("i64"),
1067 }
1068 }
1069
1070 pub fn number_format(&self, format: Option<&str>) -> MappedType {
1083 let normalized = self.normalize_format(format);
1084 match normalized.as_deref() {
1085 Some("float") if self.config.float_precision == FloatPrecision::F32 => {
1086 MappedType::plain("f32")
1087 }
1088 Some("float") => MappedType::plain("f64"),
1089 Some("double") => MappedType::plain("f64"),
1090 _ => MappedType::plain("f64"),
1091 }
1092 }
1093
1094 pub fn boolean(&self) -> MappedType {
1095 MappedType::plain("bool")
1096 }
1097
1098 pub fn untyped_array(&self) -> MappedType {
1099 MappedType::plain("Vec<serde_json::Value>")
1100 }
1101
1102 pub fn dynamic_json(&self) -> MappedType {
1103 MappedType::plain("serde_json::Value")
1104 }
1105
1106 pub fn null_unit(&self) -> MappedType {
1107 MappedType::plain("()")
1108 }
1109
1110 pub fn map(&self, ty: OpenApiSchemaType, details: &SchemaDetails) -> MappedType {
1112 let format = details.format.as_deref();
1113 match ty {
1114 OpenApiSchemaType::String => self.string_format(format),
1115 OpenApiSchemaType::Integer => self.integer_format(format),
1116 OpenApiSchemaType::Number => self.number_format(format),
1117 OpenApiSchemaType::Boolean => self.boolean(),
1118 OpenApiSchemaType::Array => self.untyped_array(),
1119 OpenApiSchemaType::Object => self.dynamic_json(),
1120 OpenApiSchemaType::Null => self.null_unit(),
1121 }
1122 }
1123}
1124
1125#[cfg(test)]
1126mod tests {
1127 use super::*;
1128
1129 fn details_with_format(format: Option<&str>) -> SchemaDetails {
1130 SchemaDetails {
1131 format: format.map(str::to_string),
1132 ..Default::default()
1133 }
1134 }
1135
1136 #[test]
1137 fn default_mapper_emits_typed_scalars_for_common_formats() {
1138 let m = TypeMapper::default();
1139 assert_eq!(
1140 m.string_format(Some("date-time")).rust_type,
1141 "chrono::DateTime<chrono::Utc>"
1142 );
1143 assert_eq!(m.string_format(Some("date")).rust_type, "chrono::NaiveDate");
1144 assert_eq!(m.string_format(Some("uuid")).rust_type, "uuid::Uuid");
1145 assert_eq!(m.string_format(Some("uri")).rust_type, "url::Url");
1146 assert_eq!(
1147 m.string_format(Some("ipv4")).rust_type,
1148 "std::net::Ipv4Addr"
1149 );
1150 assert_eq!(m.string_format(Some("byte")).rust_type, "Vec<u8>");
1151 assert_eq!(m.string_format(Some("binary")).rust_type, "bytes::Bytes");
1152 }
1153
1154 #[test]
1155 fn date_time_uses_default_chrono_serde() {
1156 let m = TypeMapper::default();
1159 let mt = m.string_format(Some("date-time"));
1160 assert_eq!(mt.rust_type, "chrono::DateTime<chrono::Utc>");
1161 assert!(mt.serde_with.is_none());
1162 assert_eq!(mt.feature, Some(TypeFeature::Chrono));
1163 }
1164
1165 #[test]
1166 fn byte_emits_base64_codec() {
1167 let m = TypeMapper::default();
1168 let mt = m.string_format(Some("byte"));
1169 assert_eq!(mt.rust_type, "Vec<u8>");
1170 assert_eq!(mt.serde_with.as_deref(), Some("base64_serde"));
1171 assert_eq!(mt.feature, Some(TypeFeature::Base64));
1172 }
1173
1174 #[test]
1175 fn byte_url_unpadded_reuses_base64_codec() {
1176 let mapper = TypeMapper::new(TypeMappingConfig {
1177 byte: ByteStrategy::Base64UrlUnpadded,
1178 ..TypeMappingConfig::default()
1179 });
1180 let mapped = mapper.string_format(Some("byte"));
1181 assert_eq!(mapped.rust_type, "Vec<u8>");
1182 assert_eq!(mapped.serde_with.as_deref(), Some("base64_serde"));
1183 assert_eq!(mapped.feature, Some(TypeFeature::Base64));
1184 }
1185
1186 #[test]
1187 fn byte_url_unpadded_parses_from_toml() {
1188 let config: TypeMappingConfig =
1189 toml::from_str(r#"byte = "base64_url_unpadded""#).expect("parse type config");
1190 assert_eq!(config.byte, ByteStrategy::Base64UrlUnpadded);
1191 }
1192
1193 #[test]
1194 fn conservative_config_collapses_everything_to_string() {
1195 let m = TypeMapper::new(TypeMappingConfig::conservative());
1196 for fmt in [
1197 Some("date-time"),
1198 Some("uuid"),
1199 Some("uri"),
1200 Some("byte"),
1201 Some("binary"),
1202 Some("ipv4"),
1203 Some("ipv6"),
1204 Some("date"),
1205 None,
1206 ] {
1207 let mt = m.string_format(fmt);
1208 assert_eq!(mt.rust_type, "String", "format = {fmt:?}");
1209 assert!(mt.serde_with.is_none(), "format = {fmt:?}");
1210 }
1211 }
1212
1213 #[test]
1214 fn unknown_formats_fall_through_to_string() {
1215 let m = TypeMapper::default();
1216 for fmt in [Some("hostname"), Some("password"), Some("idn-email")] {
1217 assert_eq!(m.string_format(fmt).rust_type, "String");
1218 }
1219 }
1220
1221 #[test]
1222 fn integer_formats_match_pre_refactor_behavior() {
1223 let m = TypeMapper::default();
1224 assert_eq!(m.integer_format(Some("int32")).rust_type, "i32");
1225 assert_eq!(m.integer_format(Some("int64")).rust_type, "i64");
1226 assert_eq!(m.integer_format(None).rust_type, "i64");
1227 }
1228
1229 #[test]
1230 fn integer_formats_default_handles_unsigned_q21() {
1231 let m = TypeMapper::default();
1232 assert_eq!(m.integer_format(Some("uint32")).rust_type, "u32");
1233 assert_eq!(m.integer_format(Some("uint64")).rust_type, "u64");
1234 assert_eq!(m.integer_format(Some("uint")).rust_type, "u64");
1236 }
1237
1238 #[test]
1239 fn unsigned_off_degrades_uint_to_i64() {
1240 let mut cfg = TypeMappingConfig::default();
1241 cfg.unsigned = false;
1242 let m = TypeMapper::new(cfg);
1243 assert_eq!(m.integer_format(Some("uint32")).rust_type, "i64");
1244 assert_eq!(m.integer_format(Some("uint64")).rust_type, "i64");
1245 }
1246
1247 #[test]
1248 fn conservative_disables_unsigned() {
1249 let m = TypeMapper::new(TypeMappingConfig::conservative());
1250 assert_eq!(m.integer_format(Some("uint64")).rust_type, "i64");
1251 }
1252
1253 #[test]
1254 fn builtin_aliases_normalize_uuid_variants_to_uuid() {
1255 let m = TypeMapper::default();
1256 for fmt in ["uuid4", "uuid_v4", "UUID"] {
1257 let mt = m.string_format(Some(fmt));
1258 assert_eq!(mt.rust_type, "uuid::Uuid", "format = {fmt}");
1259 }
1260 }
1261
1262 #[test]
1263 fn builtin_aliases_normalize_unix_time_to_int64() {
1264 let m = TypeMapper::default();
1265 for fmt in ["unix-time", "unix_time", "unixtime", "timestamp"] {
1266 let mt = m.integer_format(Some(fmt));
1267 assert_eq!(mt.rust_type, "i64", "format = {fmt}");
1268 }
1269 }
1270
1271 #[test]
1272 fn user_alias_overrides_builtin() {
1273 let mut cfg = TypeMappingConfig::default();
1274 cfg.format_aliases
1276 .insert("uuid4".to_string(), "hostname".to_string());
1277 let m = TypeMapper::new(cfg);
1278 assert_eq!(m.string_format(Some("uuid4")).rust_type, "String");
1280 }
1281
1282 #[test]
1283 fn used_features_records_referenced_crates() {
1284 let m = TypeMapper::default();
1285 let _ = m.string_format(Some("date-time"));
1286 let _ = m.string_format(Some("uuid"));
1287 let used = m.used_features();
1288 assert!(used.contains(TypeFeature::Chrono));
1289 assert!(used.contains(TypeFeature::Uuid));
1290 assert!(!used.contains(TypeFeature::Bytes));
1291 }
1292
1293 #[test]
1294 fn format_alias_normalizes_before_dispatch() {
1295 let mut cfg = TypeMappingConfig::default();
1296 cfg.format_aliases
1297 .insert("uuid4".to_string(), "uuid".to_string());
1298 let m = TypeMapper::new(cfg);
1299 assert_eq!(m.string_format(Some("uuid4")).rust_type, "uuid::Uuid");
1300 }
1301
1302 #[test]
1303 fn conservative_helper_round_trips() {
1304 let cfg = TypeMappingConfig::conservative();
1305 assert!(matches!(cfg.date_time, DateStrategy::String));
1306 assert!(matches!(cfg.uuid, UuidStrategy::String));
1307 }
1308
1309 #[test]
1310 fn dep_requirement_renders_features_list() {
1311 let dep = TypeFeature::Chrono.dep_requirement();
1312 assert_eq!(dep.crate_name, "chrono");
1313 assert_eq!(dep.features, vec!["serde"]);
1314 assert_eq!(
1315 dep.to_toml_line(),
1316 r#"chrono = { version = "0.4", features = ["serde"] }"#
1317 );
1318 }
1319
1320 #[test]
1321 fn dep_requirement_omits_features_when_none() {
1322 let dep = TypeFeature::Base64.dep_requirement();
1323 assert_eq!(dep.to_toml_line(), r#"base64 = "0.22""#);
1324 }
1325
1326 #[test]
1327 fn collect_dep_requirements_is_sorted_and_unique() {
1328 let mut used = UsedFeatures::default();
1329 used.insert(TypeFeature::Url);
1330 used.insert(TypeFeature::Chrono);
1331 used.insert(TypeFeature::Chrono); used.insert(TypeFeature::Uuid);
1333 let deps = collect_dep_requirements(&used);
1334 assert_eq!(
1335 deps.iter().map(|d| d.crate_name).collect::<Vec<_>>(),
1336 vec!["chrono", "url", "uuid"]
1337 );
1338 }
1339
1340 #[test]
1341 fn render_required_deps_toml_is_none_when_empty() {
1342 let deps: Vec<DepRequirement> = Vec::new();
1343 assert!(render_required_deps_toml(&deps).is_none());
1344 }
1345
1346 #[test]
1347 fn render_required_deps_toml_includes_dependencies_block() {
1348 let deps = vec![
1349 TypeFeature::Chrono.dep_requirement(),
1350 TypeFeature::Uuid.dep_requirement(),
1351 ];
1352 let toml = render_required_deps_toml(&deps).expect("non-empty");
1353 assert!(toml.contains("[dependencies]"));
1354 assert!(toml.contains("chrono = "));
1355 assert!(toml.contains("uuid = "));
1356 assert!(toml.contains("# Generated by openapi-to-rust"));
1357 }
1358
1359 #[test]
1360 fn map_dispatches_through_helpers() {
1361 let m = TypeMapper::default();
1362 assert_eq!(
1363 m.map(
1364 OpenApiSchemaType::String,
1365 &details_with_format(Some("uuid"))
1366 )
1367 .rust_type,
1368 "uuid::Uuid"
1369 );
1370 assert_eq!(
1371 m.map(
1372 OpenApiSchemaType::Integer,
1373 &details_with_format(Some("int32"))
1374 )
1375 .rust_type,
1376 "i32"
1377 );
1378 }
1379}