Skip to content

Commit 7a0e5d0

Browse files
lcawlswallezl-trotta
authored
[OpenAPI] Lift enum member descriptions in property descriptions (#4313) (#4355)
(cherry picked from commit 4fb0647) Co-authored-by: Sylvain Wallez <[email protected]> Co-authored-by: Laura Trotta <[email protected]>
1 parent 73c00c3 commit 7a0e5d0

File tree

11 files changed

+388
-194
lines changed

11 files changed

+388
-194
lines changed

compiler-rs/Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

compiler-rs/clients_schema_to_openapi/src/components.rs

+4-3
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
use clients_schema::TypeName;
1919
use openapiv3::{Components, Parameter, ReferenceOr, RequestBody, Response, Schema, StatusCode};
20-
20+
use crate::Configuration;
2121
use crate::utils::SchemaName;
2222

2323
// Separator used to combine parts of a component path.
@@ -29,13 +29,14 @@ use crate::utils::SchemaName;
2929
pub const SEPARATOR: char = '-';
3030

3131
pub struct TypesAndComponents<'a> {
32+
pub config: &'a Configuration,
3233
pub model: &'a clients_schema::IndexedModel,
3334
pub components: &'a mut Components,
3435
}
3536

3637
impl<'a> TypesAndComponents<'a> {
37-
pub fn new(model: &'a clients_schema::IndexedModel, components: &'a mut Components) -> TypesAndComponents<'a> {
38-
TypesAndComponents { model, components }
38+
pub fn new(config: &'a Configuration, model: &'a clients_schema::IndexedModel, components: &'a mut Components) -> TypesAndComponents<'a> {
39+
TypesAndComponents { config, model, components }
3940
}
4041

4142
pub fn add_request_body(&mut self, endpoint: &str, body: RequestBody) -> ReferenceOr<RequestBody> {

compiler-rs/clients_schema_to_openapi/src/lib.rs

+19-5
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,27 @@ use openapiv3::{Components, OpenAPI};
2727
use clients_schema::transform::ExpandConfig;
2828
use crate::components::TypesAndComponents;
2929

30+
pub struct Configuration {
31+
pub flavor: Option<Flavor>,
32+
pub lift_enum_descriptions: bool,
33+
}
34+
35+
impl Default for Configuration {
36+
fn default() -> Self {
37+
Self {
38+
flavor: None,
39+
lift_enum_descriptions: true,
40+
}
41+
}
42+
}
43+
3044
/// Convert an API model into an OpenAPI v3 schema, optionally filtered for a given flavor
31-
pub fn convert_schema(mut schema: IndexedModel, flavor: Option<Flavor>) -> anyhow::Result<OpenAPI> {
45+
pub fn convert_schema(mut schema: IndexedModel, config: Configuration) -> anyhow::Result<OpenAPI> {
3246
// Expand generics
3347
schema = clients_schema::transform::expand_generics(schema, ExpandConfig::default())?;
3448

3549
// Filter flavor
36-
let filter: Option<fn(&Option<Availabilities>) -> bool> = match flavor {
50+
let filter: Option<fn(&Option<Availabilities>) -> bool> = match config.flavor {
3751
None => None,
3852
Some(Flavor::Stack) => Some(|a| {
3953
// Generate only public items for Stack
@@ -49,7 +63,7 @@ pub fn convert_schema(mut schema: IndexedModel, flavor: Option<Flavor>) -> anyho
4963
schema = clients_schema::transform::filter_availability(schema, filter)?;
5064
}
5165

52-
convert_expanded_schema(&schema)
66+
convert_expanded_schema(&schema, &config)
5367
}
5468

5569
/// Convert an API model into an OpenAPI v3 schema. The input model must have all generics expanded, conversion
@@ -58,7 +72,7 @@ pub fn convert_schema(mut schema: IndexedModel, flavor: Option<Flavor>) -> anyho
5872
/// Note: there are ways to represent [generics in JSON Schema], but its unlikely that tooling will understand it.
5973
///
6074
/// [generics in JSON Schema]: https://json-schema.org/blog/posts/dynamicref-and-generics
61-
pub fn convert_expanded_schema(model: &IndexedModel) -> anyhow::Result<OpenAPI> {
75+
pub fn convert_expanded_schema(model: &IndexedModel, config: &Configuration) -> anyhow::Result<OpenAPI> {
6276
let mut openapi = OpenAPI {
6377
openapi: "3.0.3".into(),
6478
info: info(model),
@@ -87,7 +101,7 @@ pub fn convert_expanded_schema(model: &IndexedModel) -> anyhow::Result<OpenAPI>
87101
extensions: Default::default(),
88102
};
89103

90-
let mut tac = TypesAndComponents::new(model, openapi.components.as_mut().unwrap());
104+
let mut tac = TypesAndComponents::new(config, model, openapi.components.as_mut().unwrap());
91105

92106
// Endpoints
93107
for endpoint in &model.endpoints {

compiler-rs/clients_schema_to_openapi/src/main.rs

+7-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use clients_schema::Flavor;
2323
use tracing::Level;
2424
use tracing_subscriber::fmt::format::FmtSpan;
2525
use tracing_subscriber::FmtSubscriber;
26+
use clients_schema_to_openapi::Configuration;
2627

2728
fn main() -> anyhow::Result<()> {
2829
let cli = Cli::parse();
@@ -83,7 +84,12 @@ impl Cli {
8384
Some(SchemaFlavor::Serverless) => Some(Flavor::Serverless),
8485
};
8586

86-
let openapi = clients_schema_to_openapi::convert_schema(model, flavor)?;
87+
let config = Configuration {
88+
flavor,
89+
..Default::default()
90+
};
91+
92+
let openapi = clients_schema_to_openapi::convert_schema(model, config)?;
8793

8894
let output: Box<dyn std::io::Write> = {
8995
if let Some(output) = self.output {

compiler-rs/clients_schema_to_openapi/src/paths.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ pub fn add_endpoint(
6262
fn parameter_data(prop: &Property, in_path: bool, tac: &mut TypesAndComponents) -> anyhow::Result<ParameterData> {
6363
Ok(ParameterData {
6464
name: prop.name.clone(),
65-
description: prop.description.clone(),
65+
description: tac.property_description(prop)?,
6666
required: in_path || prop.required, // Path parameters are always required
6767
deprecated: Some(prop.deprecation.is_some()),
6868
format: ParameterSchemaOrContent::Schema(tac.convert_value_of(&prop.typ)?),

compiler-rs/clients_schema_to_openapi/src/schemas.rs

+161-8
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,15 @@
1515
// specific language governing permissions and limitations
1616
// under the License.
1717

18+
use std::fmt::Write;
1819
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};
2321
use indexmap::IndexMap;
2422
use openapiv3::{
2523
AdditionalProperties, ArrayType, Discriminator, ExternalDocumentation, NumberType, ObjectType, ReferenceOr, Schema,
2624
SchemaData, SchemaKind, StringType, Type,
2725
};
2826
use openapiv3::SchemaKind::AnyOf;
29-
3027
use crate::components::TypesAndComponents;
3128
use crate::utils::{IntoSchema, ReferenceOrBoxed, SchemaName};
3229

@@ -249,7 +246,7 @@ impl<'a> TypesAndComponents<'a> {
249246
let mut result = self.convert_value_of(&prop.typ)?;
250247
// TODO: how can we just wrap a reference so that we can add docs?
251248
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)?;
253250
}
254251
Ok(result)
255252
}
@@ -468,15 +465,171 @@ impl<'a> TypesAndComponents<'a> {
468465
// TODO: base.codegen_names as extension?
469466
}
470467

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<()> {
472469
data.external_docs = self.convert_external_docs(prop);
473470
data.deprecated = prop.deprecation.is_some();
474-
data.description = prop.description.clone();
471+
data.description = self.property_description(prop)?;
475472
data.extensions = crate::availability_as_extensions(&prop.availability);
476473
// TODO: prop.aliases as extensions
477474
// TODO: prop.server_default as extension
478475
// TODO: prop.doc_id as extension (new representation of since and stability)
479476
// TODO: prop.es_quirk as extension?
480477
// 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);
481574
}
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))
482635
}

compiler-rs/compiler-wasm-lib/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ clients_schema = {path="../clients_schema"}
1717
clients_schema_to_openapi = {path="../clients_schema_to_openapi"}
1818
serde_json = { workspace = true }
1919
anyhow = { workspace = true }
20+
tracing = "0.1"
2021

2122
console_error_panic_hook = { workspace = true, optional = true }
2223
tracing-wasm = "0.2.1"
Binary file not shown.

compiler-rs/compiler-wasm-lib/src/lib.rs

+6-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use anyhow::bail;
1919
use clients_schema::{Flavor, IndexedModel};
2020
use wasm_bindgen::prelude::*;
21+
use clients_schema_to_openapi::Configuration;
2122

2223
#[wasm_bindgen]
2324
pub fn convert_schema_to_openapi(json: &str, flavor: &str) -> Result<String, String> {
@@ -33,8 +34,12 @@ fn convert0(json: &str, flavor: &str) -> anyhow::Result<String> {
3334
_ => bail!("Unknown flavor {}", flavor),
3435
};
3536

37+
let config = Configuration {
38+
flavor,
39+
..Default::default()
40+
};
3641
let schema = IndexedModel::from_reader(json.as_bytes())?;
37-
let openapi = clients_schema_to_openapi::convert_schema(schema, flavor)?;
42+
let openapi = clients_schema_to_openapi::convert_schema(schema, config)?;
3843
let result = serde_json::to_string_pretty(&openapi)?;
3944
Ok(result)
4045
}

0 commit comments

Comments
 (0)