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