use std::cell::RefCell;
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use serde::{Deserialize, Serialize};
use crate::openapi::{SchemaDetails, SchemaType as OpenApiSchemaType};
#[derive(Debug, Clone)]
pub struct MappedType {
pub rust_type: String,
pub serde_with: Option<String>,
pub feature: Option<TypeFeature>,
}
impl MappedType {
pub fn plain(rust_type: impl Into<String>) -> Self {
Self {
rust_type: rust_type.into(),
serde_with: None,
feature: None,
}
}
pub fn with_feature(rust_type: impl Into<String>, feature: TypeFeature) -> Self {
Self {
rust_type: rust_type.into(),
serde_with: None,
feature: Some(feature),
}
}
pub fn with_codec(
rust_type: impl Into<String>,
codec_path: impl Into<String>,
feature: TypeFeature,
) -> Self {
Self {
rust_type: rust_type.into(),
serde_with: Some(codec_path.into()),
feature: Some(feature),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum TypeFeature {
Chrono,
Time,
TimeDate,
TimeTime,
Iso8601,
Uuid,
Bytes,
Base64,
Url,
EmailAddress,
}
impl TypeFeature {
pub fn dep_requirement(self) -> DepRequirement {
match self {
Self::Chrono => DepRequirement::new("chrono", "0.4").with_features(&["serde"]),
Self::Time => DepRequirement::new("time", "0.3").with_features(&[
"serde",
"formatting",
"parsing",
]),
Self::TimeDate | Self::TimeTime => DepRequirement::new("time", "0.3").with_features(&[
"serde",
"formatting",
"parsing",
"macros",
]),
Self::Iso8601 => DepRequirement::new("iso8601", "0.6"),
Self::Uuid => DepRequirement::new("uuid", "1").with_features(&["serde", "v4"]),
Self::Bytes => DepRequirement::new("bytes", "1").with_features(&["serde"]),
Self::Base64 => DepRequirement::new("base64", "0.22"),
Self::Url => DepRequirement::new("url", "2").with_features(&["serde"]),
Self::EmailAddress => DepRequirement::new("email_address", "0.2"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DepRequirement {
pub crate_name: &'static str,
pub version: &'static str,
pub features: Vec<&'static str>,
}
impl DepRequirement {
pub fn new(crate_name: &'static str, version: &'static str) -> Self {
Self {
crate_name,
version,
features: Vec::new(),
}
}
pub fn with_features(mut self, features: &[&'static str]) -> Self {
self.features = features.to_vec();
self
}
pub fn to_toml_line(&self) -> String {
if self.features.is_empty() {
format!("{} = \"{}\"", self.crate_name, self.version)
} else {
let feats = self
.features
.iter()
.map(|f| format!("\"{f}\""))
.collect::<Vec<_>>()
.join(", ");
format!(
"{} = {{ version = \"{}\", features = [{}] }}",
self.crate_name, self.version, feats
)
}
}
}
pub fn render_required_deps_toml(deps: &[DepRequirement]) -> Option<String> {
if deps.is_empty() {
return None;
}
let mut out = String::new();
out.push_str(
"# Generated by openapi-to-rust.\n\
# These crates are required by the typed-scalar formats used\n\
# in your OpenAPI spec. Copy these lines into the [dependencies]\n\
# section of your consuming crate's Cargo.toml.\n\
#\n\
# To opt out of typed scalars (and avoid these deps), set\n\
# the relevant strategies to \"string\" in [generator.types],\n\
# or pass --types-conservative on the CLI.\n\
\n\
[dependencies]\n",
);
for dep in deps {
out.push_str(&dep.to_toml_line());
out.push('\n');
}
Some(out)
}
pub fn collect_dep_requirements(used: &UsedFeatures) -> Vec<DepRequirement> {
let mut deps: Vec<DepRequirement> = used.iter().map(|f| f.dep_requirement()).collect();
deps.sort_by_key(|d| d.crate_name);
let mut merged: Vec<DepRequirement> = Vec::new();
for dep in deps {
match merged.last_mut() {
Some(last) if last.crate_name == dep.crate_name => {
for feat in dep.features {
if !last.features.contains(&feat) {
last.features.push(feat);
}
}
}
_ => merged.push(dep),
}
}
merged
}
#[derive(Debug, Default, Clone)]
pub struct UsedFeatures {
set: BTreeSet<TypeFeature>,
}
impl UsedFeatures {
pub fn insert(&mut self, feature: TypeFeature) {
self.set.insert(feature);
}
pub fn contains(&self, feature: TypeFeature) -> bool {
self.set.contains(&feature)
}
pub fn iter(&self) -> impl Iterator<Item = &TypeFeature> {
self.set.iter()
}
pub fn is_empty(&self) -> bool {
self.set.is_empty()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum DateStrategy {
String,
#[default]
Chrono,
Time,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum DurationStrategy {
#[default]
String,
Chrono,
Iso8601,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum UuidStrategy {
String,
#[default]
Uuid,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ByteStrategy {
String,
#[default]
Base64,
VecU8,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum BinaryStrategy {
String,
#[default]
Bytes,
VecU8,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum IpStrategy {
String,
#[default]
Std,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum UriStrategy {
String,
#[default]
Url,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum EmailStrategy {
#[default]
String,
EmailAddress,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default, rename_all = "snake_case")]
pub struct TypeMappingConfig {
pub date_time: DateStrategy,
pub date: DateStrategy,
pub time: DateStrategy,
pub duration: DurationStrategy,
pub uuid: UuidStrategy,
pub byte: ByteStrategy,
pub binary: BinaryStrategy,
pub ipv4: IpStrategy,
pub ipv6: IpStrategy,
pub uri: UriStrategy,
pub email: EmailStrategy,
#[serde(default = "default_true")]
pub unsigned: bool,
#[serde(default)]
pub format_aliases: BTreeMap<String, String>,
pub shape: Option<TypeShapeConfig>,
pub constraints: Option<TypeConstraintsConfig>,
pub enums: Option<TypeEnumsConfig>,
}
fn default_true() -> bool {
true
}
impl Default for TypeMappingConfig {
fn default() -> Self {
Self {
date_time: DateStrategy::default(),
date: DateStrategy::default(),
time: DateStrategy::default(),
duration: DurationStrategy::default(),
uuid: UuidStrategy::default(),
byte: ByteStrategy::default(),
binary: BinaryStrategy::default(),
ipv4: IpStrategy::default(),
ipv6: IpStrategy::default(),
uri: UriStrategy::default(),
email: EmailStrategy::default(),
unsigned: true,
format_aliases: BTreeMap::new(),
shape: None,
constraints: None,
enums: None,
}
}
}
fn builtin_format_aliases() -> &'static [(&'static str, &'static str)] {
&[
("uuid4", "uuid"),
("uuid_v4", "uuid"),
("UUID", "uuid"),
("unix-time", "int64"),
("unix_time", "int64"),
("unixtime", "int64"),
("timestamp", "int64"),
]
}
impl TypeMappingConfig {
pub fn constraint_mode(&self) -> ConstraintMode {
self.constraints
.as_ref()
.and_then(|c| c.mode)
.unwrap_or_default()
}
pub fn x_enum_varnames_enabled(&self) -> bool {
self.enums
.as_ref()
.and_then(|e| e.x_enum_varnames)
.unwrap_or(true)
}
pub fn x_enum_descriptions_enabled(&self) -> bool {
self.enums
.as_ref()
.and_then(|e| e.x_enum_descriptions)
.unwrap_or(true)
}
pub fn conservative() -> Self {
Self {
date_time: DateStrategy::String,
date: DateStrategy::String,
time: DateStrategy::String,
duration: DurationStrategy::String,
uuid: UuidStrategy::String,
byte: ByteStrategy::String,
binary: BinaryStrategy::String,
ipv4: IpStrategy::String,
ipv6: IpStrategy::String,
uri: UriStrategy::String,
email: EmailStrategy::String,
unsigned: false,
format_aliases: BTreeMap::new(),
shape: None,
constraints: None,
enums: None,
}
}
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[serde(default, rename_all = "snake_case")]
pub struct TypeShapeConfig {
pub additional_properties_typed: Option<bool>,
pub unique_items_to_set: Option<bool>,
pub primitive_unions: Option<bool>,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[serde(default, rename_all = "snake_case")]
pub struct TypeConstraintsConfig {
pub mode: Option<ConstraintMode>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ConstraintMode {
Off,
#[default]
Doc,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[serde(default, rename_all = "snake_case")]
pub struct TypeEnumsConfig {
pub x_enum_varnames: Option<bool>,
pub x_enum_descriptions: Option<bool>,
}
pub struct TypeMapper {
config: TypeMappingConfig,
used: RefCell<UsedFeatures>,
}
impl Default for TypeMapper {
fn default() -> Self {
Self::new(TypeMappingConfig::default())
}
}
impl TypeMapper {
pub fn new(config: TypeMappingConfig) -> Self {
Self {
config,
used: RefCell::new(UsedFeatures::default()),
}
}
pub fn used_features(&self) -> UsedFeatures {
self.used.borrow().clone()
}
pub fn config(&self) -> &TypeMappingConfig {
&self.config
}
pub fn config_shape_primitive_unions(&self) -> Option<bool> {
self.config.shape.as_ref().and_then(|s| s.primitive_unions)
}
pub fn config_shape_additional_properties_typed(&self) -> Option<bool> {
self.config
.shape
.as_ref()
.and_then(|s| s.additional_properties_typed)
}
pub fn config_constraint_mode(&self) -> ConstraintMode {
self.config
.constraints
.as_ref()
.and_then(|c| c.mode)
.unwrap_or_default()
}
fn record(&self, feature: TypeFeature) {
self.used.borrow_mut().insert(feature);
}
pub fn string_format(&self, format: Option<&str>) -> MappedType {
let normalized = self.normalize_format(format);
match normalized.as_deref() {
Some("date-time") => self.map_date_time(self.config.date_time),
Some("date") => self.map_date(self.config.date),
Some("time") => self.map_time(self.config.time),
Some("duration") => self.map_duration(self.config.duration),
Some("uuid") => self.map_uuid(self.config.uuid),
Some("byte") => self.map_byte(self.config.byte),
Some("binary") => self.map_binary(self.config.binary),
Some("ipv4") => self.map_ipv4(self.config.ipv4),
Some("ipv6") => self.map_ipv6(self.config.ipv6),
Some("uri") | Some("url") => self.map_uri(self.config.uri),
Some("email") => self.map_email(self.config.email),
_ => MappedType::plain("String"),
}
}
fn normalize_format(&self, format: Option<&str>) -> Option<String> {
let raw = format?;
if let Some(target) = self.config.format_aliases.get(raw) {
return Some(target.clone());
}
for (from, to) in builtin_format_aliases() {
if *from == raw {
return Some((*to).to_string());
}
}
Some(raw.to_string())
}
fn map_date_time(&self, strat: DateStrategy) -> MappedType {
match strat {
DateStrategy::String => MappedType::plain("String"),
DateStrategy::Chrono => {
self.record(TypeFeature::Chrono);
MappedType::with_feature("chrono::DateTime<chrono::Utc>", TypeFeature::Chrono)
}
DateStrategy::Time => {
self.record(TypeFeature::Time);
MappedType::with_codec(
"time::OffsetDateTime",
"time::serde::rfc3339",
TypeFeature::Time,
)
}
}
}
fn map_date(&self, strat: DateStrategy) -> MappedType {
match strat {
DateStrategy::String => MappedType::plain("String"),
DateStrategy::Chrono => {
self.record(TypeFeature::Chrono);
MappedType::with_feature("chrono::NaiveDate", TypeFeature::Chrono)
}
DateStrategy::Time => {
self.record(TypeFeature::TimeDate);
MappedType::with_codec("time::Date", "time_date_format", TypeFeature::TimeDate)
}
}
}
fn map_time(&self, strat: DateStrategy) -> MappedType {
match strat {
DateStrategy::String => MappedType::plain("String"),
DateStrategy::Chrono => {
self.record(TypeFeature::Chrono);
MappedType::with_feature("chrono::NaiveTime", TypeFeature::Chrono)
}
DateStrategy::Time => {
self.record(TypeFeature::TimeTime);
MappedType::with_codec("time::Time", "time_time_format", TypeFeature::TimeTime)
}
}
}
fn map_duration(&self, strat: DurationStrategy) -> MappedType {
match strat {
DurationStrategy::String => MappedType::plain("String"),
DurationStrategy::Chrono => {
MappedType::plain("String")
}
DurationStrategy::Iso8601 => {
self.record(TypeFeature::Iso8601);
MappedType::with_feature("iso8601::Duration", TypeFeature::Iso8601)
}
}
}
fn map_uuid(&self, strat: UuidStrategy) -> MappedType {
match strat {
UuidStrategy::String => MappedType::plain("String"),
UuidStrategy::Uuid => {
self.record(TypeFeature::Uuid);
MappedType::with_feature("uuid::Uuid", TypeFeature::Uuid)
}
}
}
fn map_byte(&self, strat: ByteStrategy) -> MappedType {
match strat {
ByteStrategy::String => MappedType::plain("String"),
ByteStrategy::VecU8 => MappedType::plain("Vec<u8>"),
ByteStrategy::Base64 => {
self.record(TypeFeature::Base64);
MappedType::with_codec("Vec<u8>", "base64_serde", TypeFeature::Base64)
}
}
}
fn map_binary(&self, strat: BinaryStrategy) -> MappedType {
match strat {
BinaryStrategy::String => MappedType::plain("String"),
BinaryStrategy::VecU8 => MappedType::plain("Vec<u8>"),
BinaryStrategy::Bytes => {
self.record(TypeFeature::Bytes);
MappedType::with_feature("bytes::Bytes", TypeFeature::Bytes)
}
}
}
fn map_ipv4(&self, strat: IpStrategy) -> MappedType {
match strat {
IpStrategy::String => MappedType::plain("String"),
IpStrategy::Std => MappedType::plain("std::net::Ipv4Addr"),
}
}
fn map_ipv6(&self, strat: IpStrategy) -> MappedType {
match strat {
IpStrategy::String => MappedType::plain("String"),
IpStrategy::Std => MappedType::plain("std::net::Ipv6Addr"),
}
}
fn map_uri(&self, strat: UriStrategy) -> MappedType {
match strat {
UriStrategy::String => MappedType::plain("String"),
UriStrategy::Url => {
self.record(TypeFeature::Url);
MappedType::with_feature("url::Url", TypeFeature::Url)
}
}
}
fn map_email(&self, strat: EmailStrategy) -> MappedType {
match strat {
EmailStrategy::String => MappedType::plain("String"),
EmailStrategy::EmailAddress => {
self.record(TypeFeature::EmailAddress);
MappedType::with_feature("email_address::EmailAddress", TypeFeature::EmailAddress)
}
}
}
pub fn integer_format(&self, format: Option<&str>) -> MappedType {
let normalized = self.normalize_format(format);
match normalized.as_deref() {
Some("int32") => MappedType::plain("i32"),
Some("int64") => MappedType::plain("i64"),
Some("uint32") if self.config.unsigned => MappedType::plain("u32"),
Some("uint64") if self.config.unsigned => MappedType::plain("u64"),
Some("uint") if self.config.unsigned => MappedType::plain("u64"),
_ => MappedType::plain("i64"),
}
}
pub fn number_format(&self, format: Option<&str>) -> MappedType {
let normalized = self.normalize_format(format);
match normalized.as_deref() {
Some("float") => MappedType::plain("f32"),
Some("double") => MappedType::plain("f64"),
_ => MappedType::plain("f64"),
}
}
pub fn boolean(&self) -> MappedType {
MappedType::plain("bool")
}
pub fn untyped_array(&self) -> MappedType {
MappedType::plain("Vec<serde_json::Value>")
}
pub fn dynamic_json(&self) -> MappedType {
MappedType::plain("serde_json::Value")
}
pub fn null_unit(&self) -> MappedType {
MappedType::plain("()")
}
pub fn map(&self, ty: OpenApiSchemaType, details: &SchemaDetails) -> MappedType {
let format = details.format.as_deref();
match ty {
OpenApiSchemaType::String => self.string_format(format),
OpenApiSchemaType::Integer => self.integer_format(format),
OpenApiSchemaType::Number => self.number_format(format),
OpenApiSchemaType::Boolean => self.boolean(),
OpenApiSchemaType::Array => self.untyped_array(),
OpenApiSchemaType::Object => self.dynamic_json(),
OpenApiSchemaType::Null => self.null_unit(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn details_with_format(format: Option<&str>) -> SchemaDetails {
SchemaDetails {
format: format.map(str::to_string),
..Default::default()
}
}
#[test]
fn default_mapper_emits_typed_scalars_for_common_formats() {
let m = TypeMapper::default();
assert_eq!(
m.string_format(Some("date-time")).rust_type,
"chrono::DateTime<chrono::Utc>"
);
assert_eq!(m.string_format(Some("date")).rust_type, "chrono::NaiveDate");
assert_eq!(m.string_format(Some("uuid")).rust_type, "uuid::Uuid");
assert_eq!(m.string_format(Some("uri")).rust_type, "url::Url");
assert_eq!(
m.string_format(Some("ipv4")).rust_type,
"std::net::Ipv4Addr"
);
assert_eq!(m.string_format(Some("byte")).rust_type, "Vec<u8>");
assert_eq!(m.string_format(Some("binary")).rust_type, "bytes::Bytes");
}
#[test]
fn date_time_uses_default_chrono_serde() {
let m = TypeMapper::default();
let mt = m.string_format(Some("date-time"));
assert_eq!(mt.rust_type, "chrono::DateTime<chrono::Utc>");
assert!(mt.serde_with.is_none());
assert_eq!(mt.feature, Some(TypeFeature::Chrono));
}
#[test]
fn byte_emits_base64_codec() {
let m = TypeMapper::default();
let mt = m.string_format(Some("byte"));
assert_eq!(mt.rust_type, "Vec<u8>");
assert_eq!(mt.serde_with.as_deref(), Some("base64_serde"));
assert_eq!(mt.feature, Some(TypeFeature::Base64));
}
#[test]
fn conservative_config_collapses_everything_to_string() {
let m = TypeMapper::new(TypeMappingConfig::conservative());
for fmt in [
Some("date-time"),
Some("uuid"),
Some("uri"),
Some("byte"),
Some("binary"),
Some("ipv4"),
Some("ipv6"),
Some("date"),
None,
] {
let mt = m.string_format(fmt);
assert_eq!(mt.rust_type, "String", "format = {fmt:?}");
assert!(mt.serde_with.is_none(), "format = {fmt:?}");
}
}
#[test]
fn unknown_formats_fall_through_to_string() {
let m = TypeMapper::default();
for fmt in [Some("hostname"), Some("password"), Some("idn-email")] {
assert_eq!(m.string_format(fmt).rust_type, "String");
}
}
#[test]
fn integer_formats_match_pre_refactor_behavior() {
let m = TypeMapper::default();
assert_eq!(m.integer_format(Some("int32")).rust_type, "i32");
assert_eq!(m.integer_format(Some("int64")).rust_type, "i64");
assert_eq!(m.integer_format(None).rust_type, "i64");
}
#[test]
fn integer_formats_default_handles_unsigned_q21() {
let m = TypeMapper::default();
assert_eq!(m.integer_format(Some("uint32")).rust_type, "u32");
assert_eq!(m.integer_format(Some("uint64")).rust_type, "u64");
assert_eq!(m.integer_format(Some("uint")).rust_type, "u64");
}
#[test]
fn unsigned_off_degrades_uint_to_i64() {
let mut cfg = TypeMappingConfig::default();
cfg.unsigned = false;
let m = TypeMapper::new(cfg);
assert_eq!(m.integer_format(Some("uint32")).rust_type, "i64");
assert_eq!(m.integer_format(Some("uint64")).rust_type, "i64");
}
#[test]
fn conservative_disables_unsigned() {
let m = TypeMapper::new(TypeMappingConfig::conservative());
assert_eq!(m.integer_format(Some("uint64")).rust_type, "i64");
}
#[test]
fn builtin_aliases_normalize_uuid_variants_to_uuid() {
let m = TypeMapper::default();
for fmt in ["uuid4", "uuid_v4", "UUID"] {
let mt = m.string_format(Some(fmt));
assert_eq!(mt.rust_type, "uuid::Uuid", "format = {fmt}");
}
}
#[test]
fn builtin_aliases_normalize_unix_time_to_int64() {
let m = TypeMapper::default();
for fmt in ["unix-time", "unix_time", "unixtime", "timestamp"] {
let mt = m.integer_format(Some(fmt));
assert_eq!(mt.rust_type, "i64", "format = {fmt}");
}
}
#[test]
fn user_alias_overrides_builtin() {
let mut cfg = TypeMappingConfig::default();
cfg.format_aliases
.insert("uuid4".to_string(), "hostname".to_string());
let m = TypeMapper::new(cfg);
assert_eq!(m.string_format(Some("uuid4")).rust_type, "String");
}
#[test]
fn used_features_records_referenced_crates() {
let m = TypeMapper::default();
let _ = m.string_format(Some("date-time"));
let _ = m.string_format(Some("uuid"));
let used = m.used_features();
assert!(used.contains(TypeFeature::Chrono));
assert!(used.contains(TypeFeature::Uuid));
assert!(!used.contains(TypeFeature::Bytes));
}
#[test]
fn format_alias_normalizes_before_dispatch() {
let mut cfg = TypeMappingConfig::default();
cfg.format_aliases
.insert("uuid4".to_string(), "uuid".to_string());
let m = TypeMapper::new(cfg);
assert_eq!(m.string_format(Some("uuid4")).rust_type, "uuid::Uuid");
}
#[test]
fn conservative_helper_round_trips() {
let cfg = TypeMappingConfig::conservative();
assert!(matches!(cfg.date_time, DateStrategy::String));
assert!(matches!(cfg.uuid, UuidStrategy::String));
}
#[test]
fn dep_requirement_renders_features_list() {
let dep = TypeFeature::Chrono.dep_requirement();
assert_eq!(dep.crate_name, "chrono");
assert_eq!(dep.features, vec!["serde"]);
assert_eq!(
dep.to_toml_line(),
r#"chrono = { version = "0.4", features = ["serde"] }"#
);
}
#[test]
fn dep_requirement_omits_features_when_none() {
let dep = TypeFeature::Base64.dep_requirement();
assert_eq!(dep.to_toml_line(), r#"base64 = "0.22""#);
}
#[test]
fn collect_dep_requirements_is_sorted_and_unique() {
let mut used = UsedFeatures::default();
used.insert(TypeFeature::Url);
used.insert(TypeFeature::Chrono);
used.insert(TypeFeature::Chrono); used.insert(TypeFeature::Uuid);
let deps = collect_dep_requirements(&used);
assert_eq!(
deps.iter().map(|d| d.crate_name).collect::<Vec<_>>(),
vec!["chrono", "url", "uuid"]
);
}
#[test]
fn render_required_deps_toml_is_none_when_empty() {
let deps: Vec<DepRequirement> = Vec::new();
assert!(render_required_deps_toml(&deps).is_none());
}
#[test]
fn render_required_deps_toml_includes_dependencies_block() {
let deps = vec![
TypeFeature::Chrono.dep_requirement(),
TypeFeature::Uuid.dep_requirement(),
];
let toml = render_required_deps_toml(&deps).expect("non-empty");
assert!(toml.contains("[dependencies]"));
assert!(toml.contains("chrono = "));
assert!(toml.contains("uuid = "));
assert!(toml.contains("# Generated by openapi-to-rust"));
}
#[test]
fn map_dispatches_through_helpers() {
let m = TypeMapper::default();
assert_eq!(
m.map(
OpenApiSchemaType::String,
&details_with_format(Some("uuid"))
)
.rust_type,
"uuid::Uuid"
);
assert_eq!(
m.map(
OpenApiSchemaType::Integer,
&details_with_format(Some("int32"))
)
.rust_type,
"i32"
);
}
}