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