|
15 | 15 | // specific language governing permissions and limitations
|
16 | 16 | // under the License.
|
17 | 17 |
|
| 18 | +use std::fmt::Write; |
18 | 19 | use anyhow::bail;
|
19 |
| -use clients_schema::{ |
20 |
| - Body, Enum, Interface, LiteralValueValue, PropertiesBody, Property, Request, Response, TypeAlias, |
21 |
| - TypeAliasVariants, TypeDefinition, TypeName, ValueOf, |
22 |
| -}; |
| 20 | +use clients_schema::{ArrayOf, Body, Enum, EnumMember, Interface, LiteralValueValue, PropertiesBody, Property, Request, Response, TypeAlias, TypeAliasVariants, TypeDefinition, TypeName, ValueOf}; |
23 | 21 | use indexmap::IndexMap;
|
24 | 22 | use openapiv3::{
|
25 | 23 | AdditionalProperties, ArrayType, Discriminator, ExternalDocumentation, NumberType, ObjectType, ReferenceOr, Schema,
|
26 | 24 | SchemaData, SchemaKind, StringType, Type,
|
27 | 25 | };
|
28 | 26 | use openapiv3::SchemaKind::AnyOf;
|
29 |
| - |
30 | 27 | use crate::components::TypesAndComponents;
|
31 | 28 | use crate::utils::{IntoSchema, ReferenceOrBoxed, SchemaName};
|
32 | 29 |
|
@@ -249,7 +246,7 @@ impl<'a> TypesAndComponents<'a> {
|
249 | 246 | let mut result = self.convert_value_of(&prop.typ)?;
|
250 | 247 | // TODO: how can we just wrap a reference so that we can add docs?
|
251 | 248 | if let ReferenceOr::Item(ref mut schema) = &mut result {
|
252 |
| - self.fill_data_with_prop(&mut schema.schema_data, prop); |
| 249 | + self.fill_data_with_prop(&mut schema.schema_data, prop)?; |
253 | 250 | }
|
254 | 251 | Ok(result)
|
255 | 252 | }
|
@@ -468,15 +465,171 @@ impl<'a> TypesAndComponents<'a> {
|
468 | 465 | // TODO: base.codegen_names as extension?
|
469 | 466 | }
|
470 | 467 |
|
471 |
| - fn fill_data_with_prop(&self, data: &mut SchemaData, prop: &Property) { |
| 468 | + fn fill_data_with_prop(&self, data: &mut SchemaData, prop: &Property) -> anyhow::Result<()> { |
472 | 469 | data.external_docs = self.convert_external_docs(prop);
|
473 | 470 | data.deprecated = prop.deprecation.is_some();
|
474 |
| - data.description = prop.description.clone(); |
| 471 | + data.description = self.property_description(prop)?; |
475 | 472 | data.extensions = crate::availability_as_extensions(&prop.availability);
|
476 | 473 | // TODO: prop.aliases as extensions
|
477 | 474 | // TODO: prop.server_default as extension
|
478 | 475 | // TODO: prop.doc_id as extension (new representation of since and stability)
|
479 | 476 | // TODO: prop.es_quirk as extension?
|
480 | 477 | // TODO: prop.codegen_name as extension?
|
| 478 | + |
| 479 | + Ok(()) |
| 480 | + } |
| 481 | + |
| 482 | + pub fn property_description(&self, prop: &Property) -> anyhow::Result<Option<String>> { |
| 483 | + if self.config.lift_enum_descriptions { |
| 484 | + Ok(lift_enum_descriptions(prop, &self.model)?.or_else(|| prop.description.clone())) |
| 485 | + } else { |
| 486 | + Ok(prop.description.clone()) |
| 487 | + } |
| 488 | + } |
| 489 | +} |
| 490 | + |
| 491 | +/// Unwraps aliases from a value definition, recursively. |
| 492 | +/// |
| 493 | +/// Returns the end value definition of the alias chain or `None` if the value definition isn't an alias. |
| 494 | +fn unwrap_alias<'a> (value: &ValueOf, model: &'a clients_schema::IndexedModel) -> anyhow::Result<Option<&'a ValueOf>> { |
| 495 | + let ValueOf::InstanceOf(io) = value else { |
| 496 | + return Ok(None); |
| 497 | + }; |
| 498 | + |
| 499 | + if io.typ.is_builtin() { |
| 500 | + return Ok(None); |
| 501 | + } |
| 502 | + |
| 503 | + let TypeDefinition::TypeAlias(alias) = model.get_type(&io.typ)? else { |
| 504 | + return Ok(None); |
| 505 | + }; |
| 506 | + |
| 507 | + // Try to unwrap further or else return the current alias |
| 508 | + let result = match unwrap_alias(&alias.typ, model)? { |
| 509 | + Some(alias_value) => Some(alias_value), |
| 510 | + None => Some(&alias.typ), |
| 511 | + }; |
| 512 | + |
| 513 | + Ok(result) |
| 514 | +} |
| 515 | + |
| 516 | +/// Checks if a value_of is a lenient array definition (i.e. `Foo | Foo[]`) and |
| 517 | +/// if successful, returns the value definition. |
| 518 | +fn unwrap_lenient_array(value: &ValueOf) -> Option<&ValueOf> { |
| 519 | + // Is this a union |
| 520 | + let ValueOf::UnionOf(u) = value else { |
| 521 | + return None |
| 522 | + }; |
| 523 | + |
| 524 | + // of a value and array_of (in any order) |
| 525 | + let (single_value, array_value) = match &u.items.as_slice() { |
| 526 | + [v, ValueOf::ArrayOf(ao)] | |
| 527 | + [ValueOf::ArrayOf(ao), v] => (v, &*ao.value), |
| 528 | + _ => return None, |
| 529 | + }; |
| 530 | + |
| 531 | + // and both value types are the same |
| 532 | + if single_value == array_value { |
| 533 | + return Some(single_value); |
| 534 | + } |
| 535 | + |
| 536 | + None |
| 537 | +} |
| 538 | + |
| 539 | +fn unwrap_array(value: &ValueOf) -> Option<&ValueOf> { |
| 540 | + match value { |
| 541 | + ValueOf::ArrayOf(ArrayOf { value }) => Some(value), |
| 542 | + _ => None, |
| 543 | + } |
| 544 | +} |
| 545 | + |
| 546 | +/// If a property value is an enumeration (possibly via aliases and arrays) |
| 547 | +fn lift_enum_descriptions(prop: &Property, model: &clients_schema::IndexedModel) -> anyhow::Result<Option<String>> { |
| 548 | + |
| 549 | + // FIXME: could be memoized on `prop.typ` as we'll redo this work every time we encounter the same value definition |
| 550 | + let value = &prop.typ; |
| 551 | + |
| 552 | + // Maybe an alias pointing to an array or lenient array |
| 553 | + let value = unwrap_alias(value, model)?.unwrap_or(value); |
| 554 | + |
| 555 | + // Unwrap lenient array |
| 556 | + let (lenient_array, value) = match unwrap_lenient_array(value) { |
| 557 | + Some(lenient_array) => (true, lenient_array), |
| 558 | + None => (false, value), |
| 559 | + }; |
| 560 | + |
| 561 | + // Unwrap array to get to the enum type |
| 562 | + let value = unwrap_array(value).unwrap_or(value); |
| 563 | + |
| 564 | + // Unwrap aliases again, in case the array value was itself an alias |
| 565 | + let value = unwrap_alias(value, model)?.unwrap_or(value); |
| 566 | + |
| 567 | + // Is this an enum? |
| 568 | + let ValueOf::InstanceOf(inst) = value else { |
| 569 | + return Ok(None); |
| 570 | + }; |
| 571 | + |
| 572 | + if inst.typ.is_builtin() { |
| 573 | + return Ok(None); |
481 | 574 | }
|
| 575 | + |
| 576 | + let TypeDefinition::Enum(enum_def) = model.get_type(&inst.typ)? else { |
| 577 | + return Ok(None); |
| 578 | + }; |
| 579 | + |
| 580 | + let mut result: String = match &prop.description { |
| 581 | + Some(desc) => desc.clone(), |
| 582 | + None => String::new(), |
| 583 | + }; |
| 584 | + |
| 585 | + // Do we have at least one enum member description? |
| 586 | + if enum_def.members.iter().any(|m| m.description.is_some()) { |
| 587 | + // Some descriptions: output a list with descriptions |
| 588 | + |
| 589 | + // Close description paragraph and add an empty line to start a new paragraph |
| 590 | + writeln!(result)?; |
| 591 | + writeln!(result)?; |
| 592 | + |
| 593 | + writeln!(result, "Supported values include:")?; |
| 594 | + for member in &enum_def.members { |
| 595 | + write!(result, " - ")?; |
| 596 | + value_and_aliases(&mut result, member)?; |
| 597 | + if let Some(desc) = &member.description { |
| 598 | + write!(result, ": {}", desc)?; |
| 599 | + } |
| 600 | + writeln!(result)?; |
| 601 | + } |
| 602 | + writeln!(result)?; |
| 603 | + |
| 604 | + } else { |
| 605 | + // No description: inline list of values, only if this wasn't a lenient array. |
| 606 | + // Otherwise (enum or enum array), bump.sh will correctly output a list of possible values. |
| 607 | + if !lenient_array { |
| 608 | + return Ok(None); |
| 609 | + } |
| 610 | + |
| 611 | + // Close description paragraph and add an empty line to start a new paragraph |
| 612 | + writeln!(result)?; |
| 613 | + writeln!(result)?; |
| 614 | + |
| 615 | + write!(result, "Supported values include: ")?; |
| 616 | + for (idx, member) in enum_def.members.iter().enumerate() { |
| 617 | + if idx > 0 { |
| 618 | + write!(result, ", ")?; |
| 619 | + } |
| 620 | + value_and_aliases(&mut result, member)?; |
| 621 | + } |
| 622 | + write!(result, "\n\n")?; |
| 623 | + } |
| 624 | + |
| 625 | + fn value_and_aliases(out: &mut String, member: &EnumMember) -> anyhow::Result<()> { |
| 626 | + write!(out, "`{}`", member.name)?; |
| 627 | + if !member.aliases.is_empty() { |
| 628 | + write!(out, " (or `{}`)", member.aliases.join("`, `"))?; |
| 629 | + } |
| 630 | + |
| 631 | + Ok(()) |
| 632 | + } |
| 633 | + |
| 634 | + Ok(Some(result)) |
482 | 635 | }
|
0 commit comments