diff --git a/examples/basic-auth.md b/examples/v2/basic-auth.md similarity index 100% rename from examples/basic-auth.md rename to examples/v2/basic-auth.md diff --git a/examples/basic-auth.yaml b/examples/v2/basic-auth.yaml similarity index 100% rename from examples/basic-auth.yaml rename to examples/v2/basic-auth.yaml diff --git a/examples/default.md b/examples/v2/default.md similarity index 100% rename from examples/default.md rename to examples/v2/default.md diff --git a/examples/default.yaml b/examples/v2/default.yaml similarity index 100% rename from examples/default.yaml rename to examples/v2/default.yaml diff --git a/examples/echo.md b/examples/v2/echo.md similarity index 100% rename from examples/echo.md rename to examples/v2/echo.md diff --git a/examples/echo.yaml b/examples/v2/echo.yaml similarity index 100% rename from examples/echo.yaml rename to examples/v2/echo.yaml diff --git a/examples/heroku-pets.md b/examples/v2/heroku-pets.md similarity index 100% rename from examples/heroku-pets.md rename to examples/v2/heroku-pets.md diff --git a/examples/heroku-pets.yaml b/examples/v2/heroku-pets.yaml similarity index 100% rename from examples/heroku-pets.yaml rename to examples/v2/heroku-pets.yaml diff --git a/examples/instagram.md b/examples/v2/instagram.md similarity index 100% rename from examples/instagram.md rename to examples/v2/instagram.md diff --git a/examples/instagram.yaml b/examples/v2/instagram.yaml similarity index 100% rename from examples/instagram.yaml rename to examples/v2/instagram.yaml diff --git a/examples/minimal.md b/examples/v2/minimal.md similarity index 100% rename from examples/minimal.md rename to examples/v2/minimal.md diff --git a/examples/minimal.yaml b/examples/v2/minimal.yaml similarity index 100% rename from examples/minimal.yaml rename to examples/v2/minimal.yaml diff --git a/examples/petstore_full.md b/examples/v2/petstore_full.md similarity index 100% rename from examples/petstore_full.md rename to examples/v2/petstore_full.md diff --git a/examples/petstore_full.yaml b/examples/v2/petstore_full.yaml similarity index 100% rename from examples/petstore_full.yaml rename to examples/v2/petstore_full.yaml diff --git a/examples/petstore_simple.md b/examples/v2/petstore_simple.md similarity index 100% rename from examples/petstore_simple.md rename to examples/v2/petstore_simple.md diff --git a/examples/petstore_simple.yaml b/examples/v2/petstore_simple.yaml similarity index 100% rename from examples/petstore_simple.yaml rename to examples/v2/petstore_simple.yaml diff --git a/examples/security.md b/examples/v2/security.md similarity index 100% rename from examples/security.md rename to examples/v2/security.md diff --git a/examples/security.yaml b/examples/v2/security.yaml similarity index 100% rename from examples/security.yaml rename to examples/v2/security.yaml diff --git a/examples/v3/petstore.md b/examples/v3/petstore.md new file mode 100644 index 0000000..fcdd62f --- /dev/null +++ b/examples/v3/petstore.md @@ -0,0 +1,385 @@ +# Swagger Petstore - OpenAPI 3.0 +This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about +Swagger at [https://swagger.io](https://swagger.io). In the third iteration of the pet store, we've switched to the design first approach! +You can now help us improve the API whether it's by making changes to the definition itself or to the code. +That way, with time, we can improve the API in general, and expose some of the new features in OAS3. + +Some useful links: + +- [The Pet Store repository](https://github.com/swagger-api/swagger-petstore) +- [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml) + +## Version: 1.0.11 + +### Terms of service +http://swagger.io/terms/ + +**Contact information:** +apiteam@swagger.io + +**License:** [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.html) + +[Find out more about Swagger](http://swagger.io) + +--- +## pet +Everything about your Pets +[Find out more](http://swagger.io) + +### [PUT] /pet +**Update an existing pet** + +Update an existing pet by Id + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Successful operation | **application/json**: [Pet](#pet)
**application/xml**: [Pet](#pet)
| +| 400 | Invalid ID supplied | | +| 404 | Pet not found | | +| 422 | Validation exception | | + +##### Security + +| Security Schema | Scopes | | +| --------------- | ------ | --- | +| petstore_auth | write:pets | read:pets | + +### [POST] /pet +**Add a new pet to the store** + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Successful operation | **application/json**: [Pet](#pet)
**application/xml**: [Pet](#pet)
| +| 400 | Invalid input | | +| 422 | Validation exception | | + +##### Security + +| Security Schema | Scopes | | +| --------------- | ------ | --- | +| petstore_auth | write:pets | read:pets | + +### [GET] /pet/findByStatus +**Finds Pets by status** + +Multiple status values can be provided with comma separated strings + +#### Responses + +| Code | Description | Schema | Links | +| ---- | ----------- | ------ | ----- | +| 200 | successful operation | **application/json**: [ [Pet](#pet) ]
**application/xml**: [ [Pet](#pet) ]
| **address**
Address link description
Parameters {
"userId": "$request.path.id",
"entityId": "$request.entity.id"
}
| +| 400 | Invalid status value | | | + +##### Security + +| Security Schema | Scopes | | +| --------------- | ------ | --- | +| petstore_auth | write:pets | read:pets | + +### ~~[GET] /pet/findByTags~~ + +***DEPRECATED*** + +**Finds Pets by tags** + +Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | successful operation | **application/json**: [ [Pet](#pet) ]
**application/xml**: [ [Pet](#pet) ]
| +| 400 | Invalid tag value | | + +##### Security + +| Security Schema | Scopes | | +| --------------- | ------ | --- | +| petstore_auth | write:pets | read:pets | + +### [GET] /pet/{petId} +**Find pet by ID** + +Returns a single pet + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | successful operation | **application/json**: [Pet](#pet)
**application/xml**: [Pet](#pet)
| +| 400 | Invalid ID supplied | | +| 404 | Pet not found | | + +##### Security + +| Security Schema | Scopes | | +| --------------- | ------ | --- | +| api_key | | | +| petstore_auth | write:pets | read:pets | + +### [POST] /pet/{petId} +**Updates a pet in the store with form data** + +#### Responses + +| Code | Description | +| ---- | ----------- | +| 400 | Invalid input | + +##### Security + +| Security Schema | Scopes | | +| --------------- | ------ | --- | +| petstore_auth | write:pets | read:pets | + +### [DELETE] /pet/{petId} +**Deletes a pet** + +delete a pet + +#### Responses + +| Code | Description | +| ---- | ----------- | +| 400 | Invalid pet value | + +##### Security + +| Security Schema | Scopes | | +| --------------- | ------ | --- | +| petstore_auth | write:pets | read:pets | + +### [POST] /pet/{petId}/uploadImage +**uploads an image** + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | successful operation | **application/json**: [ApiResponse](#apiresponse)
| + +##### Security + +| Security Schema | Scopes | | +| --------------- | ------ | --- | +| petstore_auth | write:pets | read:pets | + +--- +## store +Access to Petstore orders +[Find out more about our store](http://swagger.io) + +### [GET] /store/inventory +**Returns pet inventories by status** + +Returns a map of status codes to quantities + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | successful operation | **application/json**: object
| + +##### Security + +| Security Schema | Scopes | +| --------------- | ------ | +| api_key | | + +### [POST] /store/order +**Place an order for a pet** + +Place a new order in the store + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | successful operation | **application/json**: [Order](#order)
| +| 400 | Invalid input | | +| 422 | Validation exception | | + +### [GET] /store/order/{orderId} +**Find purchase order by ID** + +For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | successful operation | **application/json**: [Order](#order)
**application/xml**: [Order](#order)
| +| 400 | Invalid ID supplied | | +| 404 | Order not found | | + +### [DELETE] /store/order/{orderId} +**Delete purchase order by ID** + +For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors + +#### Responses + +| Code | Description | +| ---- | ----------- | +| 400 | Invalid ID supplied | +| 404 | Order not found | + +--- +## user +Operations about user + +### [POST] /user +**Create user** + +This can only be done by the logged in user. + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| default | successful operation | **application/json**: [User](#user)
**application/xml**: [User](#user)
| + +### [POST] /user/createWithList +**Creates list of users with given input array** + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Successful operation | **application/json**: [User](#user)
**application/xml**: [User](#user)
| +| default | successful operation | | + +### [GET] /user/login +**Logs user into the system** + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | successful operation
**Headers:**
**X-Rate-Limit**: calls per hour allowed by the user
**X-Expires-After**: date in UTC when token expires
| **application/xml**: string
**application/json**: string
| +| 400 | Invalid username/password supplied | | + +### [GET] /user/logout +**Logs out current logged in user session** + +#### Responses + +| Code | Description | +| ---- | ----------- | +| default | successful operation | + +### [GET] /user/{username} +**Get user by user name** + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | successful operation | **application/json**: [User](#user)
**application/xml**: [User](#user)
| +| 400 | Invalid username supplied | | +| 404 | User not found | | + +### [PUT] /user/{username} +**Update user** + +This can only be done by the logged in user. + +#### Responses + +| Code | Description | +| ---- | ----------- | +| default | successful operation | + +### [DELETE] /user/{username} +**Delete user** + +This can only be done by the logged in user. + +#### Responses + +| Code | Description | +| ---- | ----------- | +| 400 | Invalid username supplied | +| 404 | User not found | + +--- +### Models + +#### Order + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| id | long | *Example:* `10` | No | +| petId | long | *Example:* `198772` | No | +| quantity | integer | *Example:* `7` | No | +| shipDate | dateTime | | No | +| status | string | Order Status
*Enum:* `"placed"`, `"approved"`, `"delivered"`
*Example:* `"approved"` | No | +| complete | boolean | | No | + +#### Customer + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| id | long | *Example:* `100000` | No | +| username | string | *Example:* `"fehguy"` | No | +| address | [ [Address](#address) ] | | No | + +#### Address + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| street | string | *Example:* `"437 Lytton"` | No | +| city | string | *Example:* `"Palo Alto"` | No | +| state | string | *Example:* `"CA"` | No | +| zip | string | *Example:* `"94301"` | No | + +#### Category + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| id | long | *Example:* `1` | No | +| name | string | *Example:* `"Dogs"` | No | + +#### User + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| id | long | *Example:* `10` | No | +| username | string | *Example:* `"theUser"` | No | +| firstName | string | *Example:* `"John"` | No | +| lastName | string | *Example:* `"James"` | No | +| email | string | *Example:* `"john@email.com"` | No | +| password | string | *Example:* `"12345"` | No | +| phone | string | *Example:* `"12345"` | No | +| userStatus | integer | User Status
*Example:* `1` | No | + +#### Tag + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| id | long | | No | +| name | string | | No | + +#### Pet + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| id | long | *Example:* `10` | No | +| name | string | *Example:* `"doggie"` | Yes | +| category | [Category](#category) | | No | +| photoUrls | [ string ] | | Yes | +| tags | [ [Tag](#tag) ] | | No | +| status | string | pet status in the store
*Enum:* `"available"`, `"pending"`, `"sold"` | No | + +#### ApiResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| code | integer | | No | +| type | string | | No | +| message | string | | No | diff --git a/examples/v3/petstore.yaml b/examples/v3/petstore.yaml new file mode 100644 index 0000000..6d7d1b4 --- /dev/null +++ b/examples/v3/petstore.yaml @@ -0,0 +1,819 @@ +openapi: 3.0.3 +info: + title: Swagger Petstore - OpenAPI 3.0 + description: |- + This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about + Swagger at [https://swagger.io](https://swagger.io). In the third iteration of the pet store, we've switched to the design first approach! + You can now help us improve the API whether it's by making changes to the definition itself or to the code. + That way, with time, we can improve the API in general, and expose some of the new features in OAS3. + + Some useful links: + - [The Pet Store repository](https://github.com/swagger-api/swagger-petstore) + - [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml) + + termsOfService: http://swagger.io/terms/ + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: http://www.apache.org/licenses/LICENSE-2.0.html + version: 1.0.11 +externalDocs: + description: Find out more about Swagger + url: http://swagger.io +servers: + - url: https://petstore3.swagger.io/api/v3 +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: http://swagger.io + - name: store + description: Access to Petstore orders + externalDocs: + description: Find out more about our store + url: http://swagger.io + - name: user + description: Operations about user +paths: + /pet: + put: + tags: + - pet + summary: Update an existing pet + description: Update an existing pet by Id + operationId: updatePet + requestBody: + description: Update an existent pet in the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Pet' + required: true + 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 + '422': + description: Validation exception + security: + - petstore_auth: + - write:pets + - read:pets + post: + tags: + - pet + summary: Add a new pet to the store + description: Add a new pet to the store + operationId: addPet + requestBody: + description: Create a new pet in the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Pet' + required: true + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid input + '422': + description: Validation exception + security: + - petstore_auth: + - write:pets + - read:pets + /pet/findByStatus: + get: + tags: + - pet + 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: false + explode: true + schema: + type: string + default: available + enum: + - available + - pending + - sold + 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' + links: + address: + # the target link operationId + operationId: getUserAddress + description: Address link description + parameters: + # get the `id` field from the request path parameter named `id` + userId: $request.path.id + entityId: $request.entity.id + requestBody: RequestBody + server: + url: https://development.gigantic-server.com/v1 + description: Development server + '400': + description: Invalid status value + security: + - petstore_auth: + - write:pets + - read:pets + /pet/findByTags: + get: + tags: + - pet + deprecated: true + summary: Finds Pets by tags + description: Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. + operationId: findPetsByTags + parameters: + - name: tags + in: query + description: Tags to filter by + required: false + explode: true + 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 + /pet/{petId}: + 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 + 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: [] + - petstore_auth: + - write:pets + - read:pets + 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 + - name: name + in: query + description: Name of pet that needs to be updated + schema: + type: string + - name: status + in: query + description: Status of pet that needs to be updated + schema: + type: string + responses: + '400': + description: Invalid input + security: + - petstore_auth: + - write:pets + - read:pets + delete: + tags: + - pet + summary: Deletes a pet + description: delete a pet + operationId: deletePet + parameters: + - name: api_key + in: header + description: '' + required: false + schema: + type: string + - 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 + /pet/{petId}/uploadImage: + post: + tags: + - pet + 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 + - name: additionalMetadata + in: query + description: Additional Metadata + required: false + schema: + type: string + requestBody: + content: + application/octet-stream: + schema: + type: string + format: binary + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + 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 + additionalProperties: + type: integer + format: int32 + security: + - api_key: [] + /store/order: + post: + tags: + - store + summary: Place an order for a pet + description: Place a new order in the store + operationId: placeOrder + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + application/xml: + schema: + $ref: '#/components/schemas/Order' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Order' + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + '400': + description: Invalid input + '422': + description: Validation exception + /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 generate exceptions. + operationId: getOrderById + parameters: + - name: orderId + in: path + description: ID of order that needs to be fetched + required: true + schema: + type: integer + format: int64 + 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: integer + format: int64 + responses: + '400': + description: Invalid ID supplied + '404': + description: Order not found + /user: + post: + tags: + - user + summary: Create user + description: This can only be done by the logged in user. + operationId: createUser + requestBody: + description: Created user object + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/User' + responses: + default: + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + /user/createWithList: + post: + tags: + - user + summary: Creates list of users with given input array + description: Creates list of users with given input array + operationId: createUsersWithListInput + requestBody: + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + default: + description: successful operation + /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: false + schema: + type: string + - name: password + in: query + description: The password for login in clear text + required: false + 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/xml: + schema: + type: string + application/json: + schema: + type: string + '400': + description: Invalid username/password supplied + /user/logout: + get: + tags: + - user + summary: Logs out current logged in user session + description: '' + operationId: logoutUser + parameters: [] + responses: + default: + description: successful operation + /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: Update 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 + requestBody: + description: Update an existent user in the store + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/User' + responses: + default: + description: successful operation + 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 +components: + schemas: + Order: + type: object + properties: + id: + type: integer + format: int64 + example: 10 + petId: + type: integer + format: int64 + example: 198772 + quantity: + type: integer + format: int32 + example: 7 + shipDate: + type: string + format: date-time + status: + type: string + description: Order Status + example: approved + enum: + - placed + - approved + - delivered + complete: + type: boolean + xml: + name: order + Customer: + type: object + properties: + id: + type: integer + format: int64 + example: 100000 + username: + type: string + example: fehguy + address: + type: array + xml: + name: addresses + wrapped: true + items: + $ref: '#/components/schemas/Address' + xml: + name: customer + Address: + type: object + properties: + street: + type: string + example: 437 Lytton + city: + type: string + example: Palo Alto + state: + type: string + example: CA + zip: + type: string + example: '94301' + xml: + name: address + Category: + type: object + properties: + id: + type: integer + format: int64 + example: 1 + name: + type: string + example: Dogs + xml: + name: category + User: + type: object + properties: + id: + type: integer + format: int64 + example: 10 + username: + type: string + example: theUser + firstName: + type: string + example: John + lastName: + type: string + example: James + email: + type: string + example: john@email.com + password: + type: string + example: '12345' + phone: + type: string + example: '12345' + userStatus: + type: integer + description: User Status + format: int32 + example: 1 + xml: + name: user + Tag: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: tag + Pet: + required: + - name + - photoUrls + type: object + properties: + id: + type: integer + format: int64 + example: 10 + name: + type: string + example: doggie + category: + $ref: '#/components/schemas/Category' + photoUrls: + type: array + xml: + wrapped: true + items: + type: string + xml: + name: photoUrl + tags: + type: array + xml: + wrapped: true + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + xml: + name: pet + ApiResponse: + type: object + properties: + code: + type: integer + format: int32 + type: + type: string + message: + type: string + xml: + name: '##default' + requestBodies: + Pet: + description: Pet object that needs to be added to the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + UserArray: + description: List of user object + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: https://petstore3.swagger.io/oauth/authorize + scopes: + write:pets: modify pets in your account + read:pets: read your pets + api_key: + type: apiKey + name: api_key + in: header diff --git a/scripts/make-examples.ts b/scripts/make-examples.ts index 9fa521a..eced9e7 100644 --- a/scripts/make-examples.ts +++ b/scripts/make-examples.ts @@ -1,19 +1,25 @@ import path from 'path'; import fs from 'fs'; -import { execSync } from 'child_process'; +import { execFileSync } from 'child_process'; -const directory = path.join(__dirname, '..', 'examples'); +const examplesDirectory = path.join(__dirname, '..', 'examples'); -fs.readdir(directory, (err, files) => { - if (err) { - console.error(`Unable to read directory: ${err}`); - return; - } +// Read all subdirectories +const subdirectories = fs.readdirSync(examplesDirectory, { withFileTypes: true }) + .filter((dirent) => dirent.isDirectory()) + .map((dirent) => dirent.name); + +subdirectories.forEach((subdirectory) => { + const subdirectoryPath = path.join(examplesDirectory, subdirectory); + + // Read all files in the subdirectory + const files = fs.readdirSync(subdirectoryPath); files.forEach((filename) => { if (!filename.match(/\.yaml$/)) return; - console.log(`Processing ${filename}`); + + console.log(`Processing ${filename} in ${subdirectory}`); try { - execSync(`node dist/swagger-markdown.js -i examples/${filename}`); + execFileSync('node', ['dist/swagger-markdown.js', '-i', `examples/${subdirectory}/${filename}`]); console.log('Done\n'); } catch (e) { console.log(e); diff --git a/src/index.ts b/src/index.ts index b8101c8..c6c1e2f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,10 +2,11 @@ import SwaggerParser from '@apidevtools/swagger-parser'; import fs from 'fs'; import markdownlint from 'markdownlint'; import markdownlintRuleHelpers from 'markdownlint-rule-helpers'; -import { OpenAPIV2 } from 'openapi-types'; +import { OpenAPIV2, OpenAPIV3 } from 'openapi-types'; import { AllSwaggerDocumentVersions, Options } from './types'; import { isV2Document, isV31Document, isV3Document } from './lib/detectDocumentVersion'; import { transformSwaggerV2 } from './transformers/documentV2'; +import { transformSwaggerV3 } from './transformers/documentV3'; // eslint-disable-next-line @typescript-eslint/no-var-requires const markdownlintConfig = require('../.markdownlint.json'); @@ -30,7 +31,11 @@ export function partiallyDereference( const [key, value] = entries[i]; if (Array.isArray(value)) { obj[key] = value.map((item) => partiallyDereference(item, $refs)); - } else if (key === '$ref' && !value.startsWith('#/definitions/')) { + } else if ( + key === '$ref' + && !value.startsWith('#/definitions/') // V2 + && !value.startsWith('#/components/') // V3 + ) { return partiallyDereference($refs.get(value), $refs); } else { obj[key] = partiallyDereference(value, $refs); @@ -53,10 +58,10 @@ export function transfromSwagger(inputDoc: AllSwaggerDocumentVersions, options: if (isV2Document(inputDoc) || options.forceVersion === '2') { // Quick hack to allow version 3 to be processed as it version 2 - // Will be removed as soon as support of version 3 will be in place + // Will be removed as soon as support of version 3 will be in place` plainDocument = transformSwaggerV2(inputDoc as OpenAPIV2.Document, options); - } else if (isV3Document(inputDoc)) { - throw new Error('OpenAPI V3 is not yet supported'); + } else if (isV3Document(inputDoc) || options.forceVersion === '3') { + plainDocument = transformSwaggerV3(inputDoc as OpenAPIV3.Document, options); } else if (isV31Document(inputDoc)) { throw new Error('OpenAPI V3.1 is not yet supported'); } else { diff --git a/src/lib/markdown/mdstring.spec.ts b/src/lib/markdown/mdstring.spec.ts index efd1a77..732f59d 100644 --- a/src/lib/markdown/mdstring.spec.ts +++ b/src/lib/markdown/mdstring.spec.ts @@ -140,4 +140,37 @@ describe('MDstring', () => { const s2 = MDstring.string(''); expect(s2.concat(b).concat(s).get()).to.be.equal(`${b}${a}${b}`); }); + describe('codeBlock', () => { + it('should wrap the existing string in a code block without language', () => { + const mdString = new MDstring('console.log("Hello, World!");'); + mdString.codeBlock(); + expect(mdString.get()).to.be.equal('```\nconsole.log("Hello, World!");\n```'); + }); + + it('should wrap the existing string in a code block with specified language', () => { + const mdString = new MDstring('console.log("Hello, World!");'); + mdString.codeBlock('javascript'); + expect(mdString.get()).to.be.equal('```javascript\nconsole.log("Hello, World!");\n```'); + }); + + it('should not modify an empty string', () => { + const mdString = new MDstring(); + mdString.codeBlock('python'); + expect(mdString.get()).to.be.equal(''); + }); + + it('should handle multi-line code blocks', () => { + const mdString = new MDstring('const x = 1;\nconst y = 2;\nconsole.log(x + y);'); + mdString.codeBlock('javascript'); + expect(mdString.get()).to.be.equal( + '```javascript\nconst x = 1;\nconst y = 2;\nconsole.log(x + y);\n```', + ); + }); + + it('should return the MDstring instance for method chaining', () => { + const mdString = new MDstring('Hello, World!'); + const result = mdString.codeBlock(); + expect(result).to.be.deep.equal(mdString); + }); + }); }); diff --git a/src/lib/markdown/mdstring.ts b/src/lib/markdown/mdstring.ts index 99512ff..9e73b6f 100644 --- a/src/lib/markdown/mdstring.ts +++ b/src/lib/markdown/mdstring.ts @@ -131,6 +131,17 @@ export class MDstring { return this; } + public codeBlock(language = ''): MDstring { + if (this._string !== '') { + this.set( + `\`\`\`${language} +${this._string} +\`\`\``, + ); + } + return this; + } + public escape(): MDstring { this.set(`${textEscape(this._string)}`); return this; diff --git a/src/transformers/v2/models/Tags.spec.ts b/src/transformers/common/Tags.spec.ts similarity index 89% rename from src/transformers/v2/models/Tags.spec.ts rename to src/transformers/common/Tags.spec.ts index 09b370a..ea80694 100644 --- a/src/transformers/v2/models/Tags.spec.ts +++ b/src/transformers/common/Tags.spec.ts @@ -9,6 +9,10 @@ const tags = [{ name: 'Tag1', description: 'Some description1', externalDocs: 'http://127.0.0.1', +}, { + name: 'Tag@#!', + description: 'Some special tag', + externalDocs: 'http://127.0.0.1', }]; describe('Tags collection', () => { diff --git a/src/transformers/common/Tags.ts b/src/transformers/common/Tags.ts new file mode 100644 index 0000000..3faa0ce --- /dev/null +++ b/src/transformers/common/Tags.ts @@ -0,0 +1,26 @@ +import { OpenAPIV2, OpenAPIV3 } from 'openapi-types'; + +type AnyTagObject = OpenAPIV2.TagObject | OpenAPIV3.TagObject; + +export class TagsCollection { + private tags: { [key: string]: T } = {}; + + public get length(): number { + return Object.keys(this.tags).length; + } + + public tag(tag: T): TagsCollection { + if (!('name' in tag)) { + throw new Error('Tag must have name property'); + } + this.tags[tag.name] = tag; + return this; + } + + public getTag(name: string): T | null { + if (name in this.tags) { + return this.tags[name]; + } + return null; + } +} diff --git a/src/transformers/v2/contact.spec.ts b/src/transformers/common/v2-3/contact.spec.ts similarity index 100% rename from src/transformers/v2/contact.spec.ts rename to src/transformers/common/v2-3/contact.spec.ts diff --git a/src/transformers/v2/contact.ts b/src/transformers/common/v2-3/contact.ts similarity index 67% rename from src/transformers/v2/contact.ts rename to src/transformers/common/v2-3/contact.ts index c78b787..154dac6 100644 --- a/src/transformers/v2/contact.ts +++ b/src/transformers/common/v2-3/contact.ts @@ -1,11 +1,12 @@ -import { OpenAPIV2 } from 'openapi-types'; -import { Markdown } from '../../lib/markdown'; +import { OpenAPIV2, OpenAPIV3 } from 'openapi-types'; +import { Markdown } from '../../../lib/markdown'; /** - * http://swagger.io/specification/#contactObject - * Contact info transformer + * @todo: add extensions, e.g. ^x- */ -export function transformContact(contact: OpenAPIV2.ContactObject) { +export function transformContact( + contact: OpenAPIV2.ContactObject | OpenAPIV3.ContactObject, +) { const md = Markdown.md(); if (Object.keys(contact).some( diff --git a/src/transformers/v2/externalDocs.spec.ts b/src/transformers/common/v2-3/externalDocs.spec.ts similarity index 100% rename from src/transformers/v2/externalDocs.spec.ts rename to src/transformers/common/v2-3/externalDocs.spec.ts diff --git a/src/transformers/common/v2-3/externalDocs.ts b/src/transformers/common/v2-3/externalDocs.ts new file mode 100644 index 0000000..12fc23d --- /dev/null +++ b/src/transformers/common/v2-3/externalDocs.ts @@ -0,0 +1,20 @@ +import { OpenAPIV2, OpenAPIV3 } from 'openapi-types'; +import { Markdown } from '../../../lib/markdown'; + +const DEFAULT_TEXT = 'Find more info here'; + +/** + * @todo: add extensions, e.g. ^x- + */ +export function transformExternalDocs( + externalDocs: OpenAPIV2.ExternalDocumentationObject | OpenAPIV3.ExternalDocumentationObject, +) { + const md = Markdown.md(); + if ('url' in externalDocs) { + md.line(md.string().link( + externalDocs.description || DEFAULT_TEXT, + externalDocs.url, + )); + } + return md.export(); +} diff --git a/src/transformers/v2/groupPathsByTags.spec.ts b/src/transformers/common/v2-3/groupPathsByTags.spec.ts similarity index 100% rename from src/transformers/v2/groupPathsByTags.spec.ts rename to src/transformers/common/v2-3/groupPathsByTags.spec.ts diff --git a/src/transformers/v2/groupPathsByTags.ts b/src/transformers/common/v2-3/groupPathsByTags.ts similarity index 53% rename from src/transformers/v2/groupPathsByTags.ts rename to src/transformers/common/v2-3/groupPathsByTags.ts index 3c509e0..c51e62f 100644 --- a/src/transformers/v2/groupPathsByTags.ts +++ b/src/transformers/common/v2-3/groupPathsByTags.ts @@ -1,21 +1,26 @@ -import { OpenAPIV2 } from 'openapi-types'; -import { ALLOWED_METHODS } from '../../types'; +import { OpenAPIV2, OpenAPIV3 } from 'openapi-types'; +import { ALLOWED_METHODS_V2, ALLOWED_METHODS_V3 } from '../../../types'; -type Tagged = { [tag: string]: OpenAPIV2.PathsObject }; +type AnyPathObject = OpenAPIV2.PathsObject | OpenAPIV3.PathsObject; + +type Tagged = { [tag: string]: T }; /** * Group path and methods by tags they have */ -export function groupPathsByTags( - inputDoc: OpenAPIV2.PathsObject, -): Tagged { - const tagged: Tagged = {}; +export function groupPathsByTags( + inputDoc: T, + allowedMethods: typeof ALLOWED_METHODS_V2 | typeof ALLOWED_METHODS_V3, +): Tagged { + const tagged: Tagged = {}; Object.keys(inputDoc).forEach((path: string) => { const data = inputDoc[path]; Object.keys(data).forEach((method) => { - if (ALLOWED_METHODS.includes(method)) { - const pathMethod: OpenAPIV2.OperationObject = data[method]; + if (allowedMethods.includes(method)) { + const pathMethod: T extends OpenAPIV2.PathsObject + ? OpenAPIV2.OperationObject + : OpenAPIV3.OperationObject = data[method]; const tags = pathMethod.tags || ['']; tags.forEach((tagName) => { if (!tagged[tagName]) { diff --git a/src/transformers/v2/headers.spec.ts b/src/transformers/common/v2-3/headers.spec.ts similarity index 100% rename from src/transformers/v2/headers.spec.ts rename to src/transformers/common/v2-3/headers.spec.ts diff --git a/src/transformers/v2/headers.ts b/src/transformers/common/v2-3/headers.ts similarity index 76% rename from src/transformers/v2/headers.ts rename to src/transformers/common/v2-3/headers.ts index 798f081..13a2887 100644 --- a/src/transformers/v2/headers.ts +++ b/src/transformers/common/v2-3/headers.ts @@ -1,6 +1,6 @@ -import { OpenAPIV2 } from 'openapi-types'; -import { Markdown } from '../../lib/markdown'; -import { MDstring } from '../../lib/markdown/mdstring'; +import { OpenAPIV2, OpenAPIV3 } from 'openapi-types'; +import { Markdown } from '../../../lib/markdown'; +import { MDstring } from '../../../lib/markdown/mdstring'; /** * Make it a little bit simpler @@ -10,7 +10,9 @@ import { MDstring } from '../../lib/markdown/mdstring'; * @param {OpenAPIV2.HeadersObject} headers * @return {*} {MDstring} */ -export function transformHeaders(headers: OpenAPIV2.HeadersObject): MDstring { +export function transformHeaders( + headers: OpenAPIV2.HeadersObject | { [index: string]: OpenAPIV3.HeaderObject }, +): MDstring { const md = Markdown.md(); let string: MDstring; const subString = md.string(); diff --git a/src/transformers/v2/info.spec.ts b/src/transformers/common/v2-3/info.spec.ts similarity index 100% rename from src/transformers/v2/info.spec.ts rename to src/transformers/common/v2-3/info.spec.ts diff --git a/src/transformers/v2/info.ts b/src/transformers/common/v2-3/info.ts similarity index 85% rename from src/transformers/v2/info.ts rename to src/transformers/common/v2-3/info.ts index 40b0d6d..ea56b16 100644 --- a/src/transformers/v2/info.ts +++ b/src/transformers/common/v2-3/info.ts @@ -1,7 +1,7 @@ -import { OpenAPIV2 } from 'openapi-types'; +import { OpenAPIV2, OpenAPIV3 } from 'openapi-types'; import { transformContact } from './contact'; import { transformLicense } from './license'; -import { Markdown } from '../../lib/markdown'; +import { Markdown } from '../../../lib/markdown'; /** * http://swagger.io/specification/#infoObject @@ -10,7 +10,7 @@ import { Markdown } from '../../lib/markdown'; * @param {Object} info * @returns {String} */ -export function transformInfo(info: OpenAPIV2.InfoObject) { +export function transformInfo(info: OpenAPIV2.InfoObject | OpenAPIV3.InfoObject) { const md = Markdown.md(); if (info !== null && typeof info === 'object') { if ('title' in info) { diff --git a/src/transformers/v2/license.spec.ts b/src/transformers/common/v2-3/license.spec.ts similarity index 100% rename from src/transformers/v2/license.spec.ts rename to src/transformers/common/v2-3/license.spec.ts diff --git a/src/transformers/v2/license.ts b/src/transformers/common/v2-3/license.ts similarity index 54% rename from src/transformers/v2/license.ts rename to src/transformers/common/v2-3/license.ts index c7039e9..56fc55c 100644 --- a/src/transformers/v2/license.ts +++ b/src/transformers/common/v2-3/license.ts @@ -1,16 +1,18 @@ -import { OpenAPIV2 } from 'openapi-types'; -import { Markdown } from '../../lib/markdown'; +import { OpenAPIV2, OpenAPIV3 } from 'openapi-types'; +import { Markdown } from '../../../lib/markdown'; +import { MDstring } from '../../../lib/markdown/mdstring'; /** - * http://swagger.io/specification/#licenseObject - * License object transformer + * @todo: add extensions, e.g. ^x- */ -export function transformLicense(license: OpenAPIV2.LicenseObject): string { +export function transformLicense( + license: OpenAPIV2.LicenseObject | OpenAPIV3.LicenseObject, +): string { const md = Markdown.md(); if ('url' in license || 'name' in license) { const licenseDocument = md.string('License:').bold(); - let url; + let url: MDstring; if ('url' in license && 'name' in license) { url = md.string().link(license.name, license.url); } else { diff --git a/src/transformers/v2/security.spec.ts b/src/transformers/common/v2-3/security.spec.ts similarity index 100% rename from src/transformers/v2/security.spec.ts rename to src/transformers/common/v2-3/security.spec.ts diff --git a/src/transformers/common/v2-3/security.ts b/src/transformers/common/v2-3/security.ts new file mode 100644 index 0000000..4cddca5 --- /dev/null +++ b/src/transformers/common/v2-3/security.ts @@ -0,0 +1,51 @@ +import { OpenAPIV2, OpenAPIV3 } from 'openapi-types'; +import { Markdown } from '../../../lib/markdown'; + +export function transformSecurity( + security: OpenAPIV2.SecurityRequirementObject[] | OpenAPIV3.SecurityRequirementObject[], +) { + const md = Markdown.md(); + md.line(md.string('Security').h5()).line(); + let hasRules = false; + + let maxLength = 0; + security.forEach((rules) => { + Object.keys(rules).forEach((key) => { + maxLength = rules[key].length > maxLength ? rules[key].length : maxLength; + }); + }); + maxLength++; + if (maxLength < 2) { + maxLength = 2; + } + + const table = md.table(); + + table.th('Security Schema') + .th('Scopes'); + + for (let i = 0; i < maxLength - 2; i++) { + table.th(''); + } + + security.forEach( + (rules: OpenAPIV2.SecurityRequirementObject | OpenAPIV3.SecurityRequirementObject) => { + const tr = table.tr(); + Object.keys(rules).forEach((key) => { + hasRules = true; + const line = [key].concat(rules[key]); + while (line.length < maxLength) { + line.push(''); + } + line.forEach((l) => tr.td(l)); + }); + }, + ); + + md.line(table); + + if (hasRules) { + return md.export(); + } + return ''; +} diff --git a/src/transformers/v2/tag.spec.ts b/src/transformers/common/v2-3/tag.spec.ts similarity index 100% rename from src/transformers/v2/tag.spec.ts rename to src/transformers/common/v2-3/tag.spec.ts diff --git a/src/transformers/v2/tag.ts b/src/transformers/common/v2-3/tag.ts similarity index 81% rename from src/transformers/v2/tag.ts rename to src/transformers/common/v2-3/tag.ts index 73eedd1..49c4209 100644 --- a/src/transformers/v2/tag.ts +++ b/src/transformers/common/v2-3/tag.ts @@ -1,7 +1,10 @@ -import { OpenAPIV2 } from 'openapi-types'; -import { Markdown } from '../../lib/markdown'; +import { OpenAPIV2, OpenAPIV3 } from 'openapi-types'; +import { Markdown } from '../../../lib/markdown'; -export function transformTag(tag: OpenAPIV2.TagObject | '') { +/** + * @todo: add extensions, e.g. ^x- + */ +export function transformTag(tag: OpenAPIV2.TagObject | OpenAPIV3.TagObject | '') { const md = Markdown.md(); // If tag is just en empty line, then this means diff --git a/src/transformers/documentV2.ts b/src/transformers/documentV2.ts index 5c1c003..c5c5ffc 100644 --- a/src/transformers/documentV2.ts +++ b/src/transformers/documentV2.ts @@ -1,14 +1,14 @@ import { OpenAPIV2 } from 'openapi-types'; -import { Options } from '../types'; -import { transformInfo } from './v2/info'; +import { ALLOWED_METHODS_V2, Options } from '../types'; +import { transformInfo } from './common/v2-3/info'; import { transformPath } from './v2/path'; import { transformSecurityDefinitions } from './v2/securityDefinitions'; -import { transformExternalDocs } from './v2/externalDocs'; +import { transformExternalDocs } from './common/v2-3/externalDocs'; import { transformDefinition } from './v2/definitions'; -import { TagsCollection } from './v2/models/Tags'; +import { TagsCollection } from './common/Tags'; import { Markdown } from '../lib/markdown'; -import { groupPathsByTags } from './v2/groupPathsByTags'; -import { transformTag } from './v2/tag'; +import { groupPathsByTags } from './common/v2-3/groupPathsByTags'; +import { transformTag } from './common/v2-3/tag'; import { transformSchemes } from './v2/schemes'; export function transformSwaggerV2( @@ -58,7 +58,10 @@ export function transformSwaggerV2( // Process Paths if ('paths' in inputDoc) { // Group paths by tag name - const tagged = groupPathsByTags(inputDoc.paths); + const tagged = groupPathsByTags( + inputDoc.paths, + ALLOWED_METHODS_V2, + ); Object.keys(tagged).forEach((tagName) => { md.line(md.string().horizontalRule()); diff --git a/src/transformers/documentV3.ts b/src/transformers/documentV3.ts new file mode 100644 index 0000000..c9a4082 --- /dev/null +++ b/src/transformers/documentV3.ts @@ -0,0 +1,80 @@ +import { OpenAPIV3 } from 'openapi-types'; +import { ALLOWED_METHODS_V3, Options } from '../types'; +import { transformInfo } from './common/v2-3/info'; +import { transformPath } from './v3/path'; +import { Markdown } from '../lib/markdown'; +import { TagsCollection } from './common/Tags'; +import { transformExternalDocs } from './common/v2-3/externalDocs'; +import { transformTag } from './common/v2-3/tag'; +import { groupPathsByTags } from './common/v2-3/groupPathsByTags'; +import { transformDefinition } from './v2/definitions'; + +export function transformSwaggerV3( + inputDoc: OpenAPIV3.Document, + options: Options, +): string { + const md = Markdown.md(); + + // Skip servers + // those are used for the mock server and won't be rendered + + // Security and Responses are supposed to be dereferenced (?) + // and shall not be present in the root namespace + + // Process info + if (!options.skipInfo && 'info' in inputDoc) { + md.line(transformInfo(inputDoc.info)); + } + + // Collect tags + const tagsCollection = new TagsCollection(); + if ('tags' in inputDoc) { + inputDoc.tags.forEach((tag) => { + tagsCollection.tag(tag); + }); + } + + if ('externalDocs' in inputDoc) { + md.line(transformExternalDocs(inputDoc.externalDocs)); + } + + // Security definitions aren't presented in v3 + + // All components must be dereferenced + + // Process Paths + if ('paths' in inputDoc) { + // Group paths by tag name + const tagged = groupPathsByTags(inputDoc.paths, ALLOWED_METHODS_V3); + + Object.keys(tagged).forEach((tagName) => { + md.line(md.string().horizontalRule()); + if (tagsCollection.length) { + // Display Tag + const tagObject = tagsCollection.getTag(tagName) || ''; + md.line(transformTag(tagObject)); + } + const pathsUnderTag = tagged[tagName]; + Object.keys(pathsUnderTag).forEach((path: string) => md.line( + transformPath( + path, + inputDoc.paths[path] as unknown, + ), + )); + }); + } + + // Models (components) + if ('components' in inputDoc) { + md.line(md.string().horizontalRule()); + md.line( + // @todo: move transform definition to the v3 folder and refactor to respect v3 only + transformDefinition( + inputDoc.components.schemas as never, + ), + ); + } + + // Glue all pieces down + return md.export(); +} diff --git a/src/transformers/v2/externalDocs.ts b/src/transformers/v2/externalDocs.ts deleted file mode 100644 index 8703e09..0000000 --- a/src/transformers/v2/externalDocs.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { OpenAPIV2 } from 'openapi-types'; -import { Markdown } from '../../lib/markdown'; - -const DEFAULT_TEXT = 'Find more info here'; - -/** - * https://swagger.io/specification/v2/#externalDocumentationObject - */ -export function transformExternalDocs(externalDocs: OpenAPIV2.ExternalDocumentationObject) { - const md = Markdown.md(); - if ('url' in externalDocs) { - md.line(md.string().link( - externalDocs.description || DEFAULT_TEXT, - externalDocs.url, - )); - } - return md.export(); -} diff --git a/src/transformers/v2/models/Tags.ts b/src/transformers/v2/models/Tags.ts deleted file mode 100644 index 6fa10bc..0000000 --- a/src/transformers/v2/models/Tags.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { OpenAPIV2 } from 'openapi-types'; - -export class TagsCollection { - private tags: { [key: string]: OpenAPIV2.TagObject } = {}; - - public get length(): number { - return Object.keys(this.tags).length; - } - - public tag(tag: OpenAPIV2.TagObject): TagsCollection { - if (!('name' in tag)) { - throw new Error('Tag must have name property'); - } - this.tags[tag.name] = tag; - return this; - } - - public getTag(name: string): OpenAPIV2.TagObject | null { - if (name in this.tags) { - return this.tags[name]; - } - return null; - } -} diff --git a/src/transformers/v2/path.ts b/src/transformers/v2/path.ts index e9d050e..444c050 100644 --- a/src/transformers/v2/path.ts +++ b/src/transformers/v2/path.ts @@ -1,10 +1,10 @@ import { OpenAPIV2 } from 'openapi-types'; import { transformResponses } from './pathResponses'; import { transformParameters } from './pathParameters'; -import { transformSecurity } from './security'; +import { transformSecurity } from '../common/v2-3/security'; import { Markdown } from '../../lib/markdown'; -import { ALLOWED_METHODS } from '../../types'; -import { transformExternalDocs } from './externalDocs'; +import { ALLOWED_METHODS_V2 } from '../../types'; +import { transformExternalDocs } from '../common/v2-3/externalDocs'; import { transformSchemes } from './schemes'; /** @@ -35,7 +35,7 @@ export function transformPath( // Go further method by methods Object.keys(data).forEach((method) => { - if (ALLOWED_METHODS.includes(method)) { + if (ALLOWED_METHODS_V2.includes(method)) { md.line(''); // Set method as a subheader md.line(md.string(method.toUpperCase()).h4()); diff --git a/src/transformers/v2/pathResponses.ts b/src/transformers/v2/pathResponses.ts index c96fb77..bc2a62b 100644 --- a/src/transformers/v2/pathResponses.ts +++ b/src/transformers/v2/pathResponses.ts @@ -2,7 +2,7 @@ import { OpenAPIV2 } from 'openapi-types'; import { Schema } from './models/Schema'; import { dataTypeResolver } from './dataTypes'; import { Markdown } from '../../lib/markdown'; -import { transformHeaders } from './headers'; +import { transformHeaders } from '../common/v2-3/headers'; /** * Build responses table @@ -15,9 +15,8 @@ export function transformResponses(responses: OpenAPIV2.ResponsesObject) { .line(); // Check if schema somewhere - const schemas = Object.keys(responses).reduce( - (acc, response) => acc || 'schema' in responses[response], - false, + const schemas = Object.values(responses).some( + (response) => 'schema' in response, ); const table = md.table(); diff --git a/src/transformers/v2/security.ts b/src/transformers/v2/security.ts deleted file mode 100644 index d8ea95d..0000000 --- a/src/transformers/v2/security.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { OpenAPIV2 } from 'openapi-types'; -import { Markdown } from '../../lib/markdown'; - -export function transformSecurity(security: OpenAPIV2.SecurityRequirementObject[]) { - const md = Markdown.md(); - md.line(md.string('Security').h5()).line(); - let hasRules = false; - - let maxLength = 0; - security.forEach((rules) => { - Object.keys(rules).forEach((key) => { - maxLength = rules[key].length > maxLength ? rules[key].length : maxLength; - }); - }); - maxLength++; - if (maxLength < 2) { - maxLength = 2; - } - - const table = md.table(); - - table.th('Security Schema') - .th('Scopes'); - - for (let i = 0; i < maxLength - 2; i++) { - table.th(''); - } - - security.forEach((rules: OpenAPIV2.SecurityRequirementObject) => { - const tr = table.tr(); - Object.keys(rules).forEach((key) => { - hasRules = true; - const line = [key].concat(rules[key]); - while (line.length < maxLength) { - line.push(''); - } - line.forEach((l) => tr.td(l)); - }); - }); - - md.line(table); - - if (hasRules) { - return md.export(); - } - return ''; -} diff --git a/src/transformers/v3/dataTypes.spec.ts b/src/transformers/v3/dataTypes.spec.ts new file mode 100644 index 0000000..0de426a --- /dev/null +++ b/src/transformers/v3/dataTypes.spec.ts @@ -0,0 +1,73 @@ +import { expect } from 'chai'; +import { dataTypeResolver } from './dataTypes'; +import { Schema } from './models/Schema'; +import { anchor } from '../../lib/anchor'; + +const fixture = [ + // References + [ + new Schema({ $ref: '#/definitions/ErrorModel' }), + `[ErrorModel](#${anchor('ErrorModel')})`, + ], + // Standard usecases + [new Schema({ type: 'integer', format: 'int32' }), 'integer'], + [new Schema({ type: 'integer' }), 'integer'], + [new Schema({ type: 'integer', format: 'int64' }), 'long'], + [new Schema({ type: 'number', format: 'float' }), 'float'], + [new Schema({ type: 'number', format: 'double' }), 'double'], + [new Schema({ type: 'number' }), 'number'], + [new Schema({ type: 'string' }), 'string'], + [new Schema({ type: 'string', format: 'byte' }), 'byte'], + [new Schema({ type: 'string', format: 'binary' }), 'binary'], + [new Schema({ type: 'boolean' }), 'boolean'], + [new Schema({ type: 'string', format: 'date' }), 'date'], + [new Schema({ type: 'string', format: 'date-time' }), 'dateTime'], + [new Schema({ type: 'string', format: 'password' }), 'password'], + [new Schema({ type: ['string', 'boolean'] }), 'string, boolean'], + [new Schema({ type: ['string', 'boolean'], format: 'truefalsy' }), 'string (truefalsy), boolean (truefalsy)'], + // Arrays + [ + new Schema({ + type: 'array', + items: { type: 'string' }, + }), '[ string ]', + ], [ + new Schema({ + type: 'array', + items: { $ref: '#/definitions/ErrorModel' }, + }), `[ [ErrorModel](#${anchor('ErrorModel')}) ]`], + // Weird usecases + [new Schema({ type: 'random', format: 'number' }), 'random (number)'], + [new Schema({ type: 'integer', format: 'int128' }), 'integer (int128)'], + [new Schema({ type: 'a', format: 'b' }), 'a (b)'], + // Empty schema + [new Schema(), ''], + [(new Schema()).setType(null as any).setFormat(null as any), ''], + // Objects + [new Schema({ + type: 'object', + properties: { + data: { + type: 'array', + items: { + type: 'object', + properties: { + notificationId: { + type: 'integer', + description: 'ID of the notification', + }, + }, + }, + }, + }, + // @todo: the schema according to the types is incorrect, although it is been seen in many places + } as any), '{ **"data"**: [ { **"notificationId"**: integer } ] }'], +]; + +describe('Data Types', () => { + it('should convert type and format to the common names', () => { + fixture.forEach((usecase) => { + expect(dataTypeResolver(usecase[0] as Schema)).to.be.equal(usecase[1]); + }); + }); +}); diff --git a/src/transformers/v3/dataTypes.ts b/src/transformers/v3/dataTypes.ts new file mode 100644 index 0000000..be4b635 --- /dev/null +++ b/src/transformers/v3/dataTypes.ts @@ -0,0 +1,106 @@ +import { anchor } from '../../lib/anchor'; +import { Markdown } from '../../lib/markdown'; +import { SchemaInterface } from './models/Schema'; + +const resolver = { + integer: { + int32: 'integer', + int64: 'long', + }, + number: { + float: 'float', + double: 'double', + }, + string: { + byte: 'byte', + binary: 'binary', + date: 'date', + 'date-time': 'dateTime', + password: 'password', + }, +}; + +export function resolveType(type: string, format: string): string | undefined { + if (type in resolver) { + if (format) { + return format in resolver[type] + ? resolver[type][format] + : `${type} (${format})`; + } + return type; + } + return undefined; +} + +/** + * Transform data types into common names + * @param {Schema} schema + * @return {String} + */ +export const dataTypeResolver = (schema: SchemaInterface): string => { + const md = Markdown.md(); + + const all = schema.getAllOf(); + if (all) { + return all.map((subSchema: SchemaInterface) => dataTypeResolver(subSchema)) + .filter((type) => type !== '') + .join(' & '); + } + + const reference = schema.getReference(); + if (reference) { + const name = reference.match(/\/([^/]*)$/i)[1]; + const link = anchor(name); + return md.string().link(name, `#${link}`).get(); + } + + // Cast it to the array + const schemaType = schema.getType(); + const types: string[] = Array.isArray(schemaType) ? schemaType : [schemaType]; + const format = schema.getFormat(); + + const resolveResults = types.map((type: string) => { + if (type in resolver) { + if (format) { + return format in resolver[type] + ? resolver[type][format] + : `${type} (${format})`; + } + return type; + } + + if (format) { + return `${type} (${format})`; + } + + if (type === 'array') { + const subType = dataTypeResolver(schema.getItems()); + return `[ ${subType} ]`; + } + + // If schema has properties, it means that type is object + // we can simply skip this check, furthemore some yaml files may just miss it + if (Object.keys(schema.properties).length > 0) { + const { properties } = schema; + const values = Object.values(properties).map((p) => dataTypeResolver(p)); + const keys = Object.keys(properties); + const pairs = []; + for (let i = 0; i < keys.length; i++) { + pairs.push( + md.string(`"${keys[i]}"`) + .bold() + .concat(`: ${values[i]}`).get(), + ); + } + return `{ ${pairs.join(', ')} }`; + } + + if (type) { + return md.string(type).get(); + } + + return md.string('').get(); + }).filter((s) => s.length > 0); // and filter out empty strings + + return md.string(resolveResults.join(', ')).get(); +}; diff --git a/src/transformers/v3/link.spec.ts b/src/transformers/v3/link.spec.ts new file mode 100644 index 0000000..fe84825 --- /dev/null +++ b/src/transformers/v3/link.spec.ts @@ -0,0 +1,66 @@ +import { expect } from 'chai'; +import { processLink } from './link'; + +describe('processLink', () => { + it('Should correctly format a simple string link', () => { + const linkName = 'TestLink'; + const linkValue = 'https://example.com'; + const result = processLink(linkName, linkValue); + expect(result.toString()).to.be.equal('**TestLink** https://example.com'); + }); + + it('Should handle a link object with only a description', () => { + const linkName = 'TestLink'; + const link = { + description: 'This is a test description', + }; + const result = processLink(linkName, link); + const expected = '**TestLink**
This is a test description'; + expect(result.toString()).to.be.equal(expected); + }); + + it('Should process a link object with parameters but no description', () => { + const linkName = 'TestLink'; + const link = { + parameters: { + param1: 'value1', + param2: 'value2', + }, + }; + const result = processLink(linkName, link); + const expected = '**TestLink**
Parameters {
"param1": "value1",
"param2": "value2"
}
'; + expect(result.toString()).to.be.equal(expected); + }); + + it('Should format a link object with both description and parameters', () => { + const linkName = 'ComplexLink'; + const link = { + description: 'A complex link with description and parameters', + parameters: { + param1: 'value1', + param2: 'value2', + }, + }; + const result = processLink(linkName, link); + const expected = '**ComplexLink**
' + + 'A complex link with description and parameters
' + + 'Parameters {
' + + '"param1": "value1",
"param2": "value2"
' + + '}
'; + expect(result.toString()).to.be.equal(expected); + }); + + it('Should handle a link object with an empty parameters object', () => { + const linkName = 'EmptyParamsLink'; + const link = { + description: 'A link with empty parameters', + parameters: {}, + }; + const result = processLink(linkName, link); + const expected = '**EmptyParamsLink**
' + + 'A link with empty parameters
' + + 'Parameters {
' + + '}
'; + expect(result.toString()).to.be.equal(expected); + }); +}); diff --git a/src/transformers/v3/link.ts b/src/transformers/v3/link.ts new file mode 100644 index 0000000..ba27174 --- /dev/null +++ b/src/transformers/v3/link.ts @@ -0,0 +1,34 @@ +import { OpenAPIV3 } from 'openapi-types'; +import { Markdown } from '../../lib/markdown'; +import { MDstring } from '../../lib/markdown/mdstring'; + +export function processLink( + linkName: string, + link: OpenAPIV3.LinkObject | string, +): MDstring { + const md = Markdown.md(); + + if (typeof link === 'string') { + return md.string(linkName).bold().concat(' ').concat(link); + } + + const linkString = md.string(linkName).bold(); + + if ('description' in link) { + linkString.br(true).concat(link.description); + } + + if ('parameters' in link) { + linkString.br(true).concat('Parameters {').br(true); + const links = []; + Object.entries(link.parameters).forEach((parameter) => { + links.push(`"${parameter[0]}": "${parameter[1]}"`); + }); + if (links.length > 0) { + linkString.concat(links.join(',
')).br(true); + } + linkString.concat('}').br(true); + } + + return linkString; +} diff --git a/src/transformers/v3/models/Schema.ts b/src/transformers/v3/models/Schema.ts new file mode 100644 index 0000000..8e9acc3 --- /dev/null +++ b/src/transformers/v3/models/Schema.ts @@ -0,0 +1,154 @@ +import { OpenAPIV3 } from 'openapi-types'; + +export interface SchemaInterface { + type?: string; + format?: string; + ref?: string; + allOf?: SchemaInterface[]; + items?: SchemaInterface; + properties?: { [name: string]: SchemaInterface }; + getType?(): string | undefined; + getFormat?(): string | undefined; + getItems?(): SchemaInterface; + getReference(): string | undefined; + getAllOf?(): SchemaInterface[]; +} + +export class Schema implements SchemaInterface { + public type?: string; + + public format?: string; + + public ref?: string; + + public allOf?: SchemaInterface[]; + + public items?: SchemaInterface; + + public properties?: { [name: string]: SchemaInterface } = {}; + + /** + * constructor + * + * @param {Object} [schema=null] + */ + constructor(schema?: Partial) { + if (schema) { + if ('type' in schema) { + this.setType(schema.type); + } + if ('format' in schema) { + this.setFormat(schema.format); + } + if ('$ref' in schema) { + this.setReference(schema.$ref as string); + } + if ('items' in schema) { + this.setItems(schema.items); + } + if ('allOf' in schema) { + this.setAllOf(schema.allOf); + } + if ('properties' in schema) { + // At this point the document is dereferenced + // So we can avoid the reference here + this.setProperties(schema.properties); + } + } + } + + /** + * @param {String} ref + */ + public setReference(ref: string): Schema { + this.ref = ref; + return this; + } + + /** + * @return {String} + */ + public getReference(): string | undefined { + return this.ref; + } + + public setProperties(properties: { + [name: string]: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject + }): Schema { + Object.keys(properties).forEach( + (name) => { + this.properties[name] = new Schema(properties[name] as OpenAPIV3.SchemaObject); + }, + ); + return this; + } + + /** + * @param {String} type + */ + public setType(type: string | string[]): Schema { + // @todo: wtf + this.type = type as string; + return this; + } + + /** + * @param {Array} allOf + */ + public setAllOf(allOf: (OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject)[]): Schema { + this.allOf = allOf.map( + ( + schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject, + ) => new Schema(schema as OpenAPIV3.SchemaObject), + ); + return this; + } + + /** + * @param {String} format + */ + public setFormat(format: string): Schema { + this.format = format; + return this; + } + + /** + * @param {Object} items + */ + setItems(items) { + this.items = new Schema(items); + return this; + } + + /** + * @return {String} + */ + public getType(): string | undefined { + return this.type; + } + + /** + * @return {String} + */ + public getFormat(): string | undefined { + return this.format; + } + + /** + * @return {Object} + */ + public getItems(): SchemaInterface { + return this.items; + } + + /** + * @return {Array} + */ + public getAllOf(): SchemaInterface[] { + return this.allOf; + } + + public getProperties() { + return this.properties; + } +} diff --git a/src/transformers/v3/path.ts b/src/transformers/v3/path.ts new file mode 100644 index 0000000..63c975e --- /dev/null +++ b/src/transformers/v3/path.ts @@ -0,0 +1,110 @@ +// import { OpenAPIV2 } from 'openapi-types'; +// import { transformResponses } from './pathResponses'; +// import { transformParameters } from './pathParameters'; +// import { transformSecurity } from '../common/v2-3/security'; +// import { Markdown } from '../../lib/markdown'; +// import { ALLOWED_METHODS } from '../../types'; +// import { transformExternalDocs } from '../common/v2-3/externalDocs'; +// import { transformSchemes } from './schemes'; +import { OpenAPIV3 } from 'openapi-types'; +import { Markdown } from '../../lib/markdown'; +import { + ALLOWED_METHODS_V3, +} from '../../types'; +import { transformExternalDocs } from '../common/v2-3/externalDocs'; +import { transformSecurity } from '../common/v2-3/security'; +import { transformResponses } from './pathResponses'; + +export function transformPath( + path: string, + data: OpenAPIV3.PathItemObject, +): string | null { + // const pathParameters: OpenAPIV3.ParameterObject = null; + + if (!path || !data) { + return null; + } + + const md = Markdown.md(); + + // Check if parameter for path are in the place + // if ('parameters' in data) { + // pathParameters = data.parameters; + // } + + // Go further method by methods + Object.keys(data).forEach((method) => { + if (ALLOWED_METHODS_V3.includes(method)) { + const pathInfo: OpenAPIV3.OperationObject = data[method]; + + const deprecated = 'deprecated' in pathInfo && pathInfo.deprecated === true; + + md.line(''); + const header = (md.string(`[${method.toUpperCase()}] ${path}`)); + if (deprecated) { + header.strikethrough(); + } + header.h3(); + md.line(header); + + // Deprecation + if (deprecated) { + md.line().line(md.string('DEPRECATED').bold().italic()).line(); + } + + // // Schemes + // if ('schemes' in pathInfo && pathInfo.schemes.length > 0) { + // md.line(transformSchemes(pathInfo.schemes)); + // } + + // Set summary + if ('summary' in pathInfo) { + md.line(md.string(pathInfo.summary).escape().bold()).line(); + } + + // Set description + if ('description' in pathInfo && pathInfo.summary !== pathInfo.description) { + md.line(md.string(pathInfo.description).escape()).line(); + } + + // Set externalDocs + if ('externalDocs' in pathInfo) { + md.line( + md.string('Documentation:').bold(), + md.string(' '), + transformExternalDocs(pathInfo.externalDocs), + ); + } + + // // Build parameters + // if ('parameters' in pathInfo || pathParameters) { + // const builtParameters = md.string(transformParameters( + // pathInfo.parameters, + // pathParameters, + // )); + // if (builtParameters.length) { + // md.line(builtParameters).line(); + // } + // } + + // Build responses + if ('responses' in pathInfo) { + const builtResponses = md.string(transformResponses( + pathInfo.responses, + )); + if (builtResponses.length) { + md.line(builtResponses).line(); + } + } + + // Build security + if ('security' in pathInfo) { + md.line( + transformSecurity(pathInfo.security), + ); + } + } + }); + + return md.export(); +} diff --git a/src/transformers/v3/pathParameters.spec.ts b/src/transformers/v3/pathParameters.spec.ts new file mode 100644 index 0000000..a16c038 --- /dev/null +++ b/src/transformers/v3/pathParameters.spec.ts @@ -0,0 +1,116 @@ +import { expect } from 'chai'; +import { transformParameters } from './pathParameters'; + +const tableFixture: string[] = [ + '##### Parameters', + '| Name | Located in | Description | Required | Schema |', + '| ---- | ---------- | ----------- | -------- | ------ |', +]; + +describe('Path parameters transformer', () => { + describe('Method parameters', () => { + const fixture = [ + { + name: 'name', + in: 'formData', + description: 'name', + type: 'string', + }, { + name: 'year', + in: 'formData', + description: 'year', + type: 'string', + required: true, + }, { + in: 'formData', + type: 'string', + }, {}, + ]; + + const results = [...tableFixture, ...[ + '| name | formData | name | No | string |', + '| year | formData | year | Yes | string |', + '| | formData | | No | string |', + '| | | | No | |', + ]]; + const res = (transformParameters(fixture as any) as string).split('\n'); + + it('Should create parameters header', () => { + expect(res[0]).to.be.equal(results[0]); + }); + + it('Should create table header', () => { + expect(res[2]).to.be.equal(results[1]); + expect(res[3]).to.be.equal(results[2]); + }); + + it('Should create table body', () => { + expect(res[4]).to.be.equal(results[3]); + expect(res[5]).to.be.equal(results[4]); + expect(res[6]).to.be.equal(results[5]); + expect(res[7]).to.be.equal(results[6]); + }); + }); + describe('Path parameters', () => { + describe('Should build parameters from path parameters', () => { + const fixture = [{ + name: 'name', + in: 'formData', + description: 'name', + type: 'string', + }]; + const results = [...tableFixture, ...[ + '| name | formData | name | No | string |', + ]]; + const res = (transformParameters(undefined as any, fixture) as string).split('\n'); + + it('Should create parameters header', () => { + expect(res[0]).to.be.equal(results[0]); + }); + + it('Should create table header', () => { + expect(res[2]).to.be.equal(results[1]); + expect(res[3]).to.be.equal(results[2]); + }); + + it('Should create table body', () => { + expect(res[4]).to.be.equal(results[3]); + }); + }); + }); + + describe('Path and method parameters', () => { + describe('Should build parameters from path and method parameters', () => { + const pathFixture = [{ + name: 'path name', + in: 'formData', + description: 'name', + type: 'string', + }]; + const methodFixture = [{ + name: 'method name', + in: 'formData', + description: 'name', + type: 'string', + }]; + const results = [...tableFixture, ...[ + '| path name | formData | name | No | string |', + '| method name | formData | name | No | string |', + ]]; + const res = (transformParameters(methodFixture, pathFixture) as string).split('\n'); + it('Should create parameters header', () => { + expect(res[0]).to.be.equal(results[0]); + }); + + it('Should create table header', () => { + expect(res[2]).to.be.equal(results[1]); + expect(res[3]).to.be.equal(results[2]); + }); + + it('Should create table body', () => { + expect(res[4]).to.be.equal(results[3]); + expect(res[5]).to.be.equal(results[4]); + }); + }); + }); +}); diff --git a/src/transformers/v3/pathParameters.ts b/src/transformers/v3/pathParameters.ts new file mode 100644 index 0000000..23e19ec --- /dev/null +++ b/src/transformers/v3/pathParameters.ts @@ -0,0 +1,50 @@ +import { OpenAPIV2, OpenAPIV3 } from 'openapi-types'; +import { dataTypeResolver } from '../common/v2-3/dataTypes'; +import { Schema } from './models/Schema'; +import { Markdown } from '../../lib/markdown'; + +export const transformParameters = ( + parameters: OpenAPIV3.ParameterObject, + pathParameters?: OpenAPIV3.ParameterObject, +) => { + const md = Markdown.md(); + + md.line(md.string('Parameters').h5()).line(); + const table = md.table(); + table.th('Name').th('Located in').th('Description').th('Required') + .th('Schema'); + + [].concat(pathParameters, parameters).forEach((keys: OpenAPIV2.Parameter) => { + if (keys) { + const tr = table.tr(); + // Name first + tr.td(keys.name || ''); + // Scope (in) + tr.td(keys.in || ''); + // description + if ('description' in keys) { + tr.td(md.string(keys.description.replace(/[\r\n]/g, ' ')).escape()); + } else { + tr.td(''); + } + tr.td(keys.required ? 'Yes' : 'No'); + + // Prepare schema to be transformed + let schema = null; + if ('schema' in keys) { + schema = new Schema(keys.schema); + } else { + schema = new Schema(); + schema.setType('type' in keys ? keys.type : null); + schema.setFormat('format' in keys ? keys.format : null); + schema.setReference('$ref' in keys ? keys.$ref : null); + schema.setItems('items' in keys ? keys.items : null); + } + + tr.td(dataTypeResolver(schema)); + } + }); + + md.line(table); + return md.export(); +}; diff --git a/src/transformers/v3/pathResponses.ts b/src/transformers/v3/pathResponses.ts new file mode 100644 index 0000000..6c4bbfb --- /dev/null +++ b/src/transformers/v3/pathResponses.ts @@ -0,0 +1,140 @@ +import { OpenAPIV3 } from 'openapi-types'; +import { Markdown } from '../../lib/markdown'; +import { transformHeaders } from '../common/v2-3/headers'; +import { processLink } from './link'; +import { dataTypeResolver } from './dataTypes'; +import { Schema } from './models/Schema'; +import { Dereferenced } from '../../types'; + +/** + * Checks if the given response object contains a schema. + * + * @param response - An OpenAPI V3 response object. + * @returns A boolean indicating whether the response contains a schema. + */ +function hasSchemaInResponse(response: OpenAPIV3.ResponseObject): boolean { + if ('content' in response) { + return Object.values(response.content).some((data) => 'schema' in data); + } + return false; +} + +/** + * Checks if any response in the given responses object contains schemas. + * + * @param responses - An object containing OpenAPI V3 response objects. + * @returns A boolean indicating whether any res + * ponse contains schemas. + */ +export function hasSchemasInResponses(responses: OpenAPIV3.ResponsesObject): boolean { + return Object.values(responses).some(hasSchemaInResponse); +} + +/** + * Checks if any response in the given responses object contains links. + * + * @param responses - An object containing OpenAPI V3 response objects. + * @returns A boolean indicating whether any response contains links. + */ +export function hasLinksInResponses(responses: OpenAPIV3.ResponsesObject): boolean { + return Object.values(responses).some((response) => 'links' in response); +} + +/** + * Build responses table + */ +export function transformResponses(responses: OpenAPIV3.ResponsesObject) { + const md = Markdown.md(); + md.line(md.string('Responses').h4()).line(); + + const hasSchemas = hasSchemasInResponses(responses); + const hasLinks = hasLinksInResponses(responses); + + const table = md.table(); + table.th('Code').th('Description'); + // if (hasContent) { + // table.th('Content'); + // } + if (hasSchemas) { + table.th('Schema'); + } + if (hasLinks) { + table.th('Links'); + } + + Object.keys(responses).forEach((responseCode) => { + const tr = table.tr(); + const response = responses[responseCode]; + + // console.log(); + // console.log(JSON.stringify(response, null, 2)); + // console.log(); + // Response + tr.td(responseCode); + + // Description + const description = md.string(); + if ('description' in response) { + description.concat( + md.string(response.description.replace(/[\r\n]/g, ' ')).escape(), + ); + } + if ('headers' in response) { + description.concat(md.string('').br(true)); + description.concat( + transformHeaders(response.headers as never), + ); + } + // if ('examples' in response) { + // Object.entries(response.examples).forEach(([contentType, example]) => { + // let formattedExample = typeof example === 'string' ? example + // : JSON.stringify(example, null, ' '); + + // formattedExample = formattedExample.replace(/\r?\n/g, '
'); + // const contentTypeMd = md.string(contentType).italic().get(); + + // description + // .concat('

') + // .concat(md.string('Example').bold()) + // .concat(` (${contentTypeMd}):
${formattedExample}
`); + // }); + // } + tr.td(description); + + // Schema + if (hasSchemaInResponse(response as Dereferenced)) { + const { content } = (response as Dereferenced); + if (content) { + const td = md.string(); + Object.keys(content).forEach((contentType: string) => { + const { schema } = content[contentType]; + if (schema) { + const schemaObject = new Schema(schema as Dereferenced); + // tr.td(dataTypeResolver(schemaObject)); + td.concat(`${md.string(contentType).bold()}: `); + td.concat(dataTypeResolver(schemaObject)).br(true); + } + }); + tr.td(td); + } else { + tr.td(''); + } + } else if (hasSchemas) { + tr.td(''); + } + + if ('links' in response) { + const linksMd = md.string(); + Object.keys(response.links).forEach((linkName: string) => { + const link = response.links[linkName]; + linksMd.concat(processLink(linkName, link as OpenAPIV3.LinkObject | string)); + }); + tr.td(linksMd); + } else if (hasLinks) { + tr.td(''); + } + }); + + md.line(table); + return md.export(); +} diff --git a/src/types.ts b/src/types.ts index 6548ff4..518384e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -16,4 +16,13 @@ export interface Options { forceVersion?: string; } -export const ALLOWED_METHODS = ['get', 'post', 'put', 'patch', 'delete', 'options', 'head']; +export type Dereferenced = Exclude, OpenAPIV2.ReferenceObject>, +OpenAPIV3.ReferenceObject +>; + +export const ALLOWED_METHODS_V2 = ['get', 'post', 'put', 'patch', 'delete', 'options', 'head']; +export const ALLOWED_METHODS_V3 = [...ALLOWED_METHODS_V2, 'trace'];