Skip to content

Commit e9bc80f

Browse files
siamakp-elasticlcawlswallez
authored
Adds examples to schema.json and OpenAPI outputs (#3737)
* This featre scans All the API folders in the specfication to locate API request and response examples. It then adds the 'examples' info to schema.json and to the OpenAPI JSON files. --------- Co-authored-by: Lisa Cawley <lcawley@elastic.co> Co-authored-by: Sylvain Wallez <sylvain@elastic.co>
1 parent 6659ee4 commit e9bc80f

File tree

11 files changed

+10398
-3
lines changed

11 files changed

+10398
-3
lines changed

compiler-rs/clients_schema/src/lib.rs

+21
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,23 @@ impl TypeDefinition {
482482
}
483483
}
484484

485+
/// The Example type is used for both requests and responses.
486+
///
487+
/// This type definition is taken from the OpenAPI spec
488+
/// https://spec.openapis.org/oas/v3.1.0#example-object
489+
/// with the exception of using String as the 'value' type.
490+
///
491+
/// The OpenAPI v3 spec also defines the 'Example' type, so
492+
/// to distinguish them, this type is called SchemaExample.
493+
494+
#[derive(Debug, Clone, Serialize, Deserialize)]
495+
pub struct SchemaExample {
496+
pub summary: Option<String>,
497+
pub description: Option<String>,
498+
pub value: Option<String>,
499+
pub external_value: Option<String>,
500+
}
501+
485502
/// Common attributes for all type definitions
486503
#[derive(Debug, Clone, Serialize, Deserialize)]
487504
#[serde(rename_all = "camelCase")]
@@ -675,6 +692,8 @@ pub struct Request {
675692

676693
#[serde(default, skip_serializing_if = "Vec::is_empty")]
677694
pub attached_behaviors: Vec<String>,
695+
696+
pub examples: Option<IndexMap<String, SchemaExample>>
678697
}
679698

680699
impl WithBaseType for Request {
@@ -703,6 +722,8 @@ pub struct Response {
703722

704723
#[serde(default, skip_serializing_if = "Vec::is_empty")]
705724
pub exceptions: Vec<ResponseException>,
725+
726+
pub examples: Option<IndexMap<String, SchemaExample>>
706727
}
707728

708729
impl WithBaseType for Response {

compiler-rs/clients_schema_to_openapi/src/paths.rs

+40-3
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,15 @@ use std::fmt::Write;
2020

2121
use anyhow::{anyhow, bail};
2222
use clients_schema::Property;
23+
use indexmap::IndexMap;
2324
use indexmap::indexmap;
2425
use icu_segmenter::SentenceSegmenter;
2526
use openapiv3::{
2627
MediaType, Parameter, ParameterData, ParameterSchemaOrContent, PathItem, PathStyle, Paths, QueryStyle, ReferenceOr,
27-
RequestBody, Response, Responses, StatusCode,
28+
RequestBody, Response, Responses, StatusCode, Example
2829
};
30+
use clients_schema::SchemaExample;
31+
use serde_json::json;
2932

3033
use crate::components::TypesAndComponents;
3134

@@ -116,15 +119,42 @@ pub fn add_endpoint(
116119

117120
//---- Prepare request body
118121

122+
// This function converts the IndexMap<String, SchemaExample> examples of
123+
// schema.json to IndexMap<String, ReferenceOr<Example>> which is the format
124+
// that OpenAPI expects.
125+
fn get_openapi_examples(schema_examples: IndexMap<String, SchemaExample>) -> IndexMap<String, ReferenceOr<Example>> {
126+
let mut openapi_examples = indexmap! {};
127+
for (name, schema_example) in schema_examples {
128+
let openapi_example = Example {
129+
value: Some(json!(schema_example.value)),
130+
description: schema_example.description.clone(),
131+
summary: schema_example.summary.clone(),
132+
external_value: None,
133+
extensions: Default::default(),
134+
};
135+
openapi_examples.insert(name.clone(), ReferenceOr::Item(openapi_example));
136+
}
137+
return openapi_examples;
138+
}
139+
140+
141+
let mut request_examples: IndexMap<String, ReferenceOr<Example>> = indexmap! {};
142+
// If this endpoint request has examples in schema.json, convert them to the
143+
// OpenAPI format and add them to the endpoint request in the OpenAPI document.
144+
if request.examples.is_some() {
145+
request_examples = get_openapi_examples(request.examples.as_ref().unwrap().clone());
146+
}
147+
119148
let request_body = tac.convert_request(request)?.map(|schema| {
120149
let media = MediaType {
121150
schema: Some(schema),
122151
example: None,
123-
examples: Default::default(),
152+
examples: request_examples,
124153
encoding: Default::default(),
125154
extensions: Default::default(),
126155
};
127156

157+
128158
let body = RequestBody {
129159
description: None,
130160
// FIXME: nd-json requests
@@ -142,17 +172,24 @@ pub fn add_endpoint(
142172

143173
//---- Prepare request responses
144174

175+
145176
// FIXME: buggy for responses with no body
146177
// TODO: handle binary responses
147178
let response_def = tac.model.get_response(endpoint.response.as_ref().unwrap())?;
179+
let mut response_examples: IndexMap<String, ReferenceOr<Example>> = indexmap! {};
180+
// If this endpoint response has examples in schema.json, convert them to the
181+
// OpenAPI format and add them to the endpoint response in the OpenAPI document.
182+
if response_def.examples.is_some() {
183+
response_examples = get_openapi_examples(response_def.examples.as_ref().unwrap().clone());
184+
}
148185
let response = Response {
149186
description: "".to_string(),
150187
headers: Default::default(),
151188
content: indexmap! {
152189
"application/json".to_string() => MediaType {
153190
schema: tac.convert_response(response_def)?,
154191
example: None,
155-
examples: Default::default(),
192+
examples: response_examples,
156193
encoding: Default::default(),
157194
extensions: Default::default(),
158195
}
Binary file not shown.

compiler/src/index.ts

+5
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import validateModel from './steps/validate-model'
2828
import addContentType from './steps/add-content-type'
2929
import readDefinitionValidation from './steps/read-definition-validation'
3030
import addDeprecation from './steps/add-deprecation'
31+
import ExamplesProcessor from './steps/add-examples'
3132

3233
const nvmrc = readFileSync(join(__dirname, '..', '..', '.nvmrc'), 'utf8')
3334
const nodejsMajor = process.version.split('.').shift()?.slice(1) ?? ''
@@ -65,6 +66,9 @@ if (outputFolder === '' || outputFolder === undefined) {
6566

6667
const compiler = new Compiler(specsFolder, outputFolder)
6768

69+
const examplesProcessor = new ExamplesProcessor(specsFolder)
70+
const addExamples = examplesProcessor.addExamples.bind(examplesProcessor)
71+
6872
compiler
6973
.generateModel()
7074
.step(addInfo)
@@ -74,6 +78,7 @@ compiler
7478
.step(validateRestSpec)
7579
.step(addDescription)
7680
.step(validateModel)
81+
.step(addExamples)
7782
.write()
7883
.then(() => {
7984
console.log('Done')

compiler/src/model/metamodel.ts

+19
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,23 @@ export class Interface extends BaseType {
260260
variants?: Container
261261
}
262262

263+
/**
264+
* The Example type is used for both requests and responses
265+
* This type definition is taken from the OpenAPI spec
266+
* https://spec.openapis.org/oas/v3.1.0#example-object
267+
* With the exception of using String as the 'value' type
268+
*/
269+
export class Example {
270+
/** Short description. */
271+
summary?: string
272+
/** Long description. */
273+
description?: string
274+
/** Embedded literal example. Mutually exclusive with `external_value` */
275+
value?: string
276+
/** A URI that points to the literal example */
277+
external_value?: string
278+
}
279+
263280
/**
264281
* A request type
265282
*/
@@ -288,6 +305,7 @@ export class Request extends BaseType {
288305
body: Body
289306
behaviors?: Behavior[]
290307
attachedBehaviors?: string[]
308+
examples?: Record<string, Example>
291309
}
292310

293311
/**
@@ -300,6 +318,7 @@ export class Response extends BaseType {
300318
behaviors?: Behavior[]
301319
attachedBehaviors?: string[]
302320
exceptions?: ResponseException[]
321+
examples?: Record<string, Example>
303322
}
304323

305324
export class ResponseException {

0 commit comments

Comments
 (0)