Skip to content

Commit

Permalink
Merge pull request #792 from postmanlabs/feature/fix-multi-examples-m…
Browse files Browse the repository at this point in the history
…atching

Added simplified request and response body matching in case of multiple examples.
  • Loading branch information
VShingala committed May 17, 2024
2 parents 228c462 + eb3cc5e commit e90b218
Show file tree
Hide file tree
Showing 5 changed files with 449 additions and 78 deletions.
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')]),
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

0 comments on commit e90b218

Please sign in to comment.