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
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 if schema.schema_data.nullable {
152 let mut non_null = schema.clone();
153 non_null.schema_data.nullable = false;
154 let value_name = format!("{name}Value");
155 let item = self.named_to_item(&value_name, &non_null)?;
156 let ty = match item {
157 Item::Alias(alias) => alias.ty,
158 item => {
159 self.extra.push(item);
160 RustType::Named(value_name)
161 }
162 };
163 return Ok(Item::Alias(Alias {
164 name: self.type_name_ident(name),
165 doc: doc_of(&schema.schema_data),
166 deprecated: deprecation_of(&schema.schema_data, name)?,
167 ty: nullable_type(self.spec, ty),
168 }));
169 }
170 let data = &schema.schema_data;
171
172 if let Some(verbatim) = extension_str(data, X_RUST_TYPE, name)? {
173 return Ok(Item::Alias(Alias {
174 name: self.type_name_ident(name),
175 doc: doc_of(data),
176 deprecated: deprecation_of(data, name)?,
177 ty: verbatim_type(data, verbatim, name)?,
178 }));
179 }
180
181 let item = match &schema.schema_kind {
182 SchemaKind::Type(Type::String(st)) if !st.enumeration.is_empty() => {
183 Item::Enum(self.string_enum(name, &st.enumeration, data)?)
184 }
185 SchemaKind::Type(Type::Integer(it)) if !it.enumeration.is_empty() => {
186 let repr = integer_type(it);
187 Item::Enum(self.integer_enum(name, &it.enumeration, &repr, data)?)
188 }
189 SchemaKind::Type(Type::Object(obj)) => self.object_to_item(name, obj, data)?,
190 SchemaKind::OneOf { one_of } | SchemaKind::AnyOf { any_of: one_of } => Item::Enum(self.make_union(
191 name,
192 one_of,
193 data,
194 matches!(schema.schema_kind, SchemaKind::AnyOf { .. }),
195 )?),
196 SchemaKind::AllOf { all_of } => match self.single_ref_all_of(all_of)? {
197 Some(target) => Item::Alias(Alias {
198 name: self.type_name_ident(name),
199 doc: doc_of(data),
200 deprecated: deprecation_of(data, name)?,
201 ty: RustType::Named(target),
202 }),
203 None => Item::Struct(self.merge_all_of(name, all_of, data)?),
204 },
205 SchemaKind::Type(_) => {
206 let ty = self.type_from_schema(name, schema)?;
207 Item::Alias(Alias {
208 name: self.type_name_ident(name),
209 doc: doc_of(data),
210 deprecated: deprecation_of(data, name)?,
211 ty,
212 })
213 }
214 SchemaKind::Any(schema) => Item::Alias(Alias {
215 name: self.type_name_ident(name),
216 doc: doc_of(data),
217 deprecated: deprecation_of(data, name)?,
218 ty: self.unconstrained_type(name, schema),
219 }),
220 SchemaKind::Not { .. } => {
221 return Err(Error::UnsupportedSchema {
222 path: name.to_owned(),
223 reason: "`not` schemas are not supported".to_owned(),
224 });
225 }
226 };
227 return Ok(item);
228 }
229
230 fn object_to_item(&mut self, name: &str, obj: &ObjectType, data: &SchemaData) -> Result<Item> {
233 if obj.properties.is_empty() {
234 let element = self.additional_properties_type(name, obj)?;
235 return Ok(Item::Alias(Alias {
236 name: self.type_name_ident(name),
237 doc: doc_of(data),
238 deprecated: deprecation_of(data, name)?,
239 ty: RustType::Map(Box::new(element)),
240 }));
241 }
242 let strukt = self.object_to_struct(name, obj, data)?;
243 return Ok(Item::Struct(strukt));
244 }
245
246 fn object_to_struct(&mut self, name: &str, obj: &ObjectType, data: &SchemaData) -> Result<Struct> {
248 let mut ordered = Vec::with_capacity(obj.properties.len());
249 for (prop_name, prop) in &obj.properties {
250 let required = obj.required.iter().any(|r| {
251 return r == prop_name;
252 });
253 let order = prop_order(prop, &format!("{name}.{prop_name}"))?;
254 let field = self.field_from_prop(name, prop_name, prop, required)?;
255 ordered.push((order, field));
256 }
257 let fields = sort_by_order(ordered);
258
259 let additional_properties = match &obj.additional_properties {
260 Some(AdditionalProperties::Schema(schema)) => {
261 let ty = self.type_from_ref_schema(name, schema.as_ref())?;
262 Some(ty)
263 }
264 Some(AdditionalProperties::Any(true)) => Some(RustType::Value),
265 Some(AdditionalProperties::Any(false)) | None => None,
266 };
267 let deny_unknown_fields = matches!(&obj.additional_properties, Some(AdditionalProperties::Any(false)));
270
271 return Ok(Struct {
272 name: self.type_name_ident(name),
273 doc: doc_of(data),
274 deprecated: deprecation_of(data, name)?,
275 fields,
276 additional_properties,
277 deny_unknown_fields,
278 });
279 }
280
281 fn field_from_prop(
283 &mut self,
284 parent: &str,
285 wire: &str,
286 prop: &ReferenceOr<Box<Schema>>,
287 required: bool,
288 ) -> Result<Field> {
289 let hint = format!("{parent}_{wire}");
290 let at = format!("{parent}.{wire}");
291 let mut ty = self.type_from_schema_ref(&hint, prop)?;
292
293 let data = match prop {
294 ReferenceOr::Item(schema) => Some(&schema.schema_data),
295 ReferenceOr::Reference { .. } => None,
296 };
297
298 let declared = data
302 .filter(|_| return !required)
303 .filter(|_| return !matches!(prop, ReferenceOr::Item(schema) if crate::lower::default::unsupported_nullable_default(schema)))
304 .and_then(|data| return data.default.as_ref());
305
306 if !required && declared.is_none() {
307 ty = ty.optional();
308 }
309
310 let default = match declared {
311 Some(json) => {
312 let variants_of = |name: &str| {
313 let ident = self.type_name_ident(name);
314 return self.extra.iter().find_map(|item| {
315 return match item {
316 Item::Enum(enom) if enom.name == ident => match &enom.kind {
317 EnumKind::Strings(variants) => Some(variants.clone()),
318 EnumKind::Union(_) | EnumKind::AnyOf(_) | EnumKind::Integers { .. } => None,
319 },
320 _ => None,
321 };
322 });
323 };
324 Some(lower_default(json, &ty, &variants_of, parent, wire)?)
325 }
326 None => None,
327 };
328
329 let doc = data.and_then(doc_of);
330 let deprecated = match data {
331 Some(data) => deprecation_of(data, &at)?,
332 None => None,
333 };
334 let serde_skip = match data {
335 Some(data) => extension_bool(data, X_RUST_SERDE_SKIP, &at)?.unwrap_or(false),
336 None => false,
337 };
338 let omit_empty = match data {
339 Some(data) => extension_bool(data, X_OMITEMPTY, &at)?,
340 None => None,
341 };
342
343 let rust_name = match data {
344 Some(data) => extension_str(data, X_RUST_NAME, &at)?,
345 None => None,
346 };
347 let ident = match rust_name {
348 Some(custom) => to_ident(custom, Case::Snake),
349 None => to_ident(wire, Case::Snake),
350 };
351 let rename = crate::naming::rename_for(wire, &ident);
352 let access = match prop {
353 ReferenceOr::Item(schema) => access_of(&schema.schema_data, &at)?,
354 ReferenceOr::Reference { reference } => match self.spec.resolve(reference) {
357 Ok(target) => access_of(&target.schema_data, &at)?,
358 Err(_) => Access::ReadWrite,
359 },
360 };
361 let mut constraints = match prop {
362 ReferenceOr::Item(schema) => crate::lower::constraints::constraints_of(schema)
363 .or_else(|| return self.single_all_of_constraints(schema, 0)),
364 ReferenceOr::Reference { reference } => self
367 .spec
368 .resolve(reference)
369 .ok()
370 .and_then(|schema| return self.referenced_constraints(schema, 0)),
371 };
372 let value_ty = match &ty {
373 RustType::Option(inner) => inner.as_ref(),
374 _ => &ty,
375 };
376 if matches!(value_ty, RustType::Nullable(_))
377 && let Some(found) = &mut constraints
378 && let Some(RustType::Nullable(inner)) = &found.checked_as
379 {
380 found.checked_as = Some(*inner.clone());
382 }
383 let field = Field {
384 name: ident,
385 rename,
386 doc,
387 deprecated,
388 ty,
389 required,
390 omit_empty,
391 serde_skip,
392 default,
393 constraints,
394 access,
395 };
396 crate::lower::constraints::check_constraints(&field)?;
397 return Ok(field);
398 }
399
400 fn referenced_constraints(&self, schema: &Schema, depth: usize) -> Option<Constraints> {
401 if let Some(found) = crate::lower::constraints::constraints_through_ref(schema) {
402 return Some(found);
403 }
404 let mut found = self.single_all_of_constraints(schema, depth)?;
405 if schema.schema_data.nullable
406 && let Some(checked) = found.checked_as.take()
407 {
408 found.checked_as = Some(nullable_type(self.spec, checked));
409 }
410 return Some(found);
411 }
412
413 fn single_all_of_constraints(&self, schema: &Schema, depth: usize) -> Option<Constraints> {
414 if depth >= MAX_SCHEMA_DEPTH || schema.schema_data.extensions.contains_key(X_RUST_TYPE) {
415 return None;
416 }
417 let SchemaKind::AllOf { all_of } = &schema.schema_kind else {
418 return None;
419 };
420 let [member] = all_of.as_slice() else {
421 return None;
422 };
423 let target = match member {
424 ReferenceOr::Reference { reference } => self.spec.resolve(reference).ok()?,
425 ReferenceOr::Item(schema) => schema,
426 };
427 return self.referenced_constraints(target, depth + 1);
428 }
429
430 fn single_ref_all_of(&self, members: &[ReferenceOr<Schema>]) -> Result<Option<String>> {
435 let [ReferenceOr::Reference { reference }] = members else {
436 return Ok(None);
437 };
438 let target = self.schema_ref_target(reference, "an allOf member")?;
439 return Ok(Some(target));
440 }
441
442 fn collapse_single_all_of(&mut self, hint: &str, members: &[ReferenceOr<Schema>]) -> Result<Option<RustType>> {
452 let [only] = members else {
453 return Ok(None);
454 };
455 let ty = match only {
456 ReferenceOr::Reference { reference } => {
457 let target = self.schema_ref_target(reference, "an allOf member")?;
458 RustType::Named(target)
459 }
460 ReferenceOr::Item(schema) => self.type_from_schema(hint, schema)?,
461 };
462 return Ok(Some(ty));
463 }
464
465 fn merge_all_of(&mut self, name: &str, members: &[ReferenceOr<Schema>], data: &SchemaData) -> Result<Struct> {
468 let mut merged = MergedObject::default();
469 self.absorb_members(name, members, &mut merged)?;
470
471 let mut ordered = Vec::with_capacity(merged.properties.len());
472 for (wire, prop) in &merged.properties {
473 let required = merged.required.iter().any(|r| {
474 return r == wire;
475 });
476 let order = prop_order(prop, &format!("{name}.{wire}"))?;
477 let field = self.field_from_prop(name, wire, prop, required)?;
478 ordered.push((order, field));
479 }
480 let fields = sort_by_order(ordered);
481
482 return Ok(Struct {
483 name: self.type_name_ident(name),
484 doc: doc_of(data),
485 deprecated: deprecation_of(data, name)?,
486 fields,
487 additional_properties: None,
488 deny_unknown_fields: false,
495 });
496 }
497
498 fn absorb_members(&mut self, name: &str, members: &[ReferenceOr<Schema>], merged: &mut MergedObject) -> Result<()> {
503 if self.depth >= MAX_SCHEMA_DEPTH {
504 return Err(Error::SchemaDepthExceeded {
505 path: name.to_owned(),
506 limit: MAX_SCHEMA_DEPTH,
507 });
508 }
509 self.depth += 1;
510 let result = self.absorb_members_inner(name, members, merged);
511 self.depth -= 1;
512 return result;
513 }
514
515 fn absorb_members_inner(
516 &mut self,
517 name: &str,
518 members: &[ReferenceOr<Schema>],
519 merged: &mut MergedObject,
520 ) -> Result<()> {
521 for member in members {
522 let schema = match member {
523 ReferenceOr::Item(schema) => schema,
524 ReferenceOr::Reference { reference } => self.spec.resolve(reference)?,
525 };
526 match &schema.schema_kind {
527 SchemaKind::Type(Type::Object(obj)) => merged.absorb(obj),
528 SchemaKind::AllOf { all_of } => self.absorb_members(name, all_of, merged)?,
529 SchemaKind::Type(_)
530 | SchemaKind::OneOf { .. }
531 | SchemaKind::AnyOf { .. }
532 | SchemaKind::Any(_)
533 | SchemaKind::Not { .. } => {
534 return Err(Error::UnsupportedSchema {
535 path: name.to_owned(),
536 reason: "allOf members must be objects or refs to objects".to_owned(),
537 });
538 }
539 }
540 }
541 return Ok(());
542 }
543
544 fn make_union(
545 &mut self,
546 name: &str,
547 members: &[ReferenceOr<Schema>],
548 data: &SchemaData,
549 any_of: bool,
550 ) -> Result<Enum> {
551 if members.is_empty() {
552 return Err(Error::UnsupportedSchema {
553 path: name.to_owned(),
554 reason: "a union must contain at least one alternative".to_owned(),
555 });
556 }
557 let variants = self.union_variants_from_members(name, members, data)?;
558 if any_of {
559 let mut methods = std::collections::HashSet::from(["as_value".to_owned(), "into_value".to_owned()]);
560 for variant in &variants {
561 let method = format!("as_{}", to_ident(variant.name.logical(), Case::Snake).logical());
562 if !methods.insert(method.clone()) {
563 return Err(Error::UnsupportedSchema {
564 path: name.to_owned(),
565 reason: format!(
566 "anyOf accessor `{method}` collides with another method. Rename the alternative with x-rust-name"
567 ),
568 });
569 }
570 }
571 }
572 return Ok(Enum {
573 name: self.type_name_ident(name),
574 doc: doc_of(data),
575 deprecated: deprecation_of(data, name)?,
576 kind: if any_of {
577 EnumKind::AnyOf(variants)
578 } else {
579 EnumKind::Union(variants)
580 },
581 });
582 }
583
584 fn union_variants_from_members(
586 &mut self,
587 name: &str,
588 members: &[ReferenceOr<Schema>],
589 data: &SchemaData,
590 ) -> Result<Vec<UnionVariant>> {
591 let mut variants = Vec::with_capacity(members.len());
592 let mut seen = std::collections::HashSet::new();
593 let mut diagnostics = crate::lower::validate::Diagnostics::new();
594 for (index, member) in members.iter().enumerate() {
595 let variant = match member {
596 ReferenceOr::Reference { reference } => {
597 let target = self.schema_ref_target(reference, "a union member")?;
598 let variant_name = data
599 .discriminator
600 .as_ref()
601 .and_then(|disc| return disc.mapping.iter().find(|(_, mapped)| return *mapped == reference))
602 .map_or_else(
603 || return self.type_name_ident(&target),
604 |(value, _)| return to_ident(value, Case::Pascal),
605 );
606 UnionVariant {
607 name: crate::naming::deconflict_ident(variant_name, &mut seen),
608 ty: RustType::Named(target),
609 }
610 }
611 ReferenceOr::Item(schema) => {
612 let Some(seed) = inline_variant_seed(schema, &format!("{name}, member {index}"))? else {
613 diagnostics.push(Error::UnsupportedSchema {
614 path: name.to_owned(),
615 reason: format!("member {index} of the union gives the variant no name"),
616 });
617 continue;
618 };
619 let ty = self.type_from_schema(&format!("{name}_{seed}"), schema)?;
626 UnionVariant {
627 name: crate::naming::deconflict_ident(to_ident(&seed, Case::Pascal), &mut seen),
628 ty,
629 }
630 }
631 };
632 variants.push(variant);
633 }
634 diagnostics.into_result()?;
635 return Ok(variants);
636 }
637
638 fn string_enum(&self, name: &str, values: &[Option<String>], data: &SchemaData) -> Result<Enum> {
646 let varnames = match extension_str_array(data, X_ENUM_VARNAMES, name)? {
647 Some(names) => Some(names),
648 None => extension_str_array(data, X_ENUM_NAMES, name)?,
649 };
650 let mut diagnostics = crate::lower::validate::Diagnostics::new();
651 let mut variants = Vec::new();
652 let mut seen = std::collections::HashSet::new();
653 let mut values_seen = std::collections::HashSet::new();
654 for (index, value) in values.iter().enumerate() {
655 let Some(value) = value else {
656 continue;
657 };
658 if !values_seen.insert(value.as_str()) {
659 diagnostics.push(Error::UnsupportedSchema {
660 path: name.to_owned(),
661 reason: format!("the `enum` gives `{value}` more than once"),
662 });
663 continue;
664 }
665 let base = match varnames.as_ref().and_then(|names| return names.get(index)) {
666 Some(custom) => to_ident(custom, Case::Pascal),
667 None => to_ident(value, Case::Pascal),
668 };
669 let ident = crate::naming::deconflict_ident(base, &mut seen);
670 let rename = crate::naming::rename_for(value, &ident);
671 variants.push(StringVariant {
672 name: ident,
673 rename,
674 doc: None,
675 });
676 }
677 diagnostics.into_result()?;
678 return Ok(Enum {
679 name: self.type_name_ident(name),
680 doc: doc_of(data),
681 deprecated: deprecation_of(data, name)?,
682 kind: EnumKind::Strings(variants),
683 });
684 }
685
686 fn integer_enum(&self, name: &str, values: &[Option<i64>], repr: &RustType, data: &SchemaData) -> Result<Enum> {
696 let varnames = match extension_str_array(data, X_ENUM_VARNAMES, name)? {
697 Some(names) => Some(names),
698 None => extension_str_array(data, X_ENUM_NAMES, name)?,
699 };
700 let mut diagnostics = crate::lower::validate::Diagnostics::new();
701 let mut variants = Vec::new();
702 let mut seen = std::collections::HashSet::new();
703 let mut values_seen = std::collections::HashSet::new();
704 for (index, value) in values.iter().enumerate() {
705 let Some(value) = value else {
706 continue;
707 };
708 if !values_seen.insert(*value) {
709 diagnostics.push(Error::UnsupportedSchema {
710 path: name.to_owned(),
711 reason: format!("the `enum` gives `{value}` more than once"),
712 });
713 continue;
714 }
715 if !fits_repr(*value, repr) {
716 diagnostics.push(Error::UnsupportedSchema {
717 path: name.to_owned(),
718 reason: format!("the `enum` gives `{value}`, which `{}` cannot hold", repr_name(repr)),
719 });
720 continue;
721 }
722 let base = match varnames.as_ref().and_then(|names| return names.get(index)) {
723 Some(custom) => to_ident(custom, Case::Pascal),
724 None => to_ident(&integer_variant_name(*value), Case::Pascal),
725 };
726 variants.push(IntegerVariant {
727 name: crate::naming::deconflict_ident(base, &mut seen),
728 value: *value,
729 doc: None,
730 });
731 }
732 diagnostics.into_result()?;
733 return Ok(Enum {
734 name: self.type_name_ident(name),
735 doc: doc_of(data),
736 deprecated: deprecation_of(data, name)?,
737 kind: EnumKind::Integers {
738 repr: repr.clone(),
739 variants,
740 },
741 });
742 }
743
744 fn type_from_schema_ref(&mut self, hint: &str, schema: &ReferenceOr<Box<Schema>>) -> Result<RustType> {
747 match schema {
748 ReferenceOr::Reference { reference } => {
749 let target = self.schema_ref_target(reference, "a property")?;
750 return Ok(RustType::Named(target));
751 }
752 ReferenceOr::Item(schema) => {
753 let ty = self.type_from_schema(hint, schema)?;
754 return Ok(ty);
755 }
756 }
757 }
758
759 fn type_from_ref_schema(&mut self, hint: &str, schema: &ReferenceOr<Schema>) -> Result<RustType> {
762 match schema {
763 ReferenceOr::Reference { reference } => {
764 let target = self.schema_ref_target(reference, "additionalProperties")?;
765 return Ok(RustType::Named(target));
766 }
767 ReferenceOr::Item(schema) => {
768 let ty = self.type_from_schema(hint, schema)?;
769 return Ok(ty);
770 }
771 }
772 }
773
774 fn type_from_schema(&mut self, hint: &str, schema: &Schema) -> Result<RustType> {
778 if self.depth >= MAX_SCHEMA_DEPTH {
779 return Err(Error::SchemaDepthExceeded {
780 path: hint.to_owned(),
781 limit: MAX_SCHEMA_DEPTH,
782 });
783 }
784 self.depth += 1;
785 let result = self.type_from_schema_inner(hint, schema).map(|ty| {
786 if schema.schema_data.nullable {
787 return nullable_type(self.spec, ty);
788 }
789 return ty;
790 });
791 self.depth -= 1;
792 return result;
793 }
794
795 fn type_from_schema_inner(&mut self, hint: &str, schema: &Schema) -> Result<RustType> {
796 let data = &schema.schema_data;
797 if let Some(verbatim) = extension_str(data, X_RUST_TYPE, hint)? {
798 return verbatim_type(data, verbatim, hint);
799 }
800
801 let ty = match &schema.schema_kind {
802 SchemaKind::Type(Type::String(st)) if !st.enumeration.is_empty() => {
803 let enom = self.string_enum(hint, &st.enumeration, data)?;
804 self.extra.push(Item::Enum(enom));
805 RustType::Named(hint.to_owned())
806 }
807 SchemaKind::Type(Type::String(st)) => string_format_type(&st.format),
808 SchemaKind::Type(Type::Integer(it)) if !it.enumeration.is_empty() => {
809 let repr = integer_type(it);
810 let enom = self.integer_enum(hint, &it.enumeration, &repr, data)?;
811 self.extra.push(Item::Enum(enom));
812 RustType::Named(hint.to_owned())
813 }
814 SchemaKind::Type(Type::Integer(it)) => integer_type(it),
815 SchemaKind::Type(Type::Number(_)) => RustType::F64,
816 SchemaKind::Type(Type::Boolean(_)) => RustType::Bool,
817 SchemaKind::Type(Type::Array(at)) => {
818 let element = match &at.items {
819 Some(items) => {
820 let item_hint = format!("{hint}_item");
821 self.type_from_schema_ref(&item_hint, items)?
822 }
823 None => RustType::Value,
824 };
825 RustType::Vec(Box::new(element))
826 }
827 SchemaKind::Type(Type::Object(obj)) => self.inline_object_type(hint, obj, data)?,
828 SchemaKind::OneOf { one_of } | SchemaKind::AnyOf { any_of: one_of } => {
829 let enom = self.make_union(
830 hint,
831 one_of,
832 data,
833 matches!(schema.schema_kind, SchemaKind::AnyOf { .. }),
834 )?;
835 self.extra.push(Item::Enum(enom));
836 RustType::Named(hint.to_owned())
837 }
838 SchemaKind::AllOf { all_of } => {
839 if let Some(ty) = self.collapse_single_all_of(hint, all_of)? {
840 ty
841 } else {
842 let strukt = self.merge_all_of(hint, all_of, data)?;
843 self.extra.push(Item::Struct(strukt));
844 RustType::Named(hint.to_owned())
845 }
846 }
847 SchemaKind::Any(schema) => self.unconstrained_type(hint, schema),
848 SchemaKind::Not { .. } => {
849 return Err(Error::UnsupportedSchema {
850 path: hint.to_owned(),
851 reason: "`not` schemas are not supported".to_owned(),
852 });
853 }
854 };
855 return Ok(ty);
856 }
857
858 fn inline_object_type(&mut self, hint: &str, obj: &ObjectType, data: &SchemaData) -> Result<RustType> {
861 if obj.properties.is_empty() {
862 let element = self.additional_properties_type(hint, obj)?;
863 return Ok(RustType::Map(Box::new(element)));
864 }
865 let strukt = self.object_to_struct(hint, obj, data)?;
866 self.extra.push(Item::Struct(strukt));
867 return Ok(RustType::Named(hint.to_owned()));
868 }
869
870 fn unconstrained_type(&self, name: &str, schema: &openapiv3::AnySchema) -> RustType {
871 if *schema != openapiv3::AnySchema::default() {
872 crate::diagnostic::report_warnings(
873 &self.spec.source().display().to_string(),
874 &[crate::diagnostic::Warning::new(
875 name,
876 "this schema combination is not implemented and becomes an unconstrained JSON value",
877 )],
878 );
879 }
880 return RustType::Value;
881 }
882
883 fn additional_properties_type(&mut self, hint: &str, obj: &ObjectType) -> Result<RustType> {
885 let element = match &obj.additional_properties {
886 Some(AdditionalProperties::Schema(schema)) => self.type_from_ref_schema(hint, schema.as_ref())?,
887 Some(AdditionalProperties::Any(_)) | None => RustType::Value,
888 };
889 return Ok(element);
890 }
891}
892
893pub(crate) fn nullable_type(spec: &Spec, ty: RustType) -> RustType {
895 if matches!(ty, RustType::Nullable(_)) {
896 return ty;
897 }
898 if let RustType::Named(name) = &ty {
899 let mut name = name.as_str();
900 let mut visited = std::collections::HashSet::new();
901 while visited.insert(name) {
902 let reference = match spec.schemas().get(name) {
903 Some(ReferenceOr::Reference { reference }) => reference,
904 Some(ReferenceOr::Item(schema)) => {
905 if schema.schema_data.nullable {
906 return ty;
907 }
908 if schema.schema_data.extensions.contains_key(X_RUST_TYPE) {
909 break;
910 }
911 let SchemaKind::AllOf { all_of } = &schema.schema_kind else {
912 break;
913 };
914 let [ReferenceOr::Reference { reference }] = all_of.as_slice() else {
916 break;
917 };
918 reference
919 }
920 None => break,
921 };
922 let Some(target) = ref_target_name(reference) else {
923 break;
924 };
925 name = target;
926 }
927 }
928 return RustType::Nullable(Box::new(ty));
929}
930
931#[derive(Default)]
933struct MergedObject {
934 properties: indexmap::IndexMap<String, ReferenceOr<Box<Schema>>>,
935 required: Vec<String>,
936}
937
938impl MergedObject {
939 fn absorb(&mut self, obj: &ObjectType) {
941 for (name, prop) in &obj.properties {
942 self.properties.insert(name.clone(), prop.clone());
943 }
944 for req in &obj.required {
945 if !self.required.contains(req) {
946 self.required.push(req.clone());
947 }
948 }
949 }
950}
951
952pub(crate) fn string_format_type(format: &VariantOrUnknownOrEmpty<StringFormat>) -> RustType {
954 let ty = match format {
955 VariantOrUnknownOrEmpty::Item(StringFormat::Date) => RustType::Date,
956 VariantOrUnknownOrEmpty::Item(StringFormat::DateTime) => RustType::DateTime,
957 VariantOrUnknownOrEmpty::Item(StringFormat::Byte) => RustType::Bytes,
958 VariantOrUnknownOrEmpty::Item(StringFormat::Binary) => RustType::Bytes,
959 VariantOrUnknownOrEmpty::Item(StringFormat::Password) => RustType::String,
960 VariantOrUnknownOrEmpty::Unknown(name) if name == "uuid" => RustType::Uuid,
961 VariantOrUnknownOrEmpty::Unknown(_) => RustType::String,
962 VariantOrUnknownOrEmpty::Empty => RustType::String,
963 };
964 return ty;
965}
966
967fn fits_repr(value: i64, repr: &RustType) -> bool {
972 return match *repr {
973 RustType::I32 => i32::try_from(value).is_ok(),
974 RustType::U32 => u32::try_from(value).is_ok(),
975 RustType::U64 => u64::try_from(value).is_ok(),
976 _ => true,
977 };
978}
979
980fn repr_name(repr: &RustType) -> &'static str {
982 return match *repr {
983 RustType::I32 => "i32",
984 RustType::U32 => "u32",
985 RustType::U64 => "u64",
986 _ => "i64",
987 };
988}
989
990fn integer_variant_name(value: i64) -> String {
992 if value < 0 {
993 return format!("value_minus_{}", value.unsigned_abs());
994 }
995 return format!("value_{value}");
996}
997
998fn inline_variant_seed(schema: &Schema, at: &str) -> Result<Option<String>> {
1009 if let Some(custom) = extension_str(&schema.schema_data, X_RUST_NAME, at)? {
1010 return Ok(Some(custom.to_owned()));
1011 }
1012 if let Some(value) = single_enum_value(schema) {
1013 return Ok(Some(value));
1014 }
1015 let Some(ty) = non_hoisting_type(schema) else {
1016 return Ok(None);
1017 };
1018 return Ok(type_variant_name(&ty).map(str::to_owned));
1019}
1020
1021fn single_enum_value(schema: &Schema) -> Option<String> {
1028 let SchemaKind::Type(Type::String(st)) = &schema.schema_kind else {
1029 return None;
1030 };
1031 let [Some(value)] = st.enumeration.as_slice() else {
1032 return None;
1033 };
1034 return Some(value.clone());
1035}
1036
1037fn non_hoisting_type(schema: &Schema) -> Option<RustType> {
1041 return match &schema.schema_kind {
1042 SchemaKind::Type(Type::String(st)) if st.enumeration.is_empty() => Some(string_format_type(&st.format)),
1043 SchemaKind::Type(Type::Integer(it)) if it.enumeration.is_empty() => Some(integer_type(it)),
1044 SchemaKind::Type(Type::Number(_)) => Some(RustType::F64),
1045 SchemaKind::Type(Type::Boolean(_)) => Some(RustType::Bool),
1046 _ => None,
1047 };
1048}
1049
1050fn type_variant_name(ty: &RustType) -> Option<&'static str> {
1054 return match ty {
1055 RustType::Bool => Some("Bool"),
1056 RustType::I32 => Some("I32"),
1057 RustType::I64 => Some("I64"),
1058 RustType::U32 => Some("U32"),
1059 RustType::U64 => Some("U64"),
1060 RustType::F64 => Some("F64"),
1061 RustType::String => Some("String"),
1062 RustType::Date => Some("Date"),
1063 RustType::DateTime => Some("DateTime"),
1064 RustType::Uuid => Some("Uuid"),
1065 RustType::Bytes => Some("Bytes"),
1066 _ => None,
1067 };
1068}
1069
1070pub(crate) fn integer_type(it: &IntegerType) -> RustType {
1082 let unsigned = matches!(crate::lower::constraints::inclusive_minimum(it), Some(minimum) if minimum >= 0);
1083 let ty = match (&it.format, unsigned) {
1084 (VariantOrUnknownOrEmpty::Item(IntegerFormat::Int32), false) => RustType::I32,
1085 (VariantOrUnknownOrEmpty::Item(IntegerFormat::Int32), true) => RustType::U32,
1086 (VariantOrUnknownOrEmpty::Item(IntegerFormat::Int64), false)
1087 | (VariantOrUnknownOrEmpty::Unknown(_) | VariantOrUnknownOrEmpty::Empty, false) => RustType::I64,
1088 (VariantOrUnknownOrEmpty::Item(IntegerFormat::Int64), true)
1089 | (VariantOrUnknownOrEmpty::Unknown(_) | VariantOrUnknownOrEmpty::Empty, true) => RustType::U64,
1090 };
1091 return ty;
1092}
1093
1094fn extension_str<'a>(data: &'a SchemaData, key: &str, at: &str) -> Result<Option<&'a str>> {
1096 return crate::lower::extension::str_value(&data.extensions, key, at);
1097}
1098
1099fn extension_bool(data: &SchemaData, key: &str, at: &str) -> Result<Option<bool>> {
1101 return crate::lower::extension::bool_value(&data.extensions, key, at);
1102}
1103
1104fn extension_i64(data: &SchemaData, key: &str, at: &str) -> Result<Option<i64>> {
1106 return crate::lower::extension::i64_value(&data.extensions, key, at);
1107}
1108
1109fn prop_order(prop: &ReferenceOr<Box<Schema>>, at: &str) -> Result<Option<i64>> {
1111 return match prop {
1112 ReferenceOr::Item(schema) => extension_i64(&schema.schema_data, X_ORDER, at),
1113 ReferenceOr::Reference { .. } => Ok(None),
1114 };
1115}
1116
1117fn sort_by_order(mut fields: Vec<(Option<i64>, Field)>) -> Vec<Field> {
1121 fields.sort_by_key(|(order, _)| {
1122 return order.unwrap_or(i64::MAX);
1123 });
1124 return fields.into_iter().map(|(_, field)| return field).collect();
1125}
1126
1127fn extension_str_array<'a>(data: &'a SchemaData, key: &str, at: &str) -> Result<Option<Vec<&'a str>>> {
1129 return crate::lower::extension::str_list_value(&data.extensions, key, at);
1130}
1131
1132const FOREIGN_DERIVE_NAMES: [&str; 3] = ["Debug", "Clone", "PartialEq"];
1138
1139fn foreign_derives_of(data: &SchemaData, path: &str) -> Result<ForeignDerives> {
1151 let Some(value) = data.extensions.get(X_RUST_DERIVE) else {
1152 return Ok(ForeignDerives::default());
1153 };
1154 let Some(array) = value.as_array() else {
1155 return Err(Error::UnsupportedSchema {
1156 path: path.to_owned(),
1157 reason: format!("`{X_RUST_DERIVE}` must be a list of trait names, for example `[Debug, Clone]`"),
1158 });
1159 };
1160
1161 let mut derives = ForeignDerives {
1162 debug: false,
1163 clone: false,
1164 partial_eq: false,
1165 };
1166 for entry in array {
1167 let Some(name) = entry.as_str() else {
1168 return Err(Error::UnsupportedSchema {
1169 path: path.to_owned(),
1170 reason: format!("every `{X_RUST_DERIVE}` entry must be a trait name written as a string"),
1171 });
1172 };
1173 match name {
1174 "Debug" => derives.debug = true,
1175 "Clone" => derives.clone = true,
1176 "PartialEq" => derives.partial_eq = true,
1177 other => {
1178 let known = FOREIGN_DERIVE_NAMES.join(", ");
1179 return Err(Error::UnsupportedSchema {
1180 path: path.to_owned(),
1181 reason: format!("`{X_RUST_DERIVE}` does not accept `{other}`. It accepts only {known}"),
1182 });
1183 }
1184 }
1185 }
1186 return Ok(derives);
1187}
1188
1189fn verbatim_type(data: &SchemaData, verbatim: &str, path: &str) -> Result<RustType> {
1193 return Ok(RustType::Verbatim {
1194 text: verbatim.to_owned(),
1195 derives: foreign_derives_of(data, path)?,
1196 });
1197}
1198
1199pub(crate) fn access_of(data: &SchemaData, at: &str) -> Result<Access> {
1204 return match (data.read_only, data.write_only) {
1205 (true, true) => Err(Error::UnsupportedSchema {
1206 path: at.to_owned(),
1207 reason: "`readOnly` and `writeOnly` are both set, so no request and no response could \
1208 carry the property. Set at most one of the two."
1209 .to_owned(),
1210 }),
1211 (true, false) => Ok(Access::ReadOnly),
1212 (false, true) => Ok(Access::WriteOnly),
1213 (false, false) => Ok(Access::ReadWrite),
1214 };
1215}
1216
1217fn deprecation_of(data: &SchemaData, at: &str) -> Result<Option<Deprecation>> {
1221 if !data.deprecated {
1222 return Ok(None);
1223 }
1224 let note = extension_str(data, X_DEPRECATED_REASON, at)?.map(str::to_owned);
1225 return Ok(Some(Deprecation { note }));
1226}
1227
1228fn doc_of(data: &SchemaData) -> Option<String> {
1230 let text = data.description.as_ref()?;
1231 let trimmed = text.trim();
1232 if trimmed.is_empty() {
1233 return None;
1234 }
1235 return Some(trimmed.to_owned());
1236}
1237
1238#[cfg(test)]
1239mod tests {
1240 use std::path::PathBuf;
1241
1242 use super::*;
1243
1244 #[test]
1245 fn nullable_type_follows_only_local_aliases() {
1246 let doc = serde_yaml::from_str(
1247 r##"
1248openapi: 3.0.3
1249info: { title: t, version: '1' }
1250paths: {}
1251components:
1252 schemas:
1253 Text: { type: string, nullable: true }
1254 Alias: { $ref: '#/components/schemas/Text' }
1255 Wrapped:
1256 allOf: [{ $ref: '#/components/schemas/Alias' }]
1257 List:
1258 type: array
1259 items: { $ref: '#/components/schemas/Text' }
1260 Map:
1261 type: object
1262 additionalProperties: { $ref: '#/components/schemas/Text' }
1263 Custom:
1264 x-rust-type: String
1265 allOf: [{ $ref: '#/components/schemas/Text' }]
1266 Inline:
1267 allOf:
1268 - type: object
1269 nullable: true
1270 properties:
1271 value: { type: string }
1272 Foreign: { $ref: 'other.yaml#/components/schemas/Text' }
1273 CycleA: { $ref: '#/components/schemas/CycleB' }
1274 CycleB:
1275 allOf: [{ $ref: '#/components/schemas/CycleA' }]
1276"##,
1277 )
1278 .expect("parse spec");
1279 let spec = Spec::from_parts(doc, PathBuf::from("inline.yaml"));
1280 for name in ["Text", "Alias", "Wrapped"] {
1281 let ty = RustType::Named(name.to_owned());
1282 assert_eq!(nullable_type(&spec, ty.clone()), ty, "{name}");
1283 }
1284 for name in [
1285 "List", "Map", "Custom", "Inline", "Foreign", "CycleA", "CycleB", "Missing",
1286 ] {
1287 let ty = RustType::Named(name.to_owned());
1288 assert_eq!(
1289 nullable_type(&spec, ty.clone()),
1290 RustType::Nullable(Box::new(ty)),
1291 "{name}"
1292 );
1293 }
1294 let nullable = RustType::Nullable(Box::new(RustType::String));
1295 assert_eq!(nullable_type(&spec, nullable.clone()), nullable);
1296 for ty in [
1297 RustType::Vec(Box::new(nullable.clone())),
1298 RustType::Map(Box::new(nullable.clone())),
1299 RustType::Option(Box::new(nullable)),
1300 RustType::External {
1301 module: "foreign".to_owned(),
1302 name: "Text".to_owned(),
1303 },
1304 ] {
1305 assert_eq!(nullable_type(&spec, ty.clone()), RustType::Nullable(Box::new(ty)));
1306 }
1307 }
1308
1309 fn emit_yaml(yaml: &str) -> String {
1311 let doc: openapiv3::OpenAPI = serde_yaml::from_str(yaml).expect("parse spec");
1312 let spec = Spec::from_parts(doc, PathBuf::from("inline.yaml"));
1313 let module = lower_models(&spec).expect("map schemas");
1314 return crate::emit::emit_module(&module, None).expect("emit module");
1315 }
1316
1317 fn lower_yaml(yaml: &str) -> Result<Module> {
1319 let doc: openapiv3::OpenAPI = serde_yaml::from_str(yaml).expect("parse spec");
1320 let spec = Spec::from_parts(doc, PathBuf::from("inline.yaml"));
1321 return lower_models(&spec);
1322 }
1323
1324 fn lower_models(spec: &Spec) -> Result<Module> {
1327 let names = crate::lower::rename::type_renames(spec, None)?;
1328 return generate_models(spec, &names);
1329 }
1330
1331 const PREAMBLE: &str = "openapi: 3.0.3\ninfo:\n title: t\n version: '1'\npaths: {}\ncomponents:\n schemas:\n";
1332
1333 fn nested_array_schema(depth: usize) -> Schema {
1337 let mut kind = SchemaKind::Type(Type::String(Default::default()));
1338 for _ in 0..depth {
1339 let items = ReferenceOr::Item(Box::new(Schema {
1340 schema_data: SchemaData::default(),
1341 schema_kind: kind,
1342 }));
1343 kind = SchemaKind::Type(Type::Array(openapiv3::ArrayType {
1344 items: Some(items),
1345 min_items: None,
1346 max_items: None,
1347 unique_items: false,
1348 }));
1349 }
1350 return Schema {
1351 schema_data: SchemaData::default(),
1352 schema_kind: kind,
1353 };
1354 }
1355
1356 fn spec_with_schema(name: &str, schema: Schema) -> Spec {
1357 let empty_doc = "openapi: 3.0.3\ninfo:\n title: t\n version: '1'\npaths: {}\n";
1358 let mut doc: openapiv3::OpenAPI = serde_yaml::from_str(empty_doc).expect("parse preamble");
1359 doc.components
1360 .get_or_insert_with(Default::default)
1361 .schemas
1362 .insert(name.to_owned(), ReferenceOr::Item(schema));
1363 return Spec::from_parts(doc, PathBuf::from("inline.yaml"));
1364 }
1365 #[test]
1370 fn schema_at_the_depth_limit_errors_instead_of_overflowing() {
1371 let spec = spec_with_schema("Deep", nested_array_schema(MAX_SCHEMA_DEPTH));
1372 let err = lower_models(&spec).expect_err("reaching the limit should hit the depth guard");
1373 assert!(
1374 matches!(err, Error::SchemaDepthExceeded { limit, .. } if limit == MAX_SCHEMA_DEPTH),
1375 "expected SchemaDepthExceeded, got {err:?}"
1376 );
1377 }
1378
1379 #[test]
1380 fn schema_just_under_the_depth_limit_still_lowers() {
1381 let spec = spec_with_schema("Deep", nested_array_schema(MAX_SCHEMA_DEPTH - 1));
1382 lower_models(&spec).expect("just under the limit should lower cleanly");
1383 }
1384
1385 fn lower_error(yaml: &str) -> Error {
1387 let doc: openapiv3::OpenAPI = serde_yaml::from_str(yaml).expect("parse spec");
1388 let spec = Spec::from_parts(doc, PathBuf::from("inline.yaml"));
1389 return lower_models(&spec).expect_err("the spec should not lower");
1390 }
1391
1392 fn bad_order_schema(name: &str) -> String {
1394 return format!(
1395 " {name}:\n type: object\n properties:\n id:\n type: string\n x-order: 'first'\n"
1396 );
1397 }
1398
1399 #[test]
1400 fn every_bad_schema_is_reported_in_one_run() {
1401 let alpha = bad_order_schema("Alpha");
1405 let beta = bad_order_schema("Beta");
1406 let gamma = bad_order_schema("Gamma");
1407 let err = lower_error(&format!("{PREAMBLE}{alpha}{beta}{gamma}"));
1408 let Error::Validation { problems } = &err else {
1409 panic!("expected Validation, got: {err:?}");
1410 };
1411 assert_eq!(problems.len(), 3, "every bad schema should be reported");
1412 let message = err.to_string();
1413 for name in ["Alpha", "Beta", "Gamma"] {
1414 assert!(message.contains(name), "message should name `{name}`: {message}");
1415 }
1416 }
1417
1418 #[test]
1419 fn a_good_schema_beside_a_bad_one_does_not_add_a_problem() {
1420 let alpha = bad_order_schema("Alpha");
1421 let err = lower_error(&format!("{PREAMBLE}{alpha} Beta:\n type: string\n"));
1422 assert!(
1423 matches!(&err, Error::InvalidExtensionValue { at, .. } if at == "Alpha.id"),
1424 "one problem should stay unwrapped, got: {err:?}",
1425 );
1426 }
1427
1428 #[test]
1429 fn a_union_member_holding_one_enum_value_is_named_by_that_value() {
1430 let out = emit_yaml(&format!(
1433 "{PREAMBLE} Signal:\n oneOf:\n - type: string\n enum: [red]\n - type: string\n enum: [amber]\n"
1434 ));
1435 assert!(out.contains("Red(SignalRed)"), "expected a named variant, got: {out}");
1436 assert!(
1437 out.contains("Amber(SignalAmber)"),
1438 "expected a named variant, got: {out}"
1439 );
1440 assert!(
1443 out.contains("rename = \"red\""),
1444 "the hoisted type should keep the wire value, got: {out}"
1445 );
1446 }
1447
1448 #[test]
1449 fn a_hoisted_union_member_type_carries_the_union_name() {
1450 let out = emit_yaml(&format!(
1453 "{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"
1454 ));
1455 assert!(out.contains("struct LeftUnknown"), "expected LeftUnknown, got: {out}");
1456 assert!(out.contains("struct RightUnknown"), "expected RightUnknown, got: {out}");
1457 assert!(
1459 out.contains("Unknown(LeftUnknown)"),
1460 "expected a short variant, got: {out}"
1461 );
1462 }
1463
1464 fn derive_error(value: &str) -> Error {
1467 let yaml = format!(
1468 "{PREAMBLE} Target:\n type: string\n x-rust-type: crate::Foreign\n x-rust-derive: {value}\n"
1469 );
1470 let doc: openapiv3::OpenAPI = serde_yaml::from_str(&yaml).expect("parse spec");
1471 let spec = Spec::from_parts(doc, PathBuf::from("inline.yaml"));
1472 return lower_models(&spec).expect_err("the extension should reject this value");
1473 }
1474
1475 #[test]
1476 fn absent_x_rust_derive_claims_every_trait() {
1477 let out = emit_yaml(&format!(
1480 "{PREAMBLE} Holder:\n type: object\n required: [value]\n properties:\n value:\n type: string\n x-rust-type: crate::Foreign\n"
1481 ));
1482 assert!(
1483 out.contains("#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]"),
1484 "absent key should change nothing:\n{out}"
1485 );
1486 }
1487
1488 #[test]
1489 fn empty_x_rust_derive_drops_every_trait() {
1490 let out = emit_yaml(&format!(
1491 "{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"
1492 ));
1493 assert!(
1494 out.contains("#[derive(serde::Serialize, serde::Deserialize)]"),
1495 "an empty list claims nothing, so only the serde derives remain:\n{out}"
1496 );
1497 }
1498
1499 #[test]
1500 fn partial_x_rust_derive_keeps_only_the_listed_traits() {
1501 let out = emit_yaml(&format!(
1502 "{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"
1503 ));
1504 assert!(
1505 out.contains("#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq)]"),
1506 "Clone was not listed, so it is dropped:\n{out}"
1507 );
1508 }
1509
1510 #[test]
1511 fn x_rust_derive_that_is_not_a_list_is_rejected() {
1512 let err = derive_error("Debug");
1513 assert!(
1514 matches!(&err, Error::UnsupportedSchema { reason, .. } if reason.contains("must be a list")),
1515 "expected a list-shape error, got {err:?}"
1516 );
1517 }
1518
1519 #[test]
1520 fn x_rust_derive_entry_that_is_not_a_string_is_rejected() {
1521 let err = derive_error("[7]");
1522 assert!(
1523 matches!(&err, Error::UnsupportedSchema { reason, .. } if reason.contains("written as a string")),
1524 "expected a string-entry error, got {err:?}"
1525 );
1526 }
1527
1528 #[test]
1529 fn misspelled_trait_name_is_an_error_and_not_an_ignored_key() {
1530 let err = derive_error("[Parialeq]");
1534 assert!(
1535 matches!(&err, Error::UnsupportedSchema { reason, .. } if reason.contains("Parialeq")),
1536 "expected the unknown name in the error, got {err:?}"
1537 );
1538 }
1539
1540 #[test]
1541 fn maps_string_and_integer_formats() {
1542 let yaml = format!(
1543 "{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"
1544 );
1545 let out = emit_yaml(&yaml);
1546 assert!(out.contains("pub day: chrono::NaiveDate"), "{out}");
1547 assert!(out.contains("pub at: chrono::DateTime<chrono::Utc>"), "{out}");
1548 assert!(out.contains("pub id: uuid::Uuid"), "{out}");
1549 assert!(out.contains("pub blob: Vec<u8>"), "{out}");
1550 assert!(out.contains("pub big: i64"), "{out}");
1551 }
1552
1553 #[test]
1554 fn an_unsigned_field_keeps_every_check_the_type_does_not_already_make() {
1555 let yaml = format!(
1556 "{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"
1557 );
1558 let out = emit_yaml(&yaml);
1559 assert!(out.contains("pub count: u32"), "{out}");
1560 assert!(out.contains("`count` must be 130 or less"), "{out}");
1562 assert!(!out.contains("must be 0 or more"), "{out}");
1566 assert!(!out.contains("*item < 0"), "{out}");
1567 }
1568
1569 #[test]
1570 fn an_exclusive_bound_moves_onto_the_whole_number_beside_it() {
1571 let cases: [(&str, &str, &str, Option<&str>); 5] = [
1575 ("minimum: -1\n exclusiveMinimum: true", "int32", "u32", None),
1576 (
1577 "minimum: 0\n exclusiveMinimum: true",
1578 "int32",
1579 "u32",
1580 Some("must be 1 or more"),
1581 ),
1582 (
1583 "minimum: -2\n exclusiveMinimum: true",
1584 "int32",
1585 "i32",
1586 Some("must be -1 or more"),
1587 ),
1588 (
1589 "maximum: 2147483648\n exclusiveMaximum: true",
1590 "int32",
1591 "i32",
1592 None,
1593 ),
1594 ("minimum: -1\n exclusiveMinimum: true", "int64", "u64", None),
1595 ];
1596 for (bound, format, ty, message) in cases {
1597 let yaml = format!(
1598 "{PREAMBLE} Thing:\n type: object\n required: [count]\n properties:\n count:\n type: integer\n format: {format}\n {bound}\n"
1599 );
1600 let out = emit_yaml(&yaml);
1601 assert!(out.contains(&format!("pub count: {ty}")), "{bound}: {out}");
1602 match message {
1603 Some(text) => assert!(out.contains(text), "{bound}: {out}"),
1604 None => assert!(!out.contains("must be"), "{bound}: {out}"),
1605 }
1606 }
1607 }
1608
1609 #[test]
1610 fn a_bound_that_lands_on_the_limit_of_the_type_writes_no_check() {
1611 let yaml = format!(
1614 "{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"
1615 );
1616 let out = emit_yaml(&yaml);
1617 assert!(out.contains("pub count: i32"), "{out}");
1618 assert!(!out.contains("must be"), "{out}");
1619 }
1620
1621 #[test]
1622 fn bounds_that_meet_nowhere_are_refused() {
1623 let cases: [(&str, &str, Option<&str>); 13] = [
1628 (
1629 "integer\n format: int32",
1630 "maximum: -2147483648\n exclusiveMaximum: true",
1631 Some("nothing lies below `-2147483648`, where `i32` starts"),
1632 ),
1633 (
1634 "integer\n format: int32",
1635 "maximum: -2147483647\n exclusiveMaximum: true",
1636 None,
1637 ),
1638 (
1639 "integer",
1640 "minimum: 10\n maximum: 5",
1641 Some("they allow `10` to `5`"),
1642 ),
1643 ("integer", "minimum: 5\n maximum: 5", None),
1644 (
1645 "integer",
1646 "maximum: -9223372036854775808\n exclusiveMaximum: true",
1647 Some("nothing lies below `-9223372036854775808`, where `i64` starts"),
1648 ),
1649 (
1650 "integer\n format: int32",
1651 "minimum: 4294967295\n exclusiveMinimum: true",
1652 Some("nothing lies above `4294967295`, where `u32` stops"),
1653 ),
1654 (
1655 "integer\n format: int32",
1656 "minimum: 2147483647\n maximum: 2147483647",
1657 None,
1658 ),
1659 (
1661 "integer\n format: int64",
1662 "minimum: 9223372036854775807\n exclusiveMinimum: true",
1663 None,
1664 ),
1665 (
1667 "integer\n format: int32",
1668 "minimum: 9223372036854775807\n exclusiveMinimum: true",
1669 Some("nothing lies above `9223372036854775807`, where `u32` stops"),
1670 ),
1671 (
1673 "number",
1674 "minimum: 10\n maximum: 5",
1675 Some("they allow `10` to `5`"),
1676 ),
1677 (
1678 "number",
1679 "minimum: 5\n maximum: 5\n exclusiveMinimum: true",
1680 Some("they meet at `5`, which an `exclusive` flag then leaves out"),
1681 ),
1682 (
1683 "number",
1684 "minimum: 0\n maximum: 1\n exclusiveMaximum: true",
1685 None,
1686 ),
1687 (
1689 "integer\n format: int32",
1690 "maximum: -5000000000",
1691 Some("the `maximum` value `-5000000000` does not fit `i32`"),
1692 ),
1693 ];
1694 for (kind, bounds, fault) in cases {
1695 let yaml = format!(
1696 "{PREAMBLE} Thing:\n type: object\n required: [count]\n properties:\n count:\n type: {kind}\n {bounds}\n"
1697 );
1698 let outcome = lower_yaml(&yaml);
1699 match fault {
1700 Some(text) => {
1701 let error = outcome.expect_err(bounds).to_string();
1702 assert!(error.contains(text), "{bounds}: {error}");
1703 }
1704 None => assert!(outcome.is_ok(), "{bounds}: got {outcome:?}"),
1705 }
1706 }
1707 }
1708
1709 #[test]
1710 fn a_negative_multiple_of_reports_one_fault_only() {
1711 let yaml = format!(
1715 "{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"
1716 );
1717 let fault = lower_yaml(&yaml).expect_err("refuse the step").to_string();
1718 assert!(fault.contains("is not above zero"), "{fault}");
1719 assert!(!fault.contains("does not fit"), "{fault}");
1720 }
1721
1722 #[test]
1723 fn an_enum_value_must_fit_the_repr_the_format_and_the_minimum_choose() {
1724 let cases: [(&str, &str, i64, bool); 8] = [
1728 ("int32", "", -1, true),
1729 ("int32", "", 4_294_967_296, false),
1730 ("int32", "\n minimum: 0", -1, false),
1731 ("int32", "\n minimum: 0", 5, true),
1732 ("int32", "\n minimum: 0", 4_294_967_296, false),
1733 ("int64", "\n minimum: 0", -1, false),
1734 ("int64", "\n minimum: 0", 4_294_967_296, true),
1735 ("int64", "", -1, true),
1736 ];
1737 for (format, minimum, value, fits) in cases {
1738 let yaml = format!(
1739 "{PREAMBLE} Offset:\n type: integer\n format: {format}{minimum}\n enum:\n - {value}\n"
1740 );
1741 let outcome = lower_yaml(&yaml);
1742 assert_eq!(
1743 outcome.is_ok(),
1744 fits,
1745 "format `{format}`, minimum `{minimum}`, value `{value}`: got {outcome:?}"
1746 );
1747 }
1748 }
1749
1750 #[test]
1751 fn a_minimum_of_zero_or_more_gives_an_unsigned_type() {
1752 let yaml = format!(
1753 "{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"
1754 );
1755 let out = emit_yaml(&yaml);
1756 assert!(out.contains("pub count: u32"), "{out}");
1757 assert!(out.contains("pub total: u64"), "{out}");
1758 assert!(out.contains("pub plain: u64"), "{out}");
1759 assert!(out.contains("pub signed: i32"), "{out}");
1761 assert!(out.contains("pub above: u32"), "{out}");
1763 }
1764
1765 #[test]
1766 fn an_integer_without_a_minimum_stays_signed() {
1767 let yaml = format!(
1768 "{PREAMBLE} Thing:\n type: object\n required: [count]\n properties:\n count:\n type: integer\n format: int32\n maximum: 10\n"
1769 );
1770 let out = emit_yaml(&yaml);
1771 assert!(out.contains("pub count: i32"), "{out}");
1772 }
1773
1774 #[test]
1775 fn object_with_only_additional_properties_becomes_map_alias() {
1776 let yaml =
1777 format!("{PREAMBLE} Dict:\n type: object\n additionalProperties:\n type: string\n");
1778 let out = emit_yaml(&yaml);
1779 assert!(
1780 out.contains("pub type Dict = std::collections::HashMap<String, String>;"),
1781 "{out}"
1782 );
1783 }
1784
1785 #[test]
1786 fn inline_nested_object_is_hoisted() {
1787 let yaml = format!(
1788 "{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"
1789 );
1790 let out = emit_yaml(&yaml);
1791 assert!(out.contains("pub struct Outer"), "{out}");
1792 assert!(out.contains("pub inner: OuterInner"), "{out}");
1793 assert!(out.contains("pub struct OuterInner"), "{out}");
1794 assert!(out.contains("pub x: String"), "{out}");
1795 }
1796
1797 #[test]
1798 fn x_rust_type_emits_verbatim_type() {
1799 let yaml = format!(
1800 "{PREAMBLE} Holder:\n type: object\n required: [v]\n properties:\n v:\n type: string\n x-rust-type: my_crate::Custom\n"
1801 );
1802 let out = emit_yaml(&yaml);
1803 assert!(out.contains("pub v: my_crate::Custom"), "{out}");
1804 }
1805
1806 #[test]
1807 fn optional_field_is_wrapped_and_skipped() {
1808 let yaml = format!(
1809 "{PREAMBLE} Maybe:\n type: object\n properties:\n note:\n type: string\n"
1810 );
1811 let out = emit_yaml(&yaml);
1812 assert!(out.contains("skip_serializing_if = \"Option::is_none\""), "{out}");
1813 assert!(out.contains("pub note: Option<String>"), "{out}");
1814 }
1815}