Skip to content

Commit 7dd45a5

Browse files
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.
1 parent 5589a7b commit 7dd45a5

File tree

9 files changed

+6753
-3
lines changed

9 files changed

+6753
-3
lines changed

compiler-rs/clients_schema/src/lib.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,25 @@ impl TypeDefinition {
482482
}
483483
}
484484

485+
/**
486+
* The Example type is used for both requests and responses
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+
* This type matches the 'Example' type in metamodel.ts. The
491+
* data serialized by the Typescript code in schema.json,
492+
* needs to be deserialized into this equivalent type.
493+
* The OpenAPI v3 spec also defines the 'Example' type, so
494+
* to distinguish them, this type is called SchemaExample.
495+
*/
496+
#[derive(Debug, Clone, Serialize, Deserialize)]
497+
pub struct SchemaExample {
498+
pub summary: Option<String>,
499+
pub description: Option<String>,
500+
pub value: Option<String>,
501+
pub external_value: Option<String>,
502+
}
503+
485504
/// Common attributes for all type definitions
486505
#[derive(Debug, Clone, Serialize, Deserialize)]
487506
#[serde(rename_all = "camelCase")]
@@ -675,6 +694,8 @@ pub struct Request {
675694

676695
#[serde(default, skip_serializing_if = "Vec::is_empty")]
677696
pub attached_behaviors: Vec<String>,
697+
698+
pub examples: Option<IndexMap<String, SchemaExample>>
678699
}
679700

680701
impl WithBaseType for Request {
@@ -703,6 +724,8 @@ pub struct Response {
703724

704725
#[serde(default, skip_serializing_if = "Vec::is_empty")]
705726
pub exceptions: Vec<ResponseException>,
727+
728+
pub examples: Option<IndexMap<String, SchemaExample>>
706729
}
707730

708731
impl WithBaseType for Response {

compiler-rs/clients_schema_to_openapi/src/paths.rs

Lines changed: 40 additions & 3 deletions
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

Lines changed: 5 additions & 0 deletions
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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,19 @@ 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+
summary?: string
271+
description?: string
272+
value?: string
273+
external_value?: string
274+
}
275+
263276
/**
264277
* A request type
265278
*/
@@ -288,6 +301,7 @@ export class Request extends BaseType {
288301
body: Body
289302
behaviors?: Behavior[]
290303
attachedBehaviors?: string[]
304+
examples?: Map<string, Example>
291305
}
292306

293307
/**
@@ -300,6 +314,7 @@ export class Response extends BaseType {
300314
behaviors?: Behavior[]
301315
attachedBehaviors?: string[]
302316
exceptions?: ResponseException[]
317+
examples?: Map<string, Example>
303318
}
304319

305320
export class ResponseException {

0 commit comments

Comments
 (0)