Skip to content

An app for getting general info and stats about the API edpoints and their 'examples' state. #3745

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions compiler-rs/clients_schema/src/lib.rs
Original file line number Diff line number Diff line change
@@ -482,6 +482,25 @@ impl TypeDefinition {
}
}

/**
* The Example type is used for both requests and responses
* This type definition is taken from the OpenAPI spec
* https://spec.openapis.org/oas/v3.1.0#example-object
* With the exception of using String as the 'value' type.
* This type matches the 'Example' type in metamodel.ts. The
* data serialized by the Typescript code in schema.json,
* needs to be deserialized into this equivalent type.
* The OpenAPI v3 spec also defines the 'Example' type, so
* to distinguish them, this type is called SchemaExample.
*/
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SchemaExample {
pub summary: Option<String>,
pub description: Option<String>,
pub value: Option<String>,
pub external_value: Option<String>,
}

/// Common attributes for all type definitions
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
@@ -675,6 +694,8 @@ pub struct Request {

#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub attached_behaviors: Vec<String>,

pub examples: Option<IndexMap<String, SchemaExample>>
}

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

#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub exceptions: Vec<ResponseException>,

pub examples: Option<IndexMap<String, SchemaExample>>
}

impl WithBaseType for Response {
43 changes: 40 additions & 3 deletions compiler-rs/clients_schema_to_openapi/src/paths.rs
Original file line number Diff line number Diff line change
@@ -20,12 +20,15 @@ use std::fmt::Write;

use anyhow::{anyhow, bail};
use clients_schema::Property;
use indexmap::IndexMap;
use indexmap::indexmap;
use icu_segmenter::SentenceSegmenter;
use openapiv3::{
MediaType, Parameter, ParameterData, ParameterSchemaOrContent, PathItem, PathStyle, Paths, QueryStyle, ReferenceOr,
RequestBody, Response, Responses, StatusCode,
RequestBody, Response, Responses, StatusCode, Example
};
use clients_schema::SchemaExample;
use serde_json::json;

use crate::components::TypesAndComponents;

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

//---- Prepare request body

// This function converts the IndexMap<String, SchemaExample> examples of
// schema.json to IndexMap<String, ReferenceOr<Example>> which is the format
// that OpenAPI expects.
fn get_openapi_examples(schema_examples: IndexMap<String, SchemaExample>) -> IndexMap<String, ReferenceOr<Example>> {
let mut openapi_examples = indexmap! {};
for (name, schema_example) in schema_examples {
let openapi_example = Example {
value: Some(json!(schema_example.value)),
description: schema_example.description.clone(),
summary: schema_example.summary.clone(),
external_value: None,
extensions: Default::default(),
};
openapi_examples.insert(name.clone(), ReferenceOr::Item(openapi_example));
}
return openapi_examples;
}


let mut request_examples: IndexMap<String, ReferenceOr<Example>> = indexmap! {};
// If this endpoint request has examples in schema.json, convert them to the
// OpenAPI format and add them to the endpoint request in the OpenAPI document.
if request.examples.is_some() {
request_examples = get_openapi_examples(request.examples.as_ref().unwrap().clone());
}

let request_body = tac.convert_request(request)?.map(|schema| {
let media = MediaType {
schema: Some(schema),
example: None,
examples: Default::default(),
examples: request_examples,
encoding: Default::default(),
extensions: Default::default(),
};


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

//---- Prepare request responses


// FIXME: buggy for responses with no body
// TODO: handle binary responses
let response_def = tac.model.get_response(endpoint.response.as_ref().unwrap())?;
let mut response_examples: IndexMap<String, ReferenceOr<Example>> = indexmap! {};
// If this endpoint response has examples in schema.json, convert them to the
// OpenAPI format and add them to the endpoint response in the OpenAPI document.
if response_def.examples.is_some() {
response_examples = get_openapi_examples(response_def.examples.as_ref().unwrap().clone());
}
let response = Response {
description: "".to_string(),
headers: Default::default(),
content: indexmap! {
"application/json".to_string() => MediaType {
schema: tac.convert_response(response_def)?,
example: None,
examples: Default::default(),
examples: response_examples,
encoding: Default::default(),
extensions: Default::default(),
}
Binary file modified compiler-rs/compiler-wasm-lib/pkg/compiler_wasm_lib_bg.wasm
Binary file not shown.
5 changes: 5 additions & 0 deletions compiler/src/index.ts
Original file line number Diff line number Diff line change
@@ -28,6 +28,7 @@ import validateModel from './steps/validate-model'
import addContentType from './steps/add-content-type'
import readDefinitionValidation from './steps/read-definition-validation'
import addDeprecation from './steps/add-deprecation'
import ExamplesProcessor from './steps/add-examples'

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

const compiler = new Compiler(specsFolder, outputFolder)

const examplesProcessor = new ExamplesProcessor(specsFolder)
const addExamples = examplesProcessor.addExamples.bind(examplesProcessor)

compiler
.generateModel()
.step(addInfo)
@@ -74,6 +78,7 @@ compiler
.step(validateRestSpec)
.step(addDescription)
.step(validateModel)
.step(addExamples)
.write()
.then(() => {
console.log('Done')
15 changes: 15 additions & 0 deletions compiler/src/model/metamodel.ts
Original file line number Diff line number Diff line change
@@ -260,6 +260,19 @@ export class Interface extends BaseType {
variants?: Container
}

/**
* The Example type is used for both requests and responses
* This type definition is taken from the OpenAPI spec
* https://spec.openapis.org/oas/v3.1.0#example-object
* With the exception of using String as the 'value' type
*/
export class Example {
summary?: string
description?: string
value?: string
external_value?: string
}

/**
* A request type
*/
@@ -288,6 +301,7 @@ export class Request extends BaseType {
body: Body
behaviors?: Behavior[]
attachedBehaviors?: string[]
examples?: Map<string, Example>
}

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

export class ResponseException {
261 changes: 261 additions & 0 deletions compiler/src/steps/add-examples.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import * as model from '../model/metamodel'
import { JsonSpec } from '../model/json-spec'
import * as path from 'path'
import * as fs from 'fs'
import * as yaml from 'js-yaml'

/**
* Scan the API folders in the specification to locate examples
* for all the API endpoints. Then add the examples to the model.
*/
export default class ExamplesProcessor {
specsFolder: string

constructor (specsFolder: string) {
this.specsFolder = specsFolder
}

// Add request and response examples for all the endpoints in the model.
// Note that the 'jsonSpec' is a parameter that is passed to a 'Step'.
// We don't need that parameter for the the 'addExamples' functionality.
async addExamples (model: model.Model, jsonSpec: Map<string, JsonSpec>): Promise<model.Model> {
const requestExamplesProcessor = new RequestExamplesProcessor(model, this.specsFolder)
const responseExamplesProcessor = new ResponseExamplesProcessor(model, this.specsFolder)
for (const endpoint of model.endpoints) {
if (endpoint.request != null) { requestExamplesProcessor.addExamples(endpoint.request) }
if (endpoint.response != null) { responseExamplesProcessor.addExamples(endpoint.response) }
}
return model
}
}

/*
* Base class for the request and response examples processors.
*/
class BaseExamplesProcessor {
model: model.Model
specsFolder: string

constructor (model: model.Model, specsFolder: string) {
this.model = model
this.specsFolder = specsFolder
}

// Log a 'warning' message.
warning (message: string): void {
console.warn('=== [ExamplesProcessor]: ' + message)
}

// Get all the subfolders in a folder.
getSubfolders (folderPath: string): string[] {
const entries = fs.readdirSync(folderPath, { withFileTypes: true })
const folders = entries
.filter(entry => entry.isDirectory())
.map(entry => entry.name)
return folders
}

// Get all the files in a folder.
getFilesInFolder (folderPath: string): string[] {
const entries = fs.readdirSync(folderPath, { withFileTypes: true })
const files = entries
.filter(entry => entry.isFile())
.map(entry => entry.name)
return files
}

// Check if a path exists and is a directory.
isDirectory (path: string): boolean {
try {
const stats = fs.statSync(path)
return stats.isDirectory()
} catch (error) {
if (error.code === 'ENOENT') {
// Path does not exist
return false
} else {
// Other error, rethrow
throw error
}
}
}

// Given the spec location of a request or response,
// return the path to the examples folder for that
// request or response.
getExamplesFolder (specLocation: string): string | undefined {
const specDir = path.dirname(specLocation)
const specPath = path.join(this.specsFolder, specDir)
const examplesFolder = path.join(specPath, 'examples')
if (this.isDirectory(examplesFolder)) {
return examplesFolder
}
return undefined
}

// Given an examples request or response folder, return all the
// valid example files in that folder.
getExampleFiles (folder: string): string[] {
if (!this.isDirectory(folder)) {
return []
}
// Currently we only allow YAML example files.
const exampleFiles = this.getFilesInFolder(folder)
.filter(file => file.endsWith('.yml') || file.endsWith('.yaml'))
if (exampleFiles.length === 0) {
this.warning(`No example files found in ${folder}`)
return []
}
return exampleFiles
}

// Look up all the example files in a folder. Use the filename without extension
// as the name of the example, and the YAML content as the example value.
// Return a map of example names to example values.
getExampleMap (folder: string): Map<string, model.Example> {
const exampleFiles = this.getExampleFiles(folder)
const examples = new Map<string, model.Example>()
for (const fileName of exampleFiles) {
const filePath = path.join(folder, fileName)
const exampleFileContent = fs.readFileSync(filePath, 'utf8')
const exampleName = path.basename(fileName, path.extname(fileName))
const example: model.Example = yaml.load(exampleFileContent)
// Some of the example files set their 'value' as a JSON string,
// and some files set their 'value' as an object. For consistency,
// if the value is not a JSON string, convert it to a JSON string.
if (typeof example.value !== 'string') {
// Convert to prettified JSON string
example.value = JSON.stringify(example.value, null, 2)
}
examples[exampleName] = example
}
return examples
}
}

/*
* Class to add the examples for an API request
*/
class RequestExamplesProcessor extends BaseExamplesProcessor {
// Traverse all the types in the model to find a type that is
// of type 'request' and has the same name and namespace as the request.
getRequestDefinition (model: model.Model, request: model.TypeName): model.Request {
for (const type of model.types) {
if (type.kind === 'request') {
if (type.name.name === request.name && type.name.namespace === request.namespace) {
return type
}
}
}
throw new Error(`Can't find the request definiton for ${request.namespace}.${request.name}`)
}

// Given the spec location, return the request examples folder, if it exists.
getExamplesRequestSubfolder (examplesSubfolder: string): string | undefined {
const subFolder = path.join(examplesSubfolder, 'request')
if (this.isDirectory(subFolder)) {
return subFolder
}
return undefined
}

// Find all the request examples for this request and add them to the model.
addExamples (request: model.TypeName): void {
const requestDefinition = this.getRequestDefinition(this.model, request)
const examplesFolder = this.getExamplesFolder(requestDefinition.specLocation)
if (examplesFolder === undefined) {
return
}
// Get the request examples subfolder.
const examplesRequestSubfolder = this.getExamplesRequestSubfolder(examplesFolder)
// If there is an examples/request folder, add the request examples to the model.
if (examplesRequestSubfolder !== undefined) {
requestDefinition.examples = this.getExampleMap(examplesRequestSubfolder)
}
}
}

/*
* Class to add the examples for an API response
*/
class ResponseExamplesProcessor extends BaseExamplesProcessor {
// Traverse all the types in the model to find a type that is
// of type 'response' and has the same name and namespace as the response.
getResponseDefinition (model: model.Model, response: model.TypeName): model.Response {
for (const type of model.types) {
if (type.kind === 'response') {
if (type.name.name === response.name && type.name.namespace === response.namespace) {
return type
}
}
}
throw new Error(`Can't find the response definiton for ${response.namespace}.${response.name}`)
}

// Given the spec location, return the response example folders if they exists.
// A response example folder can be of either of these forms:
// response
// {nnn}_response
// Where {nnn} is the HTTP response code. If the folder is named 'response',
// assume that the response code is 200, otherwise pick up the response code
// from the folder name.
// Return a map of status code to the folder path.
getExamplesResponseSubfolderMap (examplesSubfolder: string): Map<string, string> | undefined {
const subfolders = this.getSubfolders(examplesSubfolder)
// If we have a "response" subfolder, stop there and return.
// We should not have a mix of response and {nnn}_response folders.
if ('response' in subfolders) {
const responseSubfolder = path.join(examplesSubfolder, 'response')
return new Map([['200', responseSubfolder]])
}
// Look for subfolders of the format '{nnn}_response'.
const rspSubfolders = subfolders.filter(folder => folder.endsWith('_response'))
const responseTypeMap = new Map<string, string>()
for (const rspSubfolder of rspSubfolders) {
const match = rspSubfolder.match(/^([0-9]{3})_response$/)
if (match == null) {
throw new Error(`Unexpected response folder: ${rspSubfolder}`)
}
const statusCode = match[1]
const responseSubfolder = path.join(examplesSubfolder, rspSubfolder)
responseTypeMap.set(statusCode, responseSubfolder)
}
return responseTypeMap
}

// Find all the response examples for this request and add them to the model.
addExamples (response: model.TypeName): void {
const responseDefinition = this.getResponseDefinition(this.model, response)
const examplesFolder = this.getExamplesFolder(responseDefinition.specLocation)
if (examplesFolder === undefined) {
return
}
// Get a map of status code to response example subfolder.
const examplesResponseSubfolderMap = this.getExamplesResponseSubfolderMap(examplesFolder)
const examples200ResponseSubfolder = examplesResponseSubfolderMap?.get('200')
// If there is an examples/response or examples/200_response folder,
// add the response examples to the model.
if (examples200ResponseSubfolder !== undefined) {
responseDefinition.examples = this.getExampleMap(examples200ResponseSubfolder)
}
}
}
2,911 changes: 2,911 additions & 0 deletions output/openapi/elasticsearch-openapi.json

Large diffs are not rendered by default.

1,504 changes: 1,504 additions & 0 deletions output/openapi/elasticsearch-serverless-openapi.json

Large diffs are not rendered by default.

1,996 changes: 1,996 additions & 0 deletions output/schema/schema.json

Large diffs are not rendered by default.

95 changes: 95 additions & 0 deletions src/scripts/examples-stats/EndpointInfoGenerator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import os
import re
from typing import List
from pathlib import Path
from constants import EXAMPLES_FOLDER
from dataclasses import dataclass, field

@dataclass
class EndpointInfo:
path: str = None
has_examples_subfolder: bool = False
num_request_examples: int = 0
num_response_examples: int = 0
num_examples: int = 0
examples_subfolders = []
recognized_examples_subfolders: set[str] = field(default_factory=set)
examples_response_codes: set[str] = field(default_factory=set)

class EndpointInfoGenerator:
def __init__(self):
self.spec_path = "."

# Get all the folders in a path
def get_folders(self, path: str) -> List[Path]:
return [f for f in os.listdir(path) if os.path.isdir(os.path.join(path, f))]

def get_files(self, path: str) -> List[str]:
return [f for f in os.listdir(path) if os.path.isfile(os.path.join(path, f))]

def is_example_file(self, file: str) -> bool:
if file.endswith(".yaml"):
return True
print(f"WARNING: Found non-YAML example file: {file}")
return False

def get_example_files_in_folder(self, path: str) -> List[str]:
example_files = []
for file in self.get_files(path):
if self.is_example_file(file):
example_files.append(file)
return example_files

def get_request_subfolder(self, endpoint_path) -> str:
examples_path = os.path.join(endpoint_path, EXAMPLES_FOLDER)
request_examples_path = os.path.join(examples_path, "request")
if os.path.exists(request_examples_path):
return "request"
return None

def get_response_subfolders(self, endpoint_path) -> int:
examples_path = os.path.join(endpoint_path, EXAMPLES_FOLDER)
response_examples_folders = []
response_examples_path = os.path.join(examples_path, "response")
if os.path.exists(response_examples_path):
response_examples_folders.append("response")
else:
examples_subfolders = self.get_folders(examples_path)
for examples_subfolder in examples_subfolders:
# Look for folders of the pattern "nnn_response"
if re.match(r"[0-9]{3}_response", examples_subfolder):
response_examples_folders.append(examples_subfolder)
return response_examples_folders

def get_response_code_from_response_folder(self, folder: str) -> str:
if folder == "response":
return "200"
match = re.match(r"(\d{3})_response", folder)
if match:
return match.group(1)
raise Exception(f"Invalid response folder: {folder}")

def get_endpoint_info(self, endpoint_path: str) -> EndpointInfo:
endpoint_path_relative_to_spec = os.path.relpath(endpoint_path, self.spec_path)
endpoint_info = EndpointInfo(path=endpoint_path_relative_to_spec)
# If there is no 'examples' folder, return EndpointInfo with
# default values
examples_path = os.path.join(endpoint_path, EXAMPLES_FOLDER)
if not os.path.exists(examples_path):
return endpoint_info
endpoint_info.examples_subfolders = self.get_folders(examples_path)
request_subfolder = self.get_request_subfolder(endpoint_path)
if request_subfolder:
endpoint_info.recognized_examples_subfolders.add(request_subfolder)
examples_request_path = os.path.join(examples_path, request_subfolder)
endpoint_info.num_request_examples = len(self.get_example_files_in_folder(examples_request_path))
response_subfolders = self.get_response_subfolders(endpoint_path)
if (len(response_subfolders) > 0):
endpoint_info.recognized_examples_subfolders.update(response_subfolders)
for response_subfolder in response_subfolders:
response_code = self.get_response_code_from_response_folder(response_subfolder)
endpoint_info.examples_response_codes.add(response_code)
examples_response_path = os.path.join(examples_path, response_subfolder)
endpoint_info.num_response_examples += len(self.get_example_files_in_folder(examples_response_path))
endpoint_info.num_examples = endpoint_info.num_request_examples + endpoint_info.num_response_examples
return endpoint_info
45 changes: 45 additions & 0 deletions src/scripts/examples-stats/EndpointPathsFinder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import os
from typing import List
from pathlib import Path

class EndpointPathsFinder:
def __init__(self):
self.spec_path = "."

def get_folders(self, path: str) -> List[Path]:
return [f for f in os.listdir(path) if os.path.isdir(os.path.join(path, f))]

def is_endpoint_group_folder(self, folder: str) -> bool:
# Other than _global, any folder starting with underscore is
# not an endpoint group folder
if folder == "_global":
return True
if folder.startswith("_"):
return False
return True

def get_endpoint_group_folders(self) -> List[Path]:
folders = self.get_folders(self.spec_path)
return [f for f in folders if self.is_endpoint_group_folder(f)]

def is_endpoint_folder(self, folder: str) -> bool:
if folder == "_types":
return False
return True

def get_endpoint_folders(self, path: str) -> List[Path]:
folders = self.get_folders(path)
return [f for f in folders if self.is_endpoint_folder(f)]

def get_endpoint_paths(self) -> List[str]:
endpoint_paths = []
group_folders = self.get_endpoint_group_folders()
for group_folder in group_folders:
group_path = os.path.join(self.spec_path, group_folder)
group_endpoint_folders = self.get_endpoint_folders(group_path)
for endpoint_folder in group_endpoint_folders:
endpoint_paths.append(os.path.join(group_path, endpoint_folder))
return endpoint_paths

def find_paths(self) -> List[str]:
return self.get_endpoint_paths()
31 changes: 31 additions & 0 deletions src/scripts/examples-stats/ExamplesInfoGenerator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from typing import List
from dataclasses import dataclass
from EndpointPathsFinder import EndpointPathsFinder
from EndpointInfoGenerator import EndpointInfo, EndpointInfoGenerator
from ExamplesStatsGenerator import ExampleStats, ExamplesStatsGenerator

@dataclass
class ExamplesInfo:
endpoints_info: List[EndpointInfo]
stats: ExampleStats

class ExamplesInfoGenerator:
def __init__(self):
self.endpoint_paths_finder = EndpointPathsFinder()
self.endpoint_info_processor = EndpointInfoGenerator()

def get_examples_info(self) -> ExamplesInfo:
endpoint_paths_finder = EndpointPathsFinder()
endpoint_paths = endpoint_paths_finder.find_paths()
endpoint_info_list = []
stats = ExampleStats()
for endpoint_path in endpoint_paths:
stats.num_endpoints += 1
endpoint_info = self.endpoint_info_processor.get_endpoint_info(endpoint_path)
endpoint_info_list.append(endpoint_info)

examples_stats_generator = ExamplesStatsGenerator(endpoint_info_list)
stats = examples_stats_generator.get_stats()

examples_info = ExamplesInfo(endpoint_info_list, stats)
return examples_info
49 changes: 49 additions & 0 deletions src/scripts/examples-stats/ExamplesStatsGenerator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from dataclasses import dataclass
from typing import List
from EndpointInfoGenerator import EndpointInfo

@dataclass
class ExampleStats:
num_endpoints: int = 0
num_endpoints_with_examples: int = 0
num_endpoints_with_no_examples: int = 0
num_endpoints_with_only_request_examples: int = 0
num_endpoints_with_only_response_examples: int = 0
num_endpoints_with_both_request_and_response_examples: int = 0
num_endpoints_with_non_200_response_code_examples: int = 0
num_request_examples: int = 0
num_response_examples: int = 0
max_examples_per_endpoint: int = 0

class ExamplesStatsGenerator:
def __init__(self, endpoint_info_list: List[EndpointInfo]):
self.endpoint_info_list = endpoint_info_list
self.spec_path = "."

def get_stats(self) -> ExampleStats:
stats = ExampleStats()
for endpoint_info in self.endpoint_info_list:
stats.num_endpoints += 1

if endpoint_info.num_examples > 0:
stats.num_endpoints_with_examples += 1

if endpoint_info.num_examples > stats.max_examples_per_endpoint:
stats.max_examples_per_endpoint = endpoint_info.num_examples

if endpoint_info.num_request_examples > 0 and endpoint_info.num_response_examples == 0:
stats.num_endpoints_with_only_request_examples += 1
elif endpoint_info.num_request_examples == 0 and endpoint_info.num_response_examples > 0:
stats.num_endpoints_with_only_response_examples += 1
elif endpoint_info.num_request_examples > 0 and endpoint_info.num_response_examples > 0:
stats.num_endpoints_with_both_request_and_response_examples += 1
else:
stats.num_endpoints_with_no_examples += 1

stats.num_request_examples += endpoint_info.num_request_examples
stats.num_response_examples += endpoint_info.num_response_examples

non_200_response_codes = endpoint_info.examples_response_codes - {"200"}
if len(non_200_response_codes) > 0:
stats.num_endpoints_with_non_200_response_code_examples += 1
return stats
2 changes: 2 additions & 0 deletions src/scripts/examples-stats/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
DEFAULT_SPEC_PATH = "../../../specification"
EXAMPLES_FOLDER = "examples"
56 changes: 56 additions & 0 deletions src/scripts/examples-stats/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#!/usr/bin/python3

import os
from constants import DEFAULT_SPEC_PATH
from ExamplesInfoGenerator import ExamplesInfoGenerator, ExamplesInfo, ExampleStats

def print_stats(stats: ExampleStats):
print ("===============")
print ("==== Stats ====")
print ("===============")
print(f"Endpoints: {stats.num_endpoints}")
print(f"Endpoints with no request or response examples: {stats.num_endpoints_with_no_examples}")
print(f"Endpoints with examples: {stats.num_endpoints_with_examples}")
print(f" {stats.num_endpoints_with_only_request_examples:>4}: Only request examples")
print(f" {stats.num_endpoints_with_only_response_examples:>4}: Only response examples")
print(f" {stats.num_endpoints_with_both_request_and_response_examples:>4}: Both request and response examples")
print(f"Endpoints with non-200 response code examples: {stats.num_endpoints_with_non_200_response_code_examples}")
print("------------------------")
print(f"Examples: {stats.num_request_examples + stats.num_response_examples}")
print(f" {stats.num_request_examples:>4}: Request examples")
print(f" {stats.num_response_examples:>4}: Response examples")
print(f"Max examples per endpoint: {stats.max_examples_per_endpoint}")
print ("===============\n")

def main():
# Using a default spc path. We should add an option
# for getting the spec path as a command line argument
os.chdir(DEFAULT_SPEC_PATH)
examples_info_generator = ExamplesInfoGenerator()
examples_info: ExamplesInfo = examples_info_generator.get_examples_info()
# === print stats
print_stats(examples_info.stats)
# === print paths with max examples
print("Paths with max examples:")
for endpoint_info in examples_info.endpoints_info:
if endpoint_info.num_examples == examples_info.stats.max_examples_per_endpoint:
print(f" {endpoint_info.path} with {endpoint_info.num_examples} examples")
print()
# === print all recognized examples subfolders
all_examples_subfolders = set()
all_recognized_examples_subfolders = set()
for endpoint_info in examples_info.endpoints_info:
all_examples_subfolders.update(endpoint_info.examples_subfolders)
all_recognized_examples_subfolders.update(endpoint_info.recognized_examples_subfolders)
print("All recognized subfolders of 'examples' folder:")
for folder in all_recognized_examples_subfolders:
print(f" {folder}")
print()
# === print unrecognized examples subfolders
unrecognized_examples_subfolders = all_examples_subfolders - all_recognized_examples_subfolders
print("unrecognized subfolders of 'examples' folder:")
for folder in unrecognized_examples_subfolders:
print(f" {folder}")

if __name__ == "__main__":
main()
15 changes: 15 additions & 0 deletions typescript-generator/src/metamodel.ts
Original file line number Diff line number Diff line change
@@ -260,6 +260,19 @@ export class Interface extends BaseType {
variants?: Container
}

/**
* The Example type is used for both requests and responses
* This type definition is taken from the OpenAPI spec
* https://spec.openapis.org/oas/v3.1.0#example-object
* With the exception of using String as the 'value' type
*/
export class Example {
summary?: string
description?: string
value?: string
external_value?: string
}

/**
* A request type
*/
@@ -288,6 +301,7 @@ export class Request extends BaseType {
body: Body
behaviors?: Behavior[]
attachedBehaviors?: string[]
examples?: Map<string, Example>
}

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

export class ResponseException {