From 8619eff68f9c09fa7f1632d81123553c0a60b311 Mon Sep 17 00:00:00 2001 From: Erik Mendoza Date: Thu, 24 Nov 2022 16:04:12 -0600 Subject: [PATCH 1/6] Fixing issue #309 --- lib/schemaUtils.js | 26 + .../petstore-detailed-referenced-path.yaml | 1216 +++++++++++++++++ .../referencedPathFromOutOfComponents.yaml | 114 ++ test/unit/base.test.js | 50 +- 4 files changed, 1405 insertions(+), 1 deletion(-) create mode 100644 test/data/valid_openapi/petstore-detailed-referenced-path.yaml create mode 100644 test/data/valid_openapi/referencedPathFromOutOfComponents.yaml diff --git a/lib/schemaUtils.js b/lib/schemaUtils.js index c40ab673c..0020b40e1 100644 --- a/lib/schemaUtils.js +++ b/lib/schemaUtils.js @@ -512,6 +512,24 @@ module.exports = { return reqParam; }, + dereferenceFromOutOfComponents: function (spec, reference) { + let splitRef = reference.split('/'), + resolvedContent; + + splitRef = splitRef.slice(1).map((elem) => { + // https://swagger.io/docs/specification/using-ref#escape + // since / is the default delimiter, slashes are escaped with ~1 + return decodeURIComponent( + elem + .replace(/~1/g, '/') + .replace(/~0/g, '~') + ); + }); + + resolvedContent = deref._getEscaped(spec, splitRef); + return resolvedContent; + }, + /** * Generates a Trie-like folder structure from the root path object of the OpenAPI specification. * @param {Object} spec - specification in json format @@ -585,6 +603,10 @@ module.exports = { pathLength = currentPath.length; + if (currentPathObject.hasOwnProperty('$ref')) { + currentPathObject = this.dereferenceFromOutOfComponents(spec, currentPathObject.$ref); + } + // get method names available for this path pathMethods = getPathMethods(Object.keys(currentPathObject)); @@ -799,6 +821,10 @@ module.exports = { collectionVariables, pathLevelServers = ''; + if (currentPathObject.hasOwnProperty('$ref')) { + currentPathObject = this.dereferenceFromOutOfComponents(spec, currentPathObject.$ref); + } + // discard the leading slash, if it exists if (path[0] === '/') { path = path.substring(1); diff --git a/test/data/valid_openapi/petstore-detailed-referenced-path.yaml b/test/data/valid_openapi/petstore-detailed-referenced-path.yaml new file mode 100644 index 000000000..47a654faf --- /dev/null +++ b/test/data/valid_openapi/petstore-detailed-referenced-path.yaml @@ -0,0 +1,1216 @@ +openapi: 3.0.0 +servers: + - url: http://petstore.swagger.io + description: 'Adding the variables' + variables: + temp1: + default: value1 + description: variable1 + enum: [value1, value2] + temp2: + default: value3 + description: variable2 + enum: [value3, value4] + - url: //petstore.swagger.io/sandbox + description: Sandbox server +info: + description: | + This is a sample server Petstore server. + You can find out more about Swagger at + [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). + For this sample, you can use the api key `special-key` to test the authorization filters. + + # Introduction + This API is documented in **OpenAPI format** and is based on + [Petstore sample](http://petstore.swagger.io/) provided by [swagger.io](http://swagger.io) team. + It was **extended** to illustrate features of [generator-openapi-repo](https://github.com/Rebilly/generator-openapi-repo) + tool and [ReDoc](https://github.com/Redocly/redoc) documentation. In addition to standard + OpenAPI syntax we use a few [vendor extensions](https://github.com/Redocly/redoc/blob/master/docs/redoc-vendor-extensions.md). + + # OpenAPI Specification + This API is documented in **OpenAPI format** and is based on + [Petstore sample](http://petstore.swagger.io/) provided by [swagger.io](http://swagger.io) team. + It was **extended** to illustrate features of [generator-openapi-repo](https://github.com/Rebilly/generator-openapi-repo) + tool and [ReDoc](https://github.com/Redocly/redoc) documentation. In addition to standard + OpenAPI syntax we use a few [vendor extensions](https://github.com/Redocly/redoc/blob/master/docs/redoc-vendor-extensions.md). + + # Cross-Origin Resource Sharing + This API features Cross-Origin Resource Sharing (CORS) implemented in compliance with [W3C spec](https://www.w3.org/TR/cors/). + And that allows cross-domain communication from the browser. + All responses have a wildcard same-origin which makes them completely public and accessible to everyone, including any code on any site. + + # Authentication + + Petstore offers two forms of authentication: + - API Key + - OAuth2 + OAuth2 - an open protocol to allow secure authorization in a simple + and standard method from web, mobile and desktop applications. + + + + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + name: API Support + email: apiteam@swagger.io + url: https://github.com/Redocly/redoc + x-logo: + url: 'https://redocly.github.io/redoc/petstore-logo.png' + altText: Petstore logo + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +externalDocs: + description: Find out how to create Github repo for your OpenAPI spec. + url: 'https://github.com/Rebilly/generator-openapi-repo' +tags: + - name: pet + description: Everything about your Pets + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + - name: pet_model + x-displayName: The Pet Model + description: | + + - name: store_model + x-displayName: The Order Model + description: | + +x-tagGroups: + - name: General + tags: + - pet + - store + - name: User Management + tags: + - user + - name: Models + tags: + - pet_model + - store_model +paths: + /pet: + parameters: + - name: Accept-Language + in: header + description: "The language you prefer for messages. Supported values are en-AU, en-CA, en-GB, en-US" + example: en-US + required: false + schema: + type: string + default: en-AU + - name: cookieParam + in: cookie + description: Some cookie + required: true + schema: + type: integer + format: int64 + post: + tags: + - pet + summary: Add a new pet to the store + description: Add new pet to the store inventory. + operationId: addPet + responses: + '405': + description: Invalid input + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + x-code-samples: + - lang: 'C#' + source: | + PetStore.v1.Pet pet = new PetStore.v1.Pet(); + pet.setApiKey("your api key"); + pet.petType = PetStore.v1.Pet.TYPE_DOG; + pet.name = "Rex"; + // set other fields + PetStoreResponse response = pet.create(); + if (response.statusCode == HttpStatusCode.Created) + { + // Successfully created + } + else + { + // Something wrong -- check response for errors + Console.WriteLine(response.getRawResponse()); + } + - lang: PHP + source: | + $form = new \PetStore\Entities\Pet(); + $form->setPetType("Dog"); + $form->setName("Rex"); + // set other fields + try { + $pet = $client->pets()->create($form); + } catch (UnprocessableEntityException $e) { + var_dump($e->getErrors()); + } + requestBody: + $ref: '#/components/requestBodies/Pet' + put: + tags: + - pet + summary: Update an existing pet + description: '' + operationId: updatePet + responses: + '400': + description: Invalid ID supplied + '404': + description: Pet not found + '405': + description: Validation exception + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + x-code-samples: + - lang: PHP + source: | + $form = new \PetStore\Entities\Pet(); + $form->setPetId(1); + $form->setPetType("Dog"); + $form->setName("Rex"); + // set other fields + try { + $pet = $client->pets()->update($form); + } catch (UnprocessableEntityException $e) { + var_dump($e->getErrors()); + } + requestBody: + $ref: '#/components/requestBodies/Pet' + servers: + - url: https://{username}.gigantic-server.com:{port}/{basePath} + description: Common url for all operations in this path + variables: + username: + default: demo + description: Assigned by the service provider + port: + enum: + - '8843' + - '443' + + default: '8843' + basePath: + default: v2 + + '/pet/{petId}': + $ref: "#/x-operations/getAPet" + + '/pet/{petId}/uploadImage': + post: + tags: + - pet + - cat + summary: uploads an image + description: '' + operationId: uploadFile + parameters: + - name: petId + in: path + description: ID of pet to update + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + requestBody: + content: + application/octet-stream: + schema: + type: string + format: binary + /pet/findByStatus: + get: + tags: + - pet + - dog + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + style: form + schema: + type: array + minItems: 1 + maxItems: 3 + items: + type: string + enum: + - available + - pending + - sold + default: available + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid status value + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + /pet/findByTags: + get: + summary: Finds Pets by tags + description: >- + Multiple tags can be provided with comma separated strings. Use tag1, + tag2, tag3 for testing. + operationId: findPetsByTags + deprecated: true + parameters: + - name: tags + in: query + description: Tags to filter by + required: true + style: form + schema: + type: array + items: + type: string + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid tag value + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + /store/inventory: + get: + tags: + - store + summary: Returns pet inventories by status + description: Returns a map of status codes to quantities + operationId: getInventory + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: object + properties: + prop123: + type: integer + format: int32 + security: + - api_key: [] + /store/order: + post: + tags: + - store + summary: Place an order for a pet + description: '' + operationId: placeOrder + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + application/xml: + schema: + $ref: '#/components/schemas/Order' + '400': + description: Invalid Order + content: + application/json: + example: + status: 400 + message: "Invalid Order" + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + description: order placed for purchasing the pet + required: true + '/store/order/{orderId}': + get: + tags: + - store + summary: Find purchase order by ID + description: >- + For valid response try integer IDs with value <= 5 or > 10. Other values + will generated exceptions + operationId: getOrderById + parameters: + - name: orderId + in: path + description: ID of pet that needs to be fetched + required: true + schema: + type: integer + format: int64 + minimum: 1 + maximum: 5 + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + application/xml: + schema: + $ref: '#/components/schemas/Order' + '400': + description: Invalid ID supplied + '404': + description: Order not found + delete: + tags: + - store + summary: Delete purchase order by ID + description: >- + For valid response try integer IDs with value < 1000. Anything above + 1000 or nonintegers will generate API errors + operationId: deleteOrder + parameters: + - name: orderId + in: path + description: ID of the order that needs to be deleted + required: true + schema: + type: string + minimum: 1 + responses: + '400': + description: Invalid ID supplied + '404': + description: Order not found + /store/subscribe: + post: + summary: Subscribe to the Store events + description: Add subscription for a store events + requestBody: + content: + application/json: + schema: + type: object + properties: + callbackUrl: + type: string + format: uri + description: This URL will be called by the server when the desired event will occur + example: https://myserver.com/send/callback/here + eventName: + type: string + description: Event name for the subscription + enum: + - orderInProgress + - orderShipped + - orderDelivered + example: orderInProgress + required: + - callbackUrl + - eventName + responses: + '201': + description: Subscription added + content: + application/json: + schema: + type: object + properties: + subscriptionId: + type: string + example: AAA-123-BBB-456 + callbacks: + orderInProgress: + '{$request.body#/callbackUrl}?event={$request.body#/eventName}': + servers: + - url: //callback-url.path-level/v1 + description: Path level server 1 + - url: //callback-url.path-level/v2 + description: Path level server 2 + post: + summary: Order in Progress (Summary) + description: A callback triggered every time an Order is updated status to "inProgress" (Description) + externalDocs: + description: Find out more + url: 'https://more-details.com/demo' + requestBody: + content: + application/json: + schema: + type: object + properties: + orderId: + type: string + example: '123' + timestamp: + type: string + format: date-time + example: '2018-10-19T16:46:45Z' + status: + type: string + example: 'inProgress' + application/xml: + schema: + type: object + properties: + orderId: + type: string + example: '123' + example: | + + + 123 + inProgress + 2018-10-19T16:46:45Z + + responses: + '200': + description: Callback successfully processed and no retries will be performed + content: + application/json: + schema: + type: object + properties: + someProp: + type: string + example: '123' + '299': + description: Response for cancelling subscription + '500': + description: Callback processing failed and retries will be performed + x-code-samples: + - lang: 'C#' + source: | + PetStore.v1.Pet pet = new PetStore.v1.Pet(); + pet.setApiKey("your api key"); + pet.petType = PetStore.v1.Pet.TYPE_DOG; + pet.name = "Rex"; + // set other fields + PetStoreResponse response = pet.create(); + if (response.statusCode == HttpStatusCode.Created) + { + // Successfully created + } + else + { + // Something wrong -- check response for errors + Console.WriteLine(response.getRawResponse()); + } + - lang: PHP + source: | + $form = new \PetStore\Entities\Pet(); + $form->setPetType("Dog"); + $form->setName("Rex"); + // set other fields + try { + $pet = $client->pets()->create($form); + } catch (UnprocessableEntityException $e) { + var_dump($e->getErrors()); + } + put: + description: Order in Progress (Only Description) + servers: + - url: //callback-url.operation-level/v1 + description: Operation level server 1 (Operation override) + - url: //callback-url.operation-level/v2 + description: Operation level server 2 (Operation override) + requestBody: + content: + application/json: + schema: + type: object + properties: + orderId: + type: string + example: '123' + timestamp: + type: string + format: date-time + example: '2018-10-19T16:46:45Z' + status: + type: string + example: 'inProgress' + application/xml: + schema: + type: object + properties: + orderId: + type: string + example: '123' + example: | + + + 123 + inProgress + 2018-10-19T16:46:45Z + + responses: + '200': + description: Callback successfully processed and no retries will be performed + content: + application/json: + schema: + type: object + properties: + someProp: + type: string + example: '123' + orderShipped: + '{$request.body#/callbackUrl}?event={$request.body#/eventName}': + post: + description: | + Very long description + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor + incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis + nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu + fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in + culpa qui officia deserunt mollit anim id est laborum. + requestBody: + content: + application/json: + schema: + type: object + properties: + orderId: + type: string + example: '123' + timestamp: + type: string + format: date-time + example: '2018-10-19T16:46:45Z' + estimatedDeliveryDate: + type: string + format: date-time + example: '2018-11-11T16:00:00Z' + responses: + '200': + description: Callback successfully processed and no retries will be performed + orderDelivered: + 'http://notificationServer.com?url={$request.body#/callbackUrl}&event={$request.body#/eventName}': + post: + deprecated: true + summary: Order delivered + description: A callback triggered every time an Order is delivered to the recipient + requestBody: + content: + application/json: + schema: + type: object + properties: + orderId: + type: string + example: '123' + timestamp: + type: string + format: date-time + example: '2018-10-19T16:46:45Z' + responses: + '200': + description: Callback successfully processed and no retries will be performed + /user: + post: + tags: + - user + summary: Create user + description: This can only be done by the logged in user. + operationId: createUser + responses: + default: + description: successful operation + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: Created user object + required: true + '/user/{username}': + get: + tags: + - user + summary: Get user by user name + description: '' + operationId: getUserByName + parameters: + - name: username + in: path + description: 'The name that needs to be fetched. Use user1 for testing. ' + required: true + schema: + type: string + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + '400': + description: Invalid username supplied + '404': + description: User not found + put: + tags: + - user + summary: Updated user + description: This can only be done by the logged in user. + operationId: updateUser + parameters: + - name: username + in: path + description: name that need to be deleted + required: true + schema: + type: string + responses: + '400': + description: Invalid user supplied + '404': + description: User not found + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: Updated user object + required: true + delete: + tags: + - user + summary: Delete user + description: This can only be done by the logged in user. + operationId: deleteUser + parameters: + - name: username + in: path + description: The name that needs to be deleted + required: true + schema: + type: string + responses: + '400': + description: Invalid username supplied + '404': + description: User not found + /user/createWithArray: + post: + tags: + - user + summary: Creates list of users with given input array + description: '' + operationId: createUsersWithArrayInput + responses: + default: + description: successful operation + requestBody: + $ref: '#/components/requestBodies/UserArray' + /user/createWithList: + post: + tags: + - user + summary: Creates list of users with given input array + description: '' + operationId: createUsersWithListInput + responses: + default: + description: successful operation + requestBody: + $ref: '#/components/requestBodies/UserArray' + /user/login: + get: + tags: + - user + summary: Logs user into the system + description: '' + operationId: loginUser + parameters: + - name: username + in: query + description: The user name for login + required: true + schema: + type: string + - name: password + in: query + description: The password for login in clear text + required: true + schema: + type: string + responses: + '200': + description: successful operation + headers: + X-Rate-Limit: + description: calls per hour allowed by the user + schema: + type: integer + format: int32 + X-Expires-After: + description: date in UTC when token expires + schema: + type: string + format: date-time + content: + application/json: + schema: + type: string + examples: + response: + value: OK + application/xml: + schema: + type: string + examples: + response: + value: OK + text/plain: + examples: + response: + value: OK + '400': + description: Invalid username/password supplied + /user/logout: + get: + summary: Logs out current logged in user session + description: '' + operationId: logoutUser + responses: + default: + description: successful operation +components: + schemas: + ApiResponse: + type: object + properties: + code: + type: integer + format: int32 + type: + type: string + message: + type: string + Cat: + description: A representation of a cat + allOf: + - $ref: '#/components/schemas/Pet' + - type: object + properties: + huntingSkill: + type: string + description: The measured skill for hunting + default: lazy + example: adventurous + enum: + - clueless + - lazy + - adventurous + - aggressive + required: + - huntingSkill + Category: + type: object + properties: + id: + description: Category ID + allOf: + - $ref: '#/components/schemas/Id' + name: + description: Category name + type: string + minLength: 1 + sub: + description: Test Sub Category + type: object + properties: + prop1: + type: string + description: Dumb Property + xml: + name: Category + Dog: + description: A representation of a dog + allOf: + - $ref: '#/components/schemas/Pet' + - type: object + properties: + packSize: + type: integer + format: int32 + description: The size of the pack the dog is from + default: 1 + minimum: 1 + required: + - packSize + HoneyBee: + description: A representation of a honey bee + allOf: + - $ref: '#/components/schemas/Pet' + - type: object + properties: + honeyPerDay: + type: number + description: Average amount of honey produced per day in ounces + example: 3.14 + multipleOf: .01 + required: + - honeyPerDay + Id: + type: integer + format: int64 + readOnly: true + Order: + type: object + properties: + id: + description: Order ID + allOf: + - $ref: '#/components/schemas/Id' + petId: + description: Pet ID + allOf: + - $ref: '#/components/schemas/Id' + quantity: + type: integer + format: int32 + minimum: 1 + default: 1 + shipDate: + description: Estimated ship date + type: string + format: date-time + status: + type: string + description: Order Status + enum: + - placed + - approved + - delivered + complete: + description: Indicates whenever order was completed or not + type: boolean + default: false + readOnly: true + requestId: + description: Unique Request Id + type: string + writeOnly: true + xml: + name: Order + Pet: + type: object + required: + - name + - photoUrls + discriminator: + propertyName: petType + mapping: + cat: '#/components/schemas/Cat' + dog: '#/components/schemas/Dog' + bee: '#/components/schemas/HoneyBee' + properties: + id: + externalDocs: + description: "Find more info here" + url: "https://example.com" + description: Pet ID + allOf: + - $ref: '#/components/schemas/Id' + category: + description: Categories this pet belongs to + allOf: + - $ref: '#/components/schemas/Category' + name: + description: The name given to a pet + type: string + example: Guru + photoUrls: + description: The list of URL to a cute photos featuring pet + type: array + maxItems: 20 + xml: + name: photoUrl + wrapped: true + items: + type: string + format: url + friend: + allOf: + - $ref: '#/components/schemas/Pet' + tags: + description: Tags attached to the pet + type: array + minItems: 1 + xml: + name: tag + wrapped: true + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: Pet status in the store + enum: + - available + - pending + - sold + petType: + description: Type of a pet + type: string + xml: + name: Pet + Tag: + type: object + properties: + id: + description: Tag ID + allOf: + - $ref: '#/components/schemas/Id' + name: + description: Tag name + type: string + minLength: 1 + xml: + name: Tag + User: + type: object + properties: + id: + $ref: '#/components/schemas/Id' + pet: + oneOf: + - $ref: '#/components/schemas/Pet' + - $ref: '#/components/schemas/Tag' + username: + description: User supplied username + type: string + minLength: 4 + example: John78 + firstName: + description: User first name + type: string + minLength: 1 + example: John + lastName: + description: User last name + type: string + minLength: 1 + example: Smith + email: + description: User email address + type: string + format: email + example: john.smith@example.com + password: + type: string + description: >- + User password, MUST contain a mix of upper and lower case letters, + as well as digits + format: password + minLength: 8 + pattern: '/(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])/' + example: drowssaP123 + phone: + description: User phone number in international format + type: string + pattern: '/^\+(?:[0-9]-?){6,14}[0-9]$/' + example: +1-202-555-0192 + userStatus: + description: User status + type: integer + format: int32 + xml: + name: User + requestBodies: + Pet: + content: + application/json: + schema: + allOf: + - description: My Pet + title: Pettie + - $ref: '#/components/schemas/Pet' + application/xml: + schema: + type: 'object' + properties: + name: + type: string + description: hooray + description: Pet object that needs to be added to the store + required: true + UserArray: + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + description: List of user object + required: true + securitySchemes: + petstore_auth: + description: | + Get access to data while protecting your account credentials. + OAuth2 is also a safer and more secure way to give you access. + type: oauth2 + flows: + implicit: + authorizationUrl: 'http://petstore.swagger.io/api/oauth/dialog' + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + api_key: + description: > + For this sample, you can use the api key `special-key` to test the + authorization filters. + type: apiKey + name: api_key + in: header + examples: + Order: + value: + quantity: 1 + shipDate: '2018-10-19T16:46:45Z' + status: placed + complete: false + +x-operations: + getAPet: + get: + tags: + - pet + summary: Find pet by ID + description: Returns a single pet + operationId: getPetById + parameters: + - name: petId + in: path + description: ID of pet to return + required: true + deprecated: true + schema: + type: integer + format: int64 + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + security: + - api_key: [] + post: + tags: + - pet + summary: Updates a pet in the store with form data + description: '' + operationId: updatePetWithForm + parameters: + - name: petId + in: path + description: ID of pet that needs to be updated + required: true + schema: + type: integer + format: int64 + responses: + '405': + description: Invalid input + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + name: + description: Updated name of the pet + type: string + status: + description: Updated status of the pet + type: string + delete: + tags: + - pet + summary: Deletes a pet + description: '' + operationId: deletePet + parameters: + - name: api_key + in: header + required: false + schema: + type: string + example: "Bearer " + - name: petId + in: path + description: Pet id to delete + required: true + schema: + type: integer + format: int64 + responses: + '400': + description: Invalid pet value + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' diff --git a/test/data/valid_openapi/referencedPathFromOutOfComponents.yaml b/test/data/valid_openapi/referencedPathFromOutOfComponents.yaml new file mode 100644 index 000000000..46ad6abf2 --- /dev/null +++ b/test/data/valid_openapi/referencedPathFromOutOfComponents.yaml @@ -0,0 +1,114 @@ +openapi: "3.0.0" +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT +servers: + - url: http://petstore.swagger.io/v1 +paths: + /pets: + get: + summary: List all pets + operationId: listPets + tags: + - pets + parameters: + - name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + format: int32 + responses: + '200': + description: A paged array of pets + headers: + x-next: + description: A link to the next page of responses + schema: + type: string + content: + application/json: + schema: + $ref: "#/components/schemas/Pets" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + post: + summary: Create a pet + operationId: createPets + tags: + - pets + responses: + '201': + description: Null response + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /pets/{petId}: + $ref: "#/x-operations/getAPet" +components: + schemas: + Pet: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + Pets: + type: array + items: + $ref: "#/components/schemas/Pet" + Error: + type: object + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string +x-operations: + getAPet: + get: + summary: Info for a specific pet + operationId: showPetById + tags: + - pets + parameters: + - name: petId + in: path + required: true + description: The id of the pet to retrieve + schema: + type: string + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" diff --git a/test/unit/base.test.js b/test/unit/base.test.js index 00e326480..bad6af075 100644 --- a/test/unit/base.test.js +++ b/test/unit/base.test.js @@ -58,7 +58,11 @@ describe('CONVERT FUNCTION TESTS ', function() { allHTTPMethodsSpec = path.join(__dirname, VALID_OPENAPI_PATH, '/all-http-methods.yaml'), invalidNullInfo = path.join(__dirname, INVALID_OPENAPI_PATH, '/invalid-null-info.json'), invalidNullInfoTitle = path.join(__dirname, INVALID_OPENAPI_PATH, '/invalid-info-null-title.json'), - invalidNullInfoVersion = path.join(__dirname, INVALID_OPENAPI_PATH, '/invalid-info-null-version.json'); + invalidNullInfoVersion = path.join(__dirname, INVALID_OPENAPI_PATH, '/invalid-info-null-version.json'), + referencedPathFromOutOfComponents = + path.join(__dirname, VALID_OPENAPI_PATH + '/referencedPathFromOutOfComponents.yaml'), + referencedPathFromOutOfComponentsTagsStrategy = + path.join(__dirname, VALID_OPENAPI_PATH + '/petstore-detailed-referenced-path.yaml'); it('Should add collection level auth with type as `bearer`' + @@ -1275,6 +1279,50 @@ describe('CONVERT FUNCTION TESTS ', function() { done(); }); }); + + describe('[Github #309 - Should convert a path when is referenced ' + + 'from a different place than components]', function() { + + it('Should convert and include the referenced paths' + + referencedPathFromOutOfComponents, function(done) { + Converter.convert({ type: 'file', data: + referencedPathFromOutOfComponents }, {}, (err, conversionResult) => { + expect(err).to.be.null; + expect(conversionResult.result).to.equal(true); + expect(conversionResult.output.length).to.equal(1); + expect(conversionResult.output[0].type).to.equal('collection'); + expect(conversionResult.output[0].data).to.have.property('info'); + expect(conversionResult.output[0].data).to.have.property('item'); + expect(conversionResult.output[0].data.item[0].item.length) + .to.equal(3); + done(); + }); + }); + + it('Should convert and include the referenced paths using tags as folder strategy' + + referencedPathFromOutOfComponentsTagsStrategy, function(done) { + Converter.convert( + { + type: 'file', + data: referencedPathFromOutOfComponentsTagsStrategy + }, + { + folderStrategy: 'Tags' + }, + (err, conversionResult) => { + expect(err).to.be.null; + expect(conversionResult.result).to.equal(true); + expect(conversionResult.output.length).to.equal(1); + expect(conversionResult.output[0].type).to.equal('collection'); + expect(conversionResult.output[0].data).to.have.property('info'); + expect(conversionResult.output[0].data).to.have.property('item'); + expect(conversionResult.output[0].data.item[0].item.length) + .to.equal(7); + done(); + } + ); + }); + }); }); describe('Converting swagger 2.0 files', function() { From b2449695dd4f4dd09b793d7105a6ff0dc5e242cc Mon Sep 17 00:00:00 2001 From: Erik Mendoza Date: Fri, 2 Dec 2022 18:09:01 -0600 Subject: [PATCH 2/6] Dereferencing elements from out of components/schemas before processing spec --- lib/deref.js | 37 +++++ lib/schemapack.js | 4 +- ...aml => referencedFromOutOfComponents.yaml} | 20 +++ .../json/multiple_refs_and_paths.json | 101 +++++++++++++ .../yaml/referencedFromOutOfComponents.yaml | 136 ++++++++++++++++++ test/unit/base.test.js | 10 +- test/unit/testRootDereference.test.js | 40 ++++++ test/unit/x31schemapack.test.js | 15 ++ 8 files changed, 357 insertions(+), 6 deletions(-) rename test/data/valid_openapi/{referencedPathFromOutOfComponents.yaml => referencedFromOutOfComponents.yaml} (85%) create mode 100644 test/data/valid_openapi31X/json/multiple_refs_and_paths.json create mode 100644 test/data/valid_openapi31X/yaml/referencedFromOutOfComponents.yaml create mode 100644 test/unit/testRootDereference.test.js diff --git a/lib/deref.js b/lib/deref.js index 6d12a9b81..98bb7e037 100644 --- a/lib/deref.js +++ b/lib/deref.js @@ -1,6 +1,7 @@ const _ = require('lodash'), mergeAllOf = require('json-schema-merge-allof'), { typesMap } = require('./common/schemaUtilsCommon'), + { isLocalRef } = require('./jsonPointer'), PARAMETER_SOURCE = { REQUEST: 'REQUEST', RESPONSE: 'RESPONSE' @@ -383,5 +384,41 @@ module.exports = { } return schema; + }, + + dereferenceFromOutOfComponents: function (spec, reference) { + let splitRef = reference.split('/'), + resolvedContent; + + splitRef = splitRef.slice(1).map((elem) => { + // https://swagger.io/docs/specification/using-ref#escape + // since / is the default delimiter, slashes are escaped with ~1 + return decodeURIComponent( + elem + .replace(/~1/g, '/') + .replace(/~0/g, '~') + ); + }); + + resolvedContent = this._getEscaped(spec, splitRef); + return resolvedContent; + }, + + dereferenceRoot: function(spec) { + let dereferencedSpec = Object.assign({}, spec); + // seenRef = [] TODO:Add circular references handling here + const that = this; + traverseUtility(dereferencedSpec).forEach(function (property) { + if ( + typeof property === 'object' && + _.size(property) === 1 && + property.hasOwnProperty('$ref') && + isLocalRef(property, '$ref') && + !property.$ref.includes('components/schemas') + ) { + this.update(that.dereferenceFromOutOfComponents(dereferencedSpec, property.$ref)); + } + }); + return dereferencedSpec; } }; diff --git a/lib/schemapack.js b/lib/schemapack.js index 0921d81fb..0b7e6cf0d 100644 --- a/lib/schemapack.js +++ b/lib/schemapack.js @@ -1,5 +1,7 @@ 'use strict'; +const deref = require('./deref.js'); + // This is the default collection name if one can't be inferred from the OpenAPI spec const COLLECTION_NAME = 'Imported from OpenAPI 3.0', { getConcreteSchemaUtils } = require('./common/versionUtils.js'), @@ -273,7 +275,7 @@ class SchemaPack { return callback(error); } - this.openapi = newOpenapi; + this.openapi = deref.dereferenceRoot(newOpenapi); // this cannot be attempted before validation specComponentsAndUtils = { concreteUtils }; Object.assign(specComponentsAndUtils, concreteUtils.getRequiredData(this.openapi)); diff --git a/test/data/valid_openapi/referencedPathFromOutOfComponents.yaml b/test/data/valid_openapi/referencedFromOutOfComponents.yaml similarity index 85% rename from test/data/valid_openapi/referencedPathFromOutOfComponents.yaml rename to test/data/valid_openapi/referencedFromOutOfComponents.yaml index 46ad6abf2..6fadff1c6 100644 --- a/test/data/valid_openapi/referencedPathFromOutOfComponents.yaml +++ b/test/data/valid_openapi/referencedFromOutOfComponents.yaml @@ -53,6 +53,20 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + put: + summary: Create a pet put + operationId: createPets + tags: + - pets + responses: + '201': + description: Null response + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/other" /pets/{petId}: $ref: "#/x-operations/getAPet" components: @@ -85,6 +99,8 @@ components: format: int32 message: type: string + other: + $ref: "#/placeOut/theSchema" x-operations: getAPet: get: @@ -112,3 +128,7 @@ x-operations: application/json: schema: $ref: "#/components/schemas/Error" + +placeOut: + theSchema: + type: string diff --git a/test/data/valid_openapi31X/json/multiple_refs_and_paths.json b/test/data/valid_openapi31X/json/multiple_refs_and_paths.json new file mode 100644 index 000000000..40eb4c6df --- /dev/null +++ b/test/data/valid_openapi31X/json/multiple_refs_and_paths.json @@ -0,0 +1,101 @@ +{ + "openapi": "3.1.0", + "info": { + "version": "1.0.0", + "title": "Swagger Petstore", + "license": { + "name": "MIT" + } + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "$ref": "#/components/pathItems/pets" + } + + }, + "components": { + "schemas": { + "RequestBody": { + "required": [ + "requestId", + "requestName" + ], + "properties": { + "requestId": { + "type": "integer", + "format": "int64", + "examples": [123456] + }, + "requestName": { + "type": "string" + } + } + }, + "ResponseBody": { + "required": [ + "responseId", + "responseName" + ], + "properties": { + "responseId": { + "type": "integer", + "format": "int64", + "examples": [234] + }, + "responseName": { + "type":"string", + "examples": ["200 OK Response"] + } + } + } + }, + "pathItems": { + "pets": { + "get": { + "summary": "List all pets", + "operationId": "listPets", + "requestBody": { + "content": { + "application/json": { + "schema": { + "properties": { + "key1": { + "$ref":"#/components/schemas/RequestBody" + }, + "key2": { + "$ref":"#/components/schemas/RequestBody" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "An paged array of pets", + "content": { + "application/json": { + "schema": { + "properties":{ + "key1": { + "$ref": "#/components/schemas/ResponseBody" + }, + "key2": { + "$ref": "#/components/schemas/ResponseBody" + } + } + } + } + } + } + } + } + } + } + } +} diff --git a/test/data/valid_openapi31X/yaml/referencedFromOutOfComponents.yaml b/test/data/valid_openapi31X/yaml/referencedFromOutOfComponents.yaml new file mode 100644 index 000000000..6d0fbb297 --- /dev/null +++ b/test/data/valid_openapi31X/yaml/referencedFromOutOfComponents.yaml @@ -0,0 +1,136 @@ +openapi: "3.1.0" +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT +servers: + - url: http://petstore.swagger.io/v1 +paths: + $ref: "#/components/pathItems" +components: + schemas: + Pet: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + Pets: + type: array + items: + $ref: "#/components/schemas/Pet" + Error: + type: object + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string + other: + $ref: "#/placeOut/theSchema" + pathItems: + /pets: + get: + summary: List all pets + operationId: listPets + tags: + - pets + parameters: + - name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + format: int32 + responses: + '200': + description: A paged array of pets + headers: + x-next: + description: A link to the next page of responses + schema: + type: string + content: + application/json: + schema: + $ref: "#/components/schemas/Pets" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + post: + summary: Create a pet + operationId: createPets + tags: + - pets + responses: + '201': + description: Null response + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + put: + summary: Create a pet put + operationId: createPets + tags: + - pets + responses: + '201': + description: Null response + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/other" + /pets/{petId}: + $ref: "#/x-operations/getAPet" +x-operations: + getAPet: + get: + summary: Info for a specific pet + operationId: showPetById + tags: + - pets + parameters: + - name: petId + in: path + required: true + description: The id of the pet to retrieve + schema: + type: string + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + +placeOut: + theSchema: + type: string diff --git a/test/unit/base.test.js b/test/unit/base.test.js index bad6af075..b318e3e8b 100644 --- a/test/unit/base.test.js +++ b/test/unit/base.test.js @@ -59,8 +59,8 @@ describe('CONVERT FUNCTION TESTS ', function() { invalidNullInfo = path.join(__dirname, INVALID_OPENAPI_PATH, '/invalid-null-info.json'), invalidNullInfoTitle = path.join(__dirname, INVALID_OPENAPI_PATH, '/invalid-info-null-title.json'), invalidNullInfoVersion = path.join(__dirname, INVALID_OPENAPI_PATH, '/invalid-info-null-version.json'), - referencedPathFromOutOfComponents = - path.join(__dirname, VALID_OPENAPI_PATH + '/referencedPathFromOutOfComponents.yaml'), + referencedFromOutOfComponents = + path.join(__dirname, VALID_OPENAPI_PATH + '/referencedFromOutOfComponents.yaml'), referencedPathFromOutOfComponentsTagsStrategy = path.join(__dirname, VALID_OPENAPI_PATH + '/petstore-detailed-referenced-path.yaml'); @@ -1284,9 +1284,9 @@ describe('CONVERT FUNCTION TESTS ', function() { 'from a different place than components]', function() { it('Should convert and include the referenced paths' + - referencedPathFromOutOfComponents, function(done) { + referencedFromOutOfComponents, function(done) { Converter.convert({ type: 'file', data: - referencedPathFromOutOfComponents }, {}, (err, conversionResult) => { + referencedFromOutOfComponents }, {}, (err, conversionResult) => { expect(err).to.be.null; expect(conversionResult.result).to.equal(true); expect(conversionResult.output.length).to.equal(1); @@ -1294,7 +1294,7 @@ describe('CONVERT FUNCTION TESTS ', function() { expect(conversionResult.output[0].data).to.have.property('info'); expect(conversionResult.output[0].data).to.have.property('item'); expect(conversionResult.output[0].data.item[0].item.length) - .to.equal(3); + .to.equal(4); done(); }); }); diff --git a/test/unit/testRootDereference.test.js b/test/unit/testRootDereference.test.js new file mode 100644 index 000000000..a367218a0 --- /dev/null +++ b/test/unit/testRootDereference.test.js @@ -0,0 +1,40 @@ +const deref = require('../../lib/deref'); + +var expect = require('chai').expect, + fs = require('fs'), + path = require('path'), + VALID_OPENAPI_PATH = '../data/valid_openapi', + VALID_OPENAPI_PATH_31 = '../data/valid_openapi31X', + { getConcreteSchemaUtils } = require('./../../lib/common/versionUtils'); + +describe('[Github #309 - Should convert a path when is referenced ' + + 'from a different place than components]', function() { + + it('Should dereference root 3.0 file', function() { + let referencedFromOutOfComponents = + path.join(__dirname, VALID_OPENAPI_PATH + '/referencedFromOutOfComponents.yaml'), + fileData = fs.readFileSync(referencedFromOutOfComponents, 'utf-8'), + concreteUtils = getConcreteSchemaUtils(fileData), + parsedContent = concreteUtils.parseSpec(fileData, {}); + const dereferencedSpec = deref.dereferenceRoot(parsedContent.openapi, {}), + referencedPathContent = JSON.stringify(parsedContent.openapi['x-operations'].getAPet), + referencedSchemaContent = JSON.stringify(parsedContent.openapi.placeOut.theSchema); + + expect(JSON.stringify(dereferencedSpec.paths['/pets/{petId}'])).to.equal(referencedPathContent); + expect(JSON.stringify(dereferencedSpec.components.schemas.other)).to.equal(referencedSchemaContent); + }); + + it('Should dereference root 3.1 file using pathItems', function() { + let referencedFromOutOfComponents = + path.join(__dirname, VALID_OPENAPI_PATH_31 + '/yaml/referencedFromOutOfComponents.yaml'), + fileData = fs.readFileSync(referencedFromOutOfComponents, 'utf-8'), + concreteUtils = getConcreteSchemaUtils(fileData), + parsedContent = concreteUtils.parseSpec(fileData, {}); + const dereferencedSpec = deref.dereferenceRoot(parsedContent.openapi, {}), + referencedPathContent = JSON.stringify(parsedContent.openapi['x-operations'].getAPet), + referencedSchemaContent = JSON.stringify(parsedContent.openapi.placeOut.theSchema); + + expect(JSON.stringify(dereferencedSpec.paths['/pets/{petId}'])).to.equal(referencedPathContent); + expect(JSON.stringify(dereferencedSpec.components.schemas.other)).to.equal(referencedSchemaContent); + }); +}); diff --git a/test/unit/x31schemapack.test.js b/test/unit/x31schemapack.test.js index 91f4fef06..aeb7076ee 100644 --- a/test/unit/x31schemapack.test.js +++ b/test/unit/x31schemapack.test.js @@ -22,6 +22,21 @@ describe('Testing openapi 3.1 schema pack convert', function() { }); }); + it('Should convert from openapi 3.1 spec to postman collection -- multiple refs_and_paths', function() { + const fileSource = path.join(__dirname, OPENAPI_31_FOLDER + '/json/multiple_refs_and_paths.json'), + fileData = fs.readFileSync(fileSource, 'utf8'), + input = { + type: 'string', + data: fileData + }, + converter = new SchemaPack(input); + + converter.convert((err, result) => { + expect(err).to.be.null; + expect(result.result).to.be.true; + }); + }); + it('Should interpret binary types correctly and set mode as file in body -- petstore', function() { const fileSource = path.join(__dirname, OPENAPI_31_FOLDER + '/json/petstore.json'), fileData = fs.readFileSync(fileSource, 'utf8'), From e6b108d28f52d043425c9d88dc31916754abcab3 Mon Sep 17 00:00:00 2001 From: Erik Mendoza Date: Fri, 2 Dec 2022 18:32:37 -0600 Subject: [PATCH 3/6] adding method descriptions --- lib/deref.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/deref.js b/lib/deref.js index 98bb7e037..74f6e9db3 100644 --- a/lib/deref.js +++ b/lib/deref.js @@ -386,6 +386,12 @@ module.exports = { return schema; }, + /** + * Take a $ref and a spec and return the referenced value + * @param {object} spec the parsed spec object + * @param {string} reference the $ref value to dereference + * @returns {object} the dereferenced $ref value + */ dereferenceFromOutOfComponents: function (spec, reference) { let splitRef = reference.split('/'), resolvedContent; @@ -404,6 +410,11 @@ module.exports = { return resolvedContent; }, + /** + * Dereferences the values referenced from out of components/schemas element + * @param {object} spec The parsed specification + * @returns {object} The specification with the values referenced from other place than components/schemas + */ dereferenceRoot: function(spec) { let dereferencedSpec = Object.assign({}, spec); // seenRef = [] TODO:Add circular references handling here From 663ab9d8578c5fedb6d06d98846a756a5b0ffb06 Mon Sep 17 00:00:00 2001 From: Erik Mendoza Date: Sat, 10 Dec 2022 03:36:22 -0600 Subject: [PATCH 4/6] Adding fix to issue 309 and related scenarios - Referencing from out of components - Referencing paths from pathItems in oas 3.1 - Adding circular references in dereference process - Adding test scenarios --- lib/bundle.js | 13 +- lib/deref.js | 36 +- lib/schemaUtils.js | 6 +- lib/schemapack.js | 5 +- lib/utils.js | 21 ++ .../referencedAllFromComponents.yaml | 206 ++++++++++++ ...eferencedAllFromComponentsAndFromOut.yaml} | 106 ++++-- .../referencedAllFromOutOfComponents.yaml | 206 ++++++++++++ ...erencedAllFromOutOfComponentsCircular.yaml | 76 +++++ ...referencedAllPathsFromOutOfComponents.yaml | 208 ++++++++++++ ...edSchemas2DependOnSameOutOfComponents.yaml | 93 ++++++ .../yaml/referencedAllFromComponents.yaml | 206 ++++++++++++ .../referencedAllFromOutOfComponents.yaml | 221 +++++++++++++ .../referencedAllPathsFromComponents.yaml | 199 +++++++++++ ...referencedAllPathsFromOutOfComponents.yaml | 199 +++++++++++ .../yaml/referencedFromOutOfComponents.yaml | 136 -------- test/unit/base.test.js | 313 ++++++++++++++++-- test/unit/deref.test.js | 27 +- test/unit/testRootDereference.test.js | 40 --- test/unit/x31schemapack.test.js | 288 ++++++++++++++++ 20 files changed, 2334 insertions(+), 271 deletions(-) create mode 100644 test/data/valid_openapi/referencedAllFromComponents.yaml rename test/data/valid_openapi/{referencedFromOutOfComponents.yaml => referencedAllFromComponentsAndFromOut.yaml} (52%) create mode 100644 test/data/valid_openapi/referencedAllFromOutOfComponents.yaml create mode 100644 test/data/valid_openapi/referencedAllFromOutOfComponentsCircular.yaml create mode 100644 test/data/valid_openapi/referencedAllPathsFromOutOfComponents.yaml create mode 100644 test/data/valid_openapi/referencedSchemas2DependOnSameOutOfComponents.yaml create mode 100644 test/data/valid_openapi31X/yaml/referencedAllFromComponents.yaml create mode 100644 test/data/valid_openapi31X/yaml/referencedAllFromOutOfComponents.yaml create mode 100644 test/data/valid_openapi31X/yaml/referencedAllPathsFromComponents.yaml create mode 100644 test/data/valid_openapi31X/yaml/referencedAllPathsFromOutOfComponents.yaml delete mode 100644 test/data/valid_openapi31X/yaml/referencedFromOutOfComponents.yaml delete mode 100644 test/unit/testRootDereference.test.js diff --git a/lib/bundle.js b/lib/bundle.js index 1a2a1e699..73de6da2a 100644 --- a/lib/bundle.js +++ b/lib/bundle.js @@ -14,7 +14,8 @@ const _ = require('lodash'), parse = require('./parse.js'), { ParseError } = require('./common/ParseError'), Utils = require('./utils'), - crypto = require('crypto'); + crypto = require('crypto'), + { isCircularReference } = require('./utils'); let path = require('path'), pathBrowserify = require('path-browserify'), @@ -481,16 +482,6 @@ function findReferenceByMainKeyInTraceFromContext(documentContext, mainKeyInTrac return relatedRef; } -/** - * Verifies if a node has same content as one of the parents so it is a circular ref - * @param {function} traverseContext - The context of the traverse function - * @param {object} contentFromTrace - The resolved content of the node to deref - * @returns {boolean} whether is circular reference or not. - */ -function isCircularReference(traverseContext, contentFromTrace) { - return traverseContext.parents.find((parent) => { return parent.node === contentFromTrace; }) !== undefined; -} - /** * Modifies content of a node if it is circular reference. * diff --git a/lib/deref.js b/lib/deref.js index 74f6e9db3..36d5642cd 100644 --- a/lib/deref.js +++ b/lib/deref.js @@ -30,7 +30,9 @@ const _ = require('lodash'), ], DEFAULT_SCHEMA_UTILS = require('./30XUtils/schemaUtils30X'), traverseUtility = require('traverse'), - PROPERTIES_TO_ASSIGN_ON_CASCADE = ['type', 'nullable']; + PROPERTIES_TO_ASSIGN_ON_CASCADE = ['type', 'nullable'], + CIRCULAR_REF_KEY = '$circularRef', + { isCircularReference } = require('./utils'); /** * @param {*} currentNode - the object from which you're trying to find references @@ -202,6 +204,9 @@ module.exports = { return this.resolveAllOf(schema.allOf, parameterSourceOption, components, schemaResolutionCache, resolveFor, resolveTo, stack, _.cloneDeep(seenRef), stackLimit); } + if (schema.hasOwnProperty(CIRCULAR_REF_KEY)) { + return schema; + } if (schema.$ref && _.isFunction(schema.$ref.split)) { let refKey = schema.$ref, outerProperties = concreteUtils.getOuterPropsIfIsSupported(schema); @@ -392,7 +397,7 @@ module.exports = { * @param {string} reference the $ref value to dereference * @returns {object} the dereferenced $ref value */ - dereferenceFromOutOfComponents: function (spec, reference) { + dereferenceElement: function (spec, reference) { let splitRef = reference.split('/'), resolvedContent; @@ -413,21 +418,36 @@ module.exports = { /** * Dereferences the values referenced from out of components/schemas element * @param {object} spec The parsed specification + * @param {object} constraintRegexp The regexp to match against the $ref element's value * @returns {object} The specification with the values referenced from other place than components/schemas */ - dereferenceRoot: function(spec) { - let dereferencedSpec = Object.assign({}, spec); - // seenRef = [] TODO:Add circular references handling here - const that = this; + dereferenceByConstraint: function(spec, constraintRegexp) { + let dereferencedSpec = _.cloneDeep(spec), + that = this, + seenContents = {}; + traverseUtility(dereferencedSpec).forEach(function (property) { if ( typeof property === 'object' && _.size(property) === 1 && property.hasOwnProperty('$ref') && isLocalRef(property, '$ref') && - !property.$ref.includes('components/schemas') + ( + property.$ref.match(constraintRegexp) + ) ) { - this.update(that.dereferenceFromOutOfComponents(dereferencedSpec, property.$ref)); + const contentFromRef = seenContents[property.$ref]; + if (isCircularReference(this, contentFromRef)) { + this.update({ [CIRCULAR_REF_KEY]: '' }); + } + else { + const dereferencedContent = that.dereferenceElement( + dereferencedSpec, + property.$ref + ); + seenContents[property.$ref] = dereferencedContent; + this.update(dereferencedContent); + } } }); return dereferencedSpec; diff --git a/lib/schemaUtils.js b/lib/schemaUtils.js index e7c42e516..835058860 100644 --- a/lib/schemaUtils.js +++ b/lib/schemaUtils.js @@ -539,6 +539,7 @@ module.exports = { */ generateTrieFromPaths: function (spec, options, fromWebhooks = false) { options = _.merge({}, defaultOptions, options); + spec = deref.dereferenceByConstraint(spec, /#\/components\/pathItems/); let concreteUtils = getConcreteSchemaUtils({ type: 'json', data: spec }), specComponentsAndUtils = { concreteUtils @@ -576,7 +577,6 @@ module.exports = { return methods; }; Object.assign(specComponentsAndUtils, concreteUtils.getRequiredData(spec)); - for (path in paths) { if (paths.hasOwnProperty(path)) { currentPathObject = paths[path]; @@ -603,10 +603,6 @@ module.exports = { pathLength = currentPath.length; - if (currentPathObject.hasOwnProperty('$ref')) { - currentPathObject = this.dereferenceFromOutOfComponents(spec, currentPathObject.$ref); - } - // get method names available for this path pathMethods = getPathMethods(Object.keys(currentPathObject)); diff --git a/lib/schemapack.js b/lib/schemapack.js index 0b7e6cf0d..ace44c743 100644 --- a/lib/schemapack.js +++ b/lib/schemapack.js @@ -1,6 +1,7 @@ 'use strict'; -const deref = require('./deref.js'); +const deref = require('./deref.js'), + { getNegativeRegexp } = require('./utils.js'); // This is the default collection name if one can't be inferred from the OpenAPI spec const COLLECTION_NAME = 'Imported from OpenAPI 3.0', @@ -275,7 +276,7 @@ class SchemaPack { return callback(error); } - this.openapi = deref.dereferenceRoot(newOpenapi); + this.openapi = deref.dereferenceByConstraint(newOpenapi, getNegativeRegexp('#/components/')); // this cannot be attempted before validation specComponentsAndUtils = { concreteUtils }; Object.assign(specComponentsAndUtils, concreteUtils.getRequiredData(this.openapi)); diff --git a/lib/utils.js b/lib/utils.js index 1d1bcc90a..a38df981e 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -142,5 +142,26 @@ module.exports = { res.push(segment); } return res.join('/'); + }, + + /** + * Generate a regexp to match with strings that does not contain the provided string + * @param {string} negatedWord The string we want to appear in the string + * @returns {object} The regexp that matches with strings that does not contain the provided string + */ + getNegativeRegexp: function (negatedWord) { + const regexpValue = `^(?!.*?${negatedWord}).*$`, + regexp = new RegExp(regexpValue); + return regexp; + }, + + /** + * Verifies if a node has same content as one of the parents in traverseUtil context so it is a circular ref + * @param {function} traverseContext - The context of the traverse function + * @param {object} contentFromTrace - The resolved content of the node to deref + * @returns {boolean} whether is circular reference or not. + */ + isCircularReference: function (traverseContext, contentFromTrace) { + return traverseContext.parents.find((parent) => { return parent.node === contentFromTrace; }) !== undefined; } }; diff --git a/test/data/valid_openapi/referencedAllFromComponents.yaml b/test/data/valid_openapi/referencedAllFromComponents.yaml new file mode 100644 index 000000000..779748316 --- /dev/null +++ b/test/data/valid_openapi/referencedAllFromComponents.yaml @@ -0,0 +1,206 @@ +openapi: "3.0.0" +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT +servers: + - url: http://petstore.swagger.io/v1 +paths: + /pets: + get: + summary: List all pets + operationId: listPets + tags: + - pets + links: + userRepositories: + $ref: '#/components/links/UserRepositories' + parameters: + - "$ref": "#/components/parameters/theParam" + responses: + "200": + "$ref": "#/components/responses/200" + default: + "$ref": "#/components/responses/default" + post: + summary: Create a pet + operationId: createPets + tags: + - pets + responses: + '201': + description: Null response + default: + "$ref": "#/components/responses/default" + callbacks: + # the name `onData` is a convenience locator + onData: + # when data is sent, it will be sent to the `callbackUrl` provided + # when making the subscription PLUS the suffix `/data` + '{$request.query.callbackUrl}/data': + post: + requestBody: + description: subscription payload + content: + application/json: + schema: + properties: + timestamp: + type: string + format: date-time + userData: + type: string + responses: + '202': + description: | + Your server implementation should return this HTTP status code + if the data was received successfully + '204': + description: | + Your server should return this HTTP status code if no longer interested + in further updates + put: + summary: Create a pet put + operationId: createPets + tags: + - pets + requestBody: + $ref: '#/components/requestBodies/PetRequest' + responses: + '201': + description: Null response + default: + "$ref": "#/components/responses/default" + /pets/{petId}: + $ref: "#/x-operations/getAPet" +components: + parameters: + theParam: + name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + format: int32 + requestBodies: + PetRequest: + content: + application/json: + schema: + "$ref": "#/components/schemas/Pet" + examples: + petExample: + "$ref": "#/components/examples/petExample" + application/xml: + schema: + type: 'object' + properties: + name: + type: string + description: hooray + description: Pet object that needs to be added to the store + required: true + responses: + '200': + description: A paged array of pets + headers: + theHeader: + "$ref": "#/components/headers/theHeader" + content: + application/json: + schema: + $ref: "#/components/schemas/Pets" + examples: + petsExample: + "$ref": "#/components/examples/petsExample" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + headers: + theHeader: + description: the header + schema: + type: string + links: + UserRepositories: + operationId: getRepositoriesByOwner + parameters: + username: $response.body#/username + examples: + petExample: + value: { + id: 1, + name: bob + } + petsExample: + value: [{ + id: 1, + name: bob + }] + schemas: + Pet: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + Pets: + type: array + items: + $ref: "#/components/schemas/Pet" + Error: + type: object + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string + other: + $ref: "#/placeOut/theSchema" +x-operations: + getAPet: + get: + summary: Info for a specific pet + operationId: showPetById + tags: + - pets + parameters: + - name: petId + in: path + required: true + description: The id of the pet to retrieve + schema: + type: string + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + +placeOut: + theSchema: + type: string diff --git a/test/data/valid_openapi/referencedFromOutOfComponents.yaml b/test/data/valid_openapi/referencedAllFromComponentsAndFromOut.yaml similarity index 52% rename from test/data/valid_openapi/referencedFromOutOfComponents.yaml rename to test/data/valid_openapi/referencedAllFromComponentsAndFromOut.yaml index 6fadff1c6..ceb7be578 100644 --- a/test/data/valid_openapi/referencedFromOutOfComponents.yaml +++ b/test/data/valid_openapi/referencedAllFromComponentsAndFromOut.yaml @@ -13,32 +13,13 @@ paths: operationId: listPets tags: - pets + links: + userRepositories: + $ref: '#/components/links/UserRepositories' parameters: - - name: limit - in: query - description: How many items to return at one time (max 100) - required: false - schema: - type: integer - format: int32 + - "$ref": "#/components/parameters/theParam" responses: - '200': - description: A paged array of pets - headers: - x-next: - description: A link to the next page of responses - schema: - type: string - content: - application/json: - schema: - $ref: "#/components/schemas/Pets" - default: - description: unexpected error - content: - application/json: - schema: - $ref: "#/components/schemas/Error" + "$ref": "#/components/responses" post: summary: Create a pet operationId: createPets @@ -48,28 +29,85 @@ paths: '201': description: Null response default: - description: unexpected error - content: - application/json: - schema: - $ref: "#/components/schemas/Error" + "$ref": "#/components/responses/default" put: summary: Create a pet put operationId: createPets tags: - pets + requestBody: + $ref: '#/components/requestBodies/Pet' responses: '201': description: Null response default: - description: unexpected error - content: - application/json: - schema: - $ref: "#/components/schemas/other" + "$ref": "#/components/responses/default" /pets/{petId}: $ref: "#/x-operations/getAPet" components: + parameters: + theParam: + name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + format: int32 + requestBodies: + Pet: + content: + application/json: + schema: + allOf: + - description: My Pet + title: Pettie + - $ref: '#/components/schemas/Pet' + examples: + response: + "$ref": "#/components/examples/response" + application/xml: + schema: + type: 'object' + properties: + name: + type: string + description: hooray + description: Pet object that needs to be added to the store + required: true + responses: + '200': + description: A paged array of pets + headers: + "$ref": "#/components/headers/theHeader" + content: + application/json: + schema: + $ref: "#/components/schemas/Pets" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + headers: + theHeader: + description: the header + schema: + type: string + links: + UserRepositories: + operationId: getRepositoriesByOwner + parameters: + username: $response.body#/username + examples: + response: + value: { + description: My Pet, + title: Pettie, + id: 1, + name: bob + } schemas: Pet: type: object diff --git a/test/data/valid_openapi/referencedAllFromOutOfComponents.yaml b/test/data/valid_openapi/referencedAllFromOutOfComponents.yaml new file mode 100644 index 000000000..213c138aa --- /dev/null +++ b/test/data/valid_openapi/referencedAllFromOutOfComponents.yaml @@ -0,0 +1,206 @@ +openapi: "3.0.0" +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT +servers: + - url: http://petstore.swagger.io/v1 +paths: + /pets: + get: + summary: List all pets + operationId: listPets + tags: + - pets + links: + userRepositories: + $ref: '#/componentsFromOut/links/UserRepositories' + parameters: + - "$ref": "#/componentsFromOut/parameters/theParam" + responses: + "200": + "$ref": "#/componentsFromOut/responses/200" + default: + "$ref": "#/componentsFromOut/responses/default" + post: + summary: Create a pet + operationId: createPets + tags: + - pets + responses: + '201': + description: Null response + default: + "$ref": "#/componentsFromOut/responses/default" + callbacks: + # the name `onData` is a convenience locator + onData: + # when data is sent, it will be sent to the `callbackUrl` provided + # when making the subscription PLUS the suffix `/data` + '{$request.query.callbackUrl}/data': + post: + requestBody: + description: subscription payload + content: + application/json: + schema: + properties: + timestamp: + type: string + format: date-time + userData: + type: string + responses: + '202': + description: | + Your server implementation should return this HTTP status code + if the data was received successfully + '204': + description: | + Your server should return this HTTP status code if no longer interested + in further updates + put: + summary: Create a pet put + operationId: createPets + tags: + - pets + requestBody: + $ref: '#/componentsFromOut/requestBodies/PetRequest' + responses: + '201': + description: Null response + default: + "$ref": "#/componentsFromOut/responses/default" + /pets/{petId}: + $ref: "#/x-operations/getAPet" +componentsFromOut: + parameters: + theParam: + name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + format: int32 + requestBodies: + PetRequest: + content: + application/json: + schema: + "$ref": "#/componentsFromOut/schemas/Pet" + examples: + petExample: + "$ref": "#/componentsFromOut/examples/petExample" + application/xml: + schema: + type: 'object' + properties: + name: + type: string + description: hooray + description: Pet object that needs to be added to the store + required: true + responses: + '200': + description: A paged array of pets + headers: + theHeader: + "$ref": "#/componentsFromOut/headers/theHeader" + content: + application/json: + schema: + $ref: "#/componentsFromOut/schemas/Pets" + examples: + petsExample: + "$ref": "#/componentsFromOut/examples/petsExample" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/componentsFromOut/schemas/Error" + headers: + theHeader: + description: the header + schema: + type: string + links: + UserRepositories: + operationId: getRepositoriesByOwner + parameters: + username: $response.body#/username + examples: + petExample: + value: { + id: 1, + name: bob + } + petsExample: + value: [{ + id: 1, + name: bob + }] + schemas: + Pet: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + Pets: + type: array + items: + $ref: "#/componentsFromOut/schemas/Pet" + Error: + type: object + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string + other: + $ref: "#/placeOut/theSchema" +x-operations: + getAPet: + get: + summary: Info for a specific pet + operationId: showPetById + tags: + - pets + parameters: + - name: petId + in: path + required: true + description: The id of the pet to retrieve + schema: + type: string + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: "#/componentsFromOut/schemas/Pet" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/componentsFromOut/schemas/Error" + +placeOut: + theSchema: + type: string diff --git a/test/data/valid_openapi/referencedAllFromOutOfComponentsCircular.yaml b/test/data/valid_openapi/referencedAllFromOutOfComponentsCircular.yaml new file mode 100644 index 000000000..def0e88fd --- /dev/null +++ b/test/data/valid_openapi/referencedAllFromOutOfComponentsCircular.yaml @@ -0,0 +1,76 @@ +openapi: "3.0.0" +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT +servers: + - url: http://petstore.swagger.io/v1 +paths: + /pets: + get: + summary: List all pets + operationId: listPets + tags: + - pets + parameters: + - "$ref": "#/componentsFromOut/parameters/theParam" + responses: + "200": + "$ref": "#/componentsFromOut/responses/200" +componentsFromOut: + parameters: + theParam: + name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + format: int32 + responses: + '200': + description: A paged array of pets + content: + application/json: + schema: + $ref: "#/componentsFromOut/schemas/Pets" + schemas: + Pet: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + petData: + $ref: "#/componentsFromOut/schemas/other" + Pets: + type: array + items: + $ref: "#/componentsFromOut/schemas/Pet" + Error: + type: object + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string + other: + $ref: "#/placeOut/theSchema" +placeOut: + theSchema: + type: object + properties: + petData: + $ref: "#/componentsFromOut/schemas/Pet" diff --git a/test/data/valid_openapi/referencedAllPathsFromOutOfComponents.yaml b/test/data/valid_openapi/referencedAllPathsFromOutOfComponents.yaml new file mode 100644 index 000000000..8bf25beb5 --- /dev/null +++ b/test/data/valid_openapi/referencedAllPathsFromOutOfComponents.yaml @@ -0,0 +1,208 @@ +openapi: "3.0.0" +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT +servers: + - url: http://petstore.swagger.io/v1 +paths: + "$ref": "#/componentsFromOut/allPaths" +componentsFromOut: + allPaths: + /pets: + get: + summary: List all pets + operationId: listPets + tags: + - pets + links: + userRepositories: + $ref: '#/componentsFromOut/links/UserRepositories' + parameters: + - "$ref": "#/componentsFromOut/parameters/theParam" + responses: + "200": + "$ref": "#/componentsFromOut/responses/200" + default: + "$ref": "#/componentsFromOut/responses/default" + post: + summary: Create a pet + operationId: createPets + tags: + - pets + responses: + '201': + description: Null response + default: + "$ref": "#/componentsFromOut/responses/default" + callbacks: + # the name `onData` is a convenience locator + onData: + # when data is sent, it will be sent to the `callbackUrl` provided + # when making the subscription PLUS the suffix `/data` + '{$request.query.callbackUrl}/data': + post: + requestBody: + description: subscription payload + content: + application/json: + schema: + properties: + timestamp: + type: string + format: date-time + userData: + type: string + responses: + '202': + description: | + Your server implementation should return this HTTP status code + if the data was received successfully + '204': + description: | + Your server should return this HTTP status code if no longer interested + in further updates + put: + summary: Create a pet put + operationId: createPets + tags: + - pets + requestBody: + $ref: '#/componentsFromOut/requestBodies/PetRequest' + responses: + '201': + description: Null response + default: + "$ref": "#/componentsFromOut/responses/default" + /pets/{petId}: + $ref: "#/x-operations/getAPet" + parameters: + theParam: + name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + format: int32 + requestBodies: + PetRequest: + content: + application/json: + schema: + "$ref": "#/componentsFromOut/schemas/Pet" + examples: + petExample: + "$ref": "#/componentsFromOut/examples/petExample" + application/xml: + schema: + type: 'object' + properties: + name: + type: string + description: hooray + description: Pet object that needs to be added to the store + required: true + responses: + '200': + description: A paged array of pets + headers: + theHeader: + "$ref": "#/componentsFromOut/headers/theHeader" + content: + application/json: + schema: + $ref: "#/componentsFromOut/schemas/Pets" + examples: + petsExample: + "$ref": "#/componentsFromOut/examples/petsExample" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/componentsFromOut/schemas/Error" + headers: + theHeader: + description: the header + schema: + type: string + links: + UserRepositories: + operationId: getRepositoriesByOwner + parameters: + username: $response.body#/username + examples: + petExample: + value: { + id: 1, + name: bob + } + petsExample: + value: [{ + id: 1, + name: bob + }] + schemas: + Pet: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + Pets: + type: array + items: + $ref: "#/componentsFromOut/schemas/Pet" + Error: + type: object + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string + other: + $ref: "#/placeOut/theSchema" +x-operations: + getAPet: + get: + summary: Info for a specific pet + operationId: showPetById + tags: + - pets + parameters: + - name: petId + in: path + required: true + description: The id of the pet to retrieve + schema: + type: string + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: "#/componentsFromOut/schemas/Pet" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/componentsFromOut/schemas/Error" + +placeOut: + theSchema: + type: string diff --git a/test/data/valid_openapi/referencedSchemas2DependOnSameOutOfComponents.yaml b/test/data/valid_openapi/referencedSchemas2DependOnSameOutOfComponents.yaml new file mode 100644 index 000000000..90b4400d1 --- /dev/null +++ b/test/data/valid_openapi/referencedSchemas2DependOnSameOutOfComponents.yaml @@ -0,0 +1,93 @@ +openapi: "3.0.0" +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT +servers: + - url: http://petstore.swagger.io/v1 +paths: + /pets: + get: + summary: List all pets + operationId: listPets + tags: + - pets + parameters: + - "$ref": "#/componentsFromOut/parameters/theParam" + responses: + "200": + "$ref": "#/componentsFromOut/responses/200" +componentsFromOut: + parameters: + theParam: + name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + format: int32 + responses: + '200': + description: A paged array of pets + content: + application/json: + schema: + $ref: "#/componentsFromOut/schemas/Pets" + schemas: + Dog: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + petData: + $ref: "#/componentsFromOut/schemas/other" + Cat: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + petData: + $ref: "#/componentsFromOut/schemas/other" + Pets: + type: object + properties: + dog: + $ref: "#/componentsFromOut/schemas/Dog" + cat: + $ref: "#/componentsFromOut/schemas/Cat" + Error: + type: object + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string + other: + type: object + properties: + child: + $ref: "#/componentsFromOut/schemas/childOfOther" + childOfOther: + type: string diff --git a/test/data/valid_openapi31X/yaml/referencedAllFromComponents.yaml b/test/data/valid_openapi31X/yaml/referencedAllFromComponents.yaml new file mode 100644 index 000000000..62c450b9c --- /dev/null +++ b/test/data/valid_openapi31X/yaml/referencedAllFromComponents.yaml @@ -0,0 +1,206 @@ +openapi: "3.1" +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT +servers: + - url: http://petstore.swagger.io/v1 +paths: + /pets: + get: + summary: List all pets + operationId: listPets + tags: + - pets + links: + userRepositories: + $ref: '#/components/links/UserRepositories' + parameters: + - "$ref": "#/components/parameters/theParam" + responses: + "200": + "$ref": "#/components/responses/200" + default: + "$ref": "#/components/responses/default" + post: + summary: Create a pet + operationId: createPets + tags: + - pets + responses: + '201': + description: Null response + default: + "$ref": "#/components/responses/default" + callbacks: + # the name `onData` is a convenience locator + onData: + # when data is sent, it will be sent to the `callbackUrl` provided + # when making the subscription PLUS the suffix `/data` + '{$request.query.callbackUrl}/data': + post: + requestBody: + description: subscription payload + content: + application/json: + schema: + properties: + timestamp: + type: string + format: date-time + userData: + type: string + responses: + '202': + description: | + Your server implementation should return this HTTP status code + if the data was received successfully + '204': + description: | + Your server should return this HTTP status code if no longer interested + in further updates + put: + summary: Create a pet put + operationId: createPets + tags: + - pets + requestBody: + $ref: '#/components/requestBodies/PetRequest' + responses: + '201': + description: Null response + default: + "$ref": "#/components/responses/default" + /pets/{petId}: + "$ref": "#/components/pathItems/getAPet" +components: + parameters: + theParam: + name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + format: int32 + requestBodies: + PetRequest: + content: + application/json: + schema: + "$ref": "#/components/schemas/Pet" + examples: + petExample: + "$ref": "#/components/examples/petExample" + application/xml: + schema: + type: 'object' + properties: + name: + type: string + description: hooray + description: Pet object that needs to be added to the store + required: true + responses: + '200': + description: A paged array of pets + headers: + theHeader: + "$ref": "#/components/headers/theHeader" + content: + application/json: + schema: + $ref: "#/components/schemas/Pets" + examples: + petsExample: + "$ref": "#/components/examples/petsExample" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + headers: + theHeader: + description: the header + schema: + type: string + links: + UserRepositories: + operationId: getRepositoriesByOwner + parameters: + username: $response.body#/username + examples: + petExample: + value: { + id: 1, + name: bob + } + petsExample: + value: [{ + id: 1, + name: bob + }] + schemas: + Pet: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + Pets: + type: array + items: + $ref: "#/components/schemas/Pet" + Error: + type: object + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string + other: + $ref: "#/placeOut/theSchema" + pathItems: + getAPet: + get: + summary: Info for a specific pet + operationId: showPetById + tags: + - pets + parameters: + - name: petId + in: path + required: true + description: The id of the pet to retrieve + schema: + type: string + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + +placeOut: + theSchema: + type: string diff --git a/test/data/valid_openapi31X/yaml/referencedAllFromOutOfComponents.yaml b/test/data/valid_openapi31X/yaml/referencedAllFromOutOfComponents.yaml new file mode 100644 index 000000000..6edd69660 --- /dev/null +++ b/test/data/valid_openapi31X/yaml/referencedAllFromOutOfComponents.yaml @@ -0,0 +1,221 @@ +openapi: "3.1" +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT +servers: + - url: http://petstore.swagger.io/v1 +paths: + /pets: + get: + summary: List all pets + operationId: listPets + tags: + - pets + links: + userRepositories: + $ref: '#/componentsFromOut/links/UserRepositories' + parameters: + - "$ref": "#/componentsFromOut/parameters/theParam" + responses: + "200": + "$ref": "#/componentsFromOut/responses/200" + default: + "$ref": "#/componentsFromOut/responses/default" + post: + summary: Create a pet + operationId: createPets + tags: + - pets + responses: + '201': + description: Null response + default: + "$ref": "#/componentsFromOut/responses/default" + callbacks: + # the name `onData` is a convenience locator + onData: + # when data is sent, it will be sent to the `callbackUrl` provided + # when making the subscription PLUS the suffix `/data` + '{$request.query.callbackUrl}/data': + post: + requestBody: + description: subscription payload + content: + application/json: + schema: + properties: + timestamp: + type: string + format: date-time + userData: + type: string + responses: + '202': + description: | + Your server implementation should return this HTTP status code + if the data was received successfully + '204': + description: | + Your server should return this HTTP status code if no longer interested + in further updates + put: + summary: Create a pet put + operationId: createPets + tags: + - pets + requestBody: + content: + application/json: + schema: + "$ref": "#/componentsFromOut/schemas/Pet" + examples: + petExample: + "$ref": "#/componentsFromOut/examples/petExample" + application/xml: + schema: + type: 'object' + properties: + name: + type: string + description: hooray + description: Pet object that needs to be added to the store + required: true + responses: + '201': + description: Null response + default: + "$ref": "#/componentsFromOut/responses/default" + /pets/{petId}: + "$ref": "#/componentsFromOut/pathItems/getAPet" +componentsFromOut: + parameters: + theParam: + name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + format: int32 + requestBodies: + PetRequest: + content: + application/json: + schema: + "$ref": "#/componentsFromOut/schemas/Pet" + examples: + petExample: + "$ref": "#/componentsFromOut/examples/petExample" + application/xml: + schema: + type: 'object' + properties: + name: + type: string + description: hooray + description: Pet object that needs to be added to the store + required: true + responses: + '200': + description: A paged array of pets + headers: + theHeader: + "$ref": "#/componentsFromOut/headers/theHeader" + content: + application/json: + schema: + $ref: "#/componentsFromOut/schemas/Pets" + examples: + petsExample: + "$ref": "#/componentsFromOut/examples/petsExample" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/componentsFromOut/schemas/Error" + headers: + theHeader: + description: the header + schema: + type: string + links: + UserRepositories: + operationId: getRepositoriesByOwner + parameters: + username: $response.body#/username + examples: + petExample: + value: { + id: 1, + name: bob + } + petsExample: + value: [{ + id: 1, + name: bob + }] + schemas: + Pet: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + Pets: + type: array + items: + $ref: "#/componentsFromOut/schemas/Pet" + Error: + type: object + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string + other: + $ref: "#/placeOut/theSchema" + pathItems: + getAPet: + get: + summary: Info for a specific pet + operationId: showPetById + tags: + - pets + parameters: + - name: petId + in: path + required: true + description: The id of the pet to retrieve + schema: + type: string + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: "#/componentsFromOut/schemas/Pet" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/componentsFromOut/schemas/Error" + +placeOut: + theSchema: + type: string diff --git a/test/data/valid_openapi31X/yaml/referencedAllPathsFromComponents.yaml b/test/data/valid_openapi31X/yaml/referencedAllPathsFromComponents.yaml new file mode 100644 index 000000000..dd4fa8bb7 --- /dev/null +++ b/test/data/valid_openapi31X/yaml/referencedAllPathsFromComponents.yaml @@ -0,0 +1,199 @@ +openapi: "3.1.0" +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT +servers: + - url: http://petstore.swagger.io/v1 +paths: + "$ref": "#/components/pathItems" +components: + parameters: + theParam: + name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + format: int32 + requestBodies: + PetRequest: + content: + application/json: + schema: + "$ref": "#/components/schemas/Pet" + examples: + petExample: + "$ref": "#/components/examples/petExample" + application/xml: + schema: + type: 'object' + properties: + name: + type: string + description: hooray + description: Pet object that needs to be added to the store + required: true + responses: + '200': + description: A paged array of pets + headers: + theHeader: + "$ref": "#/components/headers/theHeader" + content: + application/json: + schema: + $ref: "#/components/schemas/Pets" + examples: + petsExample: + "$ref": "#/components/examples/petsExample" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + headers: + theHeader: + description: the header + schema: + type: string + links: + UserRepositories: + operationId: getRepositoriesByOwner + parameters: + username: $response.body#/username + examples: + petExample: + value: { + id: 1, + name: bob + } + petsExample: + value: [{ + id: 1, + name: bob + }] + schemas: + Pet: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + Pets: + type: array + items: + $ref: "#/components/schemas/Pet" + Error: + type: object + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string + pathItems: + /pets: + get: + summary: List all pets + operationId: listPets + tags: + - pets + links: + userRepositories: + $ref: '#/components/links/UserRepositories' + parameters: + - "$ref": "#/components/parameters/theParam" + responses: + "200": + "$ref": "#/components/responses/200" + default: + "$ref": "#/components/responses/default" + post: + summary: Create a pet + operationId: createPets + tags: + - pets + responses: + '201': + description: Null response + default: + "$ref": "#/components/responses/default" + callbacks: + # the name `onData` is a convenience locator + onData: + # when data is sent, it will be sent to the `callbackUrl` provided + # when making the subscription PLUS the suffix `/data` + '{$request.query.callbackUrl}/data': + post: + requestBody: + description: subscription payload + content: + application/json: + schema: + properties: + timestamp: + type: string + format: date-time + userData: + type: string + responses: + '202': + description: | + Your server implementation should return this HTTP status code + if the data was received successfully + '204': + description: | + Your server should return this HTTP status code if no longer interested + in further updates + put: + summary: Create a pet put + operationId: createPets + tags: + - pets + requestBody: + $ref: '#/components/requestBodies/PetRequest' + responses: + '201': + description: Null response + default: + "$ref": "#/components/responses/default" + /pets/{petId}: + get: + summary: Info for a specific pet + operationId: showPetById + tags: + - pets + parameters: + - name: petId + in: path + required: true + description: The id of the pet to retrieve + schema: + type: string + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" diff --git a/test/data/valid_openapi31X/yaml/referencedAllPathsFromOutOfComponents.yaml b/test/data/valid_openapi31X/yaml/referencedAllPathsFromOutOfComponents.yaml new file mode 100644 index 000000000..7fb0c82c7 --- /dev/null +++ b/test/data/valid_openapi31X/yaml/referencedAllPathsFromOutOfComponents.yaml @@ -0,0 +1,199 @@ +openapi: "3.1.0" +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT +servers: + - url: http://petstore.swagger.io/v1 +paths: + "$ref": "#/componentsFromOut/pathItems" +componentsFromOut: + parameters: + theParam: + name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + format: int32 + requestBodies: + PetRequest: + content: + application/json: + schema: + "$ref": "#/componentsFromOut/schemas/Pet" + examples: + petExample: + "$ref": "#/componentsFromOut/examples/petExample" + application/xml: + schema: + type: 'object' + properties: + name: + type: string + description: hooray + description: Pet object that needs to be added to the store + required: true + responses: + '200': + description: A paged array of pets + headers: + theHeader: + "$ref": "#/componentsFromOut/headers/theHeader" + content: + application/json: + schema: + $ref: "#/componentsFromOut/schemas/Pets" + examples: + petsExample: + "$ref": "#/componentsFromOut/examples/petsExample" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/componentsFromOut/schemas/Error" + headers: + theHeader: + description: the header + schema: + type: string + links: + UserRepositories: + operationId: getRepositoriesByOwner + parameters: + username: $response.body#/username + examples: + petExample: + value: { + id: 1, + name: bob + } + petsExample: + value: [{ + id: 1, + name: bob + }] + schemas: + Pet: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + Pets: + type: array + items: + $ref: "#/componentsFromOut/schemas/Pet" + Error: + type: object + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string + pathItems: + /pets: + get: + summary: List all pets + operationId: listPets + tags: + - pets + links: + userRepositories: + $ref: '#/componentsFromOut/links/UserRepositories' + parameters: + - "$ref": "#/componentsFromOut/parameters/theParam" + responses: + "200": + "$ref": "#/componentsFromOut/responses/200" + default: + "$ref": "#/componentsFromOut/responses/default" + post: + summary: Create a pet + operationId: createPets + tags: + - pets + responses: + '201': + description: Null response + default: + "$ref": "#/componentsFromOut/responses/default" + callbacks: + # the name `onData` is a convenience locator + onData: + # when data is sent, it will be sent to the `callbackUrl` provided + # when making the subscription PLUS the suffix `/data` + '{$request.query.callbackUrl}/data': + post: + requestBody: + description: subscription payload + content: + application/json: + schema: + properties: + timestamp: + type: string + format: date-time + userData: + type: string + responses: + '202': + description: | + Your server implementation should return this HTTP status code + if the data was received successfully + '204': + description: | + Your server should return this HTTP status code if no longer interested + in further updates + put: + summary: Create a pet put + operationId: createPets + tags: + - pets + requestBody: + $ref: '#/componentsFromOut/requestBodies/PetRequest' + responses: + '201': + description: Null response + default: + "$ref": "#/componentsFromOut/responses/default" + /pets/{petId}: + get: + summary: Info for a specific pet + operationId: showPetById + tags: + - pets + parameters: + - name: petId + in: path + required: true + description: The id of the pet to retrieve + schema: + type: string + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: "#/componentsFromOut/schemas/Pet" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/componentsFromOut/schemas/Error" diff --git a/test/data/valid_openapi31X/yaml/referencedFromOutOfComponents.yaml b/test/data/valid_openapi31X/yaml/referencedFromOutOfComponents.yaml deleted file mode 100644 index 6d0fbb297..000000000 --- a/test/data/valid_openapi31X/yaml/referencedFromOutOfComponents.yaml +++ /dev/null @@ -1,136 +0,0 @@ -openapi: "3.1.0" -info: - version: 1.0.0 - title: Swagger Petstore - license: - name: MIT -servers: - - url: http://petstore.swagger.io/v1 -paths: - $ref: "#/components/pathItems" -components: - schemas: - Pet: - type: object - required: - - id - - name - properties: - id: - type: integer - format: int64 - name: - type: string - tag: - type: string - Pets: - type: array - items: - $ref: "#/components/schemas/Pet" - Error: - type: object - required: - - code - - message - properties: - code: - type: integer - format: int32 - message: - type: string - other: - $ref: "#/placeOut/theSchema" - pathItems: - /pets: - get: - summary: List all pets - operationId: listPets - tags: - - pets - parameters: - - name: limit - in: query - description: How many items to return at one time (max 100) - required: false - schema: - type: integer - format: int32 - responses: - '200': - description: A paged array of pets - headers: - x-next: - description: A link to the next page of responses - schema: - type: string - content: - application/json: - schema: - $ref: "#/components/schemas/Pets" - default: - description: unexpected error - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - post: - summary: Create a pet - operationId: createPets - tags: - - pets - responses: - '201': - description: Null response - default: - description: unexpected error - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - put: - summary: Create a pet put - operationId: createPets - tags: - - pets - responses: - '201': - description: Null response - default: - description: unexpected error - content: - application/json: - schema: - $ref: "#/components/schemas/other" - /pets/{petId}: - $ref: "#/x-operations/getAPet" -x-operations: - getAPet: - get: - summary: Info for a specific pet - operationId: showPetById - tags: - - pets - parameters: - - name: petId - in: path - required: true - description: The id of the pet to retrieve - schema: - type: string - responses: - '200': - description: Expected response to a valid request - content: - application/json: - schema: - $ref: "#/components/schemas/Pet" - default: - description: unexpected error - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - -placeOut: - theSchema: - type: string diff --git a/test/unit/base.test.js b/test/unit/base.test.js index 13745e2ff..8791a3c72 100644 --- a/test/unit/base.test.js +++ b/test/unit/base.test.js @@ -59,10 +59,18 @@ describe('CONVERT FUNCTION TESTS ', function() { invalidNullInfo = path.join(__dirname, INVALID_OPENAPI_PATH, '/invalid-null-info.json'), invalidNullInfoTitle = path.join(__dirname, INVALID_OPENAPI_PATH, '/invalid-info-null-title.json'), invalidNullInfoVersion = path.join(__dirname, INVALID_OPENAPI_PATH, '/invalid-info-null-version.json'), - referencedFromOutOfComponents = - path.join(__dirname, VALID_OPENAPI_PATH + '/referencedFromOutOfComponents.yaml'), + referencedAllFromOutOfComponents = + path.join(__dirname, VALID_OPENAPI_PATH + '/referencedAllFromOutOfComponents.yaml'), + referencedAllPathsFromOutOfComponents = + path.join(__dirname, VALID_OPENAPI_PATH + '/referencedAllPathsFromOutOfComponents.yaml'), referencedPathFromOutOfComponentsTagsStrategy = - path.join(__dirname, VALID_OPENAPI_PATH + '/petstore-detailed-referenced-path.yaml'); + path.join(__dirname, VALID_OPENAPI_PATH + '/petstore-detailed-referenced-path.yaml'), + referencedAllFromComponents = + path.join(__dirname, VALID_OPENAPI_PATH + '/referencedAllFromComponents.yaml'), + referencedAllFromOutOfComponentsCircular = + path.join(__dirname, VALID_OPENAPI_PATH + '/referencedAllFromOutOfComponentsCircular.yaml'), + referencedSchemas2DependOnSameOutOfComponents = + path.join(__dirname, VALID_OPENAPI_PATH + '/referencedSchemas2DependOnSameOutOfComponents.yaml'); it('Should add collection level auth with type as `bearer`' + @@ -1298,44 +1306,281 @@ describe('CONVERT FUNCTION TESTS ', function() { describe('[Github #309 - Should convert a path when is referenced ' + 'from a different place than components]', function() { - it('Should convert and include the referenced paths' + - referencedFromOutOfComponents, function(done) { - Converter.convert({ type: 'file', data: - referencedFromOutOfComponents }, {}, (err, conversionResult) => { - expect(err).to.be.null; - expect(conversionResult.result).to.equal(true); - expect(conversionResult.output.length).to.equal(1); - expect(conversionResult.output[0].type).to.equal('collection'); - expect(conversionResult.output[0].data).to.have.property('info'); - expect(conversionResult.output[0].data).to.have.property('item'); - expect(conversionResult.output[0].data.item[0].item.length) - .to.equal(4); - done(); + describe('Validate that all references from out of components are resolved correctly', function() { + it('Should convert and include the referenced responses by referencing individual', function(done) { + Converter.convert({ type: 'file', data: + referencedAllFromOutOfComponents }, {}, (err, conversionResult) => { + const individualResponseReferencedContainer = conversionResult.output[0] + .data.item[0].item[0].response; + expect(err).to.be.null; + expect(conversionResult.result).to.equal(true); + expect(conversionResult.output[0].data.item[0].item.length) + .to.equal(4); + expect(individualResponseReferencedContainer.length) + .to.equal(2); + done(); + }); + }); + + it('Should convert and include the referenced parameters by referencing' + + ' individual', function(done) { + Converter.convert({ type: 'file', data: + referencedAllFromOutOfComponents }, {}, (err, conversionResult) => { + const referencedParameterContainer = conversionResult.output[0] + .data.item[0].item[0].request.url.query; + expect(err).to.be.null; + expect(conversionResult.result).to.equal(true); + expect(conversionResult.output[0].data.item[0].item.length) + .to.equal(4); + expect(referencedParameterContainer.length) + .to.equal(1); + expect(referencedParameterContainer[0].key) + .to.equal('limit'); + done(); + }); + }); + + it('Should convert and include the referenced examples by referencing individual', function(done) { + Converter.convert( + { type: 'file', data: referencedAllFromOutOfComponents }, + { requestParametersResolution: 'Example' }, + (err, conversionResult) => { + const referencedRequestExample = conversionResult.output[0] + .data.item[0].item[2].request.body.raw, + referencedResponseExample = conversionResult.output[0] + .data.item[0].item[0].response[0].body; + expect(err).to.be.null; + expect(conversionResult.result).to.equal(true); + expect(conversionResult.output[0].data.item[0].item.length) + .to.equal(4); + expect(referencedRequestExample) + .to.equal('{\n \"id\": 1,\n \"name\": \"bob\"\n}'); + expect(referencedResponseExample) + .to.equal('[\n {\n \"id\": 1,\n \"name\": \"bob\"\n }\n]'); + done(); + } + ); + }); + + it('Should convert and include the referenced requestBody by referencing individual', function(done) { + Converter.convert( + { type: 'file', data: referencedAllFromOutOfComponents }, + {}, + (err, conversionResult) => { + const referencedRequestBody = conversionResult.output[0] + .data.item[0].item[2].request.body.raw; + expect(err).to.be.null; + expect(conversionResult.result).to.equal(true); + expect(conversionResult.output[0].data.item[0].item.length) + .to.equal(4); + expect(referencedRequestBody) + .to.equal('{\n \"id\": \"\",\n \"name\": \"\",\n \"tag\": \"\"\n}'); + done(); + } + ); + }); + + it('Should convert and include the referenced headers by referencing individual', function(done) { + Converter.convert( + { type: 'file', data: referencedAllFromOutOfComponents }, + {}, + (err, conversionResult) => { + const referencedHeader = conversionResult.output[0] + .data.item[0].item[0].response[0].header[0]; + expect(err).to.be.null; + expect(conversionResult.result).to.equal(true); + expect(conversionResult.output[0].data.item[0].item.length) + .to.equal(4); + expect(referencedHeader.description) + .to.equal('the header'); + expect(referencedHeader.key) + .to.equal('theHeader'); + done(); + } + ); + }); + + it('Should convert and include the paths by referencing all from out of components', function(done) { + Converter.convert( + { type: 'file', data: referencedAllPathsFromOutOfComponents }, + {}, + (err, conversionResult) => { + const referencedHeader = conversionResult.output[0] + .data.item[0].item[0].response[0].header[0]; + expect(err).to.be.null; + expect(conversionResult.result).to.equal(true); + expect(conversionResult.output[0].data.item[0].item.length) + .to.equal(4); + expect(referencedHeader.description) + .to.equal('the header'); + expect(referencedHeader.key) + .to.equal('theHeader'); + done(); + } + ); + }); + + it('Should convert and include the referenced paths using tags as folder strategy', function(done) { + Converter.convert( + { + type: 'file', + data: referencedPathFromOutOfComponentsTagsStrategy + }, + { + folderStrategy: 'Tags' + }, + (err, conversionResult) => { + expect(err).to.be.null; + expect(conversionResult.result).to.equal(true); + expect(conversionResult.output.length).to.equal(1); + expect(conversionResult.output[0].type).to.equal('collection'); + expect(conversionResult.output[0].data).to.have.property('info'); + expect(conversionResult.output[0].data).to.have.property('item'); + expect(conversionResult.output[0].data.item[0].item.length) + .to.equal(7); + done(); + } + ); + }); + + it('Should avoid circular references', function(done) { + Converter.convert( + { + type: 'file', + data: referencedAllFromOutOfComponentsCircular + }, + {}, + (err, conversionResult) => { + const responseBody = JSON.parse(conversionResult.output[0].data.item[0].response[0].body); + expect(err).to.be.null; + expect(conversionResult.result).to.equal(true); + expect(conversionResult.output.length).to.equal(1); + expect(conversionResult.output[0].type).to.equal('collection'); + expect(conversionResult.output[0].data).to.have.property('info'); + expect(conversionResult.output[0].data).to.have.property('item'); + expect(responseBody[0]).to.include.all.keys('id', 'name', 'tag', 'petData'); + expect(responseBody[0].petData.petData.$circularRef) + .to.equal(''); + done(); + } + ); + }); + + it('Should resolve two schemas that depend on the same branch from out of components', function(done) { + Converter.convert( + { + type: 'file', + data: referencedSchemas2DependOnSameOutOfComponents + }, + {}, + (err, conversionResult) => { + const responseBody = conversionResult.output[0].data.item[0].response[0].body; + expect(err).to.be.null; + expect(conversionResult.result).to.equal(true); + expect(conversionResult.output.length).to.equal(1); + expect(conversionResult.output[0].type).to.equal('collection'); + expect(conversionResult.output[0].data).to.have.property('info'); + expect(conversionResult.output[0].data).to.have.property('item'); + expect(responseBody.includes('$circularRef')).to.false; + expect(JSON.parse(responseBody)).to.include.all.keys('dog', 'cat'); + done(); + } + ); }); }); - it('Should convert and include the referenced paths using tags as folder strategy' + - referencedPathFromOutOfComponentsTagsStrategy, function(done) { - Converter.convert( - { - type: 'file', - data: referencedPathFromOutOfComponentsTagsStrategy - }, - { - folderStrategy: 'Tags' - }, - (err, conversionResult) => { + describe('Validate that all references from components are resolved correctly', function () { + it('Should convert and include the referenced responses by referencing individual', function(done) { + Converter.convert({ type: 'file', data: + referencedAllFromComponents }, {}, (err, conversionResult) => { + const individualResponseReferencedContainer = conversionResult.output[0] + .data.item[0].item[0].response; expect(err).to.be.null; expect(conversionResult.result).to.equal(true); - expect(conversionResult.output.length).to.equal(1); - expect(conversionResult.output[0].type).to.equal('collection'); - expect(conversionResult.output[0].data).to.have.property('info'); - expect(conversionResult.output[0].data).to.have.property('item'); expect(conversionResult.output[0].data.item[0].item.length) - .to.equal(7); + .to.equal(4); + expect(individualResponseReferencedContainer.length) + .to.equal(2); done(); - } - ); + }); + }); + + it('Should convert and include the referenced parameters by referencing individual', function(done) { + Converter.convert({ type: 'file', data: + referencedAllFromComponents }, {}, (err, conversionResult) => { + const referencedParameterContainer = conversionResult.output[0] + .data.item[0].item[0].request.url.query; + expect(err).to.be.null; + expect(conversionResult.result).to.equal(true); + expect(conversionResult.output[0].data.item[0].item.length) + .to.equal(4); + expect(referencedParameterContainer.length) + .to.equal(1); + expect(referencedParameterContainer[0].key) + .to.equal('limit'); + done(); + }); + }); + + it('Should convert and include the referenced examples by referencing individual', function(done) { + Converter.convert( + { type: 'file', data: referencedAllFromComponents }, + { requestParametersResolution: 'Example' }, + (err, conversionResult) => { + const referencedRequestExample = conversionResult.output[0] + .data.item[0].item[2].request.body.raw, + referencedResponseExample = conversionResult.output[0] + .data.item[0].item[0].response[0].body; + expect(err).to.be.null; + expect(conversionResult.result).to.equal(true); + expect(conversionResult.output[0].data.item[0].item.length) + .to.equal(4); + expect(referencedRequestExample) + .to.equal('{\n \"id\": 1,\n \"name\": \"bob\"\n}'); + expect(referencedResponseExample) + .to.equal('[\n {\n \"id\": 1,\n \"name\": \"bob\"\n }\n]'); + done(); + } + ); + }); + + it('Should convert and include the referenced requestBody by referencing individual', function(done) { + Converter.convert( + { type: 'file', data: referencedAllFromComponents }, + {}, + (err, conversionResult) => { + const referencedRequestBody = conversionResult.output[0] + .data.item[0].item[2].request.body.raw; + expect(err).to.be.null; + expect(conversionResult.result).to.equal(true); + expect(conversionResult.output[0].data.item[0].item.length) + .to.equal(4); + expect(referencedRequestBody) + .to.equal('{\n \"id\": \"\",\n \"name\": \"\",\n \"tag\": \"\"\n}'); + done(); + } + ); + }); + + it('Should convert and include the referenced headers by referencing individual', function(done) { + Converter.convert( + { type: 'file', data: referencedAllFromComponents }, + {}, + (err, conversionResult) => { + const referencedHeader = conversionResult.output[0] + .data.item[0].item[0].response[0].header[0]; + expect(err).to.be.null; + expect(conversionResult.result).to.equal(true); + expect(conversionResult.output[0].data.item[0].item.length) + .to.equal(4); + expect(referencedHeader.description) + .to.equal('the header'); + expect(referencedHeader.key) + .to.equal('theHeader'); + done(); + } + ); + }); }); }); }); diff --git a/test/unit/deref.test.js b/test/unit/deref.test.js index 8fb314f66..dcd521c08 100644 --- a/test/unit/deref.test.js +++ b/test/unit/deref.test.js @@ -1,7 +1,12 @@ var expect = require('chai').expect, _ = require('lodash'), deref = require('../../lib/deref.js'), - schemaUtils30X = require('./../../lib/30XUtils/schemaUtils30X'); + schemaUtils30X = require('./../../lib/30XUtils/schemaUtils30X'), + { getNegativeRegexp } = require('../../lib/utils'), + fs = require('fs'), + path = require('path'), + VALID_OPENAPI_PATH = '../data/valid_openapi', + { getConcreteSchemaUtils } = require('./../../lib/common/versionUtils'); describe('DEREF FUNCTION TESTS ', function() { describe('resolveRefs Function', function () { @@ -414,3 +419,23 @@ describe('DEREF FUNCTION TESTS ', function() { }); }); }); + +describe('dereferenceByConstraint method', function() { + + it('Should dereference ONLY all out of components in root file', function() { + let referencedAllFromComponentsAndOut = + path.join(__dirname, VALID_OPENAPI_PATH + '/referencedAllFromComponentsAndFromOut.yaml'), + fileData = fs.readFileSync(referencedAllFromComponentsAndOut, 'utf-8'), + concreteUtils = getConcreteSchemaUtils(fileData), + parsedContent = concreteUtils.parseSpec(fileData, {}); + const dereferencedSpec = deref.dereferenceByConstraint( + parsedContent.openapi, + getNegativeRegexp('#/components/') + ), + referencedPathContent = JSON.stringify(parsedContent.openapi['x-operations'].getAPet), + referencedSchemaContent = JSON.stringify(parsedContent.openapi.placeOut.theSchema); + + expect(JSON.stringify(dereferencedSpec.paths['/pets/{petId}'])).to.equal(referencedPathContent); + expect(JSON.stringify(dereferencedSpec.components.schemas.other)).to.equal(referencedSchemaContent); + }); +}); diff --git a/test/unit/testRootDereference.test.js b/test/unit/testRootDereference.test.js deleted file mode 100644 index a367218a0..000000000 --- a/test/unit/testRootDereference.test.js +++ /dev/null @@ -1,40 +0,0 @@ -const deref = require('../../lib/deref'); - -var expect = require('chai').expect, - fs = require('fs'), - path = require('path'), - VALID_OPENAPI_PATH = '../data/valid_openapi', - VALID_OPENAPI_PATH_31 = '../data/valid_openapi31X', - { getConcreteSchemaUtils } = require('./../../lib/common/versionUtils'); - -describe('[Github #309 - Should convert a path when is referenced ' + - 'from a different place than components]', function() { - - it('Should dereference root 3.0 file', function() { - let referencedFromOutOfComponents = - path.join(__dirname, VALID_OPENAPI_PATH + '/referencedFromOutOfComponents.yaml'), - fileData = fs.readFileSync(referencedFromOutOfComponents, 'utf-8'), - concreteUtils = getConcreteSchemaUtils(fileData), - parsedContent = concreteUtils.parseSpec(fileData, {}); - const dereferencedSpec = deref.dereferenceRoot(parsedContent.openapi, {}), - referencedPathContent = JSON.stringify(parsedContent.openapi['x-operations'].getAPet), - referencedSchemaContent = JSON.stringify(parsedContent.openapi.placeOut.theSchema); - - expect(JSON.stringify(dereferencedSpec.paths['/pets/{petId}'])).to.equal(referencedPathContent); - expect(JSON.stringify(dereferencedSpec.components.schemas.other)).to.equal(referencedSchemaContent); - }); - - it('Should dereference root 3.1 file using pathItems', function() { - let referencedFromOutOfComponents = - path.join(__dirname, VALID_OPENAPI_PATH_31 + '/yaml/referencedFromOutOfComponents.yaml'), - fileData = fs.readFileSync(referencedFromOutOfComponents, 'utf-8'), - concreteUtils = getConcreteSchemaUtils(fileData), - parsedContent = concreteUtils.parseSpec(fileData, {}); - const dereferencedSpec = deref.dereferenceRoot(parsedContent.openapi, {}), - referencedPathContent = JSON.stringify(parsedContent.openapi['x-operations'].getAPet), - referencedSchemaContent = JSON.stringify(parsedContent.openapi.placeOut.theSchema); - - expect(JSON.stringify(dereferencedSpec.paths['/pets/{petId}'])).to.equal(referencedPathContent); - expect(JSON.stringify(dereferencedSpec.components.schemas.other)).to.equal(referencedSchemaContent); - }); -}); diff --git a/test/unit/x31schemapack.test.js b/test/unit/x31schemapack.test.js index aeb7076ee..2405b9b4d 100644 --- a/test/unit/x31schemapack.test.js +++ b/test/unit/x31schemapack.test.js @@ -134,6 +134,294 @@ describe('Testing openapi 3.1 schema pack convert', function() { .to.be.equal('{\n \"id\": 1234\n}'); }); }); + + describe('[Github #309 - Should convert a path when is referenced ' + + 'from a different place than components]', function() { + const referencedAllFromOutOfComponents = + path.join(__dirname, OPENAPI_31_FOLDER + '/yaml/referencedAllFromOutOfComponents.yaml'), + referencedAllPathsFromOutOfComponents = + path.join(__dirname, OPENAPI_31_FOLDER + '/yaml/referencedAllPathsFromOutOfComponents.yaml'), + referencedAllFromComponents = + path.join(__dirname, OPENAPI_31_FOLDER + '/yaml/referencedAllFromComponents.yaml'), + referencedAllPathsFromComponents = + path.join(__dirname, OPENAPI_31_FOLDER + '/yaml/referencedAllPathsFromComponents.yaml'); + + describe('Validate that all references from out of components are resolved correctly', function() { + it('Should convert and include the referenced responses by referencing individually', function(done) { + const Converter = new SchemaPack( + { type: 'file', data: referencedAllFromOutOfComponents }, + {} + ); + Converter.convert((err, conversionResult) => { + const individualResponseReferencedContainer = conversionResult.output[0] + .data.item[0].item[0].response; + expect(err).to.be.null; + expect(conversionResult.result).to.equal(true); + expect(conversionResult.output[0].data.item[0].item.length) + .to.equal(4); + expect(individualResponseReferencedContainer.length) + .to.equal(2); + done(); + }); + }); + + it('Should convert and include the referenced parameters by referencing individually', function(done) { + const Converter = new SchemaPack( + { type: 'file', data: referencedAllFromOutOfComponents }, + {} + ); + Converter.convert((err, conversionResult) => { + const referencedParameterContainer = conversionResult.output[0] + .data.item[0].item[0].request.url.query; + expect(err).to.be.null; + expect(conversionResult.result).to.equal(true); + expect(conversionResult.output[0].data.item[0].item.length) + .to.equal(4); + expect(referencedParameterContainer.length) + .to.equal(1); + expect(referencedParameterContainer[0].key) + .to.equal('limit'); + done(); + }); + }); + + it('Should convert and include the referenced examples by referencing individually', function(done) { + const Converter = new SchemaPack( + { type: 'file', data: referencedAllFromOutOfComponents }, + { requestParametersResolution: 'Example' } + ); + Converter.convert( + (err, conversionResult) => { + const referencedRequestExample = conversionResult.output[0] + .data.item[0].item[2].request.body.raw, + referencedResponseExample = conversionResult.output[0] + .data.item[0].item[0].response[0].body; + expect(err).to.be.null; + expect(conversionResult.result).to.equal(true); + expect(conversionResult.output[0].data.item[0].item.length) + .to.equal(4); + expect(referencedRequestExample) + .to.equal('{\n \"id\": 1,\n \"name\": \"bob\"\n}'); + expect(referencedResponseExample) + .to.equal('[\n {\n \"id\": 1,\n \"name\": \"bob\"\n }\n]'); + done(); + } + ); + }); + + it('Should convert and include the referenced requestBody by referencing individually', function(done) { + const Converter = new SchemaPack( + { type: 'file', data: referencedAllFromOutOfComponents }, + {} + ); + Converter.convert( + (err, conversionResult) => { + const referencedRequestBody = JSON.parse(conversionResult.output[0] + .data.item[0].item[2].request.body.raw); + expect(err).to.be.null; + expect(conversionResult.result).to.equal(true); + expect(conversionResult.output[0].data.item[0].item.length) + .to.equal(4); + expect(referencedRequestBody) + .to.have.all.keys('id', 'name', 'tag'); + done(); + } + ); + }); + + it('Should convert and include the referenced headers by referencing individually', function(done) { + const Converter = new SchemaPack( + { type: 'file', data: referencedAllFromOutOfComponents }, + {} + ); + Converter.convert( + (err, conversionResult) => { + const referencedHeader = conversionResult.output[0] + .data.item[0].item[0].response[0].header[0]; + expect(err).to.be.null; + expect(conversionResult.result).to.equal(true); + expect(conversionResult.output[0].data.item[0].item.length) + .to.equal(4); + expect(referencedHeader.description) + .to.equal('the header'); + expect(referencedHeader.key) + .to.equal('theHeader'); + done(); + } + ); + }); + + it('Should convert and include the referenced paths by referencing individually', function(done) { + const Converter = new SchemaPack( + { type: 'file', data: referencedAllFromOutOfComponents }, + {} + ); + Converter.convert( + (err, conversionResult) => { + expect(err).to.be.null; + expect(conversionResult.result).to.equal(true); + expect(conversionResult.output[0].data.item[0].item.length) + .to.equal(4); + done(); + } + ); + }); + + it('Should convert and include the referenced paths by referencing all from out', function(done) { + const Converter = new SchemaPack( + { type: 'file', data: referencedAllPathsFromOutOfComponents }, + {} + ); + Converter.convert( + (err, conversionResult) => { + expect(err).to.be.null; + expect(conversionResult.result).to.equal(true); + expect(conversionResult.output[0].data.item[0].item.length) + .to.equal(4); + done(); + } + ); + }); + }); + + describe('Validate that all references from components are resolved correctly', function() { + it('Should convert and include the referenced responses by referencing individually', function(done) { + const Converter = new SchemaPack( + { type: 'file', data: referencedAllFromComponents }, + {} + ); + Converter.convert((err, conversionResult) => { + const individualResponseReferencedContainer = conversionResult.output[0] + .data.item[0].item[0].response; + expect(err).to.be.null; + expect(conversionResult.result).to.equal(true); + expect(conversionResult.output[0].data.item[0].item.length) + .to.equal(4); + expect(individualResponseReferencedContainer.length) + .to.equal(2); + done(); + }); + }); + + it('Should convert and include the referenced parameters by referencing individually', function(done) { + const Converter = new SchemaPack( + { type: 'file', data: referencedAllFromComponents }, + {} + ); + Converter.convert((err, conversionResult) => { + const referencedParameterContainer = conversionResult.output[0] + .data.item[0].item[0].request.url.query; + expect(err).to.be.null; + expect(conversionResult.result).to.equal(true); + expect(conversionResult.output[0].data.item[0].item.length) + .to.equal(4); + expect(referencedParameterContainer.length) + .to.equal(1); + expect(referencedParameterContainer[0].key) + .to.equal('limit'); + done(); + }); + }); + + it('Should convert and include the referenced examples by referencing individually', function(done) { + const Converter = new SchemaPack( + { type: 'file', data: referencedAllFromComponents }, + { requestParametersResolution: 'Example' } + ); + Converter.convert( + (err, conversionResult) => { + const referencedRequestExample = conversionResult.output[0] + .data.item[0].item[2].request.body.raw, + referencedResponseExample = conversionResult.output[0] + .data.item[0].item[0].response[0].body; + expect(err).to.be.null; + expect(conversionResult.result).to.equal(true); + expect(conversionResult.output[0].data.item[0].item.length) + .to.equal(4); + expect(referencedRequestExample) + .to.equal('{\n \"id\": 1,\n \"name\": \"bob\"\n}'); + expect(referencedResponseExample) + .to.equal('[\n {\n \"id\": 1,\n \"name\": \"bob\"\n }\n]'); + done(); + } + ); + }); + + it('Should convert and include the referenced requestBody by referencing individually', function(done) { + const Converter = new SchemaPack( + { type: 'file', data: referencedAllFromComponents }, + {} + ); + Converter.convert( + (err, conversionResult) => { + const referencedRequestBody = JSON.parse(conversionResult.output[0] + .data.item[0].item[2].request.body.raw); + expect(err).to.be.null; + expect(conversionResult.result).to.equal(true); + expect(conversionResult.output[0].data.item[0].item.length) + .to.equal(4); + expect(referencedRequestBody) + .to.have.all.keys('id', 'name', 'tag'); + done(); + } + ); + }); + + it('Should convert and include the referenced headers by referencing individually', function(done) { + const Converter = new SchemaPack( + { type: 'file', data: referencedAllFromComponents }, + {} + ); + Converter.convert( + (err, conversionResult) => { + const referencedHeader = conversionResult.output[0] + .data.item[0].item[0].response[0].header[0]; + expect(err).to.be.null; + expect(conversionResult.result).to.equal(true); + expect(conversionResult.output[0].data.item[0].item.length) + .to.equal(4); + expect(referencedHeader.description) + .to.equal('the header'); + expect(referencedHeader.key) + .to.equal('theHeader'); + done(); + } + ); + }); + + it('Should convert and include the referenced paths by referencing individually', function(done) { + const Converter = new SchemaPack( + { type: 'file', data: referencedAllFromComponents }, + {} + ); + Converter.convert( + (err, conversionResult) => { + expect(err).to.be.null; + expect(conversionResult.result).to.equal(true); + expect(conversionResult.output[0].data.item[0].item.length) + .to.equal(4); + done(); + } + ); + }); + + it('Should convert and include the referenced paths by referencing all from pathItems', function(done) { + const Converter = new SchemaPack( + { type: 'file', data: referencedAllPathsFromComponents }, + {} + ); + Converter.convert( + (err, conversionResult) => { + expect(err).to.be.null; + expect(conversionResult.result).to.equal(true); + expect(conversionResult.output[0].data.item[0].item.length) + .to.equal(4); + done(); + } + ); + }); + }); + }); }); From a0c249f7e0eb3cfc74f59f06650b5ac7126ea4df Mon Sep 17 00:00:00 2001 From: Erik Mendoza Date: Mon, 12 Dec 2022 10:23:22 -0600 Subject: [PATCH 5/6] Removing duplicated methods The method and implementation were changed to other location --- lib/schemaUtils.js | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/lib/schemaUtils.js b/lib/schemaUtils.js index 835058860..24cbed738 100644 --- a/lib/schemaUtils.js +++ b/lib/schemaUtils.js @@ -512,24 +512,6 @@ module.exports = { return reqParam; }, - dereferenceFromOutOfComponents: function (spec, reference) { - let splitRef = reference.split('/'), - resolvedContent; - - splitRef = splitRef.slice(1).map((elem) => { - // https://swagger.io/docs/specification/using-ref#escape - // since / is the default delimiter, slashes are escaped with ~1 - return decodeURIComponent( - elem - .replace(/~1/g, '/') - .replace(/~0/g, '~') - ); - }); - - resolvedContent = deref._getEscaped(spec, splitRef); - return resolvedContent; - }, - /** * Generates a Trie-like folder structure from the root path object of the OpenAPI specification. * @param {Object} spec - specification in json format @@ -817,10 +799,6 @@ module.exports = { collectionVariables, pathLevelServers = ''; - if (currentPathObject.hasOwnProperty('$ref')) { - currentPathObject = this.dereferenceFromOutOfComponents(spec, currentPathObject.$ref); - } - // discard the leading slash, if it exists if (path[0] === '/') { path = path.substring(1); From 5dfdd84945e82905bce9e530bda5f6ed3f1efeb9 Mon Sep 17 00:00:00 2001 From: Erik Mendoza Date: Tue, 13 Dec 2022 15:47:21 -0600 Subject: [PATCH 6/6] Adding comments related to dereference --- lib/schemaUtils.js | 1 + lib/schemapack.js | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/schemaUtils.js b/lib/schemaUtils.js index 24cbed738..976a4a93b 100644 --- a/lib/schemaUtils.js +++ b/lib/schemaUtils.js @@ -521,6 +521,7 @@ module.exports = { */ generateTrieFromPaths: function (spec, options, fromWebhooks = false) { options = _.merge({}, defaultOptions, options); + // We only dereference the elements(if exist) that references to components/pathItems spec = deref.dereferenceByConstraint(spec, /#\/components\/pathItems/); let concreteUtils = getConcreteSchemaUtils({ type: 'json', data: spec }), specComponentsAndUtils = { diff --git a/lib/schemapack.js b/lib/schemapack.js index ace44c743..9a1980abd 100644 --- a/lib/schemapack.js +++ b/lib/schemapack.js @@ -275,7 +275,8 @@ class SchemaPack { if (error) { return callback(error); } - + // Here we resolve only the elements to reference to aou of components. + // All the dereferences to components are resolved in their own process this.openapi = deref.dereferenceByConstraint(newOpenapi, getNegativeRegexp('#/components/')); // this cannot be attempted before validation specComponentsAndUtils = { concreteUtils };