Validate API OpenAPI 3 spec (#6519)
* Add @stoplight/spectral to dev dependencies for validation * Add yarn api-docs:lint command * Fix linting errors * Lower severity level * Add ibm-openapi-validator as linter, as it inspects slightly differently
This commit is contained in:
parent
b43df4aff7
commit
4c6b901b7a
5 changed files with 1221 additions and 120 deletions
100
.validaterc
Normal file
100
.validaterc
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
{
|
||||
"shared": {
|
||||
"operations": {
|
||||
"no_operation_id": "warning",
|
||||
"operation_id_case_convention": [
|
||||
"warning",
|
||||
"lower_camel_case"
|
||||
],
|
||||
"no_summary": "warning",
|
||||
"no_array_responses": "off",
|
||||
"parameter_order": "warning",
|
||||
"unused_tag": "warning",
|
||||
"operation_id_naming_convention": "off"
|
||||
},
|
||||
"pagination": {
|
||||
"pagination_style": "warning"
|
||||
},
|
||||
"parameters": {
|
||||
"no_parameter_description": "error",
|
||||
"param_name_case_convention": [
|
||||
"error",
|
||||
"lower_snake_case"
|
||||
],
|
||||
"invalid_type_format_pair": "error",
|
||||
"content_type_parameter": "error",
|
||||
"accept_type_parameter": "error",
|
||||
"authorization_parameter": "warning",
|
||||
"required_param_has_default": "warning"
|
||||
},
|
||||
"paths": {
|
||||
"missing_path_parameter": "error",
|
||||
"duplicate_path_parameter": "warning",
|
||||
"snake_case_only": "off",
|
||||
"paths_case_convention": [
|
||||
"error",
|
||||
"lower_snake_case"
|
||||
]
|
||||
},
|
||||
"responses": {
|
||||
"inline_response_schema": "warning"
|
||||
},
|
||||
"security_definitions": {
|
||||
"unused_security_schemes": "warning",
|
||||
"unused_security_scopes": "warning"
|
||||
},
|
||||
"security": {
|
||||
"invalid_non_empty_security_array": "error"
|
||||
},
|
||||
"schemas": {
|
||||
"invalid_type_format_pair": "off",
|
||||
"snake_case_only": "off",
|
||||
"no_schema_description": "off",
|
||||
"no_property_description": "off",
|
||||
"description_mentions_json": "warning",
|
||||
"array_of_arrays": "warning",
|
||||
"property_case_convention": [
|
||||
"error",
|
||||
"lower_snake_case"
|
||||
],
|
||||
"enum_case_convention": [
|
||||
"error",
|
||||
"lower_snake_case"
|
||||
]
|
||||
},
|
||||
"walker": {
|
||||
"no_empty_descriptions": "error",
|
||||
"has_circular_references": "warning",
|
||||
"$ref_siblings": "off",
|
||||
"duplicate_sibling_description": "warning",
|
||||
"incorrect_ref_pattern": "warning"
|
||||
}
|
||||
},
|
||||
"swagger2": {
|
||||
"operations": {
|
||||
"no_consumes_for_put_or_post": "error",
|
||||
"get_op_has_consumes": "warning",
|
||||
"no_produces": "warning"
|
||||
}
|
||||
},
|
||||
"oas3": {
|
||||
"operations": {
|
||||
"no_request_body_content": "error",
|
||||
"no_request_body_name": "warning"
|
||||
},
|
||||
"parameters": {
|
||||
"no_in_property": "error",
|
||||
"invalid_in_property": "error",
|
||||
"missing_schema_or_content": "error",
|
||||
"has_schema_and_content": "error"
|
||||
},
|
||||
"responses": {
|
||||
"no_response_codes": "error",
|
||||
"no_success_response_codes": "warning",
|
||||
"no_response_body": "warning"
|
||||
},
|
||||
"schemas": {
|
||||
"json_or_param_binary_string": "warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
113
docs/api_v0.yml
113
docs/api_v0.yml
|
|
@ -1,4 +1,4 @@
|
|||
openapi: "3.0.2"
|
||||
openapi: "3.0.3"
|
||||
info:
|
||||
title: DEV API (beta)
|
||||
description: |
|
||||
|
|
@ -301,7 +301,7 @@ components:
|
|||
`cover_image` is the main image of the article
|
||||
|
||||
*If the markdown contains a front matter, it will take precedence
|
||||
on the equivalent params given in the JSON payload.*
|
||||
on the equivalent params given in the payload.*
|
||||
type: string
|
||||
published:
|
||||
description: |
|
||||
|
|
@ -365,7 +365,7 @@ components:
|
|||
`cover_image` is the main image of the article
|
||||
|
||||
*If the markdown contains a front matter, it will take precedence
|
||||
on the equivalent params given in the JSON payload.*
|
||||
on the equivalent params given in the payload.*
|
||||
type: string
|
||||
published:
|
||||
description: |
|
||||
|
|
@ -465,6 +465,9 @@ components:
|
|||
description: Crossposting or published date time
|
||||
type: string
|
||||
format: date-time
|
||||
body_markdown:
|
||||
description: The body of the article in Markdown format
|
||||
type: string
|
||||
user:
|
||||
$ref: "#/components/schemas/SharedUser"
|
||||
organization:
|
||||
|
|
@ -1350,7 +1353,7 @@ paths:
|
|||
format: int32
|
||||
example: 99
|
||||
responses:
|
||||
200:
|
||||
"200":
|
||||
description: A list of articles
|
||||
content:
|
||||
application/json:
|
||||
|
|
@ -1404,7 +1407,7 @@ paths:
|
|||
$ref: "#/components/examples/ArticleCreateOrganization"
|
||||
|
||||
responses:
|
||||
201:
|
||||
"201":
|
||||
description: A newly created article
|
||||
content:
|
||||
application/json:
|
||||
|
|
@ -1419,7 +1422,7 @@ paths:
|
|||
schema:
|
||||
type: string
|
||||
format: url
|
||||
400:
|
||||
"400":
|
||||
description: BadRequest
|
||||
content:
|
||||
application/json:
|
||||
|
|
@ -1428,7 +1431,7 @@ paths:
|
|||
examples:
|
||||
error-bad-request:
|
||||
$ref: "#/components/examples/ErrorBadRequest"
|
||||
401:
|
||||
"401":
|
||||
description: Unauthorized
|
||||
content:
|
||||
application/json:
|
||||
|
|
@ -1437,7 +1440,7 @@ paths:
|
|||
examples:
|
||||
error-unauthorized:
|
||||
$ref: "#/components/examples/ErrorUnauthorized"
|
||||
422:
|
||||
"422":
|
||||
description: Unprocessable Entity
|
||||
content:
|
||||
application/json:
|
||||
|
|
@ -1484,7 +1487,7 @@ paths:
|
|||
format: int32
|
||||
example: 150589
|
||||
responses:
|
||||
200:
|
||||
"200":
|
||||
description: An article
|
||||
content:
|
||||
application/json:
|
||||
|
|
@ -1493,7 +1496,7 @@ paths:
|
|||
examples:
|
||||
article-success:
|
||||
$ref: "#/components/examples/ArticleShow"
|
||||
404:
|
||||
"404":
|
||||
description: Resource not found
|
||||
content:
|
||||
application/json:
|
||||
|
|
@ -1555,42 +1558,38 @@ paths:
|
|||
article-update-organization:
|
||||
$ref: "#/components/examples/ArticleCreateOrganization"
|
||||
responses:
|
||||
200:
|
||||
"200":
|
||||
description: The updated article
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
$ref: "#/components/schemas/ArticleShow"
|
||||
examples:
|
||||
article-success:
|
||||
$ref: "#/components/examples/ArticleShow"
|
||||
400:
|
||||
"400":
|
||||
description: BadRequest
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
$ref: "#/components/schemas/APIError"
|
||||
examples:
|
||||
error-bad-request:
|
||||
$ref: "#/components/examples/ErrorBadRequest"
|
||||
401:
|
||||
"401":
|
||||
description: Unauthorized
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
$ref: "#/components/schemas/APIError"
|
||||
examples:
|
||||
error-unauthorized:
|
||||
$ref: "#/components/examples/ErrorUnauthorized"
|
||||
422:
|
||||
"422":
|
||||
description: Unprocessable Entity
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
$ref: "#/components/schemas/APIError"
|
||||
examples:
|
||||
error-unprocessable-entity:
|
||||
|
|
@ -1642,7 +1641,7 @@ paths:
|
|||
format: int32
|
||||
example: 30
|
||||
responses:
|
||||
200:
|
||||
"200":
|
||||
description: A list of published articles
|
||||
content:
|
||||
application/json:
|
||||
|
|
@ -1694,7 +1693,7 @@ paths:
|
|||
format: int32
|
||||
example: 30
|
||||
responses:
|
||||
200:
|
||||
"200":
|
||||
description: A list of published articles
|
||||
content:
|
||||
application/json:
|
||||
|
|
@ -1746,7 +1745,7 @@ paths:
|
|||
format: int32
|
||||
example: 30
|
||||
responses:
|
||||
200:
|
||||
"200":
|
||||
description: A list of articles
|
||||
content:
|
||||
application/json:
|
||||
|
|
@ -1800,7 +1799,7 @@ paths:
|
|||
format: int32
|
||||
example: 30
|
||||
responses:
|
||||
200:
|
||||
"200":
|
||||
description: A list of articles
|
||||
content:
|
||||
application/json:
|
||||
|
|
@ -1838,7 +1837,7 @@ paths:
|
|||
format: int32
|
||||
example: 270180
|
||||
responses:
|
||||
200:
|
||||
"200":
|
||||
description: A list of threads of comments
|
||||
content:
|
||||
application/json:
|
||||
|
|
@ -1853,7 +1852,7 @@ paths:
|
|||
$ref: "#/components/examples/CommentsDeleted"
|
||||
comments-success-hidden:
|
||||
$ref: "#/components/examples/CommentsHidden"
|
||||
404:
|
||||
"404":
|
||||
description: Resource not found
|
||||
content:
|
||||
application/json:
|
||||
|
|
@ -1891,7 +1890,7 @@ paths:
|
|||
type: string
|
||||
example: m35m
|
||||
responses:
|
||||
200:
|
||||
"200":
|
||||
description: A comment and its descendants
|
||||
content:
|
||||
application/json:
|
||||
|
|
@ -1904,7 +1903,7 @@ paths:
|
|||
$ref: "#/components/examples/CommentDeleted"
|
||||
comments-success-hidden:
|
||||
$ref: "#/components/examples/CommentHidden"
|
||||
404:
|
||||
"404":
|
||||
description: Resource not found
|
||||
content:
|
||||
application/json:
|
||||
|
|
@ -1960,7 +1959,7 @@ paths:
|
|||
type: string
|
||||
example: cfp
|
||||
responses:
|
||||
200:
|
||||
"200":
|
||||
description: A list of listings
|
||||
content:
|
||||
application/json:
|
||||
|
|
@ -2009,7 +2008,7 @@ paths:
|
|||
$ref: "#/components/examples/ListingCreateOrganization"
|
||||
|
||||
responses:
|
||||
201:
|
||||
"201":
|
||||
description: A newly created Listing
|
||||
content:
|
||||
application/json:
|
||||
|
|
@ -2018,7 +2017,7 @@ paths:
|
|||
examples:
|
||||
article-success:
|
||||
$ref: "#/components/examples/Listing"
|
||||
400:
|
||||
"400":
|
||||
description: BadRequest
|
||||
content:
|
||||
application/json:
|
||||
|
|
@ -2027,7 +2026,7 @@ paths:
|
|||
examples:
|
||||
error-bad-request:
|
||||
$ref: "#/components/examples/ErrorBadRequest"
|
||||
401:
|
||||
"401":
|
||||
description: Unauthorized
|
||||
content:
|
||||
application/json:
|
||||
|
|
@ -2036,7 +2035,7 @@ paths:
|
|||
examples:
|
||||
error-unauthorized:
|
||||
$ref: "#/components/examples/ErrorUnauthorized"
|
||||
402:
|
||||
"402":
|
||||
description: PaymentRequired
|
||||
content:
|
||||
application/json:
|
||||
|
|
@ -2045,7 +2044,7 @@ paths:
|
|||
examples:
|
||||
error-unauthorized:
|
||||
$ref: "#/components/examples/ErrorPaymentRequired"
|
||||
422:
|
||||
"422":
|
||||
description: Unprocessable Entity
|
||||
content:
|
||||
application/json:
|
||||
|
|
@ -2119,7 +2118,7 @@ paths:
|
|||
format: int32
|
||||
example: 30
|
||||
responses:
|
||||
200:
|
||||
"200":
|
||||
description: A list of listings
|
||||
content:
|
||||
application/json:
|
||||
|
|
@ -2158,7 +2157,7 @@ paths:
|
|||
format: int32
|
||||
example: 1
|
||||
responses:
|
||||
200:
|
||||
"200":
|
||||
description: A listing
|
||||
content:
|
||||
application/json:
|
||||
|
|
@ -2167,7 +2166,7 @@ paths:
|
|||
examples:
|
||||
article-success:
|
||||
$ref: "#/components/examples/Listing"
|
||||
404:
|
||||
"404":
|
||||
description: Resource not found
|
||||
content:
|
||||
application/json:
|
||||
|
|
@ -2228,52 +2227,47 @@ paths:
|
|||
listing-update:
|
||||
$ref: "#/components/examples/ListingUpdate"
|
||||
responses:
|
||||
200:
|
||||
"200":
|
||||
description: The updated article
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
$ref: "#/components/schemas/ArticleShow"
|
||||
examples:
|
||||
article-success:
|
||||
$ref: "#/components/examples/ArticleShow"
|
||||
400:
|
||||
"400":
|
||||
description: BadRequest
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
$ref: "#/components/schemas/APIError"
|
||||
examples:
|
||||
error-bad-request:
|
||||
$ref: "#/components/examples/ErrorBadRequest"
|
||||
401:
|
||||
"401":
|
||||
description: Unauthorized
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
$ref: "#/components/schemas/APIError"
|
||||
examples:
|
||||
error-unauthorized:
|
||||
$ref: "#/components/examples/ErrorUnauthorized"
|
||||
402:
|
||||
"402":
|
||||
description: PaymentRequired
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
$ref: "#/components/schemas/APIError"
|
||||
examples:
|
||||
error-unauthorized:
|
||||
$ref: "#/components/examples/ErrorPaymentRequired"
|
||||
422:
|
||||
"422":
|
||||
description: Unprocessable Entity
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
$ref: "#/components/schemas/APIError"
|
||||
examples:
|
||||
error-unprocessable-entity:
|
||||
|
|
@ -2311,7 +2305,7 @@ paths:
|
|||
tags:
|
||||
- webhooks
|
||||
responses:
|
||||
200:
|
||||
"200":
|
||||
description: A list of webhooks
|
||||
content:
|
||||
application/json:
|
||||
|
|
@ -2359,42 +2353,38 @@ paths:
|
|||
$ref: "#/components/examples/WebhookCreate"
|
||||
|
||||
responses:
|
||||
201:
|
||||
"201":
|
||||
description: A newly created webhook
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
$ref: "#/components/schemas/WebhookShow"
|
||||
examples:
|
||||
webhook-success:
|
||||
$ref: "#/components/examples/WebhookShow"
|
||||
400:
|
||||
"400":
|
||||
description: BadRequest
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
$ref: "#/components/schemas/APIError"
|
||||
examples:
|
||||
error-bad-request:
|
||||
$ref: "#/components/examples/ErrorBadRequest"
|
||||
401:
|
||||
"401":
|
||||
description: Unauthorized
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
$ref: "#/components/schemas/APIError"
|
||||
examples:
|
||||
error-unauthorized:
|
||||
$ref: "#/components/examples/ErrorUnauthorized"
|
||||
422:
|
||||
"422":
|
||||
description: Unprocessable Entity
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
$ref: "#/components/schemas/APIError"
|
||||
examples:
|
||||
error-unprocessable-entity:
|
||||
|
|
@ -2430,32 +2420,29 @@ paths:
|
|||
format: int32
|
||||
example: 123
|
||||
responses:
|
||||
200:
|
||||
"200":
|
||||
description: A webhook endpoint
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
$ref: "#/components/schemas/WebhookShow"
|
||||
examples:
|
||||
webhook-success:
|
||||
$ref: "#/components/examples/WebhookShow"
|
||||
401:
|
||||
"401":
|
||||
description: Unauthorized
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
$ref: "#/components/schemas/APIError"
|
||||
examples:
|
||||
article-unauthorized:
|
||||
$ref: "#/components/examples/ErrorUnauthorized"
|
||||
404:
|
||||
"404":
|
||||
description: Resource not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
$ref: "#/components/schemas/APIError"
|
||||
examples:
|
||||
article-not-found:
|
||||
|
|
@ -2486,9 +2473,9 @@ paths:
|
|||
format: int32
|
||||
example: 123
|
||||
responses:
|
||||
204:
|
||||
"204":
|
||||
description: A successful deletion
|
||||
401:
|
||||
"401":
|
||||
description: Unauthorized
|
||||
content:
|
||||
application/json:
|
||||
|
|
@ -2497,7 +2484,7 @@ paths:
|
|||
examples:
|
||||
article-unauthorized:
|
||||
$ref: "#/components/examples/ErrorUnauthorized"
|
||||
404:
|
||||
"404":
|
||||
description: Resource not found
|
||||
content:
|
||||
application/json:
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@
|
|||
title: Contributing to the API Specification Docs
|
||||
---
|
||||
|
||||
# Contributing to API spec docs
|
||||
# Contributing to API specification docs
|
||||
|
||||
The API docs follow the
|
||||
The API v0 is described with the
|
||||
[OpenAPI 3 specification](https://spec.openapis.org/oas/v3.0.3).
|
||||
|
||||
## Where the docs are located
|
||||
## Where you can find the spec file
|
||||
|
||||
We auto-generate the documentation from `api_v0.yml` within the `/docs`
|
||||
directory. We use [ReDoc](https://github.com/Redocly/redoc) to turn the OpenAPI
|
||||
|
|
@ -15,8 +15,8 @@ directory. We use [ReDoc](https://github.com/Redocly/redoc) to turn the OpenAPI
|
|||
|
||||
## Updating API docs
|
||||
|
||||
Whenever you make changes to the API docs, make sure to bump the version in
|
||||
`api_v0.yml`.
|
||||
Whenever you make changes to the API docs, make sure to bump the version at the
|
||||
top of `api_v0.yml`, in `info.version`.
|
||||
|
||||
## Running and editing the docs locally
|
||||
|
||||
|
|
@ -27,7 +27,19 @@ yarn api-docs:serve
|
|||
```
|
||||
|
||||
This will let you browse the auto-generated version of the doc locally, and it
|
||||
will reload the doc after every modification of the spec file.
|
||||
will reload the documentation after every change of the specification file.
|
||||
|
||||
## Linting and validation
|
||||
|
||||
We use [spectral](https://github.com/stoplightio/spectral) and
|
||||
[ibm-openapi-validator](https://github.com/IBM/openapi-validator) to validate
|
||||
the spec file. The validation is performed as a `pre-commit` hook.
|
||||
|
||||
You can also manually validate the document, running:
|
||||
|
||||
```shell
|
||||
yarn api-docs:lint
|
||||
```
|
||||
|
||||
If you have Visual Studio Code, we suggest you install the following extensions
|
||||
that enable validation and navigation within the spec file:
|
||||
|
|
|
|||
15
package.json
15
package.json
|
|
@ -9,11 +9,12 @@
|
|||
"test": "test"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "jest app/javascript/ --coverage",
|
||||
"test:watch": "jest app/javascript/ --watch",
|
||||
"storybook": "start-storybook -p 6006 -c app/javascript/.storybook -s app/assets",
|
||||
"api-docs:lint": "spectral lint -F hint -v docs/api_v0.yml && lint-openapi -e docs/api_v0.yml",
|
||||
"api-docs:serve": "redoc-cli serve docs/api_v0.yml --options.pathInMiddlePanel --options.jsonSampleExpandLevel=all --options.menuToggle -t docs/api_template.hbs --watch",
|
||||
"build-storybook": "build-storybook -c app/javascript/.storybook -s app/assets",
|
||||
"api-docs:serve": "yarn redoc-cli serve docs/api_v0.yml --options.pathInMiddlePanel --options.jsonSampleExpandLevel=all --options.menuToggle -t docs/api_template.hbs --watch"
|
||||
"storybook": "start-storybook -p 6006 -c app/javascript/.storybook -s app/assets",
|
||||
"test": "jest app/javascript/ --coverage",
|
||||
"test:watch": "jest app/javascript/ --watch"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
|
|
@ -25,6 +26,10 @@
|
|||
"prettier --write",
|
||||
"eslint --fix -c app/assets/javascripts/.eslintrc.js"
|
||||
],
|
||||
"docs/api_v0.yml": [
|
||||
"spectral lint -F hint",
|
||||
"lint-openapi -e"
|
||||
],
|
||||
"*.{js,jsx}": [
|
||||
"prettier --write",
|
||||
"eslint --fix"
|
||||
|
|
@ -64,6 +69,7 @@
|
|||
},
|
||||
"homepage": "https://github.com/thepracticaldev/dev.to#readme",
|
||||
"devDependencies": {
|
||||
"@stoplight/spectral": "5.1.0",
|
||||
"@storybook/addon-actions": "3.4.12",
|
||||
"@storybook/addon-knobs": "3.4.12",
|
||||
"@storybook/addon-links": "3.4.12",
|
||||
|
|
@ -82,6 +88,7 @@
|
|||
"eslint-plugin-react": "^7.18.3",
|
||||
"faker": "^4.1.0",
|
||||
"husky": "^4.2.3",
|
||||
"ibm-openapi-validator": "0.24.1",
|
||||
"jest": "^23.5.0",
|
||||
"jest-axe": "^3.3.0",
|
||||
"jest-fetch-mock": "^3.0.1",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue