1use openapiv3::AdditionalProperties;
4use openapiv3::Discriminator;
5use openapiv3::IntegerFormat;
6use openapiv3::IntegerType;
7use openapiv3::ObjectType;
8use openapiv3::ReferenceOr;
9use openapiv3::Schema;
10use openapiv3::SchemaData;
11use openapiv3::SchemaKind;
12use openapiv3::StringFormat;
13use openapiv3::Type;
14use openapiv3::VariantOrUnknownOrEmpty;
15
16use crate::error::Error;
17use crate::error::Result;
18use crate::ir::Alias;
19use crate::ir::Deprecation;
20use crate::ir::Enum;
21use crate::ir::EnumKind;
22use crate::ir::Field;
23use crate::ir::ForeignDerives;
24use crate::ir::IntegerVariant;
25use crate::ir::Item;
26use crate::ir::Module;
27use crate::ir::RustType;
28use crate::ir::StringVariant;
29use crate::ir::Struct;
30use crate::ir::UnionVariant;
31use crate::loader::Spec;
32use crate::loader::ref_target_name;
33use crate::loader::schema_ref_reason;
34use crate::lower::default::lower_default;
35use crate::naming::Case;
36use crate::naming::X_RUST_NAME;
37use crate::naming::to_ident;
38
39pub(crate) const X_RUST_TYPE: &str = "x-rust-type";
41const X_RUST_DERIVE: &str = "x-rust-derive";
44const X_RUST_SERDE_SKIP: &str = "x-rust-serde-skip";
46const X_OMITEMPTY: &str = "x-omitempty";
48const X_ORDER: &str = "x-order";
50const X_DEPRECATED_REASON: &str = "x-deprecated-reason";
52const X_ENUM_VARNAMES: &str = "x-enum-varnames";
54const X_ENUM_NAMES: &str = "x-enumNames";
56
57const MAX_SCHEMA_DEPTH: usize = 100;
62
63pub fn generate_models(spec: &Spec, names: &crate::lower::rename::TypeNames) -> Result<Module> {
77 let renames = names.renames();
78 let mut mapper = Mapper {
79 spec,
80 renames,
81 extra: Vec::new(),
82 depth: 0,
83 };
84 let mut items = Vec::new();
85 let mut diagnostics = crate::lower::validate::Diagnostics::new();
86 for (name, entry) in spec.schemas() {
87 match entry {
88 ReferenceOr::Item(schema) => match mapper.named_to_item(name, schema) {
89 Ok(item) => items.push(item),
90 Err(problem) => diagnostics.push(problem),
91 },
92 ReferenceOr::Reference { reference } => {
93 match mapper.schema_ref_target(reference, "a top-level schema alias") {
94 Ok(target) => items.push(Item::Alias(Alias {
95 name: mapper.type_name_ident(name),
96 doc: None,
97 deprecated: None,
98 ty: RustType::Named(target),
99 })),
100 Err(problem) => diagnostics.push(problem),
101 }
102 }
103 }
104 }
105 diagnostics.into_result()?;
106 items.append(&mut mapper.extra);
107 let mut module = Module { items };
108 crate::lower::rename::rewrite_module(&mut module, renames);
109 return Ok(module);
110}
111
112struct Mapper<'a> {
114 spec: &'a Spec,
115 renames: &'a std::collections::HashMap<String, String>,
117 extra: Vec<Item>,
118 depth: usize,
120}
121
122impl Mapper<'_> {
123 fn schema_ref_target(&self, reference: &str, site: &str) -> Result<String> {
130 let target = ref_target_name(reference).ok_or_else(|| {
131 return Error::UnsupportedRef {
132 reference: reference.to_owned(),
133 reason: schema_ref_reason(reference, site),
134 };
135 })?;
136 if !self.spec.schemas().contains_key(target) {
137 return Err(Error::UnresolvedRef(reference.to_owned()));
138 }
139 return Ok(target.to_owned());
140 }
141
142 fn type_name_ident(&self, name: &str) -> crate::naming::RustIdent {
144 let effective = self.renames.get(name).map(String::as_str).unwrap_or(name);
145 return to_ident(effective, Case::Pascal);
146 }
147
148 fn named_to_item(&mut self, name: &str, schema: &Schema) -> Result<Item> {
150 let data = &schema.schema_data;
151
152 if let Some(verbatim) = extension_str(data, X_RUST_TYPE, name)? {
153 return Ok(Item::Alias(Alias {
154 name: self.type_name_ident(name),
155 doc: doc_of(data),
156 deprecated: deprecation_of(data, name)?,
157 ty: verbatim_type(data, verbatim, name)?,
158 }));
159 }
160
161 let item = match &schema.schema_kind {
162 SchemaKind::Type(Type::String(st)) if !st.enumeration.is_empty() => {
163 Item::Enum(self.string_enum(name, &st.enumeration, data)?)
164 }
165 SchemaKind::Type(Type::Integer(it)) if !it.enumeration.is_empty() => {
166 let repr = integer_type(it);
167 Item::Enum(self.integer_enum(name, &it.enumeration, &repr, data)?)
168 }
169 SchemaKind::Type(Type::Object(obj)) => self.object_to_item(name, obj, data)?,
170 SchemaKind::OneOf { one_of } | SchemaKind::AnyOf { any_of: one_of } => {
171 Item::Enum(self.make_union(name, one_of, data)?)
172 }
173 SchemaKind::AllOf { all_of } => match self.single_ref_all_of(all_of)? {
174 Some(target) => Item::Alias(Alias {
175 name: self.type_name_ident(name),
176 doc: doc_of(data),
177 deprecated: deprecation_of(data, name)?,
178 ty: RustType::Named(target),
179 }),
180 None => Item::Struct(self.merge_all_of(name, all_of, data)?),
181 },
182 SchemaKind::Type(_) => {
183 let ty = self.type_from_schema(name, schema)?;
184 Item::Alias(Alias {
185 name: self.type_name_ident(name),
186 doc: doc_of(data),
187 deprecated: deprecation_of(data, name)?,
188 ty,
189 })
190 }
191 SchemaKind::Any(_) => Item::Alias(Alias {
192 name: self.type_name_ident(name),
193 doc: doc_of(data),
194 deprecated: deprecation_of(data, name)?,
195 ty: RustType::Value,
196 }),
197 SchemaKind::Not { .. } => {
198 return Err(Error::UnsupportedSchema {
199 path: name.to_owned(),
200 reason: "`not` schemas are not supported".to_owned(),
201 });
202 }
203 };
204 return Ok(item);
205 }
206
207 fn object_to_item(&mut self, name: &str, obj: &ObjectType, data: &SchemaData) -> Result<Item> {
210 if obj.properties.is_empty() {
211 let element = self.additional_properties_type(name, obj)?;
212 return Ok(Item::Alias(Alias {
213 name: self.type_name_ident(name),
214 doc: doc_of(data),
215 deprecated: deprecation_of(data, name)?,
216 ty: RustType::Map(Box::new(element)),
217 }));
218 }
219 let strukt = self.object_to_struct(name, obj, data)?;
220 return Ok(Item::Struct(strukt));
221 }
222
223 fn object_to_struct(&mut self, name: &str, obj: &ObjectType, data: &SchemaData) -> Result<Struct> {
225 let mut ordered = Vec::with_capacity(obj.properties.len());
226 for (prop_name, prop) in &obj.properties {
227 let required = obj.required.iter().any(|r| {
228 return r == prop_name;
229 });
230 let order = prop_order(prop, &format!("{name}.{prop_name}"))?;
231 let field = self.field_from_prop(name, prop_name, prop, required)?;
232 ordered.push((order, field));
233 }
234 let fields = sort_by_order(ordered);
235
236 let additional_properties = match &obj.additional_properties {
237 Some(AdditionalProperties::Schema(schema)) => {
238 let ty = self.type_from_ref_schema(name, schema.as_ref())?;
239 Some(ty)
240 }
241 Some(AdditionalProperties::Any(true)) => Some(RustType::Value),
242 Some(AdditionalProperties::Any(false)) | None => None,
243 };
244 let deny_unknown_fields = matches!(&obj.additional_properties, Some(AdditionalProperties::Any(false)));
247
248 return Ok(Struct {
249 name: self.type_name_ident(name),
250 doc: doc_of(data),
251 deprecated: deprecation_of(data, name)?,
252 fields,
253 additional_properties,
254 deny_unknown_fields,
255 });
256 }
257
258 fn field_from_prop(
260 &mut self,
261 parent: &str,
262 wire: &str,
263 prop: &ReferenceOr<Box<Schema>>,
264 required: bool,
265 ) -> Result<Field> {
266 let hint = format!("{parent}_{wire}");
267 let at = format!("{parent}.{wire}");
268 let mut ty = self.type_from_schema_ref(&hint, prop)?;
269
270 let data = match prop {
271 ReferenceOr::Item(schema) => Some(&schema.schema_data),
272 ReferenceOr::Reference { .. } => None,
273 };
274
275 let declared = data
279 .filter(|_| return !required)
280 .and_then(|data| return data.default.as_ref());
281
282 let nullable = data.map(|data| return data.nullable).unwrap_or(false);
283 if (!required && declared.is_none()) || nullable {
287 ty = ty.optional();
288 }
289
290 let default = match declared {
291 Some(json) => {
292 let variants_of = |name: &str| {
293 let ident = self.type_name_ident(name);
294 return self.extra.iter().find_map(|item| {
295 return match item {
296 Item::Enum(enom) if enom.name == ident => match &enom.kind {
297 EnumKind::Strings(variants) => Some(variants.clone()),
298 EnumKind::Union(_) | EnumKind::Integers { .. } => None,
299 },
300 _ => None,
301 };
302 });
303 };
304 Some(lower_default(json, &ty, &variants_of, parent, wire)?)
305 }
306 None => None,
307 };
308
309 let doc = data.and_then(doc_of);
310 let deprecated = match data {
311 Some(data) => deprecation_of(data, &at)?,
312 None => None,
313 };
314 let serde_skip = match data {
315 Some(data) => extension_bool(data, X_RUST_SERDE_SKIP, &at)?.unwrap_or(false),
316 None => false,
317 };
318 let omit_empty = match data {
319 Some(data) => extension_bool(data, X_OMITEMPTY, &at)?,
320 None => None,
321 };
322
323 let rust_name = match data {
324 Some(data) => extension_str(data, X_RUST_NAME, &at)?,
325 None => None,
326 };
327 let ident = match rust_name {
328 Some(custom) => to_ident(custom, Case::Snake),
329 None => to_ident(wire, Case::Snake),
330 };
331 let rename = crate::naming::rename_for(wire, &ident);
332 let constraints = match prop {
333 ReferenceOr::Item(schema) => crate::lower::constraints::constraints_of(schema),
334 ReferenceOr::Reference { reference } => self
337 .spec
338 .resolve(reference)
339 .ok()
340 .and_then(crate::lower::constraints::constraints_through_ref),
341 };
342 let field = Field {
343 name: ident,
344 rename,
345 doc,
346 deprecated,
347 ty,
348 required,
349 omit_empty,
350 serde_skip,
351 default,
352 constraints,
353 };
354 crate::lower::constraints::check_constraints(&field)?;
355 return Ok(field);
356 }
357
358 fn single_ref_all_of(&self, members: &[ReferenceOr<Schema>]) -> Result<Option<String>> {
363 let [ReferenceOr::Reference { reference }] = members else {
364 return Ok(None);
365 };
366 let target = self.schema_ref_target(reference, "an allOf member")?;
367 return Ok(Some(target));
368 }
369
370 fn collapse_single_all_of(&mut self, hint: &str, members: &[ReferenceOr<Schema>]) -> Result<Option<RustType>> {
380 let [only] = members else {
381 return Ok(None);
382 };
383 let ty = match only {
384 ReferenceOr::Reference { reference } => {
385 let target = self.schema_ref_target(reference, "an allOf member")?;
386 RustType::Named(target)
387 }
388 ReferenceOr::Item(schema) => self.type_from_schema(hint, schema)?,
389 };
390 return Ok(Some(ty));
391 }
392
393 fn merge_all_of(&mut self, name: &str, members: &[ReferenceOr<Schema>], data: &SchemaData) -> Result<Struct> {
396 let mut merged = MergedObject::default();
397 self.absorb_members(name, members, &mut merged)?;
398
399 let mut ordered = Vec::with_capacity(merged.properties.len());
400 for (wire, prop) in &merged.properties {
401 let required = merged.required.iter().any(|r| {
402 return r == wire;
403 });
404 let order = prop_order(prop, &format!("{name}.{wire}"))?;
405 let field = self.field_from_prop(name, wire, prop, required)?;
406 ordered.push((order, field));
407 }
408 let fields = sort_by_order(ordered);
409
410 return Ok(Struct {
411 name: self.type_name_ident(name),
412 doc: doc_of(data),
413 deprecated: deprecation_of(data, name)?,
414 fields,
415 additional_properties: None,
416 deny_unknown_fields: false,
423 });
424 }
425
426 fn absorb_members(&mut self, name: &str, members: &[ReferenceOr<Schema>], merged: &mut MergedObject) -> Result<()> {
431 if self.depth >= MAX_SCHEMA_DEPTH {
432 return Err(Error::SchemaDepthExceeded {
433 path: name.to_owned(),
434 limit: MAX_SCHEMA_DEPTH,
435 });
436 }
437 self.depth += 1;
438 let result = self.absorb_members_inner(name, members, merged);
439 self.depth -= 1;
440 return result;
441 }
442
443 fn absorb_members_inner(
444 &mut self,
445 name: &str,
446 members: &[ReferenceOr<Schema>],
447 merged: &mut MergedObject,
448 ) -> Result<()> {
449 for member in members {
450 let schema = match member {
451 ReferenceOr::Item(schema) => schema,
452 ReferenceOr::Reference { reference } => self.spec.resolve(reference)?,
453 };
454 match &schema.schema_kind {
455 SchemaKind::Type(Type::Object(obj)) => merged.absorb(obj),
456 SchemaKind::AllOf { all_of } => self.absorb_members(name, all_of, merged)?,
457 SchemaKind::Type(_)
458 | SchemaKind::OneOf { .. }
459 | SchemaKind::AnyOf { .. }
460 | SchemaKind::Any(_)
461 | SchemaKind::Not { .. } => {
462 return Err(Error::UnsupportedSchema {
463 path: name.to_owned(),
464 reason: "allOf members must be objects or refs to objects".to_owned(),
465 });
466 }
467 }
468 }
469 return Ok(());
470 }
471 fn make_union(&mut self, name: &str, members: &[ReferenceOr<Schema>], data: &SchemaData) -> Result<Enum> {
472 let variants = match &data.discriminator {
473 Some(disc) if !disc.mapping.is_empty() => self.union_variants_from_mapping(disc)?,
474 Some(_) | None => self.union_variants_from_members(name, members)?,
475 };
476 check_variant_types(name, &variants)?;
477 return Ok(Enum {
478 name: self.type_name_ident(name),
479 doc: doc_of(data),
480 deprecated: deprecation_of(data, name)?,
481 kind: EnumKind::Union(variants),
482 });
483 }
484
485 fn union_variants_from_mapping(&self, disc: &Discriminator) -> Result<Vec<UnionVariant>> {
487 let mut variants = Vec::with_capacity(disc.mapping.len());
488 let mut seen = std::collections::HashSet::new();
489 for (value, reference) in &disc.mapping {
490 let target = self.schema_ref_target(reference, "a discriminator mapping")?;
491 variants.push(UnionVariant {
492 name: crate::naming::deconflict_ident(to_ident(value, Case::Pascal), &mut seen),
493 ty: RustType::Named(target),
494 });
495 }
496 return Ok(variants);
497 }
498
499 fn union_variants_from_members(
501 &mut self,
502 name: &str,
503 members: &[ReferenceOr<Schema>],
504 ) -> Result<Vec<UnionVariant>> {
505 let mut variants = Vec::with_capacity(members.len());
506 let mut seen = std::collections::HashSet::new();
507 let mut diagnostics = crate::lower::validate::Diagnostics::new();
508 for (index, member) in members.iter().enumerate() {
509 let variant = match member {
510 ReferenceOr::Reference { reference } => {
511 let target = self.schema_ref_target(reference, "a union member")?;
512 UnionVariant {
517 name: crate::naming::deconflict_ident(self.type_name_ident(&target), &mut seen),
518 ty: RustType::Named(target),
519 }
520 }
521 ReferenceOr::Item(schema) => {
522 let Some(seed) = inline_variant_seed(schema, &format!("{name}, member {index}"))? else {
523 diagnostics.push(Error::UnsupportedSchema {
524 path: name.to_owned(),
525 reason: format!("member {index} of the union gives the variant no name"),
526 });
527 continue;
528 };
529 let ty = self.type_from_schema(&format!("{name}_{seed}"), schema)?;
536 UnionVariant {
537 name: crate::naming::deconflict_ident(to_ident(&seed, Case::Pascal), &mut seen),
538 ty,
539 }
540 }
541 };
542 variants.push(variant);
543 }
544 diagnostics.into_result()?;
545 return Ok(variants);
546 }
547
548 fn string_enum(&self, name: &str, values: &[Option<String>], data: &SchemaData) -> Result<Enum> {
556 let varnames = match extension_str_array(data, X_ENUM_VARNAMES, name)? {
557 Some(names) => Some(names),
558 None => extension_str_array(data, X_ENUM_NAMES, name)?,
559 };
560 let mut diagnostics = crate::lower::validate::Diagnostics::new();
561 let mut variants = Vec::new();
562 let mut seen = std::collections::HashSet::new();
563 let mut values_seen = std::collections::HashSet::new();
564 for (index, value) in values.iter().flatten().enumerate() {
565 if !values_seen.insert(value.as_str()) {
566 diagnostics.push(Error::UnsupportedSchema {
567 path: name.to_owned(),
568 reason: format!("the `enum` gives `{value}` more than once"),
569 });
570 continue;
571 }
572 let base = match varnames.as_ref().and_then(|names| return names.get(index)) {
573 Some(custom) => to_ident(custom, Case::Pascal),
574 None => to_ident(value, Case::Pascal),
575 };
576 let ident = crate::naming::deconflict_ident(base, &mut seen);
577 let rename = crate::naming::rename_for(value, &ident);
578 variants.push(StringVariant {
579 name: ident,
580 rename,
581 doc: None,
582 });
583 }
584 diagnostics.into_result()?;
585 return Ok(Enum {
586 name: self.type_name_ident(name),
587 doc: doc_of(data),
588 deprecated: deprecation_of(data, name)?,
589 kind: EnumKind::Strings(variants),
590 });
591 }
592
593 fn integer_enum(&self, name: &str, values: &[Option<i64>], repr: &RustType, data: &SchemaData) -> Result<Enum> {
603 let varnames = match extension_str_array(data, X_ENUM_VARNAMES, name)? {
604 Some(names) => Some(names),
605 None => extension_str_array(data, X_ENUM_NAMES, name)?,
606 };
607 let mut diagnostics = crate::lower::validate::Diagnostics::new();
608 let mut variants = Vec::new();
609 let mut seen = std::collections::HashSet::new();
610 let mut values_seen = std::collections::HashSet::new();
611 for (index, value) in values.iter().flatten().enumerate() {
612 if !values_seen.insert(*value) {
613 diagnostics.push(Error::UnsupportedSchema {
614 path: name.to_owned(),
615 reason: format!("the `enum` gives `{value}` more than once"),
616 });
617 continue;
618 }
619 if !fits_repr(*value, repr) {
620 diagnostics.push(Error::UnsupportedSchema {
621 path: name.to_owned(),
622 reason: format!("the `enum` gives `{value}`, which `{}` cannot hold", repr_name(repr)),
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(&integer_variant_name(*value), Case::Pascal),
629 };
630 variants.push(IntegerVariant {
631 name: crate::naming::deconflict_ident(base, &mut seen),
632 value: *value,
633 doc: None,
634 });
635 }
636 diagnostics.into_result()?;
637 return Ok(Enum {
638 name: self.type_name_ident(name),
639 doc: doc_of(data),
640 deprecated: deprecation_of(data, name)?,
641 kind: EnumKind::Integers {
642 repr: repr.clone(),
643 variants,
644 },
645 });
646 }
647
648 fn type_from_schema_ref(&mut self, hint: &str, schema: &ReferenceOr<Box<Schema>>) -> Result<RustType> {
651 match schema {
652 ReferenceOr::Reference { reference } => {
653 let target = self.schema_ref_target(reference, "a property")?;
654 return Ok(RustType::Named(target));
655 }
656 ReferenceOr::Item(schema) => {
657 let ty = self.type_from_schema(hint, schema)?;
658 return Ok(ty);
659 }
660 }
661 }
662
663 fn type_from_ref_schema(&mut self, hint: &str, schema: &ReferenceOr<Schema>) -> Result<RustType> {
666 match schema {
667 ReferenceOr::Reference { reference } => {
668 let target = self.schema_ref_target(reference, "additionalProperties")?;
669 return Ok(RustType::Named(target));
670 }
671 ReferenceOr::Item(schema) => {
672 let ty = self.type_from_schema(hint, schema)?;
673 return Ok(ty);
674 }
675 }
676 }
677
678 fn type_from_schema(&mut self, hint: &str, schema: &Schema) -> Result<RustType> {
682 if self.depth >= MAX_SCHEMA_DEPTH {
683 return Err(Error::SchemaDepthExceeded {
684 path: hint.to_owned(),
685 limit: MAX_SCHEMA_DEPTH,
686 });
687 }
688 self.depth += 1;
689 let result = self.type_from_schema_inner(hint, schema);
690 self.depth -= 1;
691 return result;
692 }
693
694 fn type_from_schema_inner(&mut self, hint: &str, schema: &Schema) -> Result<RustType> {
695 let data = &schema.schema_data;
696 if let Some(verbatim) = extension_str(data, X_RUST_TYPE, hint)? {
697 return verbatim_type(data, verbatim, hint);
698 }
699
700 let ty = match &schema.schema_kind {
701 SchemaKind::Type(Type::String(st)) if !st.enumeration.is_empty() => {
702 let enom = self.string_enum(hint, &st.enumeration, data)?;
703 self.extra.push(Item::Enum(enom));
704 RustType::Named(hint.to_owned())
705 }
706 SchemaKind::Type(Type::String(st)) => string_format_type(&st.format),
707 SchemaKind::Type(Type::Integer(it)) if !it.enumeration.is_empty() => {
708 let repr = integer_type(it);
709 let enom = self.integer_enum(hint, &it.enumeration, &repr, data)?;
710 self.extra.push(Item::Enum(enom));
711 RustType::Named(hint.to_owned())
712 }
713 SchemaKind::Type(Type::Integer(it)) => integer_type(it),
714 SchemaKind::Type(Type::Number(_)) => RustType::F64,
715 SchemaKind::Type(Type::Boolean(_)) => RustType::Bool,
716 SchemaKind::Type(Type::Array(at)) => {
717 let element = match &at.items {
718 Some(items) => {
719 let item_hint = format!("{hint}_item");
720 self.type_from_schema_ref(&item_hint, items)?
721 }
722 None => RustType::Value,
723 };
724 RustType::Vec(Box::new(element))
725 }
726 SchemaKind::Type(Type::Object(obj)) => self.inline_object_type(hint, obj, data)?,
727 SchemaKind::OneOf { one_of } | SchemaKind::AnyOf { any_of: one_of } => {
728 let enom = self.make_union(hint, one_of, data)?;
729 self.extra.push(Item::Enum(enom));
730 RustType::Named(hint.to_owned())
731 }
732 SchemaKind::AllOf { all_of } => {
733 if let Some(ty) = self.collapse_single_all_of(hint, all_of)? {
734 ty
735 } else {
736 let strukt = self.merge_all_of(hint, all_of, data)?;
737 self.extra.push(Item::Struct(strukt));
738 RustType::Named(hint.to_owned())
739 }
740 }
741 SchemaKind::Any(_) => RustType::Value,
742 SchemaKind::Not { .. } => {
743 return Err(Error::UnsupportedSchema {
744 path: hint.to_owned(),
745 reason: "`not` schemas are not supported".to_owned(),
746 });
747 }
748 };
749 return Ok(ty);
750 }
751
752 fn inline_object_type(&mut self, hint: &str, obj: &ObjectType, data: &SchemaData) -> Result<RustType> {
755 if obj.properties.is_empty() {
756 let element = self.additional_properties_type(hint, obj)?;
757 return Ok(RustType::Map(Box::new(element)));
758 }
759 let strukt = self.object_to_struct(hint, obj, data)?;
760 self.extra.push(Item::Struct(strukt));
761 return Ok(RustType::Named(hint.to_owned()));
762 }
763
764 fn additional_properties_type(&mut self, hint: &str, obj: &ObjectType) -> Result<RustType> {
766 let element = match &obj.additional_properties {
767 Some(AdditionalProperties::Schema(schema)) => self.type_from_ref_schema(hint, schema.as_ref())?,
768 Some(AdditionalProperties::Any(_)) | None => RustType::Value,
769 };
770 return Ok(element);
771 }
772}
773
774#[derive(Default)]
776struct MergedObject {
777 properties: indexmap::IndexMap<String, ReferenceOr<Box<Schema>>>,
778 required: Vec<String>,
779}
780
781impl MergedObject {
782 fn absorb(&mut self, obj: &ObjectType) {
784 for (name, prop) in &obj.properties {
785 self.properties.insert(name.clone(), prop.clone());
786 }
787 for req in &obj.required {
788 if !self.required.contains(req) {
789 self.required.push(req.clone());
790 }
791 }
792 }
793}
794
795pub(crate) fn string_format_type(format: &VariantOrUnknownOrEmpty<StringFormat>) -> RustType {
797 let ty = match format {
798 VariantOrUnknownOrEmpty::Item(StringFormat::Date) => RustType::Date,
799 VariantOrUnknownOrEmpty::Item(StringFormat::DateTime) => RustType::DateTime,
800 VariantOrUnknownOrEmpty::Item(StringFormat::Byte) => RustType::Bytes,
801 VariantOrUnknownOrEmpty::Item(StringFormat::Binary) => RustType::Bytes,
802 VariantOrUnknownOrEmpty::Item(StringFormat::Password) => RustType::String,
803 VariantOrUnknownOrEmpty::Unknown(name) if name == "uuid" => RustType::Uuid,
804 VariantOrUnknownOrEmpty::Unknown(_) => RustType::String,
805 VariantOrUnknownOrEmpty::Empty => RustType::String,
806 };
807 return ty;
808}
809
810fn fits_repr(value: i64, repr: &RustType) -> bool {
815 return match *repr {
816 RustType::I32 => i32::try_from(value).is_ok(),
817 RustType::U32 => u32::try_from(value).is_ok(),
818 RustType::U64 => u64::try_from(value).is_ok(),
819 _ => true,
820 };
821}
822
823fn repr_name(repr: &RustType) -> &'static str {
825 return match *repr {
826 RustType::I32 => "i32",
827 RustType::U32 => "u32",
828 RustType::U64 => "u64",
829 _ => "i64",
830 };
831}
832
833fn integer_variant_name(value: i64) -> String {
835 if value < 0 {
836 return format!("value_minus_{}", value.unsigned_abs());
837 }
838 return format!("value_{value}");
839}
840
841fn inline_variant_seed(schema: &Schema, at: &str) -> Result<Option<String>> {
852 if let Some(custom) = extension_str(&schema.schema_data, X_RUST_NAME, at)? {
853 return Ok(Some(custom.to_owned()));
854 }
855 if let Some(value) = single_enum_value(schema) {
856 return Ok(Some(value));
857 }
858 let Some(ty) = non_hoisting_type(schema) else {
859 return Ok(None);
860 };
861 return Ok(type_variant_name(&ty).map(str::to_owned));
862}
863
864fn single_enum_value(schema: &Schema) -> Option<String> {
871 let SchemaKind::Type(Type::String(st)) = &schema.schema_kind else {
872 return None;
873 };
874 let [Some(value)] = st.enumeration.as_slice() else {
875 return None;
876 };
877 return Some(value.clone());
878}
879
880fn non_hoisting_type(schema: &Schema) -> Option<RustType> {
884 return match &schema.schema_kind {
885 SchemaKind::Type(Type::String(st)) if st.enumeration.is_empty() => Some(string_format_type(&st.format)),
886 SchemaKind::Type(Type::Integer(it)) if it.enumeration.is_empty() => Some(integer_type(it)),
887 SchemaKind::Type(Type::Number(_)) => Some(RustType::F64),
888 SchemaKind::Type(Type::Boolean(_)) => Some(RustType::Bool),
889 _ => None,
890 };
891}
892
893fn type_variant_name(ty: &RustType) -> Option<&'static str> {
897 return match ty {
898 RustType::Bool => Some("Bool"),
899 RustType::I32 => Some("I32"),
900 RustType::I64 => Some("I64"),
901 RustType::U32 => Some("U32"),
902 RustType::U64 => Some("U64"),
903 RustType::F64 => Some("F64"),
904 RustType::String => Some("String"),
905 RustType::Date => Some("Date"),
906 RustType::DateTime => Some("DateTime"),
907 RustType::Uuid => Some("Uuid"),
908 RustType::Bytes => Some("Bytes"),
909 _ => None,
910 };
911}
912
913fn check_variant_types(name: &str, variants: &[UnionVariant]) -> Result<()> {
920 let mut diagnostics = crate::lower::validate::Diagnostics::new();
921 for (index, variant) in variants.iter().enumerate() {
922 let Some(earlier) = variants.iter().take(index).find(|other| return other.ty == variant.ty) else {
923 continue;
924 };
925 diagnostics.push(Error::UnsupportedSchema {
926 path: name.to_owned(),
927 reason: format!(
928 "the union holds `{}` twice, as `{}` and as `{}`",
929 variant.ty.label(),
930 earlier.name.logical(),
931 variant.name.logical()
932 ),
933 });
934 }
935 return diagnostics.into_result();
936}
937
938pub(crate) fn integer_type(it: &IntegerType) -> RustType {
950 let unsigned = matches!(crate::lower::constraints::inclusive_minimum(it), Some(minimum) if minimum >= 0);
951 let ty = match (&it.format, unsigned) {
952 (VariantOrUnknownOrEmpty::Item(IntegerFormat::Int32), false) => RustType::I32,
953 (VariantOrUnknownOrEmpty::Item(IntegerFormat::Int32), true) => RustType::U32,
954 (VariantOrUnknownOrEmpty::Item(IntegerFormat::Int64), false)
955 | (VariantOrUnknownOrEmpty::Unknown(_) | VariantOrUnknownOrEmpty::Empty, false) => RustType::I64,
956 (VariantOrUnknownOrEmpty::Item(IntegerFormat::Int64), true)
957 | (VariantOrUnknownOrEmpty::Unknown(_) | VariantOrUnknownOrEmpty::Empty, true) => RustType::U64,
958 };
959 return ty;
960}
961
962fn extension_str<'a>(data: &'a SchemaData, key: &str, at: &str) -> Result<Option<&'a str>> {
964 return crate::lower::extension::str_value(&data.extensions, key, at);
965}
966
967fn extension_bool(data: &SchemaData, key: &str, at: &str) -> Result<Option<bool>> {
969 return crate::lower::extension::bool_value(&data.extensions, key, at);
970}
971
972fn extension_i64(data: &SchemaData, key: &str, at: &str) -> Result<Option<i64>> {
974 return crate::lower::extension::i64_value(&data.extensions, key, at);
975}
976
977fn prop_order(prop: &ReferenceOr<Box<Schema>>, at: &str) -> Result<Option<i64>> {
979 return match prop {
980 ReferenceOr::Item(schema) => extension_i64(&schema.schema_data, X_ORDER, at),
981 ReferenceOr::Reference { .. } => Ok(None),
982 };
983}
984
985fn sort_by_order(mut fields: Vec<(Option<i64>, Field)>) -> Vec<Field> {
989 fields.sort_by_key(|(order, _)| {
990 return order.unwrap_or(i64::MAX);
991 });
992 return fields.into_iter().map(|(_, field)| return field).collect();
993}
994
995fn extension_str_array<'a>(data: &'a SchemaData, key: &str, at: &str) -> Result<Option<Vec<&'a str>>> {
997 return crate::lower::extension::str_list_value(&data.extensions, key, at);
998}
999
1000const FOREIGN_DERIVE_NAMES: [&str; 3] = ["Debug", "Clone", "PartialEq"];
1006
1007fn foreign_derives_of(data: &SchemaData, path: &str) -> Result<ForeignDerives> {
1019 let Some(value) = data.extensions.get(X_RUST_DERIVE) else {
1020 return Ok(ForeignDerives::default());
1021 };
1022 let Some(array) = value.as_array() else {
1023 return Err(Error::UnsupportedSchema {
1024 path: path.to_owned(),
1025 reason: format!("`{X_RUST_DERIVE}` must be a list of trait names, for example `[Debug, Clone]`"),
1026 });
1027 };
1028
1029 let mut derives = ForeignDerives {
1030 debug: false,
1031 clone: false,
1032 partial_eq: false,
1033 };
1034 for entry in array {
1035 let Some(name) = entry.as_str() else {
1036 return Err(Error::UnsupportedSchema {
1037 path: path.to_owned(),
1038 reason: format!("every `{X_RUST_DERIVE}` entry must be a trait name written as a string"),
1039 });
1040 };
1041 match name {
1042 "Debug" => derives.debug = true,
1043 "Clone" => derives.clone = true,
1044 "PartialEq" => derives.partial_eq = true,
1045 other => {
1046 let known = FOREIGN_DERIVE_NAMES.join(", ");
1047 return Err(Error::UnsupportedSchema {
1048 path: path.to_owned(),
1049 reason: format!("`{X_RUST_DERIVE}` does not accept `{other}`. It accepts only {known}"),
1050 });
1051 }
1052 }
1053 }
1054 return Ok(derives);
1055}
1056
1057fn verbatim_type(data: &SchemaData, verbatim: &str, path: &str) -> Result<RustType> {
1061 return Ok(RustType::Verbatim {
1062 text: verbatim.to_owned(),
1063 derives: foreign_derives_of(data, path)?,
1064 });
1065}
1066
1067fn deprecation_of(data: &SchemaData, at: &str) -> Result<Option<Deprecation>> {
1071 if !data.deprecated {
1072 return Ok(None);
1073 }
1074 let note = extension_str(data, X_DEPRECATED_REASON, at)?.map(str::to_owned);
1075 return Ok(Some(Deprecation { note }));
1076}
1077
1078fn doc_of(data: &SchemaData) -> Option<String> {
1080 let text = data.description.as_ref()?;
1081 let trimmed = text.trim();
1082 if trimmed.is_empty() {
1083 return None;
1084 }
1085 return Some(trimmed.to_owned());
1086}
1087
1088#[cfg(test)]
1089mod tests {
1090 use std::path::PathBuf;
1091
1092 use super::*;
1093
1094 fn emit_yaml(yaml: &str) -> String {
1096 let doc: openapiv3::OpenAPI = serde_yaml::from_str(yaml).expect("parse spec");
1097 let spec = Spec::from_parts(doc, PathBuf::from("inline.yaml"));
1098 let module = lower_models(&spec).expect("map schemas");
1099 return crate::emit::emit_module(&module, None).expect("emit module");
1100 }
1101
1102 fn lower_yaml(yaml: &str) -> Result<Module> {
1104 let doc: openapiv3::OpenAPI = serde_yaml::from_str(yaml).expect("parse spec");
1105 let spec = Spec::from_parts(doc, PathBuf::from("inline.yaml"));
1106 return lower_models(&spec);
1107 }
1108
1109 fn lower_models(spec: &Spec) -> Result<Module> {
1112 let names = crate::lower::rename::type_renames(spec, None)?;
1113 return generate_models(spec, &names);
1114 }
1115
1116 const PREAMBLE: &str = "openapi: 3.0.3\ninfo:\n title: t\n version: '1'\npaths: {}\ncomponents:\n schemas:\n";
1117
1118 fn nested_array_schema(depth: usize) -> Schema {
1122 let mut kind = SchemaKind::Type(Type::String(Default::default()));
1123 for _ in 0..depth {
1124 let items = ReferenceOr::Item(Box::new(Schema {
1125 schema_data: SchemaData::default(),
1126 schema_kind: kind,
1127 }));
1128 kind = SchemaKind::Type(Type::Array(openapiv3::ArrayType {
1129 items: Some(items),
1130 min_items: None,
1131 max_items: None,
1132 unique_items: false,
1133 }));
1134 }
1135 return Schema {
1136 schema_data: SchemaData::default(),
1137 schema_kind: kind,
1138 };
1139 }
1140
1141 fn spec_with_schema(name: &str, schema: Schema) -> Spec {
1142 let empty_doc = "openapi: 3.0.3\ninfo:\n title: t\n version: '1'\npaths: {}\n";
1143 let mut doc: openapiv3::OpenAPI = serde_yaml::from_str(empty_doc).expect("parse preamble");
1144 doc.components
1145 .get_or_insert_with(Default::default)
1146 .schemas
1147 .insert(name.to_owned(), ReferenceOr::Item(schema));
1148 return Spec::from_parts(doc, PathBuf::from("inline.yaml"));
1149 }
1150 #[test]
1155 fn schema_at_the_depth_limit_errors_instead_of_overflowing() {
1156 let spec = spec_with_schema("Deep", nested_array_schema(MAX_SCHEMA_DEPTH));
1157 let err = lower_models(&spec).expect_err("reaching the limit should hit the depth guard");
1158 assert!(
1159 matches!(err, Error::SchemaDepthExceeded { limit, .. } if limit == MAX_SCHEMA_DEPTH),
1160 "expected SchemaDepthExceeded, got {err:?}"
1161 );
1162 }
1163
1164 #[test]
1165 fn schema_just_under_the_depth_limit_still_lowers() {
1166 let spec = spec_with_schema("Deep", nested_array_schema(MAX_SCHEMA_DEPTH - 1));
1167 lower_models(&spec).expect("just under the limit should lower cleanly");
1168 }
1169
1170 fn lower_error(yaml: &str) -> Error {
1172 let doc: openapiv3::OpenAPI = serde_yaml::from_str(yaml).expect("parse spec");
1173 let spec = Spec::from_parts(doc, PathBuf::from("inline.yaml"));
1174 return lower_models(&spec).expect_err("the spec should not lower");
1175 }
1176
1177 fn bad_order_schema(name: &str) -> String {
1179 return format!(
1180 " {name}:\n type: object\n properties:\n id:\n type: string\n x-order: 'first'\n"
1181 );
1182 }
1183
1184 #[test]
1185 fn every_bad_schema_is_reported_in_one_run() {
1186 let alpha = bad_order_schema("Alpha");
1190 let beta = bad_order_schema("Beta");
1191 let gamma = bad_order_schema("Gamma");
1192 let err = lower_error(&format!("{PREAMBLE}{alpha}{beta}{gamma}"));
1193 let Error::Validation { problems } = &err else {
1194 panic!("expected Validation, got: {err:?}");
1195 };
1196 assert_eq!(problems.len(), 3, "every bad schema should be reported");
1197 let message = err.to_string();
1198 for name in ["Alpha", "Beta", "Gamma"] {
1199 assert!(message.contains(name), "message should name `{name}`: {message}");
1200 }
1201 }
1202
1203 #[test]
1204 fn a_good_schema_beside_a_bad_one_does_not_add_a_problem() {
1205 let alpha = bad_order_schema("Alpha");
1206 let err = lower_error(&format!("{PREAMBLE}{alpha} Beta:\n type: string\n"));
1207 assert!(
1208 matches!(&err, Error::InvalidExtensionValue { at, .. } if at == "Alpha.id"),
1209 "one problem should stay unwrapped, got: {err:?}",
1210 );
1211 }
1212
1213 #[test]
1214 fn a_union_member_holding_one_enum_value_is_named_by_that_value() {
1215 let out = emit_yaml(&format!(
1218 "{PREAMBLE} Signal:\n oneOf:\n - type: string\n enum: [red]\n - type: string\n enum: [amber]\n"
1219 ));
1220 assert!(out.contains("Red(SignalRed)"), "expected a named variant, got: {out}");
1221 assert!(
1222 out.contains("Amber(SignalAmber)"),
1223 "expected a named variant, got: {out}"
1224 );
1225 assert!(
1228 out.contains("rename = \"red\""),
1229 "the hoisted type should keep the wire value, got: {out}"
1230 );
1231 }
1232
1233 #[test]
1234 fn a_hoisted_union_member_type_carries_the_union_name() {
1235 let out = emit_yaml(&format!(
1238 "{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"
1239 ));
1240 assert!(out.contains("struct LeftUnknown"), "expected LeftUnknown, got: {out}");
1241 assert!(out.contains("struct RightUnknown"), "expected RightUnknown, got: {out}");
1242 assert!(
1244 out.contains("Unknown(LeftUnknown)"),
1245 "expected a short variant, got: {out}"
1246 );
1247 }
1248
1249 fn derive_error(value: &str) -> Error {
1252 let yaml = format!(
1253 "{PREAMBLE} Target:\n type: string\n x-rust-type: crate::Foreign\n x-rust-derive: {value}\n"
1254 );
1255 let doc: openapiv3::OpenAPI = serde_yaml::from_str(&yaml).expect("parse spec");
1256 let spec = Spec::from_parts(doc, PathBuf::from("inline.yaml"));
1257 return lower_models(&spec).expect_err("the extension should reject this value");
1258 }
1259
1260 #[test]
1261 fn absent_x_rust_derive_claims_every_trait() {
1262 let out = emit_yaml(&format!(
1265 "{PREAMBLE} Holder:\n type: object\n required: [value]\n properties:\n value:\n type: string\n x-rust-type: crate::Foreign\n"
1266 ));
1267 assert!(
1268 out.contains("#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]"),
1269 "absent key should change nothing:\n{out}"
1270 );
1271 }
1272
1273 #[test]
1274 fn empty_x_rust_derive_drops_every_trait() {
1275 let out = emit_yaml(&format!(
1276 "{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"
1277 ));
1278 assert!(
1279 out.contains("#[derive(serde::Serialize, serde::Deserialize)]"),
1280 "an empty list claims nothing, so only the serde derives remain:\n{out}"
1281 );
1282 }
1283
1284 #[test]
1285 fn partial_x_rust_derive_keeps_only_the_listed_traits() {
1286 let out = emit_yaml(&format!(
1287 "{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"
1288 ));
1289 assert!(
1290 out.contains("#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq)]"),
1291 "Clone was not listed, so it is dropped:\n{out}"
1292 );
1293 }
1294
1295 #[test]
1296 fn x_rust_derive_that_is_not_a_list_is_rejected() {
1297 let err = derive_error("Debug");
1298 assert!(
1299 matches!(&err, Error::UnsupportedSchema { reason, .. } if reason.contains("must be a list")),
1300 "expected a list-shape error, got {err:?}"
1301 );
1302 }
1303
1304 #[test]
1305 fn x_rust_derive_entry_that_is_not_a_string_is_rejected() {
1306 let err = derive_error("[7]");
1307 assert!(
1308 matches!(&err, Error::UnsupportedSchema { reason, .. } if reason.contains("written as a string")),
1309 "expected a string-entry error, got {err:?}"
1310 );
1311 }
1312
1313 #[test]
1314 fn misspelled_trait_name_is_an_error_and_not_an_ignored_key() {
1315 let err = derive_error("[Parialeq]");
1319 assert!(
1320 matches!(&err, Error::UnsupportedSchema { reason, .. } if reason.contains("Parialeq")),
1321 "expected the unknown name in the error, got {err:?}"
1322 );
1323 }
1324
1325 #[test]
1326 fn maps_string_and_integer_formats() {
1327 let yaml = format!(
1328 "{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"
1329 );
1330 let out = emit_yaml(&yaml);
1331 assert!(out.contains("pub day: chrono::NaiveDate"), "{out}");
1332 assert!(out.contains("pub at: chrono::DateTime<chrono::Utc>"), "{out}");
1333 assert!(out.contains("pub id: uuid::Uuid"), "{out}");
1334 assert!(out.contains("pub blob: Vec<u8>"), "{out}");
1335 assert!(out.contains("pub big: i64"), "{out}");
1336 }
1337
1338 #[test]
1339 fn an_unsigned_field_keeps_every_check_the_type_does_not_already_make() {
1340 let yaml = format!(
1341 "{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"
1342 );
1343 let out = emit_yaml(&yaml);
1344 assert!(out.contains("pub count: u32"), "{out}");
1345 assert!(out.contains("`count` must be 130 or less"), "{out}");
1347 assert!(!out.contains("must be 0 or more"), "{out}");
1351 assert!(!out.contains("*item < 0"), "{out}");
1352 }
1353
1354 #[test]
1355 fn an_exclusive_bound_moves_onto_the_whole_number_beside_it() {
1356 let cases: [(&str, &str, &str, Option<&str>); 5] = [
1360 ("minimum: -1\n exclusiveMinimum: true", "int32", "u32", None),
1361 (
1362 "minimum: 0\n exclusiveMinimum: true",
1363 "int32",
1364 "u32",
1365 Some("must be 1 or more"),
1366 ),
1367 (
1368 "minimum: -2\n exclusiveMinimum: true",
1369 "int32",
1370 "i32",
1371 Some("must be -1 or more"),
1372 ),
1373 (
1374 "maximum: 2147483648\n exclusiveMaximum: true",
1375 "int32",
1376 "i32",
1377 None,
1378 ),
1379 ("minimum: -1\n exclusiveMinimum: true", "int64", "u64", None),
1380 ];
1381 for (bound, format, ty, message) in cases {
1382 let yaml = format!(
1383 "{PREAMBLE} Thing:\n type: object\n required: [count]\n properties:\n count:\n type: integer\n format: {format}\n {bound}\n"
1384 );
1385 let out = emit_yaml(&yaml);
1386 assert!(out.contains(&format!("pub count: {ty}")), "{bound}: {out}");
1387 match message {
1388 Some(text) => assert!(out.contains(text), "{bound}: {out}"),
1389 None => assert!(!out.contains("must be"), "{bound}: {out}"),
1390 }
1391 }
1392 }
1393
1394 #[test]
1395 fn a_bound_that_lands_on_the_limit_of_the_type_writes_no_check() {
1396 let yaml = format!(
1399 "{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"
1400 );
1401 let out = emit_yaml(&yaml);
1402 assert!(out.contains("pub count: i32"), "{out}");
1403 assert!(!out.contains("must be"), "{out}");
1404 }
1405
1406 #[test]
1407 fn bounds_that_meet_nowhere_are_refused() {
1408 let cases: [(&str, &str, Option<&str>); 13] = [
1413 (
1414 "integer\n format: int32",
1415 "maximum: -2147483648\n exclusiveMaximum: true",
1416 Some("nothing lies below `-2147483648`, where `i32` starts"),
1417 ),
1418 (
1419 "integer\n format: int32",
1420 "maximum: -2147483647\n exclusiveMaximum: true",
1421 None,
1422 ),
1423 (
1424 "integer",
1425 "minimum: 10\n maximum: 5",
1426 Some("they allow `10` to `5`"),
1427 ),
1428 ("integer", "minimum: 5\n maximum: 5", None),
1429 (
1430 "integer",
1431 "maximum: -9223372036854775808\n exclusiveMaximum: true",
1432 Some("nothing lies below `-9223372036854775808`, where `i64` starts"),
1433 ),
1434 (
1435 "integer\n format: int32",
1436 "minimum: 4294967295\n exclusiveMinimum: true",
1437 Some("nothing lies above `4294967295`, where `u32` stops"),
1438 ),
1439 (
1440 "integer\n format: int32",
1441 "minimum: 2147483647\n maximum: 2147483647",
1442 None,
1443 ),
1444 (
1446 "integer\n format: int64",
1447 "minimum: 9223372036854775807\n exclusiveMinimum: true",
1448 None,
1449 ),
1450 (
1452 "integer\n format: int32",
1453 "minimum: 9223372036854775807\n exclusiveMinimum: true",
1454 Some("nothing lies above `9223372036854775807`, where `u32` stops"),
1455 ),
1456 (
1458 "number",
1459 "minimum: 10\n maximum: 5",
1460 Some("they allow `10` to `5`"),
1461 ),
1462 (
1463 "number",
1464 "minimum: 5\n maximum: 5\n exclusiveMinimum: true",
1465 Some("they meet at `5`, which an `exclusive` flag then leaves out"),
1466 ),
1467 (
1468 "number",
1469 "minimum: 0\n maximum: 1\n exclusiveMaximum: true",
1470 None,
1471 ),
1472 (
1474 "integer\n format: int32",
1475 "maximum: -5000000000",
1476 Some("the `maximum` value `-5000000000` does not fit `i32`"),
1477 ),
1478 ];
1479 for (kind, bounds, fault) in cases {
1480 let yaml = format!(
1481 "{PREAMBLE} Thing:\n type: object\n required: [count]\n properties:\n count:\n type: {kind}\n {bounds}\n"
1482 );
1483 let outcome = lower_yaml(&yaml);
1484 match fault {
1485 Some(text) => {
1486 let error = outcome.expect_err(bounds).to_string();
1487 assert!(error.contains(text), "{bounds}: {error}");
1488 }
1489 None => assert!(outcome.is_ok(), "{bounds}: got {outcome:?}"),
1490 }
1491 }
1492 }
1493
1494 #[test]
1495 fn a_negative_multiple_of_reports_one_fault_only() {
1496 let yaml = format!(
1500 "{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"
1501 );
1502 let fault = lower_yaml(&yaml).expect_err("refuse the step").to_string();
1503 assert!(fault.contains("is not above zero"), "{fault}");
1504 assert!(!fault.contains("does not fit"), "{fault}");
1505 }
1506
1507 #[test]
1508 fn an_enum_value_must_fit_the_repr_the_format_and_the_minimum_choose() {
1509 let cases: [(&str, &str, i64, bool); 8] = [
1513 ("int32", "", -1, true),
1514 ("int32", "", 4_294_967_296, false),
1515 ("int32", "\n minimum: 0", -1, false),
1516 ("int32", "\n minimum: 0", 5, true),
1517 ("int32", "\n minimum: 0", 4_294_967_296, false),
1518 ("int64", "\n minimum: 0", -1, false),
1519 ("int64", "\n minimum: 0", 4_294_967_296, true),
1520 ("int64", "", -1, true),
1521 ];
1522 for (format, minimum, value, fits) in cases {
1523 let yaml = format!(
1524 "{PREAMBLE} Offset:\n type: integer\n format: {format}{minimum}\n enum:\n - {value}\n"
1525 );
1526 let outcome = lower_yaml(&yaml);
1527 assert_eq!(
1528 outcome.is_ok(),
1529 fits,
1530 "format `{format}`, minimum `{minimum}`, value `{value}`: got {outcome:?}"
1531 );
1532 }
1533 }
1534
1535 #[test]
1536 fn a_minimum_of_zero_or_more_gives_an_unsigned_type() {
1537 let yaml = format!(
1538 "{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"
1539 );
1540 let out = emit_yaml(&yaml);
1541 assert!(out.contains("pub count: u32"), "{out}");
1542 assert!(out.contains("pub total: u64"), "{out}");
1543 assert!(out.contains("pub plain: u64"), "{out}");
1544 assert!(out.contains("pub signed: i32"), "{out}");
1546 assert!(out.contains("pub above: u32"), "{out}");
1548 }
1549
1550 #[test]
1551 fn an_integer_without_a_minimum_stays_signed() {
1552 let yaml = format!(
1553 "{PREAMBLE} Thing:\n type: object\n required: [count]\n properties:\n count:\n type: integer\n format: int32\n maximum: 10\n"
1554 );
1555 let out = emit_yaml(&yaml);
1556 assert!(out.contains("pub count: i32"), "{out}");
1557 }
1558
1559 #[test]
1560 fn object_with_only_additional_properties_becomes_map_alias() {
1561 let yaml =
1562 format!("{PREAMBLE} Dict:\n type: object\n additionalProperties:\n type: string\n");
1563 let out = emit_yaml(&yaml);
1564 assert!(
1565 out.contains("pub type Dict = std::collections::HashMap<String, String>;"),
1566 "{out}"
1567 );
1568 }
1569
1570 #[test]
1571 fn inline_nested_object_is_hoisted() {
1572 let yaml = format!(
1573 "{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"
1574 );
1575 let out = emit_yaml(&yaml);
1576 assert!(out.contains("pub struct Outer"), "{out}");
1577 assert!(out.contains("pub inner: OuterInner"), "{out}");
1578 assert!(out.contains("pub struct OuterInner"), "{out}");
1579 assert!(out.contains("pub x: String"), "{out}");
1580 }
1581
1582 #[test]
1583 fn x_rust_type_emits_verbatim_type() {
1584 let yaml = format!(
1585 "{PREAMBLE} Holder:\n type: object\n required: [v]\n properties:\n v:\n type: string\n x-rust-type: my_crate::Custom\n"
1586 );
1587 let out = emit_yaml(&yaml);
1588 assert!(out.contains("pub v: my_crate::Custom"), "{out}");
1589 }
1590
1591 #[test]
1592 fn optional_field_is_wrapped_and_skipped() {
1593 let yaml = format!(
1594 "{PREAMBLE} Maybe:\n type: object\n properties:\n note:\n type: string\n"
1595 );
1596 let out = emit_yaml(&yaml);
1597 assert!(out.contains("skip_serializing_if = \"Option::is_none\""), "{out}");
1598 assert!(out.contains("pub note: Option<String>"), "{out}");
1599 }
1600}