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::Access;
19use crate::ir::Alias;
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
58const 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 let data = &schema.schema_data;
152
153 if let Some(verbatim) = extension_str(data, X_RUST_TYPE, name)? {
154 return Ok(Item::Alias(Alias {
155 name: self.type_name_ident(name),
156 doc: doc_of(data),
157 deprecated: deprecation_of(data, name)?,
158 ty: verbatim_type(data, verbatim, name)?,
159 }));
160 }
161
162 let item = match &schema.schema_kind {
163 SchemaKind::Type(Type::String(st)) if !st.enumeration.is_empty() => {
164 Item::Enum(self.string_enum(name, &st.enumeration, data)?)
165 }
166 SchemaKind::Type(Type::Integer(it)) if !it.enumeration.is_empty() => {
167 let repr = integer_type(it);
168 Item::Enum(self.integer_enum(name, &it.enumeration, &repr, data)?)
169 }
170 SchemaKind::Type(Type::Object(obj)) => self.object_to_item(name, obj, data)?,
171 SchemaKind::OneOf { one_of } | SchemaKind::AnyOf { any_of: one_of } => {
172 Item::Enum(self.make_union(name, one_of, data)?)
173 }
174 SchemaKind::AllOf { all_of } => match self.single_ref_all_of(all_of)? {
175 Some(target) => Item::Alias(Alias {
176 name: self.type_name_ident(name),
177 doc: doc_of(data),
178 deprecated: deprecation_of(data, name)?,
179 ty: RustType::Named(target),
180 }),
181 None => Item::Struct(self.merge_all_of(name, all_of, data)?),
182 },
183 SchemaKind::Type(_) => {
184 let ty = self.type_from_schema(name, schema)?;
185 Item::Alias(Alias {
186 name: self.type_name_ident(name),
187 doc: doc_of(data),
188 deprecated: deprecation_of(data, name)?,
189 ty,
190 })
191 }
192 SchemaKind::Any(_) => Item::Alias(Alias {
193 name: self.type_name_ident(name),
194 doc: doc_of(data),
195 deprecated: deprecation_of(data, name)?,
196 ty: RustType::Value,
197 }),
198 SchemaKind::Not { .. } => {
199 return Err(Error::UnsupportedSchema {
200 path: name.to_owned(),
201 reason: "`not` schemas are not supported".to_owned(),
202 });
203 }
204 };
205 return Ok(item);
206 }
207
208 fn object_to_item(&mut self, name: &str, obj: &ObjectType, data: &SchemaData) -> Result<Item> {
211 if obj.properties.is_empty() {
212 let element = self.additional_properties_type(name, obj)?;
213 return Ok(Item::Alias(Alias {
214 name: self.type_name_ident(name),
215 doc: doc_of(data),
216 deprecated: deprecation_of(data, name)?,
217 ty: RustType::Map(Box::new(element)),
218 }));
219 }
220 let strukt = self.object_to_struct(name, obj, data)?;
221 return Ok(Item::Struct(strukt));
222 }
223
224 fn object_to_struct(&mut self, name: &str, obj: &ObjectType, data: &SchemaData) -> Result<Struct> {
226 let mut ordered = Vec::with_capacity(obj.properties.len());
227 for (prop_name, prop) in &obj.properties {
228 let required = obj.required.iter().any(|r| {
229 return r == prop_name;
230 });
231 let order = prop_order(prop, &format!("{name}.{prop_name}"))?;
232 let field = self.field_from_prop(name, prop_name, prop, required)?;
233 ordered.push((order, field));
234 }
235 let fields = sort_by_order(ordered);
236
237 let additional_properties = match &obj.additional_properties {
238 Some(AdditionalProperties::Schema(schema)) => {
239 let ty = self.type_from_ref_schema(name, schema.as_ref())?;
240 Some(ty)
241 }
242 Some(AdditionalProperties::Any(true)) => Some(RustType::Value),
243 Some(AdditionalProperties::Any(false)) | None => None,
244 };
245 let deny_unknown_fields = matches!(&obj.additional_properties, Some(AdditionalProperties::Any(false)));
248
249 return Ok(Struct {
250 name: self.type_name_ident(name),
251 doc: doc_of(data),
252 deprecated: deprecation_of(data, name)?,
253 fields,
254 additional_properties,
255 deny_unknown_fields,
256 });
257 }
258
259 fn field_from_prop(
261 &mut self,
262 parent: &str,
263 wire: &str,
264 prop: &ReferenceOr<Box<Schema>>,
265 required: bool,
266 ) -> Result<Field> {
267 let hint = format!("{parent}_{wire}");
268 let at = format!("{parent}.{wire}");
269 let mut ty = self.type_from_schema_ref(&hint, prop)?;
270
271 let data = match prop {
272 ReferenceOr::Item(schema) => Some(&schema.schema_data),
273 ReferenceOr::Reference { .. } => None,
274 };
275
276 let declared = data
280 .filter(|_| return !required)
281 .and_then(|data| return data.default.as_ref());
282
283 let nullable = data.map(|data| return data.nullable).unwrap_or(false);
284 if (!required && declared.is_none()) || nullable {
288 ty = ty.optional();
289 }
290
291 let default = match declared {
292 Some(json) => {
293 let variants_of = |name: &str| {
294 let ident = self.type_name_ident(name);
295 return self.extra.iter().find_map(|item| {
296 return match item {
297 Item::Enum(enom) if enom.name == ident => match &enom.kind {
298 EnumKind::Strings(variants) => Some(variants.clone()),
299 EnumKind::Union(_) | EnumKind::Integers { .. } => None,
300 },
301 _ => None,
302 };
303 });
304 };
305 Some(lower_default(json, &ty, &variants_of, parent, wire)?)
306 }
307 None => None,
308 };
309
310 let doc = data.and_then(doc_of);
311 let deprecated = match data {
312 Some(data) => deprecation_of(data, &at)?,
313 None => None,
314 };
315 let serde_skip = match data {
316 Some(data) => extension_bool(data, X_RUST_SERDE_SKIP, &at)?.unwrap_or(false),
317 None => false,
318 };
319 let omit_empty = match data {
320 Some(data) => extension_bool(data, X_OMITEMPTY, &at)?,
321 None => None,
322 };
323
324 let rust_name = match data {
325 Some(data) => extension_str(data, X_RUST_NAME, &at)?,
326 None => None,
327 };
328 let ident = match rust_name {
329 Some(custom) => to_ident(custom, Case::Snake),
330 None => to_ident(wire, Case::Snake),
331 };
332 let rename = crate::naming::rename_for(wire, &ident);
333 let access = match prop {
334 ReferenceOr::Item(schema) => access_of(&schema.schema_data, &at)?,
335 ReferenceOr::Reference { reference } => match self.spec.resolve(reference) {
338 Ok(target) => access_of(&target.schema_data, &at)?,
339 Err(_) => Access::ReadWrite,
340 },
341 };
342 let constraints = match prop {
343 ReferenceOr::Item(schema) => crate::lower::constraints::constraints_of(schema),
344 ReferenceOr::Reference { reference } => self
347 .spec
348 .resolve(reference)
349 .ok()
350 .and_then(crate::lower::constraints::constraints_through_ref),
351 };
352 let field = Field {
353 name: ident,
354 rename,
355 doc,
356 deprecated,
357 ty,
358 required,
359 omit_empty,
360 serde_skip,
361 default,
362 constraints,
363 access,
364 };
365 crate::lower::constraints::check_constraints(&field)?;
366 return Ok(field);
367 }
368
369 fn single_ref_all_of(&self, members: &[ReferenceOr<Schema>]) -> Result<Option<String>> {
374 let [ReferenceOr::Reference { reference }] = members else {
375 return Ok(None);
376 };
377 let target = self.schema_ref_target(reference, "an allOf member")?;
378 return Ok(Some(target));
379 }
380
381 fn collapse_single_all_of(&mut self, hint: &str, members: &[ReferenceOr<Schema>]) -> Result<Option<RustType>> {
391 let [only] = members else {
392 return Ok(None);
393 };
394 let ty = match only {
395 ReferenceOr::Reference { reference } => {
396 let target = self.schema_ref_target(reference, "an allOf member")?;
397 RustType::Named(target)
398 }
399 ReferenceOr::Item(schema) => self.type_from_schema(hint, schema)?,
400 };
401 return Ok(Some(ty));
402 }
403
404 fn merge_all_of(&mut self, name: &str, members: &[ReferenceOr<Schema>], data: &SchemaData) -> Result<Struct> {
407 let mut merged = MergedObject::default();
408 self.absorb_members(name, members, &mut merged)?;
409
410 let mut ordered = Vec::with_capacity(merged.properties.len());
411 for (wire, prop) in &merged.properties {
412 let required = merged.required.iter().any(|r| {
413 return r == wire;
414 });
415 let order = prop_order(prop, &format!("{name}.{wire}"))?;
416 let field = self.field_from_prop(name, wire, prop, required)?;
417 ordered.push((order, field));
418 }
419 let fields = sort_by_order(ordered);
420
421 return Ok(Struct {
422 name: self.type_name_ident(name),
423 doc: doc_of(data),
424 deprecated: deprecation_of(data, name)?,
425 fields,
426 additional_properties: None,
427 deny_unknown_fields: false,
434 });
435 }
436
437 fn absorb_members(&mut self, name: &str, members: &[ReferenceOr<Schema>], merged: &mut MergedObject) -> Result<()> {
442 if self.depth >= MAX_SCHEMA_DEPTH {
443 return Err(Error::SchemaDepthExceeded {
444 path: name.to_owned(),
445 limit: MAX_SCHEMA_DEPTH,
446 });
447 }
448 self.depth += 1;
449 let result = self.absorb_members_inner(name, members, merged);
450 self.depth -= 1;
451 return result;
452 }
453
454 fn absorb_members_inner(
455 &mut self,
456 name: &str,
457 members: &[ReferenceOr<Schema>],
458 merged: &mut MergedObject,
459 ) -> Result<()> {
460 for member in members {
461 let schema = match member {
462 ReferenceOr::Item(schema) => schema,
463 ReferenceOr::Reference { reference } => self.spec.resolve(reference)?,
464 };
465 match &schema.schema_kind {
466 SchemaKind::Type(Type::Object(obj)) => merged.absorb(obj),
467 SchemaKind::AllOf { all_of } => self.absorb_members(name, all_of, merged)?,
468 SchemaKind::Type(_)
469 | SchemaKind::OneOf { .. }
470 | SchemaKind::AnyOf { .. }
471 | SchemaKind::Any(_)
472 | SchemaKind::Not { .. } => {
473 return Err(Error::UnsupportedSchema {
474 path: name.to_owned(),
475 reason: "allOf members must be objects or refs to objects".to_owned(),
476 });
477 }
478 }
479 }
480 return Ok(());
481 }
482 fn make_union(&mut self, name: &str, members: &[ReferenceOr<Schema>], data: &SchemaData) -> Result<Enum> {
483 let variants = match &data.discriminator {
484 Some(disc) if !disc.mapping.is_empty() => self.union_variants_from_mapping(disc)?,
485 Some(_) | None => self.union_variants_from_members(name, members)?,
486 };
487 check_variant_types(name, &variants)?;
488 return Ok(Enum {
489 name: self.type_name_ident(name),
490 doc: doc_of(data),
491 deprecated: deprecation_of(data, name)?,
492 kind: EnumKind::Union(variants),
493 });
494 }
495
496 fn union_variants_from_mapping(&self, disc: &Discriminator) -> Result<Vec<UnionVariant>> {
498 let mut variants = Vec::with_capacity(disc.mapping.len());
499 let mut seen = std::collections::HashSet::new();
500 for (value, reference) in &disc.mapping {
501 let target = self.schema_ref_target(reference, "a discriminator mapping")?;
502 variants.push(UnionVariant {
503 name: crate::naming::deconflict_ident(to_ident(value, Case::Pascal), &mut seen),
504 ty: RustType::Named(target),
505 });
506 }
507 return Ok(variants);
508 }
509
510 fn union_variants_from_members(
512 &mut self,
513 name: &str,
514 members: &[ReferenceOr<Schema>],
515 ) -> Result<Vec<UnionVariant>> {
516 let mut variants = Vec::with_capacity(members.len());
517 let mut seen = std::collections::HashSet::new();
518 let mut diagnostics = crate::lower::validate::Diagnostics::new();
519 for (index, member) in members.iter().enumerate() {
520 let variant = match member {
521 ReferenceOr::Reference { reference } => {
522 let target = self.schema_ref_target(reference, "a union member")?;
523 UnionVariant {
528 name: crate::naming::deconflict_ident(self.type_name_ident(&target), &mut seen),
529 ty: RustType::Named(target),
530 }
531 }
532 ReferenceOr::Item(schema) => {
533 let Some(seed) = inline_variant_seed(schema, &format!("{name}, member {index}"))? else {
534 diagnostics.push(Error::UnsupportedSchema {
535 path: name.to_owned(),
536 reason: format!("member {index} of the union gives the variant no name"),
537 });
538 continue;
539 };
540 let ty = self.type_from_schema(&format!("{name}_{seed}"), schema)?;
547 UnionVariant {
548 name: crate::naming::deconflict_ident(to_ident(&seed, Case::Pascal), &mut seen),
549 ty,
550 }
551 }
552 };
553 variants.push(variant);
554 }
555 diagnostics.into_result()?;
556 return Ok(variants);
557 }
558
559 fn string_enum(&self, name: &str, values: &[Option<String>], data: &SchemaData) -> Result<Enum> {
567 let varnames = match extension_str_array(data, X_ENUM_VARNAMES, name)? {
568 Some(names) => Some(names),
569 None => extension_str_array(data, X_ENUM_NAMES, name)?,
570 };
571 let mut diagnostics = crate::lower::validate::Diagnostics::new();
572 let mut variants = Vec::new();
573 let mut seen = std::collections::HashSet::new();
574 let mut values_seen = std::collections::HashSet::new();
575 for (index, value) in values.iter().flatten().enumerate() {
576 if !values_seen.insert(value.as_str()) {
577 diagnostics.push(Error::UnsupportedSchema {
578 path: name.to_owned(),
579 reason: format!("the `enum` gives `{value}` more than once"),
580 });
581 continue;
582 }
583 let base = match varnames.as_ref().and_then(|names| return names.get(index)) {
584 Some(custom) => to_ident(custom, Case::Pascal),
585 None => to_ident(value, Case::Pascal),
586 };
587 let ident = crate::naming::deconflict_ident(base, &mut seen);
588 let rename = crate::naming::rename_for(value, &ident);
589 variants.push(StringVariant {
590 name: ident,
591 rename,
592 doc: None,
593 });
594 }
595 diagnostics.into_result()?;
596 return Ok(Enum {
597 name: self.type_name_ident(name),
598 doc: doc_of(data),
599 deprecated: deprecation_of(data, name)?,
600 kind: EnumKind::Strings(variants),
601 });
602 }
603
604 fn integer_enum(&self, name: &str, values: &[Option<i64>], repr: &RustType, data: &SchemaData) -> Result<Enum> {
614 let varnames = match extension_str_array(data, X_ENUM_VARNAMES, name)? {
615 Some(names) => Some(names),
616 None => extension_str_array(data, X_ENUM_NAMES, name)?,
617 };
618 let mut diagnostics = crate::lower::validate::Diagnostics::new();
619 let mut variants = Vec::new();
620 let mut seen = std::collections::HashSet::new();
621 let mut values_seen = std::collections::HashSet::new();
622 for (index, value) in values.iter().flatten().enumerate() {
623 if !values_seen.insert(*value) {
624 diagnostics.push(Error::UnsupportedSchema {
625 path: name.to_owned(),
626 reason: format!("the `enum` gives `{value}` more than once"),
627 });
628 continue;
629 }
630 if !fits_repr(*value, repr) {
631 diagnostics.push(Error::UnsupportedSchema {
632 path: name.to_owned(),
633 reason: format!("the `enum` gives `{value}`, which `{}` cannot hold", repr_name(repr)),
634 });
635 continue;
636 }
637 let base = match varnames.as_ref().and_then(|names| return names.get(index)) {
638 Some(custom) => to_ident(custom, Case::Pascal),
639 None => to_ident(&integer_variant_name(*value), Case::Pascal),
640 };
641 variants.push(IntegerVariant {
642 name: crate::naming::deconflict_ident(base, &mut seen),
643 value: *value,
644 doc: None,
645 });
646 }
647 diagnostics.into_result()?;
648 return Ok(Enum {
649 name: self.type_name_ident(name),
650 doc: doc_of(data),
651 deprecated: deprecation_of(data, name)?,
652 kind: EnumKind::Integers {
653 repr: repr.clone(),
654 variants,
655 },
656 });
657 }
658
659 fn type_from_schema_ref(&mut self, hint: &str, schema: &ReferenceOr<Box<Schema>>) -> Result<RustType> {
662 match schema {
663 ReferenceOr::Reference { reference } => {
664 let target = self.schema_ref_target(reference, "a property")?;
665 return Ok(RustType::Named(target));
666 }
667 ReferenceOr::Item(schema) => {
668 let ty = self.type_from_schema(hint, schema)?;
669 return Ok(ty);
670 }
671 }
672 }
673
674 fn type_from_ref_schema(&mut self, hint: &str, schema: &ReferenceOr<Schema>) -> Result<RustType> {
677 match schema {
678 ReferenceOr::Reference { reference } => {
679 let target = self.schema_ref_target(reference, "additionalProperties")?;
680 return Ok(RustType::Named(target));
681 }
682 ReferenceOr::Item(schema) => {
683 let ty = self.type_from_schema(hint, schema)?;
684 return Ok(ty);
685 }
686 }
687 }
688
689 fn type_from_schema(&mut self, hint: &str, schema: &Schema) -> Result<RustType> {
693 if self.depth >= MAX_SCHEMA_DEPTH {
694 return Err(Error::SchemaDepthExceeded {
695 path: hint.to_owned(),
696 limit: MAX_SCHEMA_DEPTH,
697 });
698 }
699 self.depth += 1;
700 let result = self.type_from_schema_inner(hint, schema);
701 self.depth -= 1;
702 return result;
703 }
704
705 fn type_from_schema_inner(&mut self, hint: &str, schema: &Schema) -> Result<RustType> {
706 let data = &schema.schema_data;
707 if let Some(verbatim) = extension_str(data, X_RUST_TYPE, hint)? {
708 return verbatim_type(data, verbatim, hint);
709 }
710
711 let ty = match &schema.schema_kind {
712 SchemaKind::Type(Type::String(st)) if !st.enumeration.is_empty() => {
713 let enom = self.string_enum(hint, &st.enumeration, data)?;
714 self.extra.push(Item::Enum(enom));
715 RustType::Named(hint.to_owned())
716 }
717 SchemaKind::Type(Type::String(st)) => string_format_type(&st.format),
718 SchemaKind::Type(Type::Integer(it)) if !it.enumeration.is_empty() => {
719 let repr = integer_type(it);
720 let enom = self.integer_enum(hint, &it.enumeration, &repr, data)?;
721 self.extra.push(Item::Enum(enom));
722 RustType::Named(hint.to_owned())
723 }
724 SchemaKind::Type(Type::Integer(it)) => integer_type(it),
725 SchemaKind::Type(Type::Number(_)) => RustType::F64,
726 SchemaKind::Type(Type::Boolean(_)) => RustType::Bool,
727 SchemaKind::Type(Type::Array(at)) => {
728 let element = match &at.items {
729 Some(items) => {
730 let item_hint = format!("{hint}_item");
731 self.type_from_schema_ref(&item_hint, items)?
732 }
733 None => RustType::Value,
734 };
735 RustType::Vec(Box::new(element))
736 }
737 SchemaKind::Type(Type::Object(obj)) => self.inline_object_type(hint, obj, data)?,
738 SchemaKind::OneOf { one_of } | SchemaKind::AnyOf { any_of: one_of } => {
739 let enom = self.make_union(hint, one_of, data)?;
740 self.extra.push(Item::Enum(enom));
741 RustType::Named(hint.to_owned())
742 }
743 SchemaKind::AllOf { all_of } => {
744 if let Some(ty) = self.collapse_single_all_of(hint, all_of)? {
745 ty
746 } else {
747 let strukt = self.merge_all_of(hint, all_of, data)?;
748 self.extra.push(Item::Struct(strukt));
749 RustType::Named(hint.to_owned())
750 }
751 }
752 SchemaKind::Any(_) => RustType::Value,
753 SchemaKind::Not { .. } => {
754 return Err(Error::UnsupportedSchema {
755 path: hint.to_owned(),
756 reason: "`not` schemas are not supported".to_owned(),
757 });
758 }
759 };
760 return Ok(ty);
761 }
762
763 fn inline_object_type(&mut self, hint: &str, obj: &ObjectType, data: &SchemaData) -> Result<RustType> {
766 if obj.properties.is_empty() {
767 let element = self.additional_properties_type(hint, obj)?;
768 return Ok(RustType::Map(Box::new(element)));
769 }
770 let strukt = self.object_to_struct(hint, obj, data)?;
771 self.extra.push(Item::Struct(strukt));
772 return Ok(RustType::Named(hint.to_owned()));
773 }
774
775 fn additional_properties_type(&mut self, hint: &str, obj: &ObjectType) -> Result<RustType> {
777 let element = match &obj.additional_properties {
778 Some(AdditionalProperties::Schema(schema)) => self.type_from_ref_schema(hint, schema.as_ref())?,
779 Some(AdditionalProperties::Any(_)) | None => RustType::Value,
780 };
781 return Ok(element);
782 }
783}
784
785#[derive(Default)]
787struct MergedObject {
788 properties: indexmap::IndexMap<String, ReferenceOr<Box<Schema>>>,
789 required: Vec<String>,
790}
791
792impl MergedObject {
793 fn absorb(&mut self, obj: &ObjectType) {
795 for (name, prop) in &obj.properties {
796 self.properties.insert(name.clone(), prop.clone());
797 }
798 for req in &obj.required {
799 if !self.required.contains(req) {
800 self.required.push(req.clone());
801 }
802 }
803 }
804}
805
806pub(crate) fn string_format_type(format: &VariantOrUnknownOrEmpty<StringFormat>) -> RustType {
808 let ty = match format {
809 VariantOrUnknownOrEmpty::Item(StringFormat::Date) => RustType::Date,
810 VariantOrUnknownOrEmpty::Item(StringFormat::DateTime) => RustType::DateTime,
811 VariantOrUnknownOrEmpty::Item(StringFormat::Byte) => RustType::Bytes,
812 VariantOrUnknownOrEmpty::Item(StringFormat::Binary) => RustType::Bytes,
813 VariantOrUnknownOrEmpty::Item(StringFormat::Password) => RustType::String,
814 VariantOrUnknownOrEmpty::Unknown(name) if name == "uuid" => RustType::Uuid,
815 VariantOrUnknownOrEmpty::Unknown(_) => RustType::String,
816 VariantOrUnknownOrEmpty::Empty => RustType::String,
817 };
818 return ty;
819}
820
821fn fits_repr(value: i64, repr: &RustType) -> bool {
826 return match *repr {
827 RustType::I32 => i32::try_from(value).is_ok(),
828 RustType::U32 => u32::try_from(value).is_ok(),
829 RustType::U64 => u64::try_from(value).is_ok(),
830 _ => true,
831 };
832}
833
834fn repr_name(repr: &RustType) -> &'static str {
836 return match *repr {
837 RustType::I32 => "i32",
838 RustType::U32 => "u32",
839 RustType::U64 => "u64",
840 _ => "i64",
841 };
842}
843
844fn integer_variant_name(value: i64) -> String {
846 if value < 0 {
847 return format!("value_minus_{}", value.unsigned_abs());
848 }
849 return format!("value_{value}");
850}
851
852fn inline_variant_seed(schema: &Schema, at: &str) -> Result<Option<String>> {
863 if let Some(custom) = extension_str(&schema.schema_data, X_RUST_NAME, at)? {
864 return Ok(Some(custom.to_owned()));
865 }
866 if let Some(value) = single_enum_value(schema) {
867 return Ok(Some(value));
868 }
869 let Some(ty) = non_hoisting_type(schema) else {
870 return Ok(None);
871 };
872 return Ok(type_variant_name(&ty).map(str::to_owned));
873}
874
875fn single_enum_value(schema: &Schema) -> Option<String> {
882 let SchemaKind::Type(Type::String(st)) = &schema.schema_kind else {
883 return None;
884 };
885 let [Some(value)] = st.enumeration.as_slice() else {
886 return None;
887 };
888 return Some(value.clone());
889}
890
891fn non_hoisting_type(schema: &Schema) -> Option<RustType> {
895 return match &schema.schema_kind {
896 SchemaKind::Type(Type::String(st)) if st.enumeration.is_empty() => Some(string_format_type(&st.format)),
897 SchemaKind::Type(Type::Integer(it)) if it.enumeration.is_empty() => Some(integer_type(it)),
898 SchemaKind::Type(Type::Number(_)) => Some(RustType::F64),
899 SchemaKind::Type(Type::Boolean(_)) => Some(RustType::Bool),
900 _ => None,
901 };
902}
903
904fn type_variant_name(ty: &RustType) -> Option<&'static str> {
908 return match ty {
909 RustType::Bool => Some("Bool"),
910 RustType::I32 => Some("I32"),
911 RustType::I64 => Some("I64"),
912 RustType::U32 => Some("U32"),
913 RustType::U64 => Some("U64"),
914 RustType::F64 => Some("F64"),
915 RustType::String => Some("String"),
916 RustType::Date => Some("Date"),
917 RustType::DateTime => Some("DateTime"),
918 RustType::Uuid => Some("Uuid"),
919 RustType::Bytes => Some("Bytes"),
920 _ => None,
921 };
922}
923
924fn check_variant_types(name: &str, variants: &[UnionVariant]) -> Result<()> {
931 let mut diagnostics = crate::lower::validate::Diagnostics::new();
932 for (index, variant) in variants.iter().enumerate() {
933 let Some(earlier) = variants.iter().take(index).find(|other| return other.ty == variant.ty) else {
934 continue;
935 };
936 diagnostics.push(Error::UnsupportedSchema {
937 path: name.to_owned(),
938 reason: format!(
939 "the union holds `{}` twice, as `{}` and as `{}`",
940 variant.ty.label(),
941 earlier.name.logical(),
942 variant.name.logical()
943 ),
944 });
945 }
946 return diagnostics.into_result();
947}
948
949pub(crate) fn integer_type(it: &IntegerType) -> RustType {
961 let unsigned = matches!(crate::lower::constraints::inclusive_minimum(it), Some(minimum) if minimum >= 0);
962 let ty = match (&it.format, unsigned) {
963 (VariantOrUnknownOrEmpty::Item(IntegerFormat::Int32), false) => RustType::I32,
964 (VariantOrUnknownOrEmpty::Item(IntegerFormat::Int32), true) => RustType::U32,
965 (VariantOrUnknownOrEmpty::Item(IntegerFormat::Int64), false)
966 | (VariantOrUnknownOrEmpty::Unknown(_) | VariantOrUnknownOrEmpty::Empty, false) => RustType::I64,
967 (VariantOrUnknownOrEmpty::Item(IntegerFormat::Int64), true)
968 | (VariantOrUnknownOrEmpty::Unknown(_) | VariantOrUnknownOrEmpty::Empty, true) => RustType::U64,
969 };
970 return ty;
971}
972
973fn extension_str<'a>(data: &'a SchemaData, key: &str, at: &str) -> Result<Option<&'a str>> {
975 return crate::lower::extension::str_value(&data.extensions, key, at);
976}
977
978fn extension_bool(data: &SchemaData, key: &str, at: &str) -> Result<Option<bool>> {
980 return crate::lower::extension::bool_value(&data.extensions, key, at);
981}
982
983fn extension_i64(data: &SchemaData, key: &str, at: &str) -> Result<Option<i64>> {
985 return crate::lower::extension::i64_value(&data.extensions, key, at);
986}
987
988fn prop_order(prop: &ReferenceOr<Box<Schema>>, at: &str) -> Result<Option<i64>> {
990 return match prop {
991 ReferenceOr::Item(schema) => extension_i64(&schema.schema_data, X_ORDER, at),
992 ReferenceOr::Reference { .. } => Ok(None),
993 };
994}
995
996fn sort_by_order(mut fields: Vec<(Option<i64>, Field)>) -> Vec<Field> {
1000 fields.sort_by_key(|(order, _)| {
1001 return order.unwrap_or(i64::MAX);
1002 });
1003 return fields.into_iter().map(|(_, field)| return field).collect();
1004}
1005
1006fn extension_str_array<'a>(data: &'a SchemaData, key: &str, at: &str) -> Result<Option<Vec<&'a str>>> {
1008 return crate::lower::extension::str_list_value(&data.extensions, key, at);
1009}
1010
1011const FOREIGN_DERIVE_NAMES: [&str; 3] = ["Debug", "Clone", "PartialEq"];
1017
1018fn foreign_derives_of(data: &SchemaData, path: &str) -> Result<ForeignDerives> {
1030 let Some(value) = data.extensions.get(X_RUST_DERIVE) else {
1031 return Ok(ForeignDerives::default());
1032 };
1033 let Some(array) = value.as_array() else {
1034 return Err(Error::UnsupportedSchema {
1035 path: path.to_owned(),
1036 reason: format!("`{X_RUST_DERIVE}` must be a list of trait names, for example `[Debug, Clone]`"),
1037 });
1038 };
1039
1040 let mut derives = ForeignDerives {
1041 debug: false,
1042 clone: false,
1043 partial_eq: false,
1044 };
1045 for entry in array {
1046 let Some(name) = entry.as_str() else {
1047 return Err(Error::UnsupportedSchema {
1048 path: path.to_owned(),
1049 reason: format!("every `{X_RUST_DERIVE}` entry must be a trait name written as a string"),
1050 });
1051 };
1052 match name {
1053 "Debug" => derives.debug = true,
1054 "Clone" => derives.clone = true,
1055 "PartialEq" => derives.partial_eq = true,
1056 other => {
1057 let known = FOREIGN_DERIVE_NAMES.join(", ");
1058 return Err(Error::UnsupportedSchema {
1059 path: path.to_owned(),
1060 reason: format!("`{X_RUST_DERIVE}` does not accept `{other}`. It accepts only {known}"),
1061 });
1062 }
1063 }
1064 }
1065 return Ok(derives);
1066}
1067
1068fn verbatim_type(data: &SchemaData, verbatim: &str, path: &str) -> Result<RustType> {
1072 return Ok(RustType::Verbatim {
1073 text: verbatim.to_owned(),
1074 derives: foreign_derives_of(data, path)?,
1075 });
1076}
1077
1078pub(crate) fn access_of(data: &SchemaData, at: &str) -> Result<Access> {
1083 return match (data.read_only, data.write_only) {
1084 (true, true) => Err(Error::UnsupportedSchema {
1085 path: at.to_owned(),
1086 reason: "`readOnly` and `writeOnly` are both set, so no request and no response could \
1087 carry the property. Set at most one of the two."
1088 .to_owned(),
1089 }),
1090 (true, false) => Ok(Access::ReadOnly),
1091 (false, true) => Ok(Access::WriteOnly),
1092 (false, false) => Ok(Access::ReadWrite),
1093 };
1094}
1095
1096fn deprecation_of(data: &SchemaData, at: &str) -> Result<Option<Deprecation>> {
1100 if !data.deprecated {
1101 return Ok(None);
1102 }
1103 let note = extension_str(data, X_DEPRECATED_REASON, at)?.map(str::to_owned);
1104 return Ok(Some(Deprecation { note }));
1105}
1106
1107fn doc_of(data: &SchemaData) -> Option<String> {
1109 let text = data.description.as_ref()?;
1110 let trimmed = text.trim();
1111 if trimmed.is_empty() {
1112 return None;
1113 }
1114 return Some(trimmed.to_owned());
1115}
1116
1117#[cfg(test)]
1118mod tests {
1119 use std::path::PathBuf;
1120
1121 use super::*;
1122
1123 fn emit_yaml(yaml: &str) -> String {
1125 let doc: openapiv3::OpenAPI = serde_yaml::from_str(yaml).expect("parse spec");
1126 let spec = Spec::from_parts(doc, PathBuf::from("inline.yaml"));
1127 let module = lower_models(&spec).expect("map schemas");
1128 return crate::emit::emit_module(&module, None).expect("emit module");
1129 }
1130
1131 fn lower_yaml(yaml: &str) -> Result<Module> {
1133 let doc: openapiv3::OpenAPI = serde_yaml::from_str(yaml).expect("parse spec");
1134 let spec = Spec::from_parts(doc, PathBuf::from("inline.yaml"));
1135 return lower_models(&spec);
1136 }
1137
1138 fn lower_models(spec: &Spec) -> Result<Module> {
1141 let names = crate::lower::rename::type_renames(spec, None)?;
1142 return generate_models(spec, &names);
1143 }
1144
1145 const PREAMBLE: &str = "openapi: 3.0.3\ninfo:\n title: t\n version: '1'\npaths: {}\ncomponents:\n schemas:\n";
1146
1147 fn nested_array_schema(depth: usize) -> Schema {
1151 let mut kind = SchemaKind::Type(Type::String(Default::default()));
1152 for _ in 0..depth {
1153 let items = ReferenceOr::Item(Box::new(Schema {
1154 schema_data: SchemaData::default(),
1155 schema_kind: kind,
1156 }));
1157 kind = SchemaKind::Type(Type::Array(openapiv3::ArrayType {
1158 items: Some(items),
1159 min_items: None,
1160 max_items: None,
1161 unique_items: false,
1162 }));
1163 }
1164 return Schema {
1165 schema_data: SchemaData::default(),
1166 schema_kind: kind,
1167 };
1168 }
1169
1170 fn spec_with_schema(name: &str, schema: Schema) -> Spec {
1171 let empty_doc = "openapi: 3.0.3\ninfo:\n title: t\n version: '1'\npaths: {}\n";
1172 let mut doc: openapiv3::OpenAPI = serde_yaml::from_str(empty_doc).expect("parse preamble");
1173 doc.components
1174 .get_or_insert_with(Default::default)
1175 .schemas
1176 .insert(name.to_owned(), ReferenceOr::Item(schema));
1177 return Spec::from_parts(doc, PathBuf::from("inline.yaml"));
1178 }
1179 #[test]
1184 fn schema_at_the_depth_limit_errors_instead_of_overflowing() {
1185 let spec = spec_with_schema("Deep", nested_array_schema(MAX_SCHEMA_DEPTH));
1186 let err = lower_models(&spec).expect_err("reaching the limit should hit the depth guard");
1187 assert!(
1188 matches!(err, Error::SchemaDepthExceeded { limit, .. } if limit == MAX_SCHEMA_DEPTH),
1189 "expected SchemaDepthExceeded, got {err:?}"
1190 );
1191 }
1192
1193 #[test]
1194 fn schema_just_under_the_depth_limit_still_lowers() {
1195 let spec = spec_with_schema("Deep", nested_array_schema(MAX_SCHEMA_DEPTH - 1));
1196 lower_models(&spec).expect("just under the limit should lower cleanly");
1197 }
1198
1199 fn lower_error(yaml: &str) -> Error {
1201 let doc: openapiv3::OpenAPI = serde_yaml::from_str(yaml).expect("parse spec");
1202 let spec = Spec::from_parts(doc, PathBuf::from("inline.yaml"));
1203 return lower_models(&spec).expect_err("the spec should not lower");
1204 }
1205
1206 fn bad_order_schema(name: &str) -> String {
1208 return format!(
1209 " {name}:\n type: object\n properties:\n id:\n type: string\n x-order: 'first'\n"
1210 );
1211 }
1212
1213 #[test]
1214 fn every_bad_schema_is_reported_in_one_run() {
1215 let alpha = bad_order_schema("Alpha");
1219 let beta = bad_order_schema("Beta");
1220 let gamma = bad_order_schema("Gamma");
1221 let err = lower_error(&format!("{PREAMBLE}{alpha}{beta}{gamma}"));
1222 let Error::Validation { problems } = &err else {
1223 panic!("expected Validation, got: {err:?}");
1224 };
1225 assert_eq!(problems.len(), 3, "every bad schema should be reported");
1226 let message = err.to_string();
1227 for name in ["Alpha", "Beta", "Gamma"] {
1228 assert!(message.contains(name), "message should name `{name}`: {message}");
1229 }
1230 }
1231
1232 #[test]
1233 fn a_good_schema_beside_a_bad_one_does_not_add_a_problem() {
1234 let alpha = bad_order_schema("Alpha");
1235 let err = lower_error(&format!("{PREAMBLE}{alpha} Beta:\n type: string\n"));
1236 assert!(
1237 matches!(&err, Error::InvalidExtensionValue { at, .. } if at == "Alpha.id"),
1238 "one problem should stay unwrapped, got: {err:?}",
1239 );
1240 }
1241
1242 #[test]
1243 fn a_union_member_holding_one_enum_value_is_named_by_that_value() {
1244 let out = emit_yaml(&format!(
1247 "{PREAMBLE} Signal:\n oneOf:\n - type: string\n enum: [red]\n - type: string\n enum: [amber]\n"
1248 ));
1249 assert!(out.contains("Red(SignalRed)"), "expected a named variant, got: {out}");
1250 assert!(
1251 out.contains("Amber(SignalAmber)"),
1252 "expected a named variant, got: {out}"
1253 );
1254 assert!(
1257 out.contains("rename = \"red\""),
1258 "the hoisted type should keep the wire value, got: {out}"
1259 );
1260 }
1261
1262 #[test]
1263 fn a_hoisted_union_member_type_carries_the_union_name() {
1264 let out = emit_yaml(&format!(
1267 "{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"
1268 ));
1269 assert!(out.contains("struct LeftUnknown"), "expected LeftUnknown, got: {out}");
1270 assert!(out.contains("struct RightUnknown"), "expected RightUnknown, got: {out}");
1271 assert!(
1273 out.contains("Unknown(LeftUnknown)"),
1274 "expected a short variant, got: {out}"
1275 );
1276 }
1277
1278 fn derive_error(value: &str) -> Error {
1281 let yaml = format!(
1282 "{PREAMBLE} Target:\n type: string\n x-rust-type: crate::Foreign\n x-rust-derive: {value}\n"
1283 );
1284 let doc: openapiv3::OpenAPI = serde_yaml::from_str(&yaml).expect("parse spec");
1285 let spec = Spec::from_parts(doc, PathBuf::from("inline.yaml"));
1286 return lower_models(&spec).expect_err("the extension should reject this value");
1287 }
1288
1289 #[test]
1290 fn absent_x_rust_derive_claims_every_trait() {
1291 let out = emit_yaml(&format!(
1294 "{PREAMBLE} Holder:\n type: object\n required: [value]\n properties:\n value:\n type: string\n x-rust-type: crate::Foreign\n"
1295 ));
1296 assert!(
1297 out.contains("#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]"),
1298 "absent key should change nothing:\n{out}"
1299 );
1300 }
1301
1302 #[test]
1303 fn empty_x_rust_derive_drops_every_trait() {
1304 let out = emit_yaml(&format!(
1305 "{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"
1306 ));
1307 assert!(
1308 out.contains("#[derive(serde::Serialize, serde::Deserialize)]"),
1309 "an empty list claims nothing, so only the serde derives remain:\n{out}"
1310 );
1311 }
1312
1313 #[test]
1314 fn partial_x_rust_derive_keeps_only_the_listed_traits() {
1315 let out = emit_yaml(&format!(
1316 "{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"
1317 ));
1318 assert!(
1319 out.contains("#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq)]"),
1320 "Clone was not listed, so it is dropped:\n{out}"
1321 );
1322 }
1323
1324 #[test]
1325 fn x_rust_derive_that_is_not_a_list_is_rejected() {
1326 let err = derive_error("Debug");
1327 assert!(
1328 matches!(&err, Error::UnsupportedSchema { reason, .. } if reason.contains("must be a list")),
1329 "expected a list-shape error, got {err:?}"
1330 );
1331 }
1332
1333 #[test]
1334 fn x_rust_derive_entry_that_is_not_a_string_is_rejected() {
1335 let err = derive_error("[7]");
1336 assert!(
1337 matches!(&err, Error::UnsupportedSchema { reason, .. } if reason.contains("written as a string")),
1338 "expected a string-entry error, got {err:?}"
1339 );
1340 }
1341
1342 #[test]
1343 fn misspelled_trait_name_is_an_error_and_not_an_ignored_key() {
1344 let err = derive_error("[Parialeq]");
1348 assert!(
1349 matches!(&err, Error::UnsupportedSchema { reason, .. } if reason.contains("Parialeq")),
1350 "expected the unknown name in the error, got {err:?}"
1351 );
1352 }
1353
1354 #[test]
1355 fn maps_string_and_integer_formats() {
1356 let yaml = format!(
1357 "{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"
1358 );
1359 let out = emit_yaml(&yaml);
1360 assert!(out.contains("pub day: chrono::NaiveDate"), "{out}");
1361 assert!(out.contains("pub at: chrono::DateTime<chrono::Utc>"), "{out}");
1362 assert!(out.contains("pub id: uuid::Uuid"), "{out}");
1363 assert!(out.contains("pub blob: Vec<u8>"), "{out}");
1364 assert!(out.contains("pub big: i64"), "{out}");
1365 }
1366
1367 #[test]
1368 fn an_unsigned_field_keeps_every_check_the_type_does_not_already_make() {
1369 let yaml = format!(
1370 "{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"
1371 );
1372 let out = emit_yaml(&yaml);
1373 assert!(out.contains("pub count: u32"), "{out}");
1374 assert!(out.contains("`count` must be 130 or less"), "{out}");
1376 assert!(!out.contains("must be 0 or more"), "{out}");
1380 assert!(!out.contains("*item < 0"), "{out}");
1381 }
1382
1383 #[test]
1384 fn an_exclusive_bound_moves_onto_the_whole_number_beside_it() {
1385 let cases: [(&str, &str, &str, Option<&str>); 5] = [
1389 ("minimum: -1\n exclusiveMinimum: true", "int32", "u32", None),
1390 (
1391 "minimum: 0\n exclusiveMinimum: true",
1392 "int32",
1393 "u32",
1394 Some("must be 1 or more"),
1395 ),
1396 (
1397 "minimum: -2\n exclusiveMinimum: true",
1398 "int32",
1399 "i32",
1400 Some("must be -1 or more"),
1401 ),
1402 (
1403 "maximum: 2147483648\n exclusiveMaximum: true",
1404 "int32",
1405 "i32",
1406 None,
1407 ),
1408 ("minimum: -1\n exclusiveMinimum: true", "int64", "u64", None),
1409 ];
1410 for (bound, format, ty, message) in cases {
1411 let yaml = format!(
1412 "{PREAMBLE} Thing:\n type: object\n required: [count]\n properties:\n count:\n type: integer\n format: {format}\n {bound}\n"
1413 );
1414 let out = emit_yaml(&yaml);
1415 assert!(out.contains(&format!("pub count: {ty}")), "{bound}: {out}");
1416 match message {
1417 Some(text) => assert!(out.contains(text), "{bound}: {out}"),
1418 None => assert!(!out.contains("must be"), "{bound}: {out}"),
1419 }
1420 }
1421 }
1422
1423 #[test]
1424 fn a_bound_that_lands_on_the_limit_of_the_type_writes_no_check() {
1425 let yaml = format!(
1428 "{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"
1429 );
1430 let out = emit_yaml(&yaml);
1431 assert!(out.contains("pub count: i32"), "{out}");
1432 assert!(!out.contains("must be"), "{out}");
1433 }
1434
1435 #[test]
1436 fn bounds_that_meet_nowhere_are_refused() {
1437 let cases: [(&str, &str, Option<&str>); 13] = [
1442 (
1443 "integer\n format: int32",
1444 "maximum: -2147483648\n exclusiveMaximum: true",
1445 Some("nothing lies below `-2147483648`, where `i32` starts"),
1446 ),
1447 (
1448 "integer\n format: int32",
1449 "maximum: -2147483647\n exclusiveMaximum: true",
1450 None,
1451 ),
1452 (
1453 "integer",
1454 "minimum: 10\n maximum: 5",
1455 Some("they allow `10` to `5`"),
1456 ),
1457 ("integer", "minimum: 5\n maximum: 5", None),
1458 (
1459 "integer",
1460 "maximum: -9223372036854775808\n exclusiveMaximum: true",
1461 Some("nothing lies below `-9223372036854775808`, where `i64` starts"),
1462 ),
1463 (
1464 "integer\n format: int32",
1465 "minimum: 4294967295\n exclusiveMinimum: true",
1466 Some("nothing lies above `4294967295`, where `u32` stops"),
1467 ),
1468 (
1469 "integer\n format: int32",
1470 "minimum: 2147483647\n maximum: 2147483647",
1471 None,
1472 ),
1473 (
1475 "integer\n format: int64",
1476 "minimum: 9223372036854775807\n exclusiveMinimum: true",
1477 None,
1478 ),
1479 (
1481 "integer\n format: int32",
1482 "minimum: 9223372036854775807\n exclusiveMinimum: true",
1483 Some("nothing lies above `9223372036854775807`, where `u32` stops"),
1484 ),
1485 (
1487 "number",
1488 "minimum: 10\n maximum: 5",
1489 Some("they allow `10` to `5`"),
1490 ),
1491 (
1492 "number",
1493 "minimum: 5\n maximum: 5\n exclusiveMinimum: true",
1494 Some("they meet at `5`, which an `exclusive` flag then leaves out"),
1495 ),
1496 (
1497 "number",
1498 "minimum: 0\n maximum: 1\n exclusiveMaximum: true",
1499 None,
1500 ),
1501 (
1503 "integer\n format: int32",
1504 "maximum: -5000000000",
1505 Some("the `maximum` value `-5000000000` does not fit `i32`"),
1506 ),
1507 ];
1508 for (kind, bounds, fault) in cases {
1509 let yaml = format!(
1510 "{PREAMBLE} Thing:\n type: object\n required: [count]\n properties:\n count:\n type: {kind}\n {bounds}\n"
1511 );
1512 let outcome = lower_yaml(&yaml);
1513 match fault {
1514 Some(text) => {
1515 let error = outcome.expect_err(bounds).to_string();
1516 assert!(error.contains(text), "{bounds}: {error}");
1517 }
1518 None => assert!(outcome.is_ok(), "{bounds}: got {outcome:?}"),
1519 }
1520 }
1521 }
1522
1523 #[test]
1524 fn a_negative_multiple_of_reports_one_fault_only() {
1525 let yaml = format!(
1529 "{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"
1530 );
1531 let fault = lower_yaml(&yaml).expect_err("refuse the step").to_string();
1532 assert!(fault.contains("is not above zero"), "{fault}");
1533 assert!(!fault.contains("does not fit"), "{fault}");
1534 }
1535
1536 #[test]
1537 fn an_enum_value_must_fit_the_repr_the_format_and_the_minimum_choose() {
1538 let cases: [(&str, &str, i64, bool); 8] = [
1542 ("int32", "", -1, true),
1543 ("int32", "", 4_294_967_296, false),
1544 ("int32", "\n minimum: 0", -1, false),
1545 ("int32", "\n minimum: 0", 5, true),
1546 ("int32", "\n minimum: 0", 4_294_967_296, false),
1547 ("int64", "\n minimum: 0", -1, false),
1548 ("int64", "\n minimum: 0", 4_294_967_296, true),
1549 ("int64", "", -1, true),
1550 ];
1551 for (format, minimum, value, fits) in cases {
1552 let yaml = format!(
1553 "{PREAMBLE} Offset:\n type: integer\n format: {format}{minimum}\n enum:\n - {value}\n"
1554 );
1555 let outcome = lower_yaml(&yaml);
1556 assert_eq!(
1557 outcome.is_ok(),
1558 fits,
1559 "format `{format}`, minimum `{minimum}`, value `{value}`: got {outcome:?}"
1560 );
1561 }
1562 }
1563
1564 #[test]
1565 fn a_minimum_of_zero_or_more_gives_an_unsigned_type() {
1566 let yaml = format!(
1567 "{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"
1568 );
1569 let out = emit_yaml(&yaml);
1570 assert!(out.contains("pub count: u32"), "{out}");
1571 assert!(out.contains("pub total: u64"), "{out}");
1572 assert!(out.contains("pub plain: u64"), "{out}");
1573 assert!(out.contains("pub signed: i32"), "{out}");
1575 assert!(out.contains("pub above: u32"), "{out}");
1577 }
1578
1579 #[test]
1580 fn an_integer_without_a_minimum_stays_signed() {
1581 let yaml = format!(
1582 "{PREAMBLE} Thing:\n type: object\n required: [count]\n properties:\n count:\n type: integer\n format: int32\n maximum: 10\n"
1583 );
1584 let out = emit_yaml(&yaml);
1585 assert!(out.contains("pub count: i32"), "{out}");
1586 }
1587
1588 #[test]
1589 fn object_with_only_additional_properties_becomes_map_alias() {
1590 let yaml =
1591 format!("{PREAMBLE} Dict:\n type: object\n additionalProperties:\n type: string\n");
1592 let out = emit_yaml(&yaml);
1593 assert!(
1594 out.contains("pub type Dict = std::collections::HashMap<String, String>;"),
1595 "{out}"
1596 );
1597 }
1598
1599 #[test]
1600 fn inline_nested_object_is_hoisted() {
1601 let yaml = format!(
1602 "{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"
1603 );
1604 let out = emit_yaml(&yaml);
1605 assert!(out.contains("pub struct Outer"), "{out}");
1606 assert!(out.contains("pub inner: OuterInner"), "{out}");
1607 assert!(out.contains("pub struct OuterInner"), "{out}");
1608 assert!(out.contains("pub x: String"), "{out}");
1609 }
1610
1611 #[test]
1612 fn x_rust_type_emits_verbatim_type() {
1613 let yaml = format!(
1614 "{PREAMBLE} Holder:\n type: object\n required: [v]\n properties:\n v:\n type: string\n x-rust-type: my_crate::Custom\n"
1615 );
1616 let out = emit_yaml(&yaml);
1617 assert!(out.contains("pub v: my_crate::Custom"), "{out}");
1618 }
1619
1620 #[test]
1621 fn optional_field_is_wrapped_and_skipped() {
1622 let yaml = format!(
1623 "{PREAMBLE} Maybe:\n type: object\n properties:\n note:\n type: string\n"
1624 );
1625 let out = emit_yaml(&yaml);
1626 assert!(out.contains("skip_serializing_if = \"Option::is_none\""), "{out}");
1627 assert!(out.contains("pub note: Option<String>"), "{out}");
1628 }
1629}