Skip to content
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

Added simplified request and response body matching in case of multiple examples. #792

Merged
merged 4 commits into from
May 17, 2024
Merged
Show file tree
Hide file tree
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
113 changes: 70 additions & 43 deletions libV2/schemaUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -1082,7 +1082,16 @@ let QUERYPARAM = 'query',

/**
* Generates postman equivalent examples which contains request and response mappings of
* each example based on examples mentioned ind definition
* each example based on examples mentioned in definition
*
* This matching between request bodies and response bodies are done in following order.
* 1. Try matching keys from request and response examples
* 2. If any key matching is found, we'll generate example from it and ignore non-matching keys
* 3. If no matching key is found, we'll generate examples based on positional matching.
*
* Positional matching means first example in request body will be matched with first example
* in response body and so on. Any left over request or response body for which
* positional matching is not found, we'll use first req/res example.
*
* @param {Object} context - Global context object
* @param {Object} responseExamples - Examples defined in the response
Expand All @@ -1092,8 +1101,51 @@ let QUERYPARAM = 'query',
* @returns {Array} Examples for corresponding operation
*/
generateExamples = (context, responseExamples, requestBodyExamples, responseBodySchema, isXMLExample) => {
const pmExamples = [];
const pmExamples = [],
responseExampleKeys = _.map(responseExamples, 'key'),
requestBodyExampleKeys = _.map(requestBodyExamples, 'key'),
matchedKeys = _.intersectionBy(responseExampleKeys, requestBodyExampleKeys, _.toLower),
usedRequestExamples = _.fill(Array(requestBodyExamples.length), false),
exampleKeyComparator = (example, key) => {
return _.toLower(example.key) === _.toLower(key);
};

// Do keys matching first and ignore any leftover req/res body for which matching is not found
if (matchedKeys.length) {
_.forEach(matchedKeys, (key) => {
const matchedRequestExamples = _.filter(requestBodyExamples, (example) => {
return exampleKeyComparator(example, key);
}),
responseExample = _.find(responseExamples, (example) => {
return exampleKeyComparator(example, key);
});

let requestExample = _.find(matchedRequestExamples, ['contentType', _.get(responseExample, 'contentType')]),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@VShingala What's the reason for doing this? The request and response can be of different types. Why give preference to same content types?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@webholik Yes, we only give preference to same content type if available. Reasoning is, It makes better example if both request and response body are in same content type.

Although, if same content type is not present we do pair different content types as well.

responseExampleData;

if (!requestExample) {
requestExample = _.head(matchedRequestExamples);
}

responseExampleData = getExampleData(context, { [responseExample.key]: responseExample.value });

if (isXMLExample) {
responseExampleData = getXMLExampleData(context, responseExampleData, responseBodySchema);
}

pmExamples.push({
request: getExampleData(context, { [requestExample.key]: requestExample.value }),
response: responseExampleData,
name: _.get(responseExample, 'value.summary') ||
(responseExample.key !== '_default' && responseExample.key) ||
_.get(requestExample, 'value.summary') || requestExample.key || 'Example'
});
});

return pmExamples;
}

// No key matching between req and res were found, so perform positional matching now
_.forEach(responseExamples, (responseExample, index) => {

if (!_.isObject(responseExample)) {
Expand All @@ -1115,46 +1167,12 @@ let QUERYPARAM = 'query',
return;
}

requestExample = _.find(requestBodyExamples, (example, index) => {
if (
example.contentType === responseExample.contentType &&
_.toLower(example.key) === _.toLower(responseExample.key)
) {
requestBodyExamples[index].isUsed = true;
return true;
}
return false;
});

// If exact content type is not matching, pick first content type with same example key
if (!requestExample) {
requestExample = _.find(requestBodyExamples, (example, index) => {
if (_.toLower(example.key) === _.toLower(responseExample.key)) {
requestBodyExamples[index].isUsed = true;
return true;
}
return false;
});
if (requestBodyExamples[index] && !usedRequestExamples[index]) {
requestExample = requestBodyExamples[index];
usedRequestExamples[index] = true;
}

if (!requestExample) {
if (requestBodyExamples[index] && !requestBodyExamples[index].isUsed) {
requestExample = requestBodyExamples[index];
requestBodyExamples[index].isUsed = true;
}
else {
for (let i = 0; i < requestBodyExamples.length; i++) {
if (!requestBodyExamples[i].isUsed) {
requestExample = requestBodyExamples[i];
requestBodyExamples[i].isUsed = true;
break;
}
}

if (!requestExample) {
requestExample = requestBodyExamples[0];
}
}
else {
requestExample = requestBodyExamples[0];
}

pmExamples.push({
Expand All @@ -1168,9 +1186,10 @@ let QUERYPARAM = 'query',
let responseExample,
responseExampleData;

// Add any left over request body examples with first response body as matching
for (let i = 0; i < requestBodyExamples.length; i++) {

if (!requestBodyExamples[i].isUsed || pmExamples.length === 0) {
if (!usedRequestExamples[i] || pmExamples.length === 0) {
if (!responseExample) {
responseExample = _.head(responseExamples);

Expand Down Expand Up @@ -1359,7 +1378,15 @@ let QUERYPARAM = 'query',
};
});
}
return generateExamples(context, responseExamples, requestBodyExamples, requestBodySchema, isBodyTypeXML);

let matchedRequestBodyExamples = _.filter(requestBodyExamples, ['contentType', bodyType]);

// If content-types are not matching, match with any present content-types
if (_.isEmpty(matchedRequestBodyExamples)) {
matchedRequestBodyExamples = requestBodyExamples;
}

return generateExamples(context, responseExamples, matchedRequestBodyExamples, requestBodySchema, isBodyTypeXML);
}

return [{ [bodyKey]: bodyData }];
Expand Down
127 changes: 127 additions & 0 deletions test/data/valid_openapi/multiContentTypesMultiExample.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
{
"openapi": "3.0.0",
"info": {
"version": "1.0.0",
"title": "Swagger Petstore",
"license": {
"name": "MIT"
}
},
"servers": [{
"url": "http://petstore.swagger.io/v1"
}],
"paths": {
"/pets": {
"post": {
"summary": "List all pets",
"operationId": "pets - updated",
"tags": [
"pets"
],
"parameters": [{
"name": "limit1",
"in": "query",
"description": "How many items to return at one time (max 100)",
"schema": {
"type": "integer",
"format": "int32"
}
}],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
},
"examples": {
"ok_example": {
"value": {
"message": "ok"
}
},
"not_ok_example": {
"value": {
"message": "fail"
}
}
}
},
"application/xml": {
"schema": {
"$ref": "#/components/schemas/Error"
},
"examples": {
"ok_example": {
"value": {
"message": "ok"
}
},
"not_ok_example": {
"value": {
"message": "fail"
}
}
}
}
}
},
"responses": {
"default": {
"description": "unexpected error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
},
"example": {
"message": "Not Found"
}
},
"application/xml": {
"schema": {
"$ref": "#/components/schemas/Error"
},
"example": {
"message": "Not Found"
}
}
}
},
"200": {
"description": "Ok",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
},
"example": {
"message": "Found",
"code": 200123
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"Error": {
"required": [
"code",
"message"
],
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"message": {
"type": "string"
}
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ paths:
value:
includedFields:
- user
extra-value:
value:
includedFields:
- eyeColor
responses:
200:
description: None
Expand All @@ -44,6 +48,10 @@ paths:
{
"user": 1
}
extra-value-2:
value:
includedFields:
- eyeColor
components:
schemas:
World:
Expand Down
Loading
Loading