1use openapiv3::AdditionalProperties;
4use openapiv3::IntegerFormat;
5use openapiv3::IntegerType;
6use openapiv3::ObjectType;
7use openapiv3::ReferenceOr;
8use openapiv3::Schema;
9use openapiv3::SchemaData;
10use openapiv3::SchemaKind;
11use openapiv3::StringFormat;
12use openapiv3::Type;
13use openapiv3::VariantOrUnknownOrEmpty;
14
15use crate::error::Error;
16use crate::error::Result;
17use crate::ir::Access;
18use crate::ir::Alias;
19use crate::ir::Constraints;
20use crate::ir::Deprecation;
21use crate::ir::Enum;
22use crate::ir::EnumKind;
23use crate::ir::Field;
24use crate::ir::ForeignDerives;
25use crate::ir::IntegerVariant;
26use crate::ir::Item;
27use crate::ir::Module;
28use crate::ir::RustType;
29use crate::ir::StringVariant;
30use crate::ir::Struct;
31use crate::ir::UnionVariant;
32use crate::loader::Spec;
33use crate::loader::ref_target_name;
34use crate::loader::schema_ref_reason;
35use crate::lower::default::lower_default;
36use crate::naming::Case;
37use crate::naming::X_RUST_NAME;
38use crate::naming::to_ident;
39
40pub(crate) const X_RUST_TYPE: &str = "x-rust-type";
42const X_RUST_DERIVE: &str = "x-rust-derive";
45const X_RUST_SERDE_SKIP: &str = "x-rust-serde-skip";
47const X_OMITEMPTY: &str = "x-omitempty";
49const X_ORDER: &str = "x-order";
51const X_DEPRECATED_REASON: &str = "x-deprecated-reason";
53const X_ENUM_VARNAMES: &str = "x-enum-varnames";
55const X_ENUM_NAMES: &str = "x-enumNames";
57
58pub(super) const MAX_SCHEMA_DEPTH: usize = 100;
63
64pub fn generate_models(spec: &Spec, names: &crate::lower::rename::TypeNames) -> Result<Module> {
78 let renames = names.renames();
79 let mut mapper = Mapper {
80 spec,
81 renames,
82 extra: Vec::new(),
83 depth: 0,
84 };
85 let mut items = Vec::new();
86 let mut diagnostics = crate::lower::validate::Diagnostics::new();
87 for (name, entry) in spec.schemas() {
88 match entry {
89 ReferenceOr::Item(schema) => match mapper.named_to_item(name, schema) {
90 Ok(item) => items.push(item),
91 Err(problem) => diagnostics.push(problem),
92 },
93 ReferenceOr::Reference { reference } => {
94 match mapper.schema_ref_target(reference, "a top-level schema alias") {
95 Ok(target) => items.push(Item::Alias(Alias {
96 name: mapper.type_name_ident(name),
97 doc: None,
98 deprecated: None,
99 ty: RustType::Named(target),
100 })),
101 Err(problem) => diagnostics.push(problem),
102 }
103 }
104 }
105 }
106 diagnostics.into_result()?;
107 items.append(&mut mapper.extra);
108 let mut module = Module { items };
109 crate::lower::rename::rewrite_module(&mut module, renames);
110 return Ok(module);
111}
112
113struct Mapper<'a> {
115 spec: &'a Spec,
116 renames: &'a std::collections::HashMap<String, String>,
118 extra: Vec<Item>,
119 depth: usize,
121}
122
123impl Mapper<'_> {
124 fn schema_ref_target(&self, reference: &str, site: &str) -> Result<String> {
131 let target = ref_target_name(reference).ok_or_else(|| {
132 return Error::UnsupportedRef {
133 reference: reference.to_owned(),
134 reason: schema_ref_reason(reference, site),
135 };
136 })?;
137 if !self.spec.schemas().contains_key(target) {
138 return Err(Error::UnresolvedRef(reference.to_owned()));
139 }
140 return Ok(target.to_owned());
141 }
142
143 fn type_name_ident(&self, name: &str) -> crate::naming::RustIdent {
145 let effective = self.renames.get(name).map(String::as_str).unwrap_or(name);
146 return to_ident(effective, Case::Pascal);
147 }
148
149 fn named_to_item(&mut self, name: &str, schema: &Schema) -> Result<Item> {
151 check_all_of_nullable(name, schema)?;
152 if schema.schema_data.nullable {
153 let mut non_null = schema.clone();
154 non_null.schema_data.nullable = false;
155 let value_name = format!("{name}Value");
156 let item = self.named_to_item(&value_name, &non_null)?;
157 let ty = match item {
158 Item::Alias(alias) => alias.ty,
159 item => {
160 self.extra.push(item);
161 RustType::Named(value_name)
162 }
163 };
164 return Ok(Item::Alias(Alias {
165 name: self.type_name_ident(name),
166 doc: doc_of(&schema.schema_data),
167 deprecated: deprecation_of(&schema.schema_data, name)?,
168 ty: nullable_type(self.spec, ty),
169 }));
170 }
171 let data = &schema.schema_data;
172
173 if let Some(verbatim) = extension_str(data, X_RUST_TYPE, name)? {
174 return Ok(Item::Alias(Alias {
175 name: self.type_name_ident(name),
176 doc: doc_of(data),
177 deprecated: deprecation_of(data, name)?,
178 ty: verbatim_type(data, verbatim, name)?,
179 }));
180 }
181
182 let item = match &schema.schema_kind {
183 SchemaKind::Type(Type::String(st)) if !st.enumeration.is_empty() => {
184 Item::Enum(self.string_enum(name, &st.enumeration, data)?)
185 }
186 SchemaKind::Type(Type::Integer(it)) if !it.enumeration.is_empty() => {
187 let repr = integer_type(it);
188 Item::Enum(self.integer_enum(name, &it.enumeration, &repr, data)?)
189 }
190 SchemaKind::Type(Type::Object(obj)) => self.object_to_item(name, obj, data)?,
191 SchemaKind::OneOf { one_of } | SchemaKind::AnyOf { any_of: one_of } => Item::Enum(self.make_union(
192 name,
193 one_of,
194 data,
195 matches!(schema.schema_kind, SchemaKind::AnyOf { .. }),
196 )?),
197 SchemaKind::AllOf { all_of } => match self.single_ref_all_of(all_of)? {
198 Some(target) => Item::Alias(Alias {
199 name: self.type_name_ident(name),
200 doc: doc_of(data),
201 deprecated: deprecation_of(data, name)?,
202 ty: RustType::Named(target),
203 }),
204 None => {
205 if let Some(ty) = self.single_all_of_map(name, all_of)? {
206 Item::Alias(Alias {
207 name: self.type_name_ident(name),
208 doc: doc_of(data),
209 deprecated: deprecation_of(data, name)?,
210 ty,
211 })
212 } else {
213 Item::Struct(self.merge_all_of(name, all_of, data)?)
214 }
215 }
216 },
217 SchemaKind::Type(_) => {
218 let ty = self.type_from_schema(name, schema)?;
219 Item::Alias(Alias {
220 name: self.type_name_ident(name),
221 doc: doc_of(data),
222 deprecated: deprecation_of(data, name)?,
223 ty,
224 })
225 }
226 SchemaKind::Any(schema) => Item::Alias(Alias {
227 name: self.type_name_ident(name),
228 doc: doc_of(data),
229 deprecated: deprecation_of(data, name)?,
230 ty: self.unconstrained_type(name, schema),
231 }),
232 SchemaKind::Not { .. } => {
233 return Err(Error::UnsupportedSchema {
234 path: name.to_owned(),
235 reason: "`not` schemas are not supported".to_owned(),
236 });
237 }
238 };
239 return Ok(item);
240 }
241
242 fn object_to_item(&mut self, name: &str, obj: &ObjectType, data: &SchemaData) -> Result<Item> {
244 if obj.properties.is_empty() && !matches!(obj.additional_properties, Some(AdditionalProperties::Any(false))) {
245 let element = self.additional_properties_type(name, obj)?;
246 return Ok(Item::Alias(Alias {
247 name: self.type_name_ident(name),
248 doc: doc_of(data),
249 deprecated: deprecation_of(data, name)?,
250 ty: RustType::Map(Box::new(element)),
251 }));
252 }
253 let strukt = self.object_to_struct(name, obj, data)?;
254 return Ok(Item::Struct(strukt));
255 }
256
257 fn object_to_struct(&mut self, name: &str, obj: &ObjectType, data: &SchemaData) -> Result<Struct> {
259 let mut ordered = Vec::with_capacity(obj.properties.len());
260 for (prop_name, prop) in &obj.properties {
261 let required = obj.required.iter().any(|r| {
262 return r == prop_name;
263 });
264 let order = prop_order(prop, &format!("{name}.{prop_name}"))?;
265 let field = self.field_from_prop(name, prop_name, prop, required)?;
266 ordered.push((order, field));
267 }
268 let fields = sort_by_order(ordered);
269
270 let additional_properties = match &obj.additional_properties {
271 Some(AdditionalProperties::Schema(_)) => Some(self.additional_properties_type(name, obj)?),
272 Some(AdditionalProperties::Any(true)) => Some(RustType::Value),
273 Some(AdditionalProperties::Any(false)) | None => None,
274 };
275 let deny_unknown_fields = matches!(&obj.additional_properties, Some(AdditionalProperties::Any(false)));
278
279 return Ok(Struct {
280 name: self.type_name_ident(name),
281 doc: doc_of(data),
282 deprecated: deprecation_of(data, name)?,
283 fields,
284 additional_properties,
285 deny_unknown_fields,
286 });
287 }
288
289 fn field_from_prop(
291 &mut self,
292 parent: &str,
293 wire: &str,
294 prop: &ReferenceOr<Box<Schema>>,
295 required: bool,
296 ) -> Result<Field> {
297 let hint = format!("{parent}_{wire}");
298 let at = format!("{parent}.{wire}");
299 let mut ty = self.type_from_schema_ref(&hint, prop)?;
300
301 let data = match prop {
302 ReferenceOr::Item(schema) => Some(&schema.schema_data),
303 ReferenceOr::Reference { .. } => None,
304 };
305
306 let declared = data
310 .filter(|_| return !required)
311 .filter(|_| return !matches!(prop, ReferenceOr::Item(schema) if crate::lower::default::unsupported_nullable_default(schema)))
312 .and_then(|data| return data.default.as_ref());
313
314 if !required && declared.is_none() {
315 ty = ty.optional();
316 }
317
318 let default = match declared {
319 Some(json) => {
320 let variants_of = |name: &str| {
321 let ident = self.type_name_ident(name);
322 return self.extra.iter().find_map(|item| {
323 return match item {
324 Item::Enum(enom) if enom.name == ident => match &enom.kind {
325 EnumKind::Strings(variants) => Some(variants.clone()),
326 EnumKind::Union(_) | EnumKind::AnyOf(_) | EnumKind::Integers { .. } => None,
327 },
328 _ => None,
329 };
330 });
331 };
332 Some(lower_default(json, &ty, &variants_of, parent, wire)?)
333 }
334 None => None,
335 };
336
337 let doc = data.and_then(doc_of);
338 let deprecated = match data {
339 Some(data) => deprecation_of(data, &at)?,
340 None => None,
341 };
342 let serde_skip = match data {
343 Some(data) => extension_bool(data, X_RUST_SERDE_SKIP, &at)?.unwrap_or(false),
344 None => false,
345 };
346 let omit_empty = match data {
347 Some(data) => extension_bool(data, X_OMITEMPTY, &at)?,
348 None => None,
349 };
350
351 let rust_name = match data {
352 Some(data) => extension_str(data, X_RUST_NAME, &at)?,
353 None => None,
354 };
355 let ident = match rust_name {
356 Some(custom) => to_ident(custom, Case::Snake),
357 None => to_ident(wire, Case::Snake),
358 };
359 let rename = crate::naming::rename_for(wire, &ident);
360 let access = match prop {
361 ReferenceOr::Item(schema) => access_of(&schema.schema_data, &at)?,
362 ReferenceOr::Reference { reference } => match self.spec.resolve(reference) {
365 Ok(target) => access_of(&target.schema_data, &at)?,
366 Err(_) => Access::ReadWrite,
367 },
368 };
369 let mut constraints = match prop {
370 ReferenceOr::Item(schema) => crate::lower::constraints::constraints_of(schema)
371 .or_else(|| return self.single_all_of_constraints(schema, 0)),
372 ReferenceOr::Reference { reference } => self
375 .spec
376 .resolve(reference)
377 .ok()
378 .and_then(|schema| return self.referenced_constraints(schema, 0)),
379 };
380 let value_ty = match &ty {
381 RustType::Option(inner) => inner.as_ref(),
382 _ => &ty,
383 };
384 if matches!(value_ty, RustType::Nullable(_))
385 && let Some(found) = &mut constraints
386 && let Some(RustType::Nullable(inner)) = &found.checked_as
387 {
388 found.checked_as = Some(*inner.clone());
390 }
391 let field = Field {
392 name: ident,
393 rename,
394 doc,
395 deprecated,
396 ty,
397 required,
398 omit_empty,
399 serde_skip,
400 default,
401 constraints,
402 access,
403 };
404 crate::lower::constraints::check_constraints(&field)?;
405 return Ok(field);
406 }
407
408 fn referenced_constraints(&self, schema: &Schema, depth: usize) -> Option<Constraints> {
409 if let Some(found) = crate::lower::constraints::constraints_through_ref(schema) {
410 return Some(found);
411 }
412 let mut found = self.single_all_of_constraints(schema, depth)?;
413 if schema.schema_data.nullable
414 && let Some(checked) = found.checked_as.take()
415 {
416 found.checked_as = Some(nullable_type(self.spec, checked));
417 }
418 return Some(found);
419 }
420
421 fn single_all_of_constraints(&self, schema: &Schema, depth: usize) -> Option<Constraints> {
422 if depth >= MAX_SCHEMA_DEPTH || schema.schema_data.extensions.contains_key(X_RUST_TYPE) {
423 return None;
424 }
425 let SchemaKind::AllOf { all_of } = &schema.schema_kind else {
426 return None;
427 };
428 let [member] = all_of.as_slice() else {
429 return None;
430 };
431 let target = match member {
432 ReferenceOr::Reference { reference } => self.spec.resolve(reference).ok()?,
433 ReferenceOr::Item(schema) => schema,
434 };
435 return self.referenced_constraints(target, depth + 1);
436 }
437
438 fn single_ref_all_of(&self, members: &[ReferenceOr<Schema>]) -> Result<Option<String>> {
443 let [ReferenceOr::Reference { reference }] = members else {
444 return Ok(None);
445 };
446 let target = self.schema_ref_target(reference, "an allOf member")?;
447 return Ok(Some(target));
448 }
449
450 fn collapse_single_all_of(&mut self, hint: &str, members: &[ReferenceOr<Schema>]) -> Result<Option<RustType>> {
460 if let Some(ty) = self.single_all_of_map(hint, members)? {
461 return Ok(Some(ty));
462 }
463 let [only] = members else {
464 return Ok(None);
465 };
466 let ty = match only {
467 ReferenceOr::Reference { reference } => {
468 let target = self.schema_ref_target(reference, "an allOf member")?;
469 RustType::Named(target)
470 }
471 ReferenceOr::Item(schema) if matches!(schema.schema_kind, SchemaKind::Type(Type::Object(_))) => {
472 return Ok(None);
473 }
474 ReferenceOr::Item(schema) => self.type_from_schema(hint, schema)?,
475 };
476 return Ok(Some(ty));
477 }
478
479 fn single_all_of_map(&mut self, hint: &str, members: &[ReferenceOr<Schema>]) -> Result<Option<RustType>> {
480 let [ReferenceOr::Item(schema)] = members else {
481 return Ok(None);
482 };
483 let SchemaKind::Type(Type::Object(object)) = &schema.schema_kind else {
484 return Ok(None);
485 };
486 if !object.properties.is_empty()
487 || matches!(object.additional_properties, Some(AdditionalProperties::Any(false)))
488 || object.min_properties.is_some()
489 || object.max_properties.is_some()
490 || !object.required.is_empty()
491 {
492 return Ok(None);
493 }
494 super::all_of::check_member_metadata(hint, schema)?;
495 return self.type_from_schema(hint, schema).map(Some);
496 }
497
498 fn merge_all_of(&mut self, name: &str, members: &[ReferenceOr<Schema>], data: &SchemaData) -> Result<Struct> {
501 let merged = super::all_of::merge(self.spec, name, members, self.depth)?;
502 return self.object_to_struct(name, &merged, data);
503 }
504
505 fn make_union(
506 &mut self,
507 name: &str,
508 members: &[ReferenceOr<Schema>],
509 data: &SchemaData,
510 any_of: bool,
511 ) -> Result<Enum> {
512 if members.is_empty() {
513 return Err(Error::UnsupportedSchema {
514 path: name.to_owned(),
515 reason: "a union must contain at least one alternative".to_owned(),
516 });
517 }
518 let variants = self.union_variants_from_members(name, members, data)?;
519 if any_of {
520 let mut methods = std::collections::HashSet::from(["as_value".to_owned(), "into_value".to_owned()]);
521 for variant in &variants {
522 let method = format!("as_{}", to_ident(variant.name.logical(), Case::Snake).logical());
523 if !methods.insert(method.clone()) {
524 return Err(Error::UnsupportedSchema {
525 path: name.to_owned(),
526 reason: format!(
527 "anyOf accessor `{method}` collides with another method. Rename the alternative with x-rust-name"
528 ),
529 });
530 }
531 }
532 }
533 return Ok(Enum {
534 name: self.type_name_ident(name),
535 doc: doc_of(data),
536 deprecated: deprecation_of(data, name)?,
537 kind: if any_of {
538 EnumKind::AnyOf(variants)
539 } else {
540 EnumKind::Union(variants)
541 },
542 });
543 }
544
545 fn union_variants_from_members(
547 &mut self,
548 name: &str,
549 members: &[ReferenceOr<Schema>],
550 data: &SchemaData,
551 ) -> Result<Vec<UnionVariant>> {
552 let mut variants = Vec::with_capacity(members.len());
553 let mut seen = std::collections::HashSet::new();
554 let mut diagnostics = crate::lower::validate::Diagnostics::new();
555 for (index, member) in members.iter().enumerate() {
556 let variant = match member {
557 ReferenceOr::Reference { reference } => {
558 let target = self.schema_ref_target(reference, "a union member")?;
559 let variant_name = data
560 .discriminator
561 .as_ref()
562 .and_then(|disc| return disc.mapping.iter().find(|(_, mapped)| return *mapped == reference))
563 .map_or_else(
564 || return self.type_name_ident(&target),
565 |(value, _)| return to_ident(value, Case::Pascal),
566 );
567 UnionVariant {
568 name: crate::naming::deconflict_ident(variant_name, &mut seen),
569 ty: RustType::Named(target),
570 }
571 }
572 ReferenceOr::Item(schema) => {
573 let Some(seed) = inline_variant_seed(schema, &format!("{name}, member {index}"))? else {
574 diagnostics.push(Error::UnsupportedSchema {
575 path: name.to_owned(),
576 reason: format!("member {index} of the union gives the variant no name"),
577 });
578 continue;
579 };
580 let ty = self.type_from_schema(&format!("{name}_{seed}"), schema)?;
587 UnionVariant {
588 name: crate::naming::deconflict_ident(to_ident(&seed, Case::Pascal), &mut seen),
589 ty,
590 }
591 }
592 };
593 variants.push(variant);
594 }
595 diagnostics.into_result()?;
596 return Ok(variants);
597 }
598
599 fn string_enum(&self, name: &str, values: &[Option<String>], data: &SchemaData) -> Result<Enum> {
607 let varnames = match extension_str_array(data, X_ENUM_VARNAMES, name)? {
608 Some(names) => Some(names),
609 None => extension_str_array(data, X_ENUM_NAMES, name)?,
610 };
611 let mut diagnostics = crate::lower::validate::Diagnostics::new();
612 let mut variants = Vec::new();
613 let mut seen = std::collections::HashSet::new();
614 let mut values_seen = std::collections::HashSet::new();
615 for (index, value) in values.iter().enumerate() {
616 let Some(value) = value else {
617 continue;
618 };
619 if !values_seen.insert(value.as_str()) {
620 diagnostics.push(Error::UnsupportedSchema {
621 path: name.to_owned(),
622 reason: format!("the `enum` gives `{value}` more than once"),
623 });
624 continue;
625 }
626 let base = match varnames.as_ref().and_then(|names| return names.get(index)) {
627 Some(custom) => to_ident(custom, Case::Pascal),
628 None => to_ident(value, Case::Pascal),
629 };
630 let ident = crate::naming::deconflict_ident(base, &mut seen);
631 let rename = crate::naming::rename_for(value, &ident);
632 variants.push(StringVariant {
633 name: ident,
634 rename,
635 doc: None,
636 });
637 }
638 diagnostics.into_result()?;
639 return Ok(Enum {
640 name: self.type_name_ident(name),
641 doc: doc_of(data),
642 deprecated: deprecation_of(data, name)?,
643 kind: EnumKind::Strings(variants),
644 });
645 }
646
647 fn integer_enum(&self, name: &str, values: &[Option<i64>], repr: &RustType, data: &SchemaData) -> Result<Enum> {
657 let varnames = match extension_str_array(data, X_ENUM_VARNAMES, name)? {
658 Some(names) => Some(names),
659 None => extension_str_array(data, X_ENUM_NAMES, name)?,
660 };
661 let mut diagnostics = crate::lower::validate::Diagnostics::new();
662 let mut variants = Vec::new();
663 let mut seen = std::collections::HashSet::new();
664 let mut values_seen = std::collections::HashSet::new();
665 for (index, value) in values.iter().enumerate() {
666 let Some(value) = value else {
667 continue;
668 };
669 if !values_seen.insert(*value) {
670 diagnostics.push(Error::UnsupportedSchema {
671 path: name.to_owned(),
672 reason: format!("the `enum` gives `{value}` more than once"),
673 });
674 continue;
675 }
676 if !fits_repr(*value, repr) {
677 diagnostics.push(Error::UnsupportedSchema {
678 path: name.to_owned(),
679 reason: format!("the `enum` gives `{value}`, which `{}` cannot hold", repr_name(repr)),
680 });
681 continue;
682 }
683 let base = match varnames.as_ref().and_then(|names| return names.get(index)) {
684 Some(custom) => to_ident(custom, Case::Pascal),
685 None => to_ident(&integer_variant_name(*value), Case::Pascal),
686 };
687 variants.push(IntegerVariant {
688 name: crate::naming::deconflict_ident(base, &mut seen),
689 value: *value,
690 doc: None,
691 });
692 }
693 diagnostics.into_result()?;
694 return Ok(Enum {
695 name: self.type_name_ident(name),
696 doc: doc_of(data),
697 deprecated: deprecation_of(data, name)?,
698 kind: EnumKind::Integers {
699 repr: repr.clone(),
700 variants,
701 },
702 });
703 }
704
705 fn type_from_schema_ref(&mut self, hint: &str, schema: &ReferenceOr<Box<Schema>>) -> Result<RustType> {
708 match schema {
709 ReferenceOr::Reference { reference } => {
710 let target = self.schema_ref_target(reference, "a property")?;
711 return Ok(RustType::Named(target));
712 }
713 ReferenceOr::Item(schema) => {
714 let ty = self.type_from_schema(hint, schema)?;
715 return Ok(ty);
716 }
717 }
718 }
719
720 fn type_from_ref_schema(&mut self, hint: &str, schema: &ReferenceOr<Schema>) -> Result<RustType> {
723 match schema {
724 ReferenceOr::Reference { reference } => {
725 let target = self.schema_ref_target(reference, "additionalProperties")?;
726 return Ok(RustType::Named(target));
727 }
728 ReferenceOr::Item(schema) => {
729 let ty = self.type_from_schema(hint, schema)?;
730 return Ok(ty);
731 }
732 }
733 }
734
735 fn type_from_schema(&mut self, hint: &str, schema: &Schema) -> Result<RustType> {
739 check_all_of_nullable(hint, schema)?;
740 if self.depth >= MAX_SCHEMA_DEPTH {
741 return Err(Error::SchemaDepthExceeded {
742 path: hint.to_owned(),
743 limit: MAX_SCHEMA_DEPTH,
744 });
745 }
746 self.depth += 1;
747 let result = self.type_from_schema_inner(hint, schema).map(|ty| {
748 if schema.schema_data.nullable {
749 return nullable_type(self.spec, ty);
750 }
751 return ty;
752 });
753 self.depth -= 1;
754 return result;
755 }
756
757 fn type_from_schema_inner(&mut self, hint: &str, schema: &Schema) -> Result<RustType> {
758 let data = &schema.schema_data;
759 if let Some(verbatim) = extension_str(data, X_RUST_TYPE, hint)? {
760 return verbatim_type(data, verbatim, hint);
761 }
762
763 let ty = match &schema.schema_kind {
764 SchemaKind::Type(Type::String(st)) if !st.enumeration.is_empty() => {
765 let enom = self.string_enum(hint, &st.enumeration, data)?;
766 self.extra.push(Item::Enum(enom));
767 RustType::Named(hint.to_owned())
768 }
769 SchemaKind::Type(Type::String(st)) => string_format_type(&st.format),
770 SchemaKind::Type(Type::Integer(it)) if !it.enumeration.is_empty() => {
771 let repr = integer_type(it);
772 let enom = self.integer_enum(hint, &it.enumeration, &repr, data)?;
773 self.extra.push(Item::Enum(enom));
774 RustType::Named(hint.to_owned())
775 }
776 SchemaKind::Type(Type::Integer(it)) => integer_type(it),
777 SchemaKind::Type(Type::Number(_)) => RustType::F64,
778 SchemaKind::Type(Type::Boolean(_)) => RustType::Bool,
779 SchemaKind::Type(Type::Array(at)) => {
780 let element = match &at.items {
781 Some(items) => {
782 let item_hint = format!("{hint}_item");
783 self.type_from_schema_ref(&item_hint, items)?
784 }
785 None => RustType::Value,
786 };
787 RustType::Vec(Box::new(element))
788 }
789 SchemaKind::Type(Type::Object(obj)) => self.inline_object_type(hint, obj, data)?,
790 SchemaKind::OneOf { one_of } | SchemaKind::AnyOf { any_of: one_of } => {
791 let enom = self.make_union(
792 hint,
793 one_of,
794 data,
795 matches!(schema.schema_kind, SchemaKind::AnyOf { .. }),
796 )?;
797 self.extra.push(Item::Enum(enom));
798 RustType::Named(hint.to_owned())
799 }
800 SchemaKind::AllOf { all_of } => {
801 if let Some(ty) = self.collapse_single_all_of(hint, all_of)? {
802 ty
803 } else {
804 let strukt = self.merge_all_of(hint, all_of, data)?;
805 self.extra.push(Item::Struct(strukt));
806 RustType::Named(hint.to_owned())
807 }
808 }
809 SchemaKind::Any(schema) => self.unconstrained_type(hint, schema),
810 SchemaKind::Not { .. } => {
811 return Err(Error::UnsupportedSchema {
812 path: hint.to_owned(),
813 reason: "`not` schemas are not supported".to_owned(),
814 });
815 }
816 };
817 return Ok(ty);
818 }
819
820 fn inline_object_type(&mut self, hint: &str, obj: &ObjectType, data: &SchemaData) -> Result<RustType> {
822 if obj.properties.is_empty() && !matches!(obj.additional_properties, Some(AdditionalProperties::Any(false))) {
823 let element = self.additional_properties_type(hint, obj)?;
824 return Ok(RustType::Map(Box::new(element)));
825 }
826 let strukt = self.object_to_struct(hint, obj, data)?;
827 self.extra.push(Item::Struct(strukt));
828 return Ok(RustType::Named(hint.to_owned()));
829 }
830
831 fn unconstrained_type(&self, name: &str, schema: &openapiv3::AnySchema) -> RustType {
832 if *schema != openapiv3::AnySchema::default() {
833 crate::diagnostic::report_warnings(
834 &self.spec.source().display().to_string(),
835 &[crate::diagnostic::Warning::new(
836 name,
837 "this schema combination is not implemented and becomes an unconstrained JSON value",
838 )],
839 );
840 }
841 return RustType::Value;
842 }
843
844 fn additional_properties_type(&mut self, hint: &str, obj: &ObjectType) -> Result<RustType> {
846 let element = match &obj.additional_properties {
847 Some(AdditionalProperties::Schema(schema)) => {
848 self.type_from_ref_schema(&format!("{hint}_value"), schema.as_ref())?
849 }
850 Some(AdditionalProperties::Any(_)) | None => RustType::Value,
851 };
852 return Ok(element);
853 }
854}
855
856fn check_all_of_nullable(path: &str, schema: &Schema) -> Result<()> {
857 if schema.schema_data.nullable
858 && !schema.schema_data.extensions.contains_key(X_RUST_TYPE)
859 && matches!(&schema.schema_kind, SchemaKind::AllOf { all_of } if all_of.len() != 1)
860 {
861 return Err(Error::UnsupportedSchema {
862 path: path.to_owned(),
863 reason: "allOf intersection: nullable multi-member compositions are not supported".to_owned(),
864 });
865 }
866 return Ok(());
867}
868
869pub(crate) fn nullable_type(spec: &Spec, ty: RustType) -> RustType {
871 if matches!(ty, RustType::Nullable(_)) {
872 return ty;
873 }
874 if let RustType::Named(name) = &ty {
875 let mut name = name.as_str();
876 let mut visited = std::collections::HashSet::new();
877 while visited.insert(name) {
878 let reference = match spec.schemas().get(name) {
879 Some(ReferenceOr::Reference { reference }) => reference,
880 Some(ReferenceOr::Item(schema)) => {
881 if schema.schema_data.nullable {
882 return ty;
883 }
884 if schema.schema_data.extensions.contains_key(X_RUST_TYPE) {
885 break;
886 }
887 let SchemaKind::AllOf { all_of } = &schema.schema_kind else {
888 break;
889 };
890 let [ReferenceOr::Reference { reference }] = all_of.as_slice() else {
892 break;
893 };
894 reference
895 }
896 None => break,
897 };
898 let Some(target) = ref_target_name(reference) else {
899 break;
900 };
901 name = target;
902 }
903 }
904 return RustType::Nullable(Box::new(ty));
905}
906
907pub(crate) fn string_format_type(format: &VariantOrUnknownOrEmpty<StringFormat>) -> RustType {
909 let ty = match format {
910 VariantOrUnknownOrEmpty::Item(StringFormat::Date) => RustType::Date,
911 VariantOrUnknownOrEmpty::Item(StringFormat::DateTime) => RustType::DateTime,
912 VariantOrUnknownOrEmpty::Item(StringFormat::Byte) => RustType::Bytes,
913 VariantOrUnknownOrEmpty::Item(StringFormat::Binary) => RustType::Bytes,
914 VariantOrUnknownOrEmpty::Item(StringFormat::Password) => RustType::String,
915 VariantOrUnknownOrEmpty::Unknown(name) if name == "uuid" => RustType::Uuid,
916 VariantOrUnknownOrEmpty::Unknown(_) => RustType::String,
917 VariantOrUnknownOrEmpty::Empty => RustType::String,
918 };
919 return ty;
920}
921
922pub(super) fn fits_repr(value: i64, repr: &RustType) -> bool {
927 return match *repr {
928 RustType::I32 => i32::try_from(value).is_ok(),
929 RustType::U32 => u32::try_from(value).is_ok(),
930 RustType::U64 => u64::try_from(value).is_ok(),
931 _ => true,
932 };
933}
934
935fn repr_name(repr: &RustType) -> &'static str {
937 return match *repr {
938 RustType::I32 => "i32",
939 RustType::U32 => "u32",
940 RustType::U64 => "u64",
941 _ => "i64",
942 };
943}
944
945fn integer_variant_name(value: i64) -> String {
947 if value < 0 {
948 return format!("value_minus_{}", value.unsigned_abs());
949 }
950 return format!("value_{value}");
951}
952
953fn inline_variant_seed(schema: &Schema, at: &str) -> Result<Option<String>> {
964 if let Some(custom) = extension_str(&schema.schema_data, X_RUST_NAME, at)? {
965 return Ok(Some(custom.to_owned()));
966 }
967 if let Some(value) = single_enum_value(schema) {
968 return Ok(Some(value));
969 }
970 let Some(ty) = non_hoisting_type(schema) else {
971 return Ok(None);
972 };
973 return Ok(type_variant_name(&ty).map(str::to_owned));
974}
975
976fn single_enum_value(schema: &Schema) -> Option<String> {
983 let SchemaKind::Type(Type::String(st)) = &schema.schema_kind else {
984 return None;
985 };
986 let [Some(value)] = st.enumeration.as_slice() else {
987 return None;
988 };
989 return Some(value.clone());
990}
991
992fn non_hoisting_type(schema: &Schema) -> Option<RustType> {
996 return match &schema.schema_kind {
997 SchemaKind::Type(Type::String(st)) if st.enumeration.is_empty() => Some(string_format_type(&st.format)),
998 SchemaKind::Type(Type::Integer(it)) if it.enumeration.is_empty() => Some(integer_type(it)),
999 SchemaKind::Type(Type::Number(_)) => Some(RustType::F64),
1000 SchemaKind::Type(Type::Boolean(_)) => Some(RustType::Bool),
1001 _ => None,
1002 };
1003}
1004
1005fn type_variant_name(ty: &RustType) -> Option<&'static str> {
1009 return match ty {
1010 RustType::Bool => Some("Bool"),
1011 RustType::I32 => Some("I32"),
1012 RustType::I64 => Some("I64"),
1013 RustType::U32 => Some("U32"),
1014 RustType::U64 => Some("U64"),
1015 RustType::F64 => Some("F64"),
1016 RustType::String => Some("String"),
1017 RustType::Date => Some("Date"),
1018 RustType::DateTime => Some("DateTime"),
1019 RustType::Uuid => Some("Uuid"),
1020 RustType::Bytes => Some("Bytes"),
1021 _ => None,
1022 };
1023}
1024
1025pub(crate) fn integer_type(it: &IntegerType) -> RustType {
1037 let unsigned = matches!(crate::lower::constraints::inclusive_minimum(it), Some(minimum) if minimum >= 0);
1038 let ty = match (&it.format, unsigned) {
1039 (VariantOrUnknownOrEmpty::Item(IntegerFormat::Int32), false) => RustType::I32,
1040 (VariantOrUnknownOrEmpty::Item(IntegerFormat::Int32), true) => RustType::U32,
1041 (VariantOrUnknownOrEmpty::Item(IntegerFormat::Int64), false)
1042 | (VariantOrUnknownOrEmpty::Unknown(_) | VariantOrUnknownOrEmpty::Empty, false) => RustType::I64,
1043 (VariantOrUnknownOrEmpty::Item(IntegerFormat::Int64), true)
1044 | (VariantOrUnknownOrEmpty::Unknown(_) | VariantOrUnknownOrEmpty::Empty, true) => RustType::U64,
1045 };
1046 return ty;
1047}
1048
1049fn extension_str<'a>(data: &'a SchemaData, key: &str, at: &str) -> Result<Option<&'a str>> {
1051 return crate::lower::extension::str_value(&data.extensions, key, at);
1052}
1053
1054fn extension_bool(data: &SchemaData, key: &str, at: &str) -> Result<Option<bool>> {
1056 return crate::lower::extension::bool_value(&data.extensions, key, at);
1057}
1058
1059fn extension_i64(data: &SchemaData, key: &str, at: &str) -> Result<Option<i64>> {
1061 return crate::lower::extension::i64_value(&data.extensions, key, at);
1062}
1063
1064fn prop_order(prop: &ReferenceOr<Box<Schema>>, at: &str) -> Result<Option<i64>> {
1066 return match prop {
1067 ReferenceOr::Item(schema) => extension_i64(&schema.schema_data, X_ORDER, at),
1068 ReferenceOr::Reference { .. } => Ok(None),
1069 };
1070}
1071
1072fn sort_by_order(mut fields: Vec<(Option<i64>, Field)>) -> Vec<Field> {
1076 fields.sort_by_key(|(order, _)| {
1077 return order.unwrap_or(i64::MAX);
1078 });
1079 return fields.into_iter().map(|(_, field)| return field).collect();
1080}
1081
1082fn extension_str_array<'a>(data: &'a SchemaData, key: &str, at: &str) -> Result<Option<Vec<&'a str>>> {
1084 return crate::lower::extension::str_list_value(&data.extensions, key, at);
1085}
1086
1087const FOREIGN_DERIVE_NAMES: [&str; 3] = ["Debug", "Clone", "PartialEq"];
1093
1094fn foreign_derives_of(data: &SchemaData, path: &str) -> Result<ForeignDerives> {
1106 let Some(value) = data.extensions.get(X_RUST_DERIVE) else {
1107 return Ok(ForeignDerives::default());
1108 };
1109 let Some(array) = value.as_array() else {
1110 return Err(Error::UnsupportedSchema {
1111 path: path.to_owned(),
1112 reason: format!("`{X_RUST_DERIVE}` must be a list of trait names, for example `[Debug, Clone]`"),
1113 });
1114 };
1115
1116 let mut derives = ForeignDerives {
1117 debug: false,
1118 clone: false,
1119 partial_eq: false,
1120 };
1121 for entry in array {
1122 let Some(name) = entry.as_str() else {
1123 return Err(Error::UnsupportedSchema {
1124 path: path.to_owned(),
1125 reason: format!("every `{X_RUST_DERIVE}` entry must be a trait name written as a string"),
1126 });
1127 };
1128 match name {
1129 "Debug" => derives.debug = true,
1130 "Clone" => derives.clone = true,
1131 "PartialEq" => derives.partial_eq = true,
1132 other => {
1133 let known = FOREIGN_DERIVE_NAMES.join(", ");
1134 return Err(Error::UnsupportedSchema {
1135 path: path.to_owned(),
1136 reason: format!("`{X_RUST_DERIVE}` does not accept `{other}`. It accepts only {known}"),
1137 });
1138 }
1139 }
1140 }
1141 return Ok(derives);
1142}
1143
1144fn verbatim_type(data: &SchemaData, verbatim: &str, path: &str) -> Result<RustType> {
1148 return Ok(RustType::Verbatim {
1149 text: verbatim.to_owned(),
1150 derives: foreign_derives_of(data, path)?,
1151 });
1152}
1153
1154pub(crate) fn access_of(data: &SchemaData, at: &str) -> Result<Access> {
1159 return match (data.read_only, data.write_only) {
1160 (true, true) => Err(Error::UnsupportedSchema {
1161 path: at.to_owned(),
1162 reason: "`readOnly` and `writeOnly` are both set, so no request and no response could \
1163 carry the property. Set at most one of the two."
1164 .to_owned(),
1165 }),
1166 (true, false) => Ok(Access::ReadOnly),
1167 (false, true) => Ok(Access::WriteOnly),
1168 (false, false) => Ok(Access::ReadWrite),
1169 };
1170}
1171
1172fn deprecation_of(data: &SchemaData, at: &str) -> Result<Option<Deprecation>> {
1176 if !data.deprecated {
1177 return Ok(None);
1178 }
1179 let note = extension_str(data, X_DEPRECATED_REASON, at)?.map(str::to_owned);
1180 return Ok(Some(Deprecation { note }));
1181}
1182
1183fn doc_of(data: &SchemaData) -> Option<String> {
1185 let text = data.description.as_ref()?;
1186 let trimmed = text.trim();
1187 if trimmed.is_empty() {
1188 return None;
1189 }
1190 return Some(trimmed.to_owned());
1191}
1192
1193#[cfg(test)]
1194mod tests {
1195 use std::path::PathBuf;
1196
1197 use super::*;
1198
1199 #[test]
1200 fn nullable_type_follows_only_local_aliases() {
1201 let doc = serde_yaml::from_str(
1202 r##"
1203openapi: 3.0.3
1204info: { title: t, version: '1' }
1205paths: {}
1206components:
1207 schemas:
1208 Text: { type: string, nullable: true }
1209 Alias: { $ref: '#/components/schemas/Text' }
1210 Wrapped:
1211 allOf: [{ $ref: '#/components/schemas/Alias' }]
1212 List:
1213 type: array
1214 items: { $ref: '#/components/schemas/Text' }
1215 Map:
1216 type: object
1217 additionalProperties: { $ref: '#/components/schemas/Text' }
1218 Custom:
1219 x-rust-type: String
1220 allOf: [{ $ref: '#/components/schemas/Text' }]
1221 Inline:
1222 allOf:
1223 - type: object
1224 nullable: true
1225 properties:
1226 value: { type: string }
1227 Foreign: { $ref: 'other.yaml#/components/schemas/Text' }
1228 CycleA: { $ref: '#/components/schemas/CycleB' }
1229 CycleB:
1230 allOf: [{ $ref: '#/components/schemas/CycleA' }]
1231"##,
1232 )
1233 .expect("parse spec");
1234 let spec = Spec::from_parts(doc, PathBuf::from("inline.yaml"));
1235 for name in ["Text", "Alias", "Wrapped"] {
1236 let ty = RustType::Named(name.to_owned());
1237 assert_eq!(nullable_type(&spec, ty.clone()), ty, "{name}");
1238 }
1239 for name in [
1240 "List", "Map", "Custom", "Inline", "Foreign", "CycleA", "CycleB", "Missing",
1241 ] {
1242 let ty = RustType::Named(name.to_owned());
1243 assert_eq!(
1244 nullable_type(&spec, ty.clone()),
1245 RustType::Nullable(Box::new(ty)),
1246 "{name}"
1247 );
1248 }
1249 let nullable = RustType::Nullable(Box::new(RustType::String));
1250 assert_eq!(nullable_type(&spec, nullable.clone()), nullable);
1251 for ty in [
1252 RustType::Vec(Box::new(nullable.clone())),
1253 RustType::Map(Box::new(nullable.clone())),
1254 RustType::Option(Box::new(nullable)),
1255 RustType::External {
1256 module: "foreign".to_owned(),
1257 name: "Text".to_owned(),
1258 },
1259 ] {
1260 assert_eq!(nullable_type(&spec, ty.clone()), RustType::Nullable(Box::new(ty)));
1261 }
1262 }
1263
1264 fn emit_yaml(yaml: &str) -> String {
1266 let doc: openapiv3::OpenAPI = serde_yaml::from_str(yaml).expect("parse spec");
1267 let spec = Spec::from_parts(doc, PathBuf::from("inline.yaml"));
1268 let module = lower_models(&spec).expect("map schemas");
1269 return crate::emit::emit_module(&module, None).expect("emit module");
1270 }
1271
1272 fn lower_yaml(yaml: &str) -> Result<Module> {
1274 let doc: openapiv3::OpenAPI = serde_yaml::from_str(yaml).expect("parse spec");
1275 let spec = Spec::from_parts(doc, PathBuf::from("inline.yaml"));
1276 return lower_models(&spec);
1277 }
1278
1279 fn lower_models(spec: &Spec) -> Result<Module> {
1282 let names = crate::lower::rename::type_renames(spec, None)?;
1283 return generate_models(spec, &names);
1284 }
1285
1286 const PREAMBLE: &str = "openapi: 3.0.3\ninfo:\n title: t\n version: '1'\npaths: {}\ncomponents:\n schemas:\n";
1287
1288 fn nested_array_schema(depth: usize) -> Schema {
1292 let mut kind = SchemaKind::Type(Type::String(Default::default()));
1293 for _ in 0..depth {
1294 let items = ReferenceOr::Item(Box::new(Schema {
1295 schema_data: SchemaData::default(),
1296 schema_kind: kind,
1297 }));
1298 kind = SchemaKind::Type(Type::Array(openapiv3::ArrayType {
1299 items: Some(items),
1300 min_items: None,
1301 max_items: None,
1302 unique_items: false,
1303 }));
1304 }
1305 return Schema {
1306 schema_data: SchemaData::default(),
1307 schema_kind: kind,
1308 };
1309 }
1310
1311 fn spec_with_schema(name: &str, schema: Schema) -> Spec {
1312 let empty_doc = "openapi: 3.0.3\ninfo:\n title: t\n version: '1'\npaths: {}\n";
1313 let mut doc: openapiv3::OpenAPI = serde_yaml::from_str(empty_doc).expect("parse preamble");
1314 doc.components
1315 .get_or_insert_with(Default::default)
1316 .schemas
1317 .insert(name.to_owned(), ReferenceOr::Item(schema));
1318 return Spec::from_parts(doc, PathBuf::from("inline.yaml"));
1319 }
1320 #[test]
1325 fn schema_at_the_depth_limit_errors_instead_of_overflowing() {
1326 let spec = spec_with_schema("Deep", nested_array_schema(MAX_SCHEMA_DEPTH));
1327 let err = lower_models(&spec).expect_err("reaching the limit should hit the depth guard");
1328 assert!(
1329 matches!(err, Error::SchemaDepthExceeded { limit, .. } if limit == MAX_SCHEMA_DEPTH),
1330 "expected SchemaDepthExceeded, got {err:?}"
1331 );
1332 }
1333
1334 #[test]
1335 fn schema_just_under_the_depth_limit_still_lowers() {
1336 let spec = spec_with_schema("Deep", nested_array_schema(MAX_SCHEMA_DEPTH - 1));
1337 lower_models(&spec).expect("just under the limit should lower cleanly");
1338 }
1339
1340 #[test]
1341 fn all_of_shares_the_inline_depth_budget() {
1342 let make_schema = |schema_kind| {
1343 return Schema {
1344 schema_data: SchemaData::default(),
1345 schema_kind,
1346 };
1347 };
1348 let compose = |schemas: Vec<Schema>| {
1349 return make_schema(SchemaKind::AllOf {
1350 all_of: schemas.into_iter().map(ReferenceOr::Item).collect(),
1351 });
1352 };
1353 for overlap in [false, true] {
1354 for excess in [false, true] {
1355 let scalar = make_schema(SchemaKind::Type(Type::String(Default::default())));
1356 let empty = make_schema(SchemaKind::Type(Type::Object(Default::default())));
1357 let mut schema = if overlap { scalar.clone() } else { empty.clone() };
1358 for _ in 0..(MAX_SCHEMA_DEPTH / 2 - 2 + usize::from(excess)) {
1359 schema = compose(vec![schema]);
1360 }
1361 if overlap {
1362 let members = [schema, scalar]
1363 .into_iter()
1364 .map(|property| {
1365 let mut object = ObjectType::default();
1366 object
1367 .properties
1368 .insert("value".to_owned(), ReferenceOr::Item(Box::new(property)));
1369 return make_schema(SchemaKind::Type(Type::Object(object)));
1370 })
1371 .collect();
1372 schema = compose(members);
1373 } else {
1374 schema = compose(vec![schema, empty]);
1375 }
1376 for _ in 0..MAX_SCHEMA_DEPTH / 2 {
1377 schema = make_schema(SchemaKind::Type(Type::Array(openapiv3::ArrayType {
1378 items: Some(ReferenceOr::Item(Box::new(schema))),
1379 min_items: None,
1380 max_items: None,
1381 unique_items: false,
1382 })));
1383 }
1384 let result = lower_models(&spec_with_schema("Deep", schema));
1385 if excess {
1386 assert!(matches!(result, Err(Error::SchemaDepthExceeded { .. })), "{result:?}");
1387 } else {
1388 result.expect("combined depth below the limit");
1389 }
1390 }
1391 }
1392 }
1393
1394 fn lower_error(yaml: &str) -> Error {
1396 let doc: openapiv3::OpenAPI = serde_yaml::from_str(yaml).expect("parse spec");
1397 let spec = Spec::from_parts(doc, PathBuf::from("inline.yaml"));
1398 return lower_models(&spec).expect_err("the spec should not lower");
1399 }
1400
1401 fn bad_order_schema(name: &str) -> String {
1403 return format!(
1404 " {name}:\n type: object\n properties:\n id:\n type: string\n x-order: 'first'\n"
1405 );
1406 }
1407
1408 #[test]
1409 fn every_bad_schema_is_reported_in_one_run() {
1410 let alpha = bad_order_schema("Alpha");
1414 let beta = bad_order_schema("Beta");
1415 let gamma = bad_order_schema("Gamma");
1416 let err = lower_error(&format!("{PREAMBLE}{alpha}{beta}{gamma}"));
1417 let Error::Validation { problems } = &err else {
1418 panic!("expected Validation, got: {err:?}");
1419 };
1420 assert_eq!(problems.len(), 3, "every bad schema should be reported");
1421 let message = err.to_string();
1422 for name in ["Alpha", "Beta", "Gamma"] {
1423 assert!(message.contains(name), "message should name `{name}`: {message}");
1424 }
1425 }
1426
1427 #[test]
1428 fn a_good_schema_beside_a_bad_one_does_not_add_a_problem() {
1429 let alpha = bad_order_schema("Alpha");
1430 let err = lower_error(&format!("{PREAMBLE}{alpha} Beta:\n type: string\n"));
1431 assert!(
1432 matches!(&err, Error::InvalidExtensionValue { at, .. } if at == "Alpha.id"),
1433 "one problem should stay unwrapped, got: {err:?}",
1434 );
1435 }
1436
1437 #[test]
1438 fn a_union_member_holding_one_enum_value_is_named_by_that_value() {
1439 let out = emit_yaml(&format!(
1442 "{PREAMBLE} Signal:\n oneOf:\n - type: string\n enum: [red]\n - type: string\n enum: [amber]\n"
1443 ));
1444 assert!(out.contains("Red(SignalRed)"), "expected a named variant, got: {out}");
1445 assert!(
1446 out.contains("Amber(SignalAmber)"),
1447 "expected a named variant, got: {out}"
1448 );
1449 assert!(
1452 out.contains("rename = \"red\""),
1453 "the hoisted type should keep the wire value, got: {out}"
1454 );
1455 }
1456
1457 #[test]
1458 fn a_hoisted_union_member_type_carries_the_union_name() {
1459 let out = emit_yaml(&format!(
1462 "{PREAMBLE} Left:\n oneOf:\n - x-rust-name: Unknown\n type: object\n required: [a]\n properties:\n a:\n type: string\n Right:\n oneOf:\n - x-rust-name: Unknown\n type: object\n required: [b]\n properties:\n b:\n type: string\n"
1463 ));
1464 assert!(out.contains("struct LeftUnknown"), "expected LeftUnknown, got: {out}");
1465 assert!(out.contains("struct RightUnknown"), "expected RightUnknown, got: {out}");
1466 assert!(
1468 out.contains("Unknown(LeftUnknown)"),
1469 "expected a short variant, got: {out}"
1470 );
1471 }
1472
1473 fn derive_error(value: &str) -> Error {
1476 let yaml = format!(
1477 "{PREAMBLE} Target:\n type: string\n x-rust-type: crate::Foreign\n x-rust-derive: {value}\n"
1478 );
1479 let doc: openapiv3::OpenAPI = serde_yaml::from_str(&yaml).expect("parse spec");
1480 let spec = Spec::from_parts(doc, PathBuf::from("inline.yaml"));
1481 return lower_models(&spec).expect_err("the extension should reject this value");
1482 }
1483
1484 #[test]
1485 fn absent_x_rust_derive_claims_every_trait() {
1486 let out = emit_yaml(&format!(
1489 "{PREAMBLE} Holder:\n type: object\n required: [value]\n properties:\n value:\n type: string\n x-rust-type: crate::Foreign\n"
1490 ));
1491 assert!(
1492 out.contains("#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]"),
1493 "absent key should change nothing:\n{out}"
1494 );
1495 }
1496
1497 #[test]
1498 fn empty_x_rust_derive_drops_every_trait() {
1499 let out = emit_yaml(&format!(
1500 "{PREAMBLE} Holder:\n type: object\n required: [value]\n properties:\n value:\n type: string\n x-rust-type: crate::Foreign\n x-rust-derive: []\n"
1501 ));
1502 assert!(
1503 out.contains("#[derive(serde::Serialize, serde::Deserialize)]"),
1504 "an empty list claims nothing, so only the serde derives remain:\n{out}"
1505 );
1506 }
1507
1508 #[test]
1509 fn partial_x_rust_derive_keeps_only_the_listed_traits() {
1510 let out = emit_yaml(&format!(
1511 "{PREAMBLE} Holder:\n type: object\n required: [value]\n properties:\n value:\n type: string\n x-rust-type: crate::Foreign\n x-rust-derive: [Debug, PartialEq]\n"
1512 ));
1513 assert!(
1514 out.contains("#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq)]"),
1515 "Clone was not listed, so it is dropped:\n{out}"
1516 );
1517 }
1518
1519 #[test]
1520 fn x_rust_derive_that_is_not_a_list_is_rejected() {
1521 let err = derive_error("Debug");
1522 assert!(
1523 matches!(&err, Error::UnsupportedSchema { reason, .. } if reason.contains("must be a list")),
1524 "expected a list-shape error, got {err:?}"
1525 );
1526 }
1527
1528 #[test]
1529 fn x_rust_derive_entry_that_is_not_a_string_is_rejected() {
1530 let err = derive_error("[7]");
1531 assert!(
1532 matches!(&err, Error::UnsupportedSchema { reason, .. } if reason.contains("written as a string")),
1533 "expected a string-entry error, got {err:?}"
1534 );
1535 }
1536
1537 #[test]
1538 fn misspelled_trait_name_is_an_error_and_not_an_ignored_key() {
1539 let err = derive_error("[Parialeq]");
1543 assert!(
1544 matches!(&err, Error::UnsupportedSchema { reason, .. } if reason.contains("Parialeq")),
1545 "expected the unknown name in the error, got {err:?}"
1546 );
1547 }
1548
1549 #[test]
1550 fn maps_string_and_integer_formats() {
1551 let yaml = format!(
1552 "{PREAMBLE} Thing:\n type: object\n required: [day, at, id, blob, big]\n properties:\n day:\n type: string\n format: date\n at:\n type: string\n format: date-time\n id:\n type: string\n format: uuid\n blob:\n type: string\n format: byte\n big:\n type: integer\n format: int64\n"
1553 );
1554 let out = emit_yaml(&yaml);
1555 assert!(out.contains("pub day: chrono::NaiveDate"), "{out}");
1556 assert!(out.contains("pub at: chrono::DateTime<chrono::Utc>"), "{out}");
1557 assert!(out.contains("pub id: uuid::Uuid"), "{out}");
1558 assert!(out.contains("pub blob: Vec<u8>"), "{out}");
1559 assert!(out.contains("pub big: i64"), "{out}");
1560 }
1561
1562 #[test]
1563 fn an_unsigned_field_keeps_every_check_the_type_does_not_already_make() {
1564 let yaml = format!(
1565 "{PREAMBLE} Thing:\n type: object\n required: [count]\n properties:\n count:\n type: integer\n format: int32\n minimum: 0\n maximum: 130\n"
1566 );
1567 let out = emit_yaml(&yaml);
1568 assert!(out.contains("pub count: u32"), "{out}");
1569 assert!(out.contains("`count` must be 130 or less"), "{out}");
1571 assert!(!out.contains("must be 0 or more"), "{out}");
1575 assert!(!out.contains("*item < 0"), "{out}");
1576 }
1577
1578 #[test]
1579 fn an_exclusive_bound_moves_onto_the_whole_number_beside_it() {
1580 let cases: [(&str, &str, &str, Option<&str>); 5] = [
1584 ("minimum: -1\n exclusiveMinimum: true", "int32", "u32", None),
1585 (
1586 "minimum: 0\n exclusiveMinimum: true",
1587 "int32",
1588 "u32",
1589 Some("must be 1 or more"),
1590 ),
1591 (
1592 "minimum: -2\n exclusiveMinimum: true",
1593 "int32",
1594 "i32",
1595 Some("must be -1 or more"),
1596 ),
1597 (
1598 "maximum: 2147483648\n exclusiveMaximum: true",
1599 "int32",
1600 "i32",
1601 None,
1602 ),
1603 ("minimum: -1\n exclusiveMinimum: true", "int64", "u64", None),
1604 ];
1605 for (bound, format, ty, message) in cases {
1606 let yaml = format!(
1607 "{PREAMBLE} Thing:\n type: object\n required: [count]\n properties:\n count:\n type: integer\n format: {format}\n {bound}\n"
1608 );
1609 let out = emit_yaml(&yaml);
1610 assert!(out.contains(&format!("pub count: {ty}")), "{bound}: {out}");
1611 match message {
1612 Some(text) => assert!(out.contains(text), "{bound}: {out}"),
1613 None => assert!(!out.contains("must be"), "{bound}: {out}"),
1614 }
1615 }
1616 }
1617
1618 #[test]
1619 fn a_bound_that_lands_on_the_limit_of_the_type_writes_no_check() {
1620 let yaml = format!(
1623 "{PREAMBLE} Thing:\n type: object\n required: [count]\n properties:\n count:\n type: integer\n format: int32\n minimum: -2147483648\n maximum: 2147483647\n"
1624 );
1625 let out = emit_yaml(&yaml);
1626 assert!(out.contains("pub count: i32"), "{out}");
1627 assert!(!out.contains("must be"), "{out}");
1628 }
1629
1630 #[test]
1631 fn bounds_that_meet_nowhere_are_refused() {
1632 let cases: [(&str, &str, Option<&str>); 13] = [
1637 (
1638 "integer\n format: int32",
1639 "maximum: -2147483648\n exclusiveMaximum: true",
1640 Some("nothing lies below `-2147483648`, where `i32` starts"),
1641 ),
1642 (
1643 "integer\n format: int32",
1644 "maximum: -2147483647\n exclusiveMaximum: true",
1645 None,
1646 ),
1647 (
1648 "integer",
1649 "minimum: 10\n maximum: 5",
1650 Some("they allow `10` to `5`"),
1651 ),
1652 ("integer", "minimum: 5\n maximum: 5", None),
1653 (
1654 "integer",
1655 "maximum: -9223372036854775808\n exclusiveMaximum: true",
1656 Some("nothing lies below `-9223372036854775808`, where `i64` starts"),
1657 ),
1658 (
1659 "integer\n format: int32",
1660 "minimum: 4294967295\n exclusiveMinimum: true",
1661 Some("nothing lies above `4294967295`, where `u32` stops"),
1662 ),
1663 (
1664 "integer\n format: int32",
1665 "minimum: 2147483647\n maximum: 2147483647",
1666 None,
1667 ),
1668 (
1670 "integer\n format: int64",
1671 "minimum: 9223372036854775807\n exclusiveMinimum: true",
1672 None,
1673 ),
1674 (
1676 "integer\n format: int32",
1677 "minimum: 9223372036854775807\n exclusiveMinimum: true",
1678 Some("nothing lies above `9223372036854775807`, where `u32` stops"),
1679 ),
1680 (
1682 "number",
1683 "minimum: 10\n maximum: 5",
1684 Some("they allow `10` to `5`"),
1685 ),
1686 (
1687 "number",
1688 "minimum: 5\n maximum: 5\n exclusiveMinimum: true",
1689 Some("they meet at `5`, which an `exclusive` flag then leaves out"),
1690 ),
1691 (
1692 "number",
1693 "minimum: 0\n maximum: 1\n exclusiveMaximum: true",
1694 None,
1695 ),
1696 (
1698 "integer\n format: int32",
1699 "maximum: -5000000000",
1700 Some("the `maximum` value `-5000000000` does not fit `i32`"),
1701 ),
1702 ];
1703 for (kind, bounds, fault) in cases {
1704 let yaml = format!(
1705 "{PREAMBLE} Thing:\n type: object\n required: [count]\n properties:\n count:\n type: {kind}\n {bounds}\n"
1706 );
1707 let outcome = lower_yaml(&yaml);
1708 match fault {
1709 Some(text) => {
1710 let error = outcome.expect_err(bounds).to_string();
1711 assert!(error.contains(text), "{bounds}: {error}");
1712 }
1713 None => assert!(outcome.is_ok(), "{bounds}: got {outcome:?}"),
1714 }
1715 }
1716 }
1717
1718 #[test]
1719 fn a_negative_multiple_of_reports_one_fault_only() {
1720 let yaml = format!(
1724 "{PREAMBLE} Thing:\n type: object\n required: [count]\n properties:\n count:\n type: integer\n format: int32\n minimum: 0\n multipleOf: -1\n"
1725 );
1726 let fault = lower_yaml(&yaml).expect_err("refuse the step").to_string();
1727 assert!(fault.contains("is not above zero"), "{fault}");
1728 assert!(!fault.contains("does not fit"), "{fault}");
1729 }
1730
1731 #[test]
1732 fn an_enum_value_must_fit_the_repr_the_format_and_the_minimum_choose() {
1733 let cases: [(&str, &str, i64, bool); 8] = [
1737 ("int32", "", -1, true),
1738 ("int32", "", 4_294_967_296, false),
1739 ("int32", "\n minimum: 0", -1, false),
1740 ("int32", "\n minimum: 0", 5, true),
1741 ("int32", "\n minimum: 0", 4_294_967_296, false),
1742 ("int64", "\n minimum: 0", -1, false),
1743 ("int64", "\n minimum: 0", 4_294_967_296, true),
1744 ("int64", "", -1, true),
1745 ];
1746 for (format, minimum, value, fits) in cases {
1747 let yaml = format!(
1748 "{PREAMBLE} Offset:\n type: integer\n format: {format}{minimum}\n enum:\n - {value}\n"
1749 );
1750 let outcome = lower_yaml(&yaml);
1751 assert_eq!(
1752 outcome.is_ok(),
1753 fits,
1754 "format `{format}`, minimum `{minimum}`, value `{value}`: got {outcome:?}"
1755 );
1756 }
1757 }
1758
1759 #[test]
1760 fn a_minimum_of_zero_or_more_gives_an_unsigned_type() {
1761 let yaml = format!(
1762 "{PREAMBLE} Thing:\n type: object\n required: [count, total, plain, signed, above]\n properties:\n count:\n type: integer\n format: int32\n minimum: 0\n total:\n type: integer\n format: int64\n minimum: 0\n plain:\n type: integer\n minimum: 0\n signed:\n type: integer\n format: int32\n minimum: -1\n above:\n type: integer\n format: int32\n minimum: 5\n"
1763 );
1764 let out = emit_yaml(&yaml);
1765 assert!(out.contains("pub count: u32"), "{out}");
1766 assert!(out.contains("pub total: u64"), "{out}");
1767 assert!(out.contains("pub plain: u64"), "{out}");
1768 assert!(out.contains("pub signed: i32"), "{out}");
1770 assert!(out.contains("pub above: u32"), "{out}");
1772 }
1773
1774 #[test]
1775 fn an_integer_without_a_minimum_stays_signed() {
1776 let yaml = format!(
1777 "{PREAMBLE} Thing:\n type: object\n required: [count]\n properties:\n count:\n type: integer\n format: int32\n maximum: 10\n"
1778 );
1779 let out = emit_yaml(&yaml);
1780 assert!(out.contains("pub count: i32"), "{out}");
1781 }
1782
1783 #[test]
1784 fn object_with_only_additional_properties_becomes_map_alias() {
1785 let yaml =
1786 format!("{PREAMBLE} Dict:\n type: object\n additionalProperties:\n type: string\n");
1787 let out = emit_yaml(&yaml);
1788 assert!(
1789 out.contains("pub type Dict = std::collections::HashMap<String, String>;"),
1790 "{out}"
1791 );
1792 }
1793
1794 #[test]
1795 fn inline_nested_object_is_hoisted() {
1796 let yaml = format!(
1797 "{PREAMBLE} Outer:\n type: object\n required: [inner]\n properties:\n inner:\n type: object\n required: [x]\n properties:\n x:\n type: string\n"
1798 );
1799 let out = emit_yaml(&yaml);
1800 assert!(out.contains("pub struct Outer"), "{out}");
1801 assert!(out.contains("pub inner: OuterInner"), "{out}");
1802 assert!(out.contains("pub struct OuterInner"), "{out}");
1803 assert!(out.contains("pub x: String"), "{out}");
1804 }
1805
1806 #[test]
1807 fn x_rust_type_emits_verbatim_type() {
1808 let yaml = format!(
1809 "{PREAMBLE} Holder:\n type: object\n required: [v]\n properties:\n v:\n type: string\n x-rust-type: my_crate::Custom\n"
1810 );
1811 let out = emit_yaml(&yaml);
1812 assert!(out.contains("pub v: my_crate::Custom"), "{out}");
1813 }
1814
1815 #[test]
1816 fn optional_field_is_wrapped_and_skipped() {
1817 let yaml = format!(
1818 "{PREAMBLE} Maybe:\n type: object\n properties:\n note:\n type: string\n"
1819 );
1820 let out = emit_yaml(&yaml);
1821 assert!(out.contains("skip_serializing_if = \"Option::is_none\""), "{out}");
1822 assert!(out.contains("pub note: Option<String>"), "{out}");
1823 }
1824}