├── .github └── workflows │ └── docs-tests.yaml ├── .gitignore ├── .markdownlint.yaml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── configurable-rules ├── .gitignore ├── api-health │ ├── README.md │ ├── openapi.yaml │ └── redocly.yaml ├── azure-apim-unsupported-keyword │ ├── README.md │ ├── openapi.yaml │ └── redocly.yaml ├── description-banned-words │ ├── README.md │ └── redocly.yaml ├── info-description │ ├── README.md │ └── redocly.yaml ├── json-schema-misconfigurations │ ├── README.md │ ├── openapi.yaml │ └── redocly.yaml ├── no-script │ ├── README.md │ └── redocly.yaml ├── operation-delete-should-not-define-requestBody │ ├── README.md │ └── redocly.yaml ├── operation-get-should-not-define-requestBody │ ├── README.md │ └── redocly.yaml ├── operation-post-should-define-request-body │ ├── README.md │ └── redocly.yaml ├── operation-summary-sentence-case │ ├── README.md │ └── redocly.yaml ├── path-excludes-pattern │ ├── README.md │ └── redocly.yaml ├── required-items-for-array-schemas │ ├── README.md │ └── redocly.yaml └── string-schemas-length-defined │ ├── README.md │ ├── openapi.yaml │ └── redocly.yaml ├── custom-plugin-decorators ├── .gitignore ├── azure-apim │ ├── README.md │ └── azure-apim.js ├── openai-is-consequential │ ├── README.md │ └── openai-is-consequential.js ├── remove-extensions │ ├── README.md │ └── remove-extensions.js ├── remove-unused-tags │ ├── README.md │ ├── redocly.yaml │ └── tags.js ├── swap-summary-description │ ├── README.md │ ├── redocly.yaml │ └── swap-fields.js ├── tag-sorting │ ├── README.md │ ├── decorator-alpha.js │ └── tag-sorting.js └── update-example-dates │ ├── README.md │ ├── decorator.js │ └── plugin.js ├── custom-plugin-rules ├── .gitignore ├── code-sample-checks │ ├── README.md │ ├── check-sdk-coverage.js │ └── x-code-sample-checks.js ├── default-enum-match │ ├── README.md │ └── default-enum-plugin.js └── markdown-validator │ ├── README.md │ ├── openapi-markdown.js │ ├── package.json │ └── rule-validate-markdown.js ├── custom-plugins ├── .gitignore └── sorting │ ├── README.md │ ├── redocly.yaml │ ├── rule-sort-methods.js │ ├── rule-sort-props.js │ ├── sort-enums.js │ ├── sort-methods.js │ ├── sort-props-alpha.js │ ├── sort-props-required.js │ ├── sort-tags.js │ └── sorting.js ├── miscellaneous └── reorder-bundled-description-properties │ ├── README.md │ ├── bundled.yaml │ ├── openapi.yaml │ ├── reorder.js │ └── schema.yaml ├── readme-template.md └── rulesets ├── .gitignore ├── common-mistakes ├── README.md └── redocly.yaml ├── security ├── README.md └── redocly.yaml └── spec-compliant ├── README.md └── redocly.yaml /.github/workflows/docs-tests.yaml: -------------------------------------------------------------------------------- 1 | name: Documentation tests 2 | on: 3 | pull_request: 4 | types: [opened, synchronize, reopened] 5 | 6 | jobs: 7 | markdownlint: 8 | name: markdownlint 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: articulate/actions-markdownlint@v1 13 | with: 14 | config: .markdownlint.yaml 15 | files: 'README.md' 16 | - uses: articulate/actions-markdownlint@v1 17 | with: 18 | config: .markdownlint.yaml 19 | files: '**/*.md' 20 | 21 | linkcheck: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Checkout Repository 25 | uses: actions/checkout@v4 26 | - name: Markup Link Checker (mlc) 27 | uses: becheran/mlc@v0.16.1 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /.markdownlint.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Default rules: https://github.com/github/super-linter/blob/master/TEMPLATES/.markdown-lint.yml 3 | 4 | # Rules by id 5 | 6 | # Unordered list style 7 | MD004: false 8 | 9 | # Unordered list indentation 10 | MD007: 11 | indent: 2 12 | 13 | # Trailing spaces 14 | MD009: false 15 | 16 | MD013: 17 | # TODO: Consider to decrease allowed line length 18 | line_length: 800 19 | tables: false 20 | 21 | ## Allow same headers in siblings 22 | MD024: 23 | siblings_only: true 24 | 25 | # Multiple top level headings in the same document 26 | MD025: 27 | front_matter_title: '' 28 | 29 | # Trailing punctuation in heading 30 | MD026: 31 | punctuation: '.,;:。,;:' 32 | 33 | # Ordered list item prefix 34 | MD029: false 35 | 36 | # Unordered lists inside of ordered lists 37 | MD030: false 38 | 39 | # Inline HTML 40 | MD033: false 41 | 42 | # No bare urls 43 | MD034: false 44 | 45 | # Emphasis used instead of a heading 46 | MD036: false 47 | 48 | # Disable "First line in file should be a top level heading" 49 | # We use uncommon format to add metadata. 50 | # TODO: Consider to use "YAML front matter". 51 | MD041: false 52 | 53 | # Rules by tags 54 | blank_lines: false 55 | 56 | MD046: false 57 | # code-block-style 58 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Redocly CLI Cookbook 2 | 3 | Thanks for sharing your Redocly CLI wisdom with everyone! In order to add your ideas in a way that everyone can understand, please follow these steps to contribute to the project. 4 | 5 | ## Add your work in a new directory 6 | 7 | Every item added to this repository belongs in its own directory. Identify which top-level directory is most appropriate, and then create a directory inside it, named for the thing you are adding. 8 | 9 | For example, to add a configurable rule that makes sure every endpoint is tagged "cupcake" (an imaginary example): 10 | 11 | ```text 12 | . 13 | ├── configurable-rules 14 | │   └── endpoints-are-tagged-cupcake 15 | ``` 16 | 17 | Give the directory a meaningful name; people may browse the collection this way. 18 | 19 | ## Add your files 20 | 21 | Put the file or files that you want to share into your new directory. Try to name them in a way that can be re-used by other people and clearly understood. 22 | 23 | ## Create the README 24 | 25 | Copy the `readme-template.md` from the root of the project into your new directory, and name it `README.md`. Edit and complete this file - this is required for your contribution to be added to our collection. Your directory structure should look like this: 26 | 27 | ```text 28 | . 29 | ├── configurable-rules 30 | │   └── endpoints-are-tagged-cupcake 31 | │   └── README.md 32 | ``` 33 | 34 | Give users a clear idea of what problem your addition solves and any other related context that can help them to be successful too. 35 | 36 | ## List your addition on the main page 37 | 38 | We keep a catalog of what's included in the main `README.md` file. Add your new entry at the end of the list in the relevant section. 39 | 40 | ## Open a pull request 41 | 42 | Commit your changes to a new branch in your own repository fork, and then open a pull request to our repo. Someone from Redocly will review and offer suggestions or let you know if anything needs changing. Once it's ready, we'll merge and others can benefit from your wisdom! 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Redocly 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Redocly CLI Cookbook 2 | 3 | A community collection of rulesets, configuration, custom plugins and other additions for [Redocly CLI](https://github.com/Redocly/redocly-cli). We know our users have some great tips, examples, and code to share, and this is the place to do just that. We would love to have your [contributions](#contributing) here too! 4 | 5 | > [!IMPORTANT] 6 | > Redocly are the repository maintainers, but we can't thoroughly test everything here. Please browse, share, and use what you find at your own risk. 7 | 8 | If you're new to Redocly CLI, start with the [documentation](https://redocly.com/docs/cli/) to get up and running, then come back here to pick out any elements you would like to re-use yourself. To keep up with new developments, either subscribe to the project repository, or [sign up for the Redocly product newsletter](https://redocly.com/product-updates/). 9 | 10 | ## Usage 11 | 12 | Use the content here as a starting point for your own work. 13 | 14 | 1. Take a look at what's available in each category, and pick any that you think apply to your situation. 15 | 16 | 2. Each section links to the documentation for that feature, incase you need an introduction or refresher. 17 | 18 | 3. Copy and paste the examples you want to use into your own setup, then edit them to fit your own needs. 19 | 20 | If you come up with something new, please consider sharing it here by opening a pull request. 21 | 22 | ## Categories 23 | 24 | ### Rulesets 25 | 26 | Combine existing [built-in rules](https://redocly.com/docs/cli/rules/built-in-rules/) in ways that serve a specific purpose, and make a [resuable ruleset](https://redocly.com/docs/cli/guides/configure-rules/#create-a-reusable-ruleset). 27 | 28 | - [Spec-compliant ruleset](rulesets/spec-compliant/) 29 | - [Spot common mistakes](rulesets/common-mistakes) 30 | - [Security ruleset](rulesets/security) adds some defensive rules to your description. 31 | 32 | ### Configurable rules 33 | 34 | There are some fantastic examples of [configurable rules](https://redocly.com/docs/cli/rules/configurable-rules/) in the wild, we hope the list here inspires you to share more of your own! 35 | 36 | - [Ban certain words from descriptions](configurable-rules/description-banned-words/) 37 | - [Require `items` field for schemas of type `array`](configurable-rules/required-items-for-array-schemas/) 38 | - [Ensure sentence case in operation summaries](configurable-rules/operation-summary-sentence-case) 39 | - [`POST` SHOULD define `requestBody` schema](configurable-rules/operation-post-should-define-request-body/) 40 | - [`GET` SHOULD NOT define `requestBody` schema](configurable-rules/operation-get-should-not-define-requestBody/) 41 | - [`DELETE` SHOULD NOT define `requestBody` schema](configurable-rules/operation-delete-should-not-define-requestBody/) 42 | - [Info section must have a description](configurable-rules/info-description) 43 | - [No ` API 39 | paths: {} 40 | ``` 41 | 42 | When you lint this OpenAPI file with the `rule/no-script-tags-in-markdown` rule, you'll see a warning: 43 | 44 | ```text 45 | Markdown descriptions should not contain script tags. 46 | ``` 47 | 48 | ## References 49 | 50 | Inspired by the Spectral rule [no-script-tags-in-markdown](https://docs.stoplight.io/docs/spectral/4dec24461f3af-open-api-rules#no-script-tags-in-markdown). 51 | -------------------------------------------------------------------------------- /configurable-rules/no-script/redocly.yaml: -------------------------------------------------------------------------------- 1 | extends: [] 2 | 3 | rules: 4 | rule/no-script-tags-in-markdown: 5 | subject: 6 | type: any 7 | property: description 8 | assertions: 9 | notPattern: '" pattern' 5 | subject: 6 | type: Operation 7 | where: 8 | - subject: 9 | type: PathItem 10 | matchParentKeys: /^([\w-\{\}/.](? [!NOTE] 29 | > This rule can be repurposed for other fields with a single sentence, but not multiple sentences. 30 | 31 | ### Rule explanation 32 | 33 | This rule asserts that each Operation `summary` matches the regex defined in `pattern`. The regex we defined matches strings that: 34 | 35 | 1. Start with an uppercase letter 36 | 2. Are followed by 1+ characters that are _not_ uppercase letters 37 | 3. End with a character that is _not_ an uppercase letter 38 | 39 | **Tip**: Building a regex for your own rule? A tool like [regexr](https://regexr.com/) can be quite useful. 40 | 41 | ## Examples 42 | 43 | Summaries _with_ sentence casing (correct): 44 | 45 | ```yaml 46 | /zoo-tickets: 47 | post: 48 | summary: Buy zoo tickets 49 | /zoo-ticket/{ticketId}: 50 | get: 51 | summary: Get zoo ticket 52 | ``` 53 | 54 | Summaries _without_ sentence casing (incorrect): 55 | 56 | ```yaml 57 | /zoo-tickets: 58 | post: 59 | summary: Buy Zoo tickets 60 | /zoo-ticket/{ticketId}: 61 | get: 62 | summary: get Zoo ticket 63 | ``` 64 | 65 | ## Excluding operations from the rule 66 | 67 | Sometimes you might need an exception to the rule. For example, your Operation summary could have a proper noun or an acronym. You can do that using an [ignore file](https://redocly.com/docs/cli/commands/lint/). 68 | 69 | ### Example - ignoring sentence casing 70 | 71 | Pretend you have the following Operation and want to keep "QR" capitalized in the summary: 72 | 73 | ```yaml 74 | /tickets/{ticketId}/qr: 75 | get: 76 | summary: Get ticket QR code 77 | ``` 78 | 79 | 1. Verify your sentence casing rule is working correctly 80 | 81 | - Run `npx @redocly/cli lint` 82 | - Console should show error on Operation summary 83 | 84 | The linting output from our example: 85 | 86 | ```bash 87 | Operation summary must be sentence cased. 88 | 89 | 218 | /tickets/{ticketId}/qr: 90 | 219 | get: 91 | 220 | summary: Get ticket QR code 92 | 221 | description: Return an image of your ticket with scannable QR code. Used for event entry. 93 | 222 | operationId: getTicketCode 94 | 95 | Error was generated by the rule/operation-summary-sentence-case rule. 96 | ``` 97 | 98 | 2. Generate an ignore file 99 | 100 | - Run `npx @redocly/cli lint --generate-ignore-file` 101 | - Console should confirm an ignore file was generated 102 | - A _.redocly.lint-ignore.yaml_ was added to your code 103 | 104 | The ignore file generated by our example: 105 | 106 | ```yaml 107 | # .redocly.lint-ignore.yaml 108 | openapi.yaml: 109 | rule/operation-summary-sentence-case: 110 | - "#/paths/~1tickets~1{ticketId}~1qr/get/summary" 111 | ``` 112 | 113 | 3. Run the linter and verify your Operation summary is ignored 114 | 115 | - Run `npx @redocly/cli lint` 116 | - Console output should confirm valid OpenAPI description 117 | 118 | The linting output from our example with ignore file added: 119 | 120 | ```bash 121 | Woohoo! Your API description is valid. 🎉 122 | 1 problem is explicitly ignored. 123 | ``` -------------------------------------------------------------------------------- /configurable-rules/operation-summary-sentence-case/redocly.yaml: -------------------------------------------------------------------------------- 1 | rules: 2 | rule/operation-summary-sentence-case: 3 | subject: 4 | type: Operation 5 | property: summary 6 | message: "Operation summary must be sentence cased." 7 | assertions: 8 | pattern: /^[A-Z]+[^A-Z]+$/ -------------------------------------------------------------------------------- /configurable-rules/path-excludes-pattern/README.md: -------------------------------------------------------------------------------- 1 | # Paths should not match a pattern 2 | 3 | Authors: 4 | 5 | - [`@tatomyr`](https://github.com/tatomyr) Andrew Tatomyr (Redocly) 6 | 7 | ## What this does and why 8 | 9 | The [`no-http-verbs-in-paths` rule](https://redocly.com/docs/cli/rules/no-http-verbs-in-paths/#no-http-verbs-in-paths) is pre-built for a very specific set of patterns. 10 | This rule is the general Swiss army knife version. 11 | If you absolutely know something should not be in the path (for example `foo`), then add the pattern to prevent it. 12 | 13 | Some common things to check using this rule: other common CRUD verbs, bad words, and internal code or terminology. 14 | 15 | ## Code 16 | 17 | Add this to the `rules` section of your `redocly.yaml`: 18 | 19 | ```yaml 20 | rules: 21 | rule/path-exclude-pattern: 22 | subject: 23 | type: Paths 24 | assertions: 25 | notPattern: \/wrong 26 | ``` 27 | 28 | If you want to exclude multiple patterns, you may write several rules like this each with a different pattern. 29 | 30 | ## Examples 31 | 32 | Here's an example of an OpenAPI description: 33 | 34 | ```yaml 35 | openapi: 3.1.0 36 | info: 37 | title: Title 38 | version: 1.0.0 39 | paths: 40 | /good: 41 | $ref: ./good.yaml 42 | /wrong: # <-- This will error 43 | $ref: ./wrong.yaml 44 | ``` 45 | 46 | ## References 47 | 48 | Built-in [`no-http-verbs-in-paths` rule](https://redocly.com/docs/cli/rules/no-http-verbs-in-paths/#no-http-verbs-in-paths). 49 | -------------------------------------------------------------------------------- /configurable-rules/path-excludes-pattern/redocly.yaml: -------------------------------------------------------------------------------- 1 | rules: 2 | rule/path-exclude-pattern: 3 | subject: 4 | type: Paths 5 | assertions: 6 | notPattern: \/wrong 7 | -------------------------------------------------------------------------------- /configurable-rules/required-items-for-array-schemas/README.md: -------------------------------------------------------------------------------- 1 | # Require `items` field for schemas of type `array` 2 | 3 | Authors: 4 | 5 | - `@tatomyr` Andrew Tatomyr (Redocly) 6 | 7 | ## What this does and why 8 | 9 | When declaring a JSON Schema, it's possible to define an array without specifying what type of items are in the array. 10 | Linting with Redocly tools simply omits this, as it is considered an arbitrary array with any kind of item. 11 | However, to enforce the explicitness, you can use a [configurable rule](https://redocly.com/docs/cli/rules/configurable-rules/). 12 | 13 | **Note:** Whilst OAS 3.0.x specification does not enforce using the `items` field in descriptions, OAS 3.1.x fully supports JSON Schema 2020-12 draft which requires it. However, Redocly CLI doesn't alter the behavior and allows the omission of `items`. 14 | 15 | ## Code 16 | 17 | Add this to the `rules` section of your `redocly.yaml`: 18 | 19 | ```yaml 20 | rules: 21 | rule/required-items-in-array-schemas: 22 | subject: 23 | type: Schema 24 | assertions: 25 | required: 26 | - items 27 | where: 28 | - subject: 29 | type: Schema 30 | property: type 31 | assertions: 32 | const: array 33 | defined: true 34 | message: The 'items' field is required for schemas of array type. 35 | ``` 36 | 37 | This rule will error if an array is declared without an `items` field. 38 | The `where` section is used to filter the rule to only apply to schemas of type `array`. 39 | Note the `defined: true` assertion, which ensures that the `type` field is defined. 40 | 41 | ## Examples 42 | 43 | Here's a sample of an OpenAPI description: 44 | 45 | ```yaml 46 | # ... 47 | components: 48 | schemas: 49 | NoItems: # This will error 50 | type: array 51 | WithItems: # This will pass 52 | type: array 53 | items: 54 | type: string 55 | NotArray: # This will pass, doesn't match the 'where' clause 56 | type: string 57 | ``` 58 | -------------------------------------------------------------------------------- /configurable-rules/required-items-for-array-schemas/redocly.yaml: -------------------------------------------------------------------------------- 1 | rules: 2 | rule/required-items-in-array-schemas: 3 | subject: 4 | type: Schema 5 | assertions: 6 | required: 7 | - items 8 | where: 9 | - subject: 10 | type: Schema 11 | property: type 12 | assertions: 13 | const: array 14 | defined: true 15 | message: The 'items' field is required for schemas of array type. 16 | -------------------------------------------------------------------------------- /configurable-rules/string-schemas-length-defined/README.md: -------------------------------------------------------------------------------- 1 | # String schemas length defined 2 | 3 | Authors: 4 | - [`adamaltman`](https://github.com/adamaltman) Adam Altman (Redocly) 5 | 6 | ## What this does and why 7 | 8 | This requires `minLength` and `maxLength` properties set on a `string` where `enum` isn't defined. 9 | 10 | ## Code 11 | 12 | The rule checks that a `string` uses the `minLength` and `maxLength` keywords unless an `enum` is defined. 13 | 14 | It does this by using a combination `requireAny` and `mutuallyRequired`. 15 | 16 | ```yaml 17 | rule/string-schemas-length-defined: 18 | subject: 19 | type: Schema 20 | where: 21 | - subject: 22 | type: Schema 23 | property: type 24 | assertions: 25 | const: string 26 | assertions: 27 | requireAny: 28 | - minLength 29 | - maxLength 30 | - enum 31 | mutuallyRequired: 32 | - minLength 33 | - maxLength 34 | ``` 35 | 36 | ## Examples 37 | 38 | The following OpenAPI has schemas prefixed with either `Good` or `Bad` to show the configurable rules catch the likely bad uses of keywords. 39 | 40 | ```yaml 41 | openapi: 3.1.0 42 | info: 43 | title: For testing strict string definitions 44 | version: 1.0.0 45 | paths: {} 46 | components: 47 | schemas: 48 | 49 | LuckyNumber: # should not be caught be these rules 50 | type: integer 51 | 52 | BadString: 53 | type: string 54 | 55 | BadStringWithMinLength: 56 | type: string 57 | minLength: 1 58 | 59 | BadStringWithMaxLength: 60 | type: string 61 | maxLength: 64 62 | 63 | GoodStringBecauseEnum: 64 | type: string 65 | enum: 66 | - ABC 67 | - DEF 68 | - GHI 69 | 70 | GoodStringBecauseMinAndMaxLength: 71 | type: string 72 | minLength: 1 73 | maxLength: 64 74 | ``` 75 | 76 | ## References 77 | 78 | Inspired by a question from Keith F. 79 | -------------------------------------------------------------------------------- /configurable-rules/string-schemas-length-defined/openapi.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | info: 3 | title: For testing strict string definitions 4 | version: 1.0.0 5 | paths: {} 6 | components: 7 | schemas: 8 | 9 | LuckyNumber: # should not be caught be these rules 10 | type: integer 11 | 12 | BadString: 13 | type: string 14 | 15 | BadStringWithMinLength: 16 | type: string 17 | minLength: 1 18 | 19 | BadStringWithMaxLength: 20 | type: string 21 | maxLength: 64 22 | 23 | GoodStringBecauseEnum: 24 | type: string 25 | enum: 26 | - ABC 27 | - DEF 28 | - GHI 29 | 30 | GoodStringBecauseMinAndMaxLength: 31 | type: string 32 | minLength: 1 33 | maxLength: 64 34 | -------------------------------------------------------------------------------- /configurable-rules/string-schemas-length-defined/redocly.yaml: -------------------------------------------------------------------------------- 1 | rules: 2 | rule/string-schemas-length-defined: 3 | subject: 4 | type: Schema 5 | where: 6 | - subject: 7 | type: Schema 8 | property: type 9 | assertions: 10 | const: string 11 | assertions: 12 | requireAny: 13 | - minLength 14 | - maxLength 15 | - enum 16 | mutuallyRequired: 17 | - minLength 18 | - maxLength 19 | -------------------------------------------------------------------------------- /custom-plugin-decorators/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Redocly/redocly-cli-cookbook/d155441515902392de067b4a77a836c9ce5fdd6d/custom-plugin-decorators/.gitignore -------------------------------------------------------------------------------- /custom-plugin-decorators/azure-apim/README.md: -------------------------------------------------------------------------------- 1 | # Azure APIM decorator 2 | 3 | Authors: 4 | 5 | - [`@adamaltman`](https://github.com/adamaltman), Adam Altman (Redocly) 6 | 7 | ## What this does and why 8 | 9 | [Azure APIM](https://learn.microsoft.com/en-us/azure/api-management/api-management-api-import-restrictions) doesn't support a variety of OpenAPI features (one of which is examples). 10 | Examples causes an error when attempting to import OpenAPI descriptions that contain examples to Azure APIM. 11 | 12 | This decorator removes examples from schemas, media types, and components. 13 | 14 | This is a decorator you can run in a pipeline to transform your OpenAPI description prior to uploading to Azure APIM. 15 | Note that Azure APIM has many restrictions, and it might require more transformation prior to uploading to Azure APIM. 16 | 17 | You certainly would want to have the examples for purposes of documentation. Redocly renders multiple media type examples beautifully. 18 | 19 | Contribute to this community cookbook by adding decorators for issues you find with your imports to Azure APIM. 20 | 21 | ## Code 22 | 23 | The code is entirely in [azure-apim.js](./azure-apim.js). 24 | 25 | 26 | The code sets the plugin name to `azure-apim` and adds a decorator named `remove-examples`. 27 | 28 | It operates on the `Schemas`, `MediaType`, and `Components` element (in OpenAPI 3.x descriptions) to remove the `examples` node. 29 | 30 | ## Examples 31 | 32 | Add the plugin to `redocly.yaml` and enable the decorator: 33 | 34 | ```yaml 35 | plugins: 36 | - ./azure-apim.js 37 | 38 | decorators: 39 | azure-apim/remove-examples: on 40 | ``` 41 | 42 | Here is an example of an operation before and after: 43 | 44 | **Before**: 45 | 46 | ```yaml 47 | /fees: 48 | get: 49 | summary: List fees 50 | operationId: GetFees 51 | description: Retrieves collection of fees. 52 | responses: 53 | "200": 54 | description: Fees retrieved. 55 | content: 56 | application/json: 57 | schema: 58 | $ref: "#/components/schemas/Fees" 59 | examples: 60 | regular-fee: 61 | $ref: "#/components/examples/RegularFee" 62 | "401": 63 | $ref: "#/components/responses/Unauthorized" 64 | "403": 65 | $ref: "#/components/responses/Forbidden" 66 | "404": 67 | $ref: "#/components/responses/NotFound" 68 | ``` 69 | 70 | **After**: 71 | 72 | ```yaml 73 | /fees: 74 | get: 75 | summary: List fees 76 | operationId: GetFees 77 | description: Retrieves collection of fees. 78 | responses: 79 | "200": 80 | description: Fees retrieved. 81 | content: 82 | application/json: 83 | schema: 84 | $ref: "#/components/schemas/Fees" 85 | "401": 86 | $ref: "#/components/responses/Unauthorized" 87 | "403": 88 | $ref: "#/components/responses/Forbidden" 89 | "404": 90 | $ref: "#/components/responses/NotFound" 91 | ``` 92 | 93 | 🎉 Notice `examples` is removed from the `application/json` media type. 94 | 95 | ## References 96 | 97 | - https://learn.microsoft.com/en-us/azure/api-management/api-management-api-import-restrictions 98 | - OpenAPI [node types](https://redocly.com/docs/openapi-visual-reference/openapi-node-types/) 99 | -------------------------------------------------------------------------------- /custom-plugin-decorators/azure-apim/azure-apim.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | id: "azure-apim", 3 | decorators: { 4 | oas3: { 5 | "remove-examples": RemoveExamples, 6 | }, 7 | }, 8 | }; 9 | 10 | /** @type {import('@redocly/cli').OasDecorator} */ 11 | function RemoveExamples() { 12 | return { 13 | Schema: { 14 | leave(Schema) { 15 | if (Schema['examples']) { 16 | delete Schema['examples']; 17 | } 18 | } 19 | }, 20 | MediaType: { 21 | leave(MediaType) { 22 | if (MediaType['examples']) { 23 | delete MediaType['examples']; 24 | } 25 | } 26 | }, 27 | Components: { 28 | leave(Components) { 29 | if (Components['examples']) { 30 | delete Components['examples']; 31 | } 32 | } 33 | } 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /custom-plugin-decorators/openai-is-consequential/README.md: -------------------------------------------------------------------------------- 1 | # Decorator to add x-openai-isConsequential tag to GET operations 2 | 3 | Authors: 4 | 5 | - [`@adamaltman`](https://github.com/adamaltman), Adam Altman (Redocly) 6 | 7 | ## What this does and why 8 | 9 | OpenAI Actions uses OpenAPI to enable GPTs to make API calls. 10 | 11 | It has support for a consequential flag (documentation no longer available, but originally at `https://platform.openai.com/docs/actions/consequential-flag`): 12 | 13 | > If the field isn't present, we default all GET operations to false and all other operations to true 14 | 15 | If you want to set the GET operations to true too, then this decorator is for you. 16 | 17 | ## Code 18 | 19 | The code is entirely in `openai-is-consequential.js`: 20 | 21 | ```javascript 22 | module.exports = { 23 | id: "openai-plugin", 24 | decorators: { 25 | oas3: { 26 | "is-consequential": OpenAIConsequential, 27 | }, 28 | }, 29 | }; 30 | 31 | /** @type {import('@redocly/cli').OasDecorator} */ 32 | function OpenAIConsequential() { 33 | return { 34 | PathItem(PathItem) { 35 | if (PathItem["get"]) { 36 | PathItem["get"]["x-openai-isConsequential"] = true; 37 | } 38 | }, 39 | }; 40 | } 41 | ``` 42 | 43 | The code sets the plugin name to `openai-plugin` and adds a decorator named `is-consequential`. 44 | 45 | It operates on the `PathItem` element (in OpenAPI 3.x descriptions). 46 | 47 | The conditional logic applies to `get` operations. Modify the `if` statement if you want to target different operations. 48 | 49 | ## Examples 50 | 51 | Add the plugin to `redocly.yaml` and enable the decorator: 52 | 53 | ```yaml 54 | plugins: 55 | - ./openai-is-consequential.js 56 | 57 | decorators: 58 | openai-plugin/is-consequential: on 59 | ``` 60 | 61 | Here is an example of an operation before and after: 62 | 63 | **Before**: 64 | 65 | ```yaml 66 | /aml-settings: 67 | get: 68 | summary: Retrieve AML settings 69 | operationId: GetAmlSettings 70 | x-sdk-operation-name: getAmlSettings 71 | description: Retrieves AML settings. 72 | responses: 73 | "200": 74 | description: AML settings retrieved. 75 | content: 76 | application/json: 77 | schema: 78 | $ref: "#/components/schemas/AmlSettings" 79 | "401": 80 | $ref: "#/components/responses/Unauthorized" 81 | "403": 82 | $ref: "#/components/responses/Forbidden" 83 | "404": 84 | $ref: "#/components/responses/NotFound" 85 | ``` 86 | 87 | **After**: 88 | 89 | ```yaml 90 | /aml-settings: 91 | get: 92 | summary: Retrieve AML settings 93 | operationId: GetAmlSettings 94 | x-sdk-operation-name: getAmlSettings 95 | description: Retrieves AML settings. 96 | responses: 97 | "200": 98 | description: AML settings retrieved. 99 | content: 100 | application/json: 101 | schema: 102 | $ref: "#/components/schemas/AmlSettings" 103 | "401": 104 | $ref: "#/components/responses/Unauthorized" 105 | "403": 106 | $ref: "#/components/responses/Forbidden" 107 | "404": 108 | $ref: "#/components/responses/NotFound" 109 | x-openai-isConsequential: true 110 | ``` 111 | 112 | 🎉 Notice `x-openai-isConsequential: true` at the last line of the after example. 113 | 114 | ## References 115 | 116 | - The [`PathItem` types documentation](https://redocly.com/docs/openapi-visual-reference/path-item/#types) 117 | -------------------------------------------------------------------------------- /custom-plugin-decorators/openai-is-consequential/openai-is-consequential.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | id: "openai-plugin", 3 | decorators: { 4 | oas3: { 5 | "is-consequential": OpenAIConsequential, 6 | }, 7 | }, 8 | }; 9 | 10 | /** @type {import('@redocly/cli').OasDecorator} */ 11 | function OpenAIConsequential() { 12 | return { 13 | PathItem(PathItem) { 14 | if (PathItem["get"]) { 15 | PathItem["get"]["x-openai-isConsequential"] = true; 16 | } 17 | }, 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /custom-plugin-decorators/remove-extensions/README.md: -------------------------------------------------------------------------------- 1 | # Remove extensions 2 | 3 | Authors: 4 | 5 | - @nicobao (when working full time at @CIDgravity) 6 | 7 | ## What this does and why 8 | 9 | Go through each node of an OpenAPI document, and remove any given [OpenAPI Extensions](https://spec.openapis.org/oas/v3.1.0#specification-extensions) (must start with `x-`). 10 | 11 | Why? See 12 | 13 | ## Code 14 | 15 | The plugin itself can be found in [`remove-extensions.js'](./remove-extensions.js). The rest of this section shows you how to set up and use the plugin. 16 | 17 | Create a `plugin.js` file to refer to this file: 18 | 19 | ```js 20 | const RemoveExtensions = require("./remove-extensions"); 21 | const id = "plugin"; 22 | 23 | /** @type {import('@redocly/cli').DecoratorsConfig} */ 24 | const decorators = { 25 | oas3: { 26 | "remove-extensions": RemoveExtensions, 27 | }, 28 | }; 29 | 30 | module.exports = { 31 | id, 32 | decorators, 33 | }; 34 | ``` 35 | 36 | Create/edit `redocly.yaml` as follows (edit with your own settings): 37 | 38 | ```yml 39 | apis: 40 | unchanged@latest: 41 | root: ./petstore.yaml 42 | with-plugin@latest: 43 | root: ./petstore.yaml 44 | decorators: 45 | plugin/remove-extensions: 46 | extensions: 47 | - x-amazon* 48 | - x-google* 49 | plugins: 50 | - "./plugin.js" 51 | ``` 52 | 53 | Run the `bundle` command to remove all the [GCP](https://cloud.google.com/endpoints/docs/openapi/openapi-extensions) and [AWS](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-swagger-extensions.html) custom OpenAPI extensions from the OpenAPI description: 54 | 55 | ```bash 56 | redocly bundle with-plugin@latest --output dist/with-plugin.yaml 57 | ``` 58 | The `extensions` parameter is optional. If empty or not set, it will remove all extensions (elements starting with `x-`). The accepted values for the `extensions` param are: 59 | 60 | - `extensions: ` 61 | - `extensions: ` 62 | - `extensions: ` 63 | 64 | Regular expressions follow [Javascript Regex convention](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions). 65 | 66 | 67 | ## Examples 68 | 69 | With the same config as above. 70 | 71 | Input OpenAPI (`petstore.yaml`): 72 | 73 | ```yaml 74 | openapi: 3.0.0 75 | info: 76 | description: 77 | "This is a sample server Petstore server. You can find out more about 78 | Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, 79 | #swagger](http://swagger.io/irc/). For this sample, you can use the api key 80 | `special-key` to test the authorization filters." 81 | version: 1.0.2 82 | title: Swagger Petstore 83 | termsOfService: http://swagger.io/terms/ 84 | contact: 85 | email: apiteam@swagger.io 86 | license: 87 | name: Apache 2.0 88 | url: http://www.apache.org/licenses/LICENSE-2.0.html 89 | tags: 90 | - name: pet 91 | description: Everything about your Pets 92 | externalDocs: 93 | description: Find out more 94 | url: http://swagger.io 95 | - name: store 96 | description: Access to Petstore orders 97 | - name: user 98 | description: Operations about user 99 | externalDocs: 100 | description: Find out more about our store 101 | url: http://swagger.io 102 | x-amazon-apigateway-api-key-source: HEADER, 103 | paths: 104 | /pet: 105 | post: 106 | x-google-plugin-key-auth: 107 | name: key-auth 108 | enabled: true 109 | tags: 110 | - pet 111 | summary: Add a new pet to the store 112 | description: "" 113 | operationId: addPet 114 | requestBody: 115 | $ref: "#/components/requestBodies/Pet" 116 | responses: 117 | "405": 118 | description: Invalid input 119 | put: 120 | x-google-plugin-key-auth: 121 | name: key-auth 122 | enabled: true 123 | x-internal: true 124 | tags: 125 | - pet 126 | summary: Update an existing pet 127 | description: "" 128 | operationId: updatePet 129 | requestBody: 130 | $ref: "#/components/requestBodies/Pet" 131 | responses: 132 | "400": 133 | description: Invalid ID supplied 134 | "404": 135 | description: Pet not found 136 | "405": 137 | description: Validation exception 138 | /pet/findByStatus: 139 | get: 140 | x-google-plugin-key-auth: 141 | name: key-auth 142 | enabled: true 143 | tags: 144 | - pet 145 | summary: Finds Pets by status 146 | description: Multiple status values can be provided with comma separated strings 147 | operationId: findPetsByStatus 148 | parameters: 149 | - name: status 150 | in: query 151 | description: Status values that need to be considered for filter 152 | required: true 153 | explode: true 154 | schema: 155 | type: array 156 | items: 157 | type: string 158 | enum: 159 | - available 160 | - pending 161 | - sold 162 | default: available 163 | responses: 164 | "200": 165 | description: successful operation 166 | content: 167 | application/xml: 168 | schema: 169 | type: array 170 | items: 171 | $ref: "#/components/schemas/Pet" 172 | application/json: 173 | schema: 174 | type: array 175 | items: 176 | $ref: "#/components/schemas/Pet" 177 | "400": 178 | description: Invalid status value 179 | /pet/findByTags: 180 | get: 181 | tags: 182 | - pet 183 | summary: Finds Pets by tags 184 | description: 185 | Multiple tags can be provided with comma separated strings. Use tag1, 186 | tag2, tag3 for testing. 187 | operationId: findPetsByTags 188 | parameters: 189 | - name: tags 190 | in: query 191 | description: Tags to filter by 192 | required: true 193 | explode: true 194 | schema: 195 | type: array 196 | items: 197 | type: string 198 | responses: 199 | "200": 200 | description: successful operation 201 | content: 202 | application/xml: 203 | schema: 204 | type: array 205 | items: 206 | $ref: "#/components/schemas/Pet" 207 | application/json: 208 | schema: 209 | type: array 210 | items: 211 | $ref: "#/components/schemas/Pet" 212 | "400": 213 | description: Invalid tag value 214 | deprecated: true 215 | "/pet/{petId}": 216 | get: 217 | tags: 218 | - pet 219 | summary: Find pet by ID 220 | description: Returns a single pet 221 | operationId: getPetById 222 | parameters: 223 | - name: petId 224 | in: path 225 | description: ID of pet to return 226 | required: true 227 | schema: 228 | type: integer 229 | format: int64 230 | responses: 231 | "200": 232 | description: successful operation 233 | content: 234 | application/xml: 235 | schema: 236 | $ref: "#/components/schemas/Pet" 237 | application/json: 238 | schema: 239 | $ref: "#/components/schemas/Pet" 240 | "400": 241 | description: Invalid ID supplied 242 | "404": 243 | description: Pet not found 244 | externalDocs: 245 | description: Find out more about Swagger 246 | url: http://swagger.io 247 | servers: 248 | - url: https://petstore.swagger.io/v2 249 | components: 250 | requestBodies: 251 | Pet: 252 | content: 253 | application/json: 254 | schema: 255 | $ref: "#/components/schemas/Pet" 256 | application/xml: 257 | schema: 258 | $ref: "#/components/schemas/Pet" 259 | description: Pet object that needs to be added to the store 260 | required: true 261 | schemas: 262 | Category: 263 | type: object 264 | properties: 265 | id: 266 | type: integer 267 | format: int64 268 | name: 269 | type: string 270 | xml: 271 | name: Category 272 | Tag: 273 | type: object 274 | properties: 275 | id: 276 | type: integer 277 | format: int64 278 | name: 279 | type: string 280 | xml: 281 | name: Tag 282 | Pet: 283 | type: object 284 | required: 285 | - name 286 | - photoUrls 287 | properties: 288 | id: 289 | type: integer 290 | format: int64 291 | category: 292 | $ref: "#/components/schemas/Category" 293 | name: 294 | x-internal: true 295 | type: string 296 | example: doggie 297 | photoUrls: 298 | type: array 299 | xml: 300 | name: photoUrl 301 | wrapped: true 302 | items: 303 | type: string 304 | tags: 305 | type: array 306 | xml: 307 | name: tag 308 | wrapped: true 309 | items: 310 | $ref: "#/components/schemas/Tag" 311 | status: 312 | x-internal: true 313 | type: string 314 | description: pet status in the store 315 | enum: 316 | - available 317 | - pending 318 | - sold 319 | xml: 320 | name: Pet 321 | ``` 322 | 323 | Output OpenAPI (`with-plugin.yaml`): 324 | 325 | ```yaml 326 | openapi: 3.0.0 327 | info: 328 | description: 329 | "This is a sample server Petstore server. You can find out more about 330 | Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, 331 | #swagger](http://swagger.io/irc/). For this sample, you can use the api key 332 | `special-key` to test the authorization filters." 333 | version: 1.0.2 334 | title: Swagger Petstore 335 | termsOfService: http://swagger.io/terms/ 336 | contact: 337 | email: apiteam@swagger.io 338 | license: 339 | name: Apache 2.0 340 | url: http://www.apache.org/licenses/LICENSE-2.0.html 341 | tags: 342 | - name: pet 343 | description: Everything about your Pets 344 | externalDocs: 345 | description: Find out more 346 | url: http://swagger.io 347 | - name: store 348 | description: Access to Petstore orders 349 | - name: user 350 | description: Operations about user 351 | externalDocs: 352 | description: Find out more about our store 353 | url: http://swagger.io 354 | paths: 355 | /pet: 356 | post: 357 | tags: 358 | - pet 359 | summary: Add a new pet to the store 360 | description: "" 361 | operationId: addPet 362 | requestBody: 363 | $ref: "#/components/requestBodies/Pet" 364 | responses: 365 | "405": 366 | description: Invalid input 367 | put: 368 | x-internal: true 369 | tags: 370 | - pet 371 | summary: Update an existing pet 372 | description: "" 373 | operationId: updatePet 374 | requestBody: 375 | $ref: "#/components/requestBodies/Pet" 376 | responses: 377 | "400": 378 | description: Invalid ID supplied 379 | "404": 380 | description: Pet not found 381 | "405": 382 | description: Validation exception 383 | /pet/findByStatus: 384 | get: 385 | tags: 386 | - pet 387 | summary: Finds Pets by status 388 | description: Multiple status values can be provided with comma separated strings 389 | operationId: findPetsByStatus 390 | parameters: 391 | - name: status 392 | in: query 393 | description: Status values that need to be considered for filter 394 | required: true 395 | explode: true 396 | schema: 397 | type: array 398 | items: 399 | type: string 400 | enum: 401 | - available 402 | - pending 403 | - sold 404 | default: available 405 | responses: 406 | "200": 407 | description: successful operation 408 | content: 409 | application/xml: 410 | schema: 411 | type: array 412 | items: 413 | $ref: "#/components/schemas/Pet" 414 | application/json: 415 | schema: 416 | type: array 417 | items: 418 | $ref: "#/components/schemas/Pet" 419 | "400": 420 | description: Invalid status value 421 | /pet/findByTags: 422 | get: 423 | tags: 424 | - pet 425 | summary: Finds Pets by tags 426 | description: 427 | Multiple tags can be provided with comma separated strings. Use tag1, 428 | tag2, tag3 for testing. 429 | operationId: findPetsByTags 430 | parameters: 431 | - name: tags 432 | in: query 433 | description: Tags to filter by 434 | required: true 435 | explode: true 436 | schema: 437 | type: array 438 | items: 439 | type: string 440 | responses: 441 | "200": 442 | description: successful operation 443 | content: 444 | application/xml: 445 | schema: 446 | type: array 447 | items: 448 | $ref: "#/components/schemas/Pet" 449 | application/json: 450 | schema: 451 | type: array 452 | items: 453 | $ref: "#/components/schemas/Pet" 454 | "400": 455 | description: Invalid tag value 456 | deprecated: true 457 | "/pet/{petId}": 458 | get: 459 | tags: 460 | - pet 461 | summary: Find pet by ID 462 | description: Returns a single pet 463 | operationId: getPetById 464 | parameters: 465 | - name: petId 466 | in: path 467 | description: ID of pet to return 468 | required: true 469 | schema: 470 | type: integer 471 | format: int64 472 | responses: 473 | "200": 474 | description: successful operation 475 | content: 476 | application/xml: 477 | schema: 478 | $ref: "#/components/schemas/Pet" 479 | application/json: 480 | schema: 481 | $ref: "#/components/schemas/Pet" 482 | "400": 483 | description: Invalid ID supplied 484 | "404": 485 | description: Pet not found 486 | externalDocs: 487 | description: Find out more about Swagger 488 | url: http://swagger.io 489 | servers: 490 | - url: https://petstore.swagger.io/v2 491 | components: 492 | requestBodies: 493 | Pet: 494 | content: 495 | application/json: 496 | schema: 497 | $ref: "#/components/schemas/Pet" 498 | application/xml: 499 | schema: 500 | $ref: "#/components/schemas/Pet" 501 | description: Pet object that needs to be added to the store 502 | required: true 503 | schemas: 504 | Category: 505 | type: object 506 | properties: 507 | id: 508 | type: integer 509 | format: int64 510 | name: 511 | type: string 512 | xml: 513 | name: Category 514 | Tag: 515 | type: object 516 | properties: 517 | id: 518 | type: integer 519 | format: int64 520 | name: 521 | type: string 522 | xml: 523 | name: Tag 524 | Pet: 525 | type: object 526 | required: 527 | - name 528 | - photoUrls 529 | properties: 530 | id: 531 | type: integer 532 | format: int64 533 | category: 534 | $ref: "#/components/schemas/Category" 535 | name: 536 | x-internal: true 537 | type: string 538 | example: doggie 539 | photoUrls: 540 | type: array 541 | xml: 542 | name: photoUrl 543 | wrapped: true 544 | items: 545 | type: string 546 | tags: 547 | type: array 548 | xml: 549 | name: tag 550 | wrapped: true 551 | items: 552 | $ref: "#/components/schemas/Tag" 553 | status: 554 | x-internal: true 555 | type: string 556 | description: pet status in the store 557 | enum: 558 | - available 559 | - pending 560 | - sold 561 | xml: 562 | name: Pet 563 | ``` 564 | 565 | ## References 566 | 567 | Copyright CIDgravity, @nicobao, 2022. 568 | 569 | Copy-pasted from: 570 | 571 | - https://github.com/CIDgravity/redoc-plugins 572 | - https://github.com/nicobao/redoc-plugins 573 | 574 | This decorator was originally licensed under both the Apache v2 license and the MIT license. 575 | -------------------------------------------------------------------------------- /custom-plugin-decorators/remove-extensions/remove-extensions.js: -------------------------------------------------------------------------------- 1 | module.exports = RemoveExtensions; 2 | 3 | /** @type {import('@redocly/cli').OasDecorator} */ 4 | 5 | const openAPIExtensions = /x-*/; 6 | 7 | // a console.assert that actually abort execution 8 | // message is str 9 | // bool is boolean 10 | function assert(bool, message) { 11 | if (!bool) { 12 | const err = message !== undefined ? new Error(message) : new Error("an error occured") 13 | throw err 14 | } 15 | } 16 | 17 | function doRemoveParamFromNode(node, param) { 18 | assert(typeof param === 'string', "extension must be a string") 19 | assert(isExtensionValid(param), `[Aborting] String "${param}" is not a valid OpenAPI extension, it must begin with "x-"`) 20 | delete node[param] 21 | console.log('Deleteted extension "%s" from object "%O"', param, node) 22 | } 23 | 24 | function isExtensionValid(extension) { 25 | assert(typeof extension === 'string', "extension must be a string") 26 | if (extension.match(openAPIExtensions)) { 27 | return true 28 | } else { 29 | return false 30 | } 31 | } 32 | 33 | function removeExtensionsFromNode(node, extensions) { 34 | const extensionsType = typeof extensions 35 | assert(extensionsType === undefined || extensionsType === 'string' || extensionsType === 'object', `Extensions must be a string or a list of string instead of being of type "${extensionsType}"`) 36 | if (extensions === undefined || extensions === null || extensions === {} || extensions === '') { 37 | console.log('Deleting all OpenAPI extensions (params starting with "x-")...') 38 | Object.keys(node).filter(param => param.match(openAPIExtensions)).forEach((param) => { 39 | doRemoveParamFromNode(node, param) 40 | }) 41 | } else if (extensionsType === 'string') { 42 | // extensions is a string representing a regex - delete all the params that match this regex 43 | Object.keys(node).filter(param => param.match(extensions)).forEach((param) => { 44 | doRemoveParamFromNode(node, param) 45 | }) 46 | } else { 47 | // extensions a list 48 | // only return something if all strings are valid OpenAPI spec, otherwise panic (handled by the assert) 49 | extensions.forEach((extension) => { 50 | // extension is a string representing a regex - delete all the params that match this regex 51 | Object.keys(node).filter(param => param.match(extension)).forEach((param) => { 52 | doRemoveParamFromNode(node, param) 53 | }) 54 | }) 55 | } 56 | } 57 | 58 | function RemoveExtensions({extensions}) { 59 | return { 60 | any: { 61 | enter: (node, _ctx) => removeExtensionsFromNode(node, extensions), 62 | } 63 | } 64 | }; 65 | -------------------------------------------------------------------------------- /custom-plugin-decorators/remove-unused-tags/README.md: -------------------------------------------------------------------------------- 1 | # Remove unused tags 2 | 3 | Authors: 4 | - [`@lornajane`](https://github.com/lornajane), Lorna Mitchell (Redocly) 5 | 6 | 7 | ## What this does and why 8 | 9 | A custom plugin with a decorator that looks at each operation in the API description, and counts how many times each tag is used. Any that are not used by any operation are removed. 10 | 11 | There is support for an array of tags to ignore; these tags won't be removed from the API description even if they are unused. 12 | 13 | This is a useful decorator to use when you are reducing a larger OpenAPI file for a specific output case. Using the [filter-out decorator](https://redocly.com/docs/cli/decorators/filter-out/) for example can leave tags that are no longer needed. 14 | 15 | ## Code 16 | 17 | The plugin code itself is in `tags.js`: 18 | 19 | ```js 20 | module.exports = { 21 | id: "tags", 22 | decorators: { 23 | oas3: { 24 | "no-unused-tags": ({ ignore }) => { 25 | console.log("Cleaning up unused tags..."); 26 | // mark the ignored tags as already used so we don't remove them 27 | const usedTags = new Set(ignore?.map((tag) => tag.toLowerCase())); 28 | return { 29 | Operation:{ 30 | enter(operation) { 31 | // log all the tags that are in use 32 | for (const tag of operation.tags) { 33 | usedTags.add(tag.toLowerCase()); 34 | } 35 | } 36 | }, 37 | Root: { 38 | leave(root) { 39 | // remove any tags that we didn't find in use or marked to ignore 40 | root.tags = root.tags.filter((tag) => usedTags.has(tag.name.toLowerCase())); 41 | return root; 42 | }, 43 | }, 44 | }; 45 | }, 46 | }, 47 | }, 48 | }; 49 | ``` 50 | 51 | In summary, this code does the following: 52 | 53 | 1. Take the supplied ignore values (if there are any), convert them to lower case, and add them to the `usedTags` set so that they don't get removed. 54 | 55 | 2. Visit each operation in the description and add all tags used to the `usedTags` set. 56 | 57 | 3. Examine the tags declared in the OpenAPI document, and remove any that aren't found in the `usedTags` set. 58 | 59 | To use the custom decorator, add configuration like the following to the `redocly.yaml` file: 60 | 61 | ```yaml 62 | plugins: 63 | - './tags.js' 64 | 65 | decorators: 66 | tags/no-unused-tags: on 67 | ``` 68 | 69 | If there are tags that should be preserved even though they are unused, add them to the ignore list: 70 | 71 | ```yaml 72 | plugins: 73 | - './tags.js' 74 | 75 | decorators: 76 | tags/no-unused-tags: 77 | ignore: 78 | - extra 79 | - KeepMe 80 | ``` 81 | 82 | Apply the decorator by running the bundle command: 83 | 84 | ```bash 85 | redocly bundle openapi.yaml -o openapi-tidy.yaml 86 | ``` 87 | 88 | The new file `openapi-tidy.yaml` contains the API description with only the in-use and ignored tags included. 89 | 90 | The checking is case-insensitive (it seems more likely to mistype a tag's case than to intentionally have two tags named the same with different case - although I'm sure both exist somewhere!). 91 | 92 | ## Examples 93 | 94 | Start by adding the decorator to `redocly.yaml` and including some ignore settings: 95 | 96 | ```yaml 97 | plugins: 98 | - './tags.js' 99 | 100 | decorators: 101 | tags/no-unused-tags: 102 | ignore: 103 | - extra 104 | - KeepMe 105 | ``` 106 | 107 | Given an API description that uses only the `Events` and `Tickets` tags, the tags section would be transformed to remove the other tags. 108 | 109 | **Before bundling/decorating**: 110 | 111 | ```yaml 112 | tags: 113 | - name: Tickets 114 | description: Museum tickets for general entrance or special events. 115 | - name: Extraneous 116 | description: This tag isn't used by any of the endpoints, so that should be detected and corrected. 117 | - name: Extra 118 | description: This tag isn't used by any of the endpoints, but we're keeping it anyway. 119 | - name: Events 120 | description: Special events hosted by the Museum 121 | ``` 122 | 123 | Run the decorator and observe the API description tags section **after bundling/decorating**: 124 | 125 | ```yaml 126 | tags: 127 | - name: Tickets 128 | description: Museum tickets for general entrance or special events. 129 | - name: Extra 130 | description: This tag isn't used by any of the endpoints, but we're keeping it anyway. 131 | - name: Events 132 | description: Special events hosted by the Museum 133 | ``` 134 | 135 | Use this decorator to tidy up when leftover tags remain in an OpenAPI description. 136 | 137 | ## References 138 | 139 | - Inspired by [issue #953 on Redocly CLI](https://github.com/Redocly/redocly-cli/issues/953). 140 | -------------------------------------------------------------------------------- /custom-plugin-decorators/remove-unused-tags/redocly.yaml: -------------------------------------------------------------------------------- 1 | plugins: 2 | - './tags.js' 3 | 4 | decorators: 5 | tags/no-unused-tags: on 6 | -------------------------------------------------------------------------------- /custom-plugin-decorators/remove-unused-tags/tags.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | id: "tags", 3 | decorators: { 4 | oas3: { 5 | "no-unused-tags": ({ ignore }) => { 6 | console.log("Cleaning up unused tags..."); 7 | // mark the ignored tags as already used so we don't remove them 8 | const usedTags = new Set(ignore?.map((tag) => tag.toLowerCase())); 9 | return { 10 | Operation:{ 11 | enter(operation) { 12 | // log all the tags that are in use 13 | for (const tag of operation.tags) { 14 | usedTags.add(tag.toLowerCase()); 15 | } 16 | } 17 | }, 18 | Root: { 19 | leave(root) { 20 | // remove any tags that we didn't find in use or marked to ignore 21 | root.tags = root.tags.filter((tag) => usedTags.has(tag.name.toLowerCase())); 22 | return root; 23 | }, 24 | }, 25 | }; 26 | }, 27 | }, 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /custom-plugin-decorators/swap-summary-description/README.md: -------------------------------------------------------------------------------- 1 | # Swap the summary and description fields 2 | 3 | Authors: 4 | - [`@lornajane`](https://github.com/lornajane), Lorna Mitchell (Redocly) 5 | 6 | ## What this does and why 7 | 8 | We sometimes see API descriptions with the fields mixed up. 9 | The most common of these is the `summary` and `description` fields on Operations in OpenAPI, some generators seem to produce the fields with the content reversed. 10 | 11 | * Summary: used when the operations are displayed in a list, it should be a very short phrase to describe the operation. 12 | * Description: used to supply more detail, used when the operation is displayed in detail. The description field also suports Markdown. 13 | 14 | This decorator takes the content of both fields and (as long as there is some content in the description field), swaps them over. 15 | 16 | ## Code 17 | 18 | The following code snippet shows the decorator, in a file named `swap-fields.js`: 19 | 20 | ```js 21 | module.exports = { 22 | id: "swap-fields", 23 | decorators: { 24 | oas3: { 25 | "summary-description": () => { 26 | return { 27 | Operation: { 28 | leave(target) { 29 | let description = ""; 30 | let summary = ""; 31 | if (target.description) { 32 | description = target.description 33 | } 34 | if (target.summary) { 35 | summary = target.summary 36 | } 37 | 38 | // only swap them if there is some description content 39 | if(description.length > 0){ 40 | target.description = summary; 41 | target.summary = description; 42 | } 43 | }, 44 | }, 45 | }; 46 | }, 47 | }, 48 | }, 49 | }; 50 | ``` 51 | 52 | Put this file alongside your `redocly.yaml` file, and add the following configuration to `redocly.yaml`: 53 | 54 | ```yaml 55 | plugins: 56 | - swap-fields.js 57 | 58 | decorators: 59 | swap-fields/summary-description: on 60 | ``` 61 | 62 | When you run `redocly bundle`, the API description(s) will have their field order updated. 63 | 64 | ## Examples 65 | 66 | Before the change (a tiny snippet from the [GitHub API Reference](https://github.com/github/rest-api-description) where I initially spotted this problem) with an OpenAPI structure around it: 67 | 68 | ```yaml 69 | openapi: 3.1.0 70 | info: 71 | version: 1.1.4 72 | title: GitHub v3 REST API 73 | description: GitHub's v3 REST API. 74 | license: 75 | name: MIT 76 | url: https://spdx.org/licenses/MIT 77 | termsOfService: https://docs.github.com/articles/github-terms-of-service 78 | contact: 79 | name: Support 80 | url: https://support.github.com/contact?tags=dotcom-rest-api 81 | webhooks: 82 | branch-protection-configuration-disabled: 83 | post: 84 | summary: |- 85 | This event occurs when there is a change to branch protection configurations for a repository. 86 | For more information, see "[About protected branches](https://docs.github.com/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/about-protected-branches)." 87 | For information about using the APIs to manage branch protection rules, see "[Branch protection rule](https://docs.github.com/graphql/reference/objects#branchprotectionrule)" in the GraphQL documentation or "[Branch protection](https://docs.github.com/rest/branches/branch-protection)" in the REST API documentation. 88 | 89 | To subscribe to this event, a GitHub App must have at least read-level access for the "Administration" repository permission. 90 | description: All branch protections were disabled for a repository. 91 | operationId: branch-protection-configuration/disabled 92 | requestBody: 93 | required: true 94 | content: 95 | application/json: 96 | schema: 97 | "$ref": "#/components/schemas/webhook-branch-protection-configuration-disabled" 98 | responses: 99 | '200': 100 | description: Return a 200 status to indicate that the data was received 101 | successfully 102 | 103 | components: 104 | schemas: 105 | webhook-branch-protection-configuration-disabled: 106 | title: branch protection configuration disabled event 107 | type: object 108 | properties: 109 | action: 110 | type: string 111 | enum: 112 | - disabled 113 | ``` 114 | 115 | After the decorator has been run, the updated file looks like the following example: 116 | 117 | ```yaml 118 | openapi: 3.1.0 119 | info: 120 | version: 1.1.4 121 | title: GitHub v3 REST API 122 | description: GitHub's v3 REST API. 123 | license: 124 | name: MIT 125 | url: https://spdx.org/licenses/MIT 126 | termsOfService: https://docs.github.com/articles/github-terms-of-service 127 | contact: 128 | name: Support 129 | url: https://support.github.com/contact?tags=dotcom-rest-api 130 | webhooks: 131 | branch-protection-configuration-disabled: 132 | post: 133 | summary: All branch protections were disabled for a repository. 134 | description: |- 135 | This event occurs when there is a change to branch protection configurations for a repository. 136 | For more information, see "[About protected branches](https://docs.github.com/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/about-protected-branches)." 137 | For information about using the APIs to manage branch protection rules, see "[Branch protection rule](https://docs.github.com/graphql/reference/objects#branchprotectionrule)" in the GraphQL documentation or "[Branch protection](https://docs.github.com/rest/branches/branch-protection)" in the REST API documentation. 138 | 139 | To subscribe to this event, a GitHub App must have at least read-level access for the "Administration" repository permission. 140 | operationId: branch-protection-configuration/disabled 141 | requestBody: 142 | required: true 143 | content: 144 | application/json: 145 | schema: 146 | $ref: '#/components/schemas/webhook-branch-protection-configuration-disabled' 147 | responses: 148 | '200': 149 | description: Return a 200 status to indicate that the data was received successfully 150 | components: 151 | schemas: 152 | webhook-branch-protection-configuration-disabled: 153 | title: branch protection configuration disabled event 154 | type: object 155 | properties: 156 | action: 157 | type: string 158 | enum: 159 | - disabled 160 | ``` 161 | 162 | You could also edit the plugin to make other field changes as you need. 163 | 164 | ## References 165 | 166 | - [GitHub REST API descriptions](https://github.com/github/rest-api-description) 167 | - [OpenAPI reference](https://spec.openapis.org/oas/latest.html) 168 | -------------------------------------------------------------------------------- /custom-plugin-decorators/swap-summary-description/redocly.yaml: -------------------------------------------------------------------------------- 1 | plugins: 2 | - swap-fields.js 3 | 4 | decorators: 5 | swap-fields/summary-description: on 6 | -------------------------------------------------------------------------------- /custom-plugin-decorators/swap-summary-description/swap-fields.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | id: "swap-fields", 3 | decorators: { 4 | oas3: { 5 | "summary-description": () => { 6 | return { 7 | Operation: { 8 | leave(target) { 9 | let description = ""; 10 | let summary = ""; 11 | if (target.description) { 12 | description = target.description 13 | } 14 | if (target.summary) { 15 | summary = target.summary 16 | } 17 | 18 | // only swap them if there is some description content 19 | if(description.length > 0){ 20 | target.description = summary; 21 | target.summary = description; 22 | } 23 | }, 24 | }, 25 | }; 26 | }, 27 | }, 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /custom-plugin-decorators/tag-sorting/README.md: -------------------------------------------------------------------------------- 1 | # Decorator to sort tags in an API description 2 | 3 | Authors: 4 | - [`@lornajane`](https://github.com/lornajane), Lorna Mitchell (Redocly) 5 | 6 | ## What this does and why 7 | 8 | Redocly CLI has the [`tags-alphabetical`](https://redocly.com/docs/cli/rules/tags-alphabetical/) rule to error if the `tags` section isn't in alphabetical order by `name`. 9 | 10 | This decorator provides functionality to _put_ the tags in order by name, in order to make any API description compatible with that rule. 11 | 12 | ## Code 13 | 14 | Here's the main plugin entrypoint, it's in `tag-sorting.js`: 15 | 16 | ```javascript 17 | 18 | const SortTagsAlphabetically = require('./decorator-alpha'); 19 | 20 | module.exports = function tagSortingPlugin() { 21 | return { 22 | id: 'tag-sorting', 23 | decorators: { 24 | oas3: { 25 | 'alphabetical': SortTagsAlphabetically, 26 | } 27 | } 28 | } 29 | } 30 | ``` 31 | 32 | The code here sets the plugin name to `tag-sorting` and adds a decorator for OpenAPI 3 called `alphabetical`. 33 | 34 | The functionality for the alphabetical sorting is in `decorator-alpha.js`: 35 | 36 | ```javascript 37 | 38 | module.exports = SortTagsAlphabetically; 39 | 40 | function SortTagsAlphabetically() { 41 | console.log("re-ordering tags: alphabetical"); 42 | return { 43 | TagList: { 44 | leave(target) { 45 | target.sort((a,b) => { 46 | if (a.name < b.name) { 47 | return -1; 48 | } 49 | }); 50 | } 51 | } 52 | } 53 | } 54 | ``` 55 | 56 | It operates on the `TagList` element, since it needs to shuffle the child entries inside it. A simple comparison of the name field is used to sort the items. 57 | 58 | ## Examples 59 | 60 | Add the plugin to `redocly.yaml` and enable the decorator: 61 | 62 | ```yaml 63 | plugins: 64 | - './tag-sorting.js' 65 | 66 | decorators: 67 | tag-sorting/alphabetical: on 68 | ``` 69 | 70 | Now with an OpenAPI description that has more than one tag listed at the top level, the decorator will re-order the entries. 71 | 72 | **Before**: 73 | 74 | ```yaml 75 | tags: 76 | - name: user 77 | description: Users 78 | - name: site 79 | description: Restaurant sites 80 | ``` 81 | 82 | **After**: 83 | 84 | ```yaml 85 | tags: 86 | - name: site 87 | description: Restaurant sites 88 | - name: user 89 | description: Users 90 | ``` 91 | 92 | ## References 93 | 94 | * [`tags-alphabetical' rule](https://redocly.com/docs/cli/rules/tags-alphabetical/) 95 | * The [`tags` types documentation](https://redocly.com/docs/openapi-visual-reference/tags/#types) 96 | 97 | -------------------------------------------------------------------------------- /custom-plugin-decorators/tag-sorting/decorator-alpha.js: -------------------------------------------------------------------------------- 1 | module.exports = SortTagsAlphabetically; 2 | 3 | function SortTagsAlphabetically() { 4 | console.log("re-ordering tags: alphabetical"); 5 | return { 6 | TagList: { 7 | leave(target) { 8 | target.sort((a,b) => { 9 | if (a.name < b.name) { 10 | return -1; 11 | } 12 | }); 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /custom-plugin-decorators/tag-sorting/tag-sorting.js: -------------------------------------------------------------------------------- 1 | const SortTagsAlphabetically = require('./decorator-alpha'); 2 | 3 | module.exports = function tagSortingPlugin() { 4 | return { 5 | id: 'tag-sorting', 6 | decorators: { 7 | oas3: { 8 | 'alphabetical': SortTagsAlphabetically, 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /custom-plugin-decorators/update-example-dates/README.md: -------------------------------------------------------------------------------- 1 | # Substitute datetime placeholders in an API description 2 | 3 | Authors: 4 | 5 | - [`@tatomyr`](https://github.com/tatomyr), Andrew Tatomyr (Redocly) 6 | 7 | ## What this does and why 8 | 9 | When an API description includes datetime values, it's nice if they present soon or realistic date values. 10 | This decorator provides the functionality to replace a special datetime placeholder, `$DateTimeNow`, with the current date and time when bundling the API description. 11 | You can also use expressions to add a set number of days to the value, for example `$DateTimeNow + 10 days`. 12 | 13 | > [!WARNING] 14 | > Placeholders aren't supported by all OpenAPI tools. Using this approach, you should bundle your OpenAPI to apply the decorators before passing the API description to other tools. 15 | 16 | ## Code 17 | 18 | The `dates-plugin` plugin defines the `decorator` section and the plugin `id`: 19 | 20 | ```javascript 21 | const updateExampleDates = require("./decorator"); 22 | 23 | module.exports = { 24 | id: "dates-plugin", 25 | decorators: { 26 | oas3: { 27 | "update-example-dates": updateExampleDates, 28 | }, 29 | }, 30 | }; 31 | ``` 32 | 33 | Here's the main part of the decorator (from `decorator.js`): 34 | 35 | ```javascript 36 | function updateExampleDates() { 37 | return { 38 | // Covers the 'examples' keyword (including examples in the 'components' section) 39 | Example: { 40 | enter(example) { 41 | traverseAndReplaceWithComputedDateTime(example.value); 42 | }, 43 | }, 44 | // Covers the 'example' in media type objects 45 | MediaType: { 46 | enter(mediaTypeObject) { 47 | if (mediaTypeObject.example) { 48 | traverseAndReplaceWithComputedDateTime(mediaTypeObject.example); 49 | } 50 | }, 51 | }, 52 | // Covers the 'example' in schemas 53 | Schema: { 54 | enter(schema) { 55 | if (schema.example) { 56 | traverseAndReplaceWithComputedDateTime(schema); 57 | } 58 | }, 59 | }, 60 | }; 61 | } 62 | ``` 63 | 64 | The decorator operates on the `Example`, `MediaType` and `Schema` nodes to cover the different ways of specifying examples. 65 | 66 | The `traverseAndReplaceWithComputedDateTime` function traverses the tree of nodes and replaces the values that contain the specific `$DateTimeNow` variable in examples with a computed datetime value: 67 | 68 | ```javascript 69 | function traverseAndReplaceWithComputedDateTime(value) { 70 | if (typeof value === "object" && value !== null) { 71 | for (const key in value) { 72 | if (typeof value[key] === "string" && /\$DateTimeNow/.test(value[key])) { 73 | // Replacing the template with a computed datetime value 74 | value[key] = computeDateTemplate(value[key]); 75 | } else { 76 | traverseAndReplaceWithComputedDateTime(value[key]); 77 | } 78 | } 79 | } 80 | } 81 | ``` 82 | 83 | The `computeDateTemplate` function is a very basic datetime calculator that can only add days to the current date, but it illustrates the idea and you can extend it to meet your own specific needs: 84 | 85 | ```javascript 86 | function computeDateTemplate(template) { 87 | const substitutedExpression = template 88 | .replace("$DateTimeNow", Date.now()) // Replacing the variable with the current date in milliseconds 89 | .replace(/(\d+)\s*days?/g, (_, t) => t * 1000 * 60 * 60 * 24) // Replacing days with milliseconds 90 | .trim(); 91 | const calculated = substitutedExpression // A basic calculator 92 | .split(/\s*\+\s*/) 93 | .reduce((sum, item) => sum + +item, 0); 94 | return new Date(calculated).toISOString(); 95 | } 96 | ``` 97 | 98 | ## Examples 99 | 100 | Add the plugin to `redocly.yaml` and enable the decorator: 101 | 102 | ```yaml 103 | plugins: 104 | - ./plugin.js 105 | 106 | decorators: 107 | dates-plugin/update-example-dates: on 108 | ``` 109 | 110 | Given the following API description, the decorator will replace `$DateTimeNow` with the current date and `$DateTimeNow + 7d` with the date a week from now: 111 | 112 | ```yaml 113 | openapi: 3.1.0 114 | paths: 115 | /date-time: 116 | get: 117 | responses: 118 | 200: 119 | content: 120 | application/json: 121 | example: 122 | somewhere-in-the-past: 2023-11-06T12:00:00.000Z 123 | today: $DateTimeNow # Will be replaced with the current date 124 | in-a-week: $DateTimeNow + 7 days # Will be replaced with the date in a week 125 | ``` 126 | 127 | Please note that API descriptions are static text documents, so the dates will get updated only when the description gets bundled. 128 | 129 | ## References 130 | 131 | - [Redocly documentation on examples in OpenAPI](https://redocly.com/docs/openapi-visual-reference/schemas/#example-and-examples) 132 | 133 | - [OpenAPI Specification on Media Type Objects](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#media-type-object) 134 | -------------------------------------------------------------------------------- /custom-plugin-decorators/update-example-dates/decorator.js: -------------------------------------------------------------------------------- 1 | function updateExampleDates() { 2 | return { 3 | // Covers the 'examples' keyword (including examples in the 'components' section) 4 | Example: { 5 | enter(example) { 6 | traverseAndReplaceWithComputedDateTime(example.value); 7 | }, 8 | }, 9 | // Covers the 'example' in media type objects 10 | MediaType: { 11 | enter(mediaTypeObject) { 12 | if (mediaTypeObject.example) { 13 | traverseAndReplaceWithComputedDateTime(mediaTypeObject.example); 14 | } 15 | }, 16 | }, 17 | // Covers the 'example' in schemas 18 | Schema: { 19 | enter(schema) { 20 | if (schema.example) { 21 | traverseAndReplaceWithComputedDateTime(schema); 22 | } 23 | }, 24 | }, 25 | }; 26 | } 27 | 28 | function traverseAndReplaceWithComputedDateTime(value) { 29 | if (typeof value === "object" && value !== null) { 30 | for (const key in value) { 31 | if (typeof value[key] === "string" && /\$DateTimeNow/.test(value[key])) { 32 | // Replacing the template with a computed datetime value 33 | value[key] = computeDateTemplate(value[key]); 34 | } else { 35 | traverseAndReplaceWithComputedDateTime(value[key]); 36 | } 37 | } 38 | } 39 | } 40 | 41 | function computeDateTemplate(template) { 42 | const substitutedExpression = template 43 | .replace("$DateTimeNow", Date.now()) // Replacing the variable with the current date in milliseconds 44 | .replace(/(\d+)\s*days?/g, (_, t) => t * 1000 * 60 * 60 * 24) // Replacing days with milliseconds 45 | .trim(); 46 | const calculated = substitutedExpression // A basic calculator 47 | .split(/\s*\+\s*/) 48 | .reduce((sum, item) => sum + +item, 0); 49 | return new Date(calculated).toISOString(); 50 | } 51 | 52 | module.exports = updateExampleDates; 53 | -------------------------------------------------------------------------------- /custom-plugin-decorators/update-example-dates/plugin.js: -------------------------------------------------------------------------------- 1 | const updateExampleDates = require("./decorator"); 2 | 3 | module.exports = { 4 | id: "dates-plugin", 5 | decorators: { 6 | oas3: { 7 | "update-example-dates": updateExampleDates, 8 | }, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /custom-plugin-rules/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Redocly/redocly-cli-cookbook/d155441515902392de067b4a77a836c9ce5fdd6d/custom-plugin-rules/.gitignore -------------------------------------------------------------------------------- /custom-plugin-rules/code-sample-checks/README.md: -------------------------------------------------------------------------------- 1 | # Check code sample coverage 2 | 3 | Authors: 4 | - Laura Rubin @SansContext (with great help from Manny Silva @hawkeyexl in the Write the Docs Slack) 5 | 6 | ## What this does and why 7 | 8 | This rule checks to see if each operation has examples for a static set of specified languages. In the original case, I have six languages that we try to cover for example code samples, and it's helpful to know which routes are missing which samples, so I can prioritize and burn down patching them. 9 | 10 | This loads the `lang:` fields specified in the `x-code-samples` array, and then compares those languages to a `const` list of languages that you specify in the top of `check-sdk-coverage.js`. If you have different wording or different SDKs, you can add those here. 11 | 12 | You probably want to set these to `warn` if you think you're going to have a lot of them. 13 | 14 | **Tip**: you might want to use these in conjunction with a configurable rule that checks if you even _have_ an `x-code-samples` array... 15 | 16 | ```yaml 17 | rule/x-code-samples-exist: 18 | subject: 19 | type: Operation 20 | property: x-code-samples 21 | assertions: 22 | defined: true 23 | message: "Each path must have an x-code-samples object." 24 | severity: warn 25 | ``` 26 | 27 | ## Code 28 | 29 | These examples show how to create a [custom plugin](https://redocly.com/docs/cli/custom-plugins) with a rule to check that the expected code samples are present. 30 | 31 | ### Plugin - `x-code-sample-checks.js` 32 | 33 | First the main plugin code in `x-code-sample-checks.js`: 34 | 35 | ```js 36 | const CheckSDKCoverage = require('./rules/check-sdk-coverage.js'); 37 | 38 | module.exports = function myRulesPlugin() { 39 | return { 40 | id: 'x-code-samples-check', 41 | rules: { 42 | oas3: { 43 | 'check-sdk-coverage': CheckSDKCoverage 44 | }, 45 | }, 46 | }; 47 | }; 48 | ``` 49 | 50 | Save this file and import the plugin by adding the following example to `redocly.yaml`: 51 | 52 | ```yaml 53 | plugins: 54 | - ./x-code-sample-checks.js 55 | ``` 56 | 57 | ### Rule - Check SDK Coverage 58 | 59 | The main functionality of the plugin is in this file `check-dks-coverage.js`, which is as follows: 60 | 61 | ```js 62 | module.exports = CheckSDKCoverage; 63 | 64 | const sdkLanguages = ['bash', 'javascript', 'python', 'ruby', 'java', 'kotlin']; 65 | 66 | function CheckSDKCoverage () { 67 | return { 68 | XCodeSampleList: { 69 | enter(codeSampleList, ctx) { 70 | // Make sure the list contains at least one bash sample 71 | const hasBashSample = codeSampleList.some((codeSample) => { 72 | return codeSample.lang === 'bash'; 73 | }); 74 | //check for the other SDK languages by making an array of the lang fields from the code samples 75 | const langArray = codeSampleList.map((codeSample) => { 76 | return codeSample.lang; 77 | }); 78 | //compare the sdkLanguages array with the langArray to find the missing languages, and save them to an array 79 | const missingLanguages = sdkLanguages.filter((lang) => { 80 | return !langArray.includes(lang); 81 | } 82 | ); 83 | //if there are missing languages, report them as warnings 84 | // might want to make this less verbose later 85 | if (missingLanguages.length > 0) { 86 | ctx.report({ 87 | message: `Only ${langArray.length} code samples: ${langArray.join(', ')} but is missing the following SDK languages: ${missingLanguages.join(', ')}`, 88 | }); 89 | } 90 | } 91 | } 92 | }; 93 | } 94 | ``` 95 | 96 | Edit the `sdkLanguages` array to specify the languages that every endpoint should include. 97 | 98 | Enable the rule in `redocly.yaml` like this: 99 | 100 | ```yaml 101 | rules: 102 | x-code-samples-check/check-sdk-coverage: error 103 | ``` 104 | 105 | ## Examples 106 | 107 | The following sections show part of an API description, and the expected output. 108 | 109 | ### API description snippet 110 | 111 | ```yaml 112 | post: 113 | operationId: custom_auth_flow 114 | summary: Custom Authentication 115 | description: 116 | [...] # Omitted for brevity 117 | security: 118 | [...] # Omitted for brevity 119 | requestBody: 120 | [...] # Omitted for brevity 121 | responses: 122 | [...] # Omitted for brevity 123 | x-code-samples: 124 | - lang: bash 125 | label: cURL 126 | source: 127 | $ref: ./code_samples/auth/custom_auth/POST/curl.sh 128 | - lang: ruby 129 | label: Ruby SDK 130 | source: 131 | $ref: ./code_samples/auth/custom_auth/POST/ruby.rb 132 | - lang: python 133 | label: Python SDK 134 | source: 135 | $ref: ./code_samples/auth/custom_auth/POST/python.py 136 | - lang: java 137 | label: Java SDK 138 | source: 139 | $ref: ./code_samples/auth/custom_auth/POST/java.java 140 | - lang: kotlin 141 | label: Kotlin SDK 142 | source: 143 | $ref: ./code_samples/auth/custom_auth/POST/kotlin.kt 144 | ``` 145 | 146 | ### Output 147 | 148 | ```bash 149 | api-docs/paths/auth/custom.yaml: 150 | 435:5 error x-code-samples-check/check-sdk-coverage Only 5 code samples: bash, ruby, python, java, kotlin but is missing the following SDK languages: javascript 151 | ``` 152 | 153 | -------------------------------------------------------------------------------- /custom-plugin-rules/code-sample-checks/check-sdk-coverage.js: -------------------------------------------------------------------------------- 1 | module.exports = CheckSDKCoverage; 2 | 3 | // If you have different SDK languages edit this array 4 | const sdkLanguages = ['bash', 'javascript', 'python', 'ruby', 'java', 'kotlin']; 5 | 6 | function CheckSDKCoverage () { 7 | return { 8 | XCodeSampleList: { 9 | enter(codeSampleList, ctx) { 10 | // Make sure the list contains at least one bash sample 11 | const hasBashSample = codeSampleList.some((codeSample) => { 12 | return codeSample.lang === 'bash'; 13 | }); 14 | //check for the other SDK languages by making an array of the lang fields from the code samples 15 | const langArray = codeSampleList.map((codeSample) => { 16 | return codeSample.lang; 17 | }); 18 | //compare the sdkLanguages array with the langArray to find the missing languages, and save them to an array 19 | const missingLanguages = sdkLanguages.filter((lang) => { 20 | return !langArray.includes(lang); 21 | } 22 | ); 23 | //if there are missing languages, report them as warnings 24 | // might want to make this less verbose later 25 | if (missingLanguages.length > 0) { 26 | ctx.report({ 27 | message: `Only ${langArray.length} code samples: ${langArray.join(', ')} but is missing the following SDK languages: ${missingLanguages.join(', ')}`, 28 | }); 29 | } 30 | } 31 | } 32 | }; 33 | } -------------------------------------------------------------------------------- /custom-plugin-rules/code-sample-checks/x-code-sample-checks.js: -------------------------------------------------------------------------------- 1 | const CheckSDKCoverage = require('./rules/check-sdk-coverage.js'); 2 | 3 | module.exports = function myRulesPlugin() { 4 | return { 5 | id: 'x-code-samples-check', 6 | rules: { 7 | oas3: { 8 | 'check-sdk-coverage': CheckSDKCoverage 9 | }, 10 | }, 11 | }; 12 | }; -------------------------------------------------------------------------------- /custom-plugin-rules/default-enum-match/README.md: -------------------------------------------------------------------------------- 1 | # Default is an Enum 2 | 3 | Authors: 4 | - [@hawkeyexl](https://github.com/hawkeyexl) Manny Silva ([Doc Detective](https://doc-detective.com)) 5 | 6 | ## What this does and why 7 | 8 | This rule makes sure that when a schema has both `default` and `enum` properties, the `default` property is set to one of the values in the `enum` array. This is useful for API documentation where you want to make sure that the default value is valid. 9 | 10 | ## Code 11 | 12 | ### Redocly config - `redocly.yaml` 13 | 14 | Update your Redocly configuration file to include the plugin and rule. Update paths as necessary. 15 | 16 | ```yaml 17 | plugins: 18 | - './default-enum-plugin.js' 19 | 20 | rules: 21 | default-enum/default-enum-match: error 22 | ``` 23 | 24 | ### Plugin - `default-enum-plugin.js` 25 | 26 | The plugin defines the rule and returns it. Update paths as necessary. 27 | 28 | ```js 29 | module.exports = function myRulesPlugin() { 30 | return { 31 | id: 'default-enum', 32 | rules: { 33 | oas3: { 34 | 'default-enum-match': DefaultEnumMatch, 35 | }, 36 | arazzo: { 37 | 'default-enum-match': DefaultEnumMatch, 38 | }, 39 | async3: { 40 | 'default-enum-match': DefaultEnumMatch, 41 | } 42 | }, 43 | }; 44 | }; 45 | 46 | function DefaultEnumMatch() { 47 | return { 48 | Schema: { 49 | enter(schema, ctx) { 50 | // If enum and default are defined, default must be one of the enum values 51 | if ( 52 | schema.enum && 53 | typeof schema.default != "undefined" && 54 | !schema.enum.includes(schema.default) 55 | ) { 56 | ctx.report({ 57 | message: `The default value must be one of the enum values.`, 58 | }); 59 | } 60 | }, 61 | }, 62 | }; 63 | } 64 | ``` 65 | 66 | ## Examples 67 | 68 | Given an OpenAPI description with the following schema: 69 | 70 | ```yaml 71 | color: 72 | type: string 73 | enum: 74 | - blue 75 | - red 76 | - yellow 77 | default: green 78 | ``` 79 | 80 | You get the following output: 81 | 82 | ```text 83 | The default value must be one of the enum values. 84 | 85 | 50 | color: 86 | 51 | type: string 87 | 52 | enum: 88 | 53 | - blue 89 | 54 | - red 90 | 55 | - yellow 91 | 56 | default: green 92 | 93 | Error was generated by the openapi-default-enum/default-enum-match rule. 94 | ``` -------------------------------------------------------------------------------- /custom-plugin-rules/default-enum-match/default-enum-plugin.js: -------------------------------------------------------------------------------- 1 | module.exports = function myRulesPlugin() { 2 | return { 3 | id: 'default-enum', 4 | rules: { 5 | oas3: { 6 | 'default-enum-match': DefaultEnumMatch, 7 | }, 8 | arazzo: { 9 | 'default-enum-match': DefaultEnumMatch, 10 | }, 11 | async3: { 12 | 'default-enum-match': DefaultEnumMatch, 13 | } 14 | }, 15 | }; 16 | }; 17 | 18 | function DefaultEnumMatch() { 19 | return { 20 | Schema: { 21 | enter(schema, ctx) { 22 | // If enum and default are defined, default must be one of the enum values 23 | if ( 24 | schema.enum && 25 | typeof schema.default != "undefined" && 26 | !schema.enum.includes(schema.default) 27 | ) { 28 | ctx.report({ 29 | message: `The default value must be one of the enum values.`, 30 | }); 31 | } 32 | }, 33 | }, 34 | }; 35 | } -------------------------------------------------------------------------------- /custom-plugin-rules/markdown-validator/README.md: -------------------------------------------------------------------------------- 1 | # Validate Markdown descriptions 2 | 3 | Authors: 4 | - [`@lornajane`](https://github.com/lornajane) Lorna Mitchell (Redocly) 5 | 6 | 7 | ## What this does and why 8 | 9 | Writing Markdown within YAML/JSON can be awkward, and our usual Markdown tooling may not be available. This plugin adds a rule that uses a third-party Markdown validator library, the excellent [markdownlint](https://github.com/DavidAnson/markdownlint), to pick the `description` fields from your OpenAPI description, and make sure it's valid. This can really help to catch typos and formatting problems, especially in longer descriptions or large APIs. 10 | 11 | By using an existing library, we get all the power and configurability of this specialist tool, and so you can edit and adapt this plugin to meet your own Markdown preferences. 12 | 13 | ## Code 14 | 15 | This rule is built on the `markdownlint` library, so we need a `package.json` file to specify the dependency: 16 | 17 | ```json 18 | { 19 | "name": "redocly-openapi-markdown-plugin", 20 | "version": "1.0.0", 21 | "description": "", 22 | "main": "openapi-markdown.js", 23 | "scripts": { 24 | "test": "echo \"Error: no test specified\" && exit 1" 25 | }, 26 | "author": "", 27 | "license": "MIT", 28 | "dependencies": { 29 | "markdownlint": "^0.31.1" 30 | } 31 | } 32 | ``` 33 | 34 | Make sure to install the dependency using your favorite package manager. For example I use `npm` so my installation command is: 35 | 36 | ```sh 37 | npm install 38 | ``` 39 | 40 | The entry point for the plugin code is in `openapi-markdown.js`: 41 | 42 | ```js 43 | const ValidateMarkdown = require('./rule-validate-markdown.js'); 44 | 45 | module.exports = { 46 | id: 'openapi-markdown', 47 | rules: { 48 | oas3: { 49 | 'validate': ValidateMarkdown, 50 | } 51 | } 52 | } 53 | ``` 54 | 55 | The rule itself is in `rule-validate-markdown.js`: 56 | 57 | ```js 58 | const markdownlint = require("markdownlint"); 59 | const config = { 60 | // the list is here https://github.com/DavidAnson/markdownlint#rules--aliases 61 | MD013: { line_length: 120 }, 62 | MD041: false, // first line should be h1 63 | MD047: false, // should end with newline 64 | }; 65 | 66 | function checkString(description, ctx) { 67 | let options = { 68 | strings: { 69 | desc: description, 70 | }, 71 | config: config, 72 | }; 73 | 74 | try { 75 | const lintResults = markdownlint.sync(options); 76 | 77 | if (lintResults.desc.length) { 78 | // desc is the key in the options.strings object 79 | let lines = description.split("\n"); 80 | 81 | for (const desc of lintResults.desc) { 82 | // grab error message 83 | let message = desc.ruleDescription; 84 | // add line number context for longer entries 85 | if (desc.lineNumber > 1) { 86 | const charsByError = lines[desc.lineNumber].substring(0, 20); 87 | message = `${message} (near: ${charsByError} ...)` 88 | } 89 | 90 | ctx.report({ 91 | message: message, 92 | location: ctx.location.child("description"), 93 | }); 94 | } 95 | } 96 | } catch (error) { 97 | console.log(error); 98 | } 99 | } 100 | 101 | function ValidateMarkdown() { 102 | console.log("OpenAPI Markdown: validate"); 103 | return { 104 | Info: { 105 | enter({ description }, ctx) { 106 | if (description) { 107 | return checkString(description, ctx); 108 | } 109 | }, 110 | }, 111 | Tag: { 112 | enter({ description }, ctx) { 113 | if (description) { 114 | return checkString(description, ctx); 115 | } 116 | }, 117 | }, 118 | Operation: { 119 | enter({ description }, ctx) { 120 | if (description) { 121 | return checkString(description, ctx); 122 | } 123 | }, 124 | }, 125 | Parameter: { 126 | enter({ description }, ctx) { 127 | if (description) { 128 | return checkString(description, ctx); 129 | } 130 | }, 131 | }, 132 | }; 133 | } 134 | 135 | module.exports = ValidateMarkdown; 136 | ``` 137 | 138 | To control the markdown validation rules in use, edit the config at the top of the file. 139 | 140 | Bring the plugin into your `redocly.yaml` file like this: 141 | 142 | ```yaml 143 | plugins: 144 | - './openapi-markdown.js' 145 | 146 | rules: 147 | openapi-markdown/validate: warn 148 | ``` 149 | 150 | When you lint your API descriptions, you'll see warnings for any invalid markdown found in the description fields. 151 | 152 | ## Examples 153 | 154 | Given an OpenAPI description with these opening lines: 155 | 156 | ```yaml 157 | openapi: 3.1.0 158 | info: 159 | title: Redocly Museum API 160 | description: |- 161 | A fake, but awesome Museum API for interacting with museum services and information. 162 | 163 | 164 | ## Made by Redocly 165 | Built with love by [Redocly](https://redocly.com). 166 | version: 1.0.0 167 | ``` 168 | 169 | Linting (with `--format=stylish` for brevity) produces the following output: 170 | 171 | ```text 172 | validating museum.yaml... 173 | OpenAPI Markdown: validate 174 | museum.yaml: 175 | 4:16 warning openapi-markdown/validate Multiple consecutive blank lines (near: ## Details... ) 176 | 4:16 warning openapi-markdown/validate Headings should be surrounded by blank lines (near: Built with love by [... ) 177 | 178 | museum.yaml: validated in 70ms 179 | 180 | Woohoo! Your API description is valid. 🎉 181 | You have 2 warnings. 182 | ``` 183 | 184 | You can configure markdownlint to pick up (or ignore) any aspects of markdown that it knows about. 185 | 186 | ## References 187 | 188 | Built on [markdownlint](https://github.com/DavidAnson/markdownlint). 189 | -------------------------------------------------------------------------------- /custom-plugin-rules/markdown-validator/openapi-markdown.js: -------------------------------------------------------------------------------- 1 | const ValidateMarkdown = require("./rule-validate-markdown.js"); 2 | 3 | module.exports = { 4 | id: "openapi-markdown", 5 | rules: { 6 | oas3: { 7 | validate: ValidateMarkdown, 8 | }, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /custom-plugin-rules/markdown-validator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redocly-openapi-markdown-plugin", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "openapi-markdown.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "MIT", 11 | "dependencies": { 12 | "markdownlint": "^0.31.1" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /custom-plugin-rules/markdown-validator/rule-validate-markdown.js: -------------------------------------------------------------------------------- 1 | const markdownlint = require("markdownlint"); 2 | const config = { 3 | // the list is here https://github.com/DavidAnson/markdownlint#rules--aliases 4 | MD013: { line_length: 120 }, 5 | MD041: false, // first line should be h1 6 | MD047: false, // should end with newline 7 | }; 8 | 9 | function checkString(description, ctx) { 10 | let options = { 11 | strings: { 12 | desc: description, 13 | }, 14 | config: config, 15 | }; 16 | 17 | try { 18 | const lintResults = markdownlint.sync(options); 19 | 20 | if (lintResults.desc.length) { 21 | // desc is the key in the options.strings object 22 | let lines = description.split("\n"); 23 | 24 | for (const desc of lintResults.desc) { 25 | // grab error message 26 | let message = desc.ruleDescription; 27 | // add line number context for longer entries 28 | if (desc.lineNumber > 1) { 29 | // computer counts from zero, humans count from 1 30 | const charsByError = lines[desc.lineNumber - 1].substring(0, 20); 31 | message = `${message} (near: ${charsByError} ...)` 32 | } 33 | 34 | ctx.report({ 35 | message: message, 36 | location: ctx.location.child("description"), 37 | }); 38 | } 39 | } 40 | } catch (error) { 41 | console.log(error); 42 | } 43 | } 44 | 45 | function ValidateMarkdown() { 46 | console.log("OpenAPI Markdown: validate"); 47 | return { 48 | Info: { 49 | enter({ description }, ctx) { 50 | if (description) { 51 | return checkString(description, ctx); 52 | } 53 | }, 54 | }, 55 | Tag: { 56 | enter({ description }, ctx) { 57 | if (description) { 58 | return checkString(description, ctx); 59 | } 60 | }, 61 | }, 62 | Operation: { 63 | enter({ description }, ctx) { 64 | if (description) { 65 | return checkString(description, ctx); 66 | } 67 | }, 68 | }, 69 | Parameter: { 70 | enter({ description }, ctx) { 71 | if (description) { 72 | return checkString(description, ctx); 73 | } 74 | }, 75 | }, 76 | }; 77 | } 78 | 79 | module.exports = ValidateMarkdown; 80 | -------------------------------------------------------------------------------- /custom-plugins/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Redocly/redocly-cli-cookbook/d155441515902392de067b4a77a836c9ce5fdd6d/custom-plugins/.gitignore -------------------------------------------------------------------------------- /custom-plugins/sorting/README.md: -------------------------------------------------------------------------------- 1 | # A suite of sorting decorators 2 | 3 | Authors: 4 | 5 | - [`@lornajane`](https://github.com/lornajane), Lorna Mitchell (Redocly) 6 | 7 | ## What this does and why 8 | 9 | There are lots of reasons that you'd want to alter the order of the items in your API description, such as putting required fields first, or just ordering things alphabetically (or logically!) to make things consistent and easier to find. 10 | When Redocly only made API documentation tools, the sorting changes were made as part of the docs build. 11 | But now we make more complex tools, and many API pipelines have more than just docs in them too - so these operations are more commonly done with a decorator to transform the API description; then it can be used with any tools. 12 | 13 | Redocly CLI has a [`tags-alphabetical`](https://redocly.com/docs/cli/rules/tags-alphabetical) rule to error if the `tags` section isn't in alphabetical order by `name`. 14 | This plugin adds some additional *rules* for checking sort orders. 15 | 16 | - `method-sort` rule to put your methods in the desired order. The default is `["post", "patch", "put", "get", "delete"]`, but you can supply your own with an `order` parameter. 17 | - `property-sort` rule to sort properties either alphabetically with `order: alpha` (the default sort order for this rule) or with `order: required` to put the required properties first. 18 | 19 | The plugin also includes *decorators* to sort your OpenAPI descriptions, perhaps to allow an existing OpenAPI description to be easily updated to meet the expectations of the sorting rules. 20 | Here's a full list of the sorting features: 21 | 22 | - `methods`: sorts methods consistently in the order you supply (or `GET`, `POST`, `PUT`, `PATCH` and `DELETE` by default), with any unsorted methods appended afterwards 23 | - `enums-alphabetical`: sorts the options for an enum field alphabetically 24 | - `properties-alphabetical`: sorts object properties in schemas alphabetically 25 | - `properties-required-first`: puts the required properties at the top of the list (run this *after* any other property sorting decorators) 26 | - `tags-alphabetical`: sorts tags alphabetically 27 | 28 | ## Code 29 | 30 | Here's the main plugin entrypoint, it's in `sorting.js`: 31 | 32 | ```javascript 33 | const SortTagsAlphabetically = require("./sort-tags"); 34 | const SortEnumsAlphabetically = require("./sort-enums"); 35 | const SortMethods = require("./sort-methods"); 36 | const SortPropertiesAlphabetically = require("./sort-props-alpha"); 37 | const SortPropertiesRequiredFirst = require("./sort-props-required"); 38 | const RuleSortMethods = require("./rule-sort-methods"); 39 | const RuleSortProps = require("./rule-sort-props"); 40 | 41 | module.exports = function Sorting() { 42 | return { 43 | id: "sorting", 44 | rules: { 45 | oas3: { 46 | "method-sort": RuleSortMethods, 47 | "property-sort": RuleSortProps, 48 | }, 49 | }, 50 | decorators: { 51 | oas3: { 52 | "tags-alphabetical": SortTagsAlphabetically, 53 | "enums-alphabetical": SortEnumsAlphabetically, 54 | methods: SortMethods, 55 | "properties-alphabetical": SortPropertiesAlphabetically, 56 | "properties-required-first": SortPropertiesRequiredFirst, 57 | }, 58 | }, 59 | }; 60 | }; 61 | ``` 62 | 63 | Each of the available rules/decorators is in its own file, rather than copying them here, you can view them in the same directory as this `README`: 64 | 65 | - [rule-sort-methods.js](./rule-sort-methods.js) 66 | - [rule-sort-props.js](./rule-sort-props.js) 67 | - [sort-tags.js](./sort-tags.js) 68 | - [sort-enums.js](./sort-enums.js) 69 | - [sort-methods.js](./sort-methods.js) 70 | - [sort-props-alpha.js](./sort-props-alpha.js) 71 | - [sort-props-required.js](./sort-props-required.js) 72 | 73 | You can copy or adapt the plugins here to meet your own needs, changing the sorting algorithms or sorting different fields. 74 | One thing to look out for is that if you need to re-order the properties of an object, then you should visit the parent of the object, and assign the new object to the key that should be updated. 75 | 76 | ## Examples 77 | 78 | Add the plugin to `redocly.yaml` and enable the decorators and/or rules: 79 | 80 | ```yaml 81 | plugins: 82 | - sorting.js 83 | 84 | decorators: 85 | sorting/methods: on 86 | order: [delete, get] 87 | sorting/tags-alphabetical: on 88 | sorting/enums-alphabetical: on 89 | sorting/properties-alphabetical: on 90 | sorting/properties-required-first: on 91 | 92 | rules: 93 | sorting/method-sort: 94 | severity: error 95 | order: [get, post, delete] 96 | sorting/property-sort: 97 | severity: warn 98 | type: required # default is alpha 99 | 100 | ``` 101 | 102 | ### Lint with rules 103 | 104 | Run the [lint command](https://redocly.com/docs/cli/commands/lint) and the rules are used during linting. 105 | Your command will look something like the following example: 106 | 107 | ```bash 108 | redocly lint openapi.yaml 109 | ``` 110 | 111 | If your OpenAPI doesn't fulfil the criteria in the configured rules, the details of the warnings/errors are shown in the output. 112 | 113 | Adjust the rule configuration or severity levels to meet your needs, and let us know if there's some other rules you'd like to see included. 114 | 115 | ### Bundle with decorators 116 | 117 | Run the [bundle command](https://redocly.com/docs/cli/commands/bundle) and the decorators are applied during bundling. 118 | Your command will look something like the following example: 119 | 120 | ```bash 121 | redocly bundle openapi.yaml -o updated-openapi.yaml 122 | ``` 123 | 124 | Use your favorite "diff" tool to look at the changes made between your existing API description and the updated version. 125 | 126 | Remove or turn off any of the decorators that don't fit your use case, and let us know if there are any other sorting features you need by opening an issue on this repository. 127 | 128 | ## References 129 | 130 | - [`tags-alphabetical' rule](https://redocly.com/docs/cli/rules/tags-alphabetical) 131 | -------------------------------------------------------------------------------- /custom-plugins/sorting/redocly.yaml: -------------------------------------------------------------------------------- 1 | plugins: 2 | - sorting.js 3 | 4 | rules: 5 | sorting/method-sort: 6 | severity: error 7 | order: [get, post, delete] 8 | sorting/property-sort: 9 | severity: warn 10 | type: required 11 | 12 | decorators: 13 | sorting/properties-alphabetical: on 14 | sorting/tags-alphabetical: on 15 | sorting/methods: 16 | order: [get, post, delete] 17 | sorting/enums-alphabetical: on 18 | sorting/properties-required-first: on 19 | 20 | 21 | -------------------------------------------------------------------------------- /custom-plugins/sorting/rule-sort-methods.js: -------------------------------------------------------------------------------- 1 | function RuleSortMethods({ order }) { 2 | console.log("check method order"); 3 | return { 4 | PathItem: { 5 | enter(pathItem, ctx) { 6 | // default method sort order, can be changed with an "order" param in config 7 | let methodOrder = ["post", "patch", "put", "get", "delete"]; 8 | if (order) { 9 | methodOrder = order; 10 | } 11 | 12 | // Identify the methods that are present and put them in order 13 | const methods = Object.getOwnPropertyNames(pathItem); 14 | const expectedOrder = methodOrder.filter((item) => 15 | methods.includes(item) 16 | ); 17 | 18 | i = 0; 19 | while (i < expectedOrder.length) { 20 | // if this method is in the array, it must be in the expected order 21 | if ( 22 | expectedOrder.includes(methods[i]) && 23 | methods[i] !== expectedOrder[i] 24 | ) { 25 | ctx.report({ 26 | message: `Unexpected method order, expected ${expectedOrder[i]} but found ${methods[i]}`, 27 | }); 28 | } 29 | i++; 30 | } 31 | }, 32 | }, 33 | }; 34 | } 35 | 36 | module.exports = RuleSortMethods; 37 | -------------------------------------------------------------------------------- /custom-plugins/sorting/rule-sort-props.js: -------------------------------------------------------------------------------- 1 | function RuleSortProps({ type }) { 2 | let sortType = "alpha"; // default 3 | const supportedSortTypes = ["alpha", "required"]; 4 | if (type && supportedSortTypes.includes(type)) { 5 | sortType = type; 6 | } 7 | console.log(`check properties order (${sortType})`); 8 | return { 9 | Schema: { 10 | enter(schema, ctx) { 11 | if (schema.type == "object") { 12 | const propList = Object.getOwnPropertyNames(schema.properties); 13 | 14 | if (sortType == "required") { 15 | // exit early if there are no required properties 16 | let requiredList = []; 17 | if (!schema.required) { 18 | return; 19 | } else { 20 | requiredList = schema.required; 21 | } 22 | 23 | const notRequiredList = propList.filter( 24 | (item) => !requiredList.includes(item) 25 | ); 26 | // if the notRequiredList is empty, everything was required, we can exit 27 | if (notRequiredList.length == 0) { 28 | return; 29 | } 30 | 31 | // loop through, if we find an optional field before a required one then report 32 | let required = true; 33 | let prevProp = ""; 34 | let prevReq = true; 35 | propList.forEach((prop) => { 36 | const isReq = requiredList.includes(prop); 37 | // did we go from an optional field to a required one? 38 | if (isReq && !prevReq) { 39 | ctx.report({ 40 | message: `Unexpected property order, found required \`${prop}\` after optional \`${prevProp}\``, 41 | }); 42 | } 43 | 44 | // need these to refer to on the next iteration 45 | prevReq = isReq; 46 | prevProp = prop; 47 | }); 48 | } else { 49 | // alpha sort is default 50 | const sortedList = [...propList].sort(); 51 | 52 | // use a loop so we can show exactly where the order failed for large objects 53 | let i = 0; 54 | 55 | while (i < propList.length) { 56 | if (sortedList[i] !== propList[i]) { 57 | ctx.report({ 58 | message: `Unexpected property order, found \`${propList[i]}\` but expected \`${sortedList[i]}\``, 59 | }); 60 | return; // if one property is out of order, there might be many others, return to avoid noise 61 | } 62 | 63 | i++; 64 | } 65 | } 66 | } 67 | }, 68 | }, 69 | }; 70 | } 71 | 72 | module.exports = RuleSortProps; 73 | -------------------------------------------------------------------------------- /custom-plugins/sorting/sort-enums.js: -------------------------------------------------------------------------------- 1 | module.exports = SortEnumsAlphabetically; 2 | 3 | function SortEnumsAlphabetically() { 4 | console.log("re-ordering enums: alphabetical"); 5 | return { 6 | Schema: { 7 | leave(target) { 8 | if (target.enum) { 9 | target.enum.sort(); 10 | } 11 | }, 12 | }, 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /custom-plugins/sorting/sort-methods.js: -------------------------------------------------------------------------------- 1 | module.exports = SortMethods; 2 | 3 | function SortMethods({ order }) { 4 | console.log("re-ordering methods"); 5 | return { 6 | PathItem: { 7 | leave(pathItem) { 8 | // start with the default ordering, override with config if we have it 9 | let methodList = ["get", "post", "patch", "put", "delete"]; 10 | if (order) { 11 | methodList = order; 12 | } 13 | 14 | let existingMethods = Object.getOwnPropertyNames(pathItem); 15 | 16 | for (const method of methodList) { 17 | const operation = pathItem[method]; 18 | // For each defined operation, delete it and re-add it to the path so they will be in the correct order: 19 | if (operation) { 20 | // remove it from the methods list so we know we processed it 21 | existingMethods = existingMethods.filter((x) => x != method); 22 | delete pathItem[method]; 23 | pathItem[method] = operation; 24 | } 25 | } 26 | 27 | // now re-add any methods that weren't in the list 28 | for (const method of existingMethods) { 29 | const operation = pathItem[method]; 30 | // Delete and re-add unprocessed operations to the path so they will be in the correct order: 31 | if (operation) { 32 | delete pathItem[method]; 33 | pathItem[method] = operation; 34 | } 35 | } 36 | }, 37 | }, 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /custom-plugins/sorting/sort-props-alpha.js: -------------------------------------------------------------------------------- 1 | module.exports = SortPropertiesAlphabetically; 2 | 3 | function SortPropertiesAlphabetically() { 4 | console.log("re-ordering properties: alphabetical"); 5 | return { 6 | Schema: { 7 | leave(schema) { 8 | if (schema.type == "object") { 9 | const propList = Object.getOwnPropertyNames(schema.properties).sort(); 10 | let newProps = {}; 11 | 12 | propList.forEach((prop) => { 13 | newProps[prop] = schema.properties[prop]; 14 | }); 15 | 16 | schema.properties = newProps; 17 | } 18 | }, 19 | }, 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /custom-plugins/sorting/sort-props-required.js: -------------------------------------------------------------------------------- 1 | module.exports = SortPropertiesRequiredFirst; 2 | 3 | function SortPropertiesRequiredFirst() { 4 | console.log("re-ordering properties: required first"); 5 | return { 6 | Schema: { 7 | leave(schema) { 8 | if (schema.type == "object") { 9 | const propList = Object.getOwnPropertyNames(schema.properties); 10 | let newProps = {}; 11 | 12 | if (schema.required && schema.required.length > 0) { 13 | const requiredList = schema.required; 14 | // put the required items in first 15 | requiredList.forEach((prop) => { 16 | newProps[prop] = schema.properties[prop]; 17 | }); 18 | 19 | // now add anything that wasn't already added 20 | propList.forEach((prop) => { 21 | if (!newProps[prop]) { 22 | newProps[prop] = schema.properties[prop]; 23 | } 24 | }); 25 | schema.properties = newProps; 26 | } 27 | } 28 | }, 29 | }, 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /custom-plugins/sorting/sort-tags.js: -------------------------------------------------------------------------------- 1 | module.exports = SortTagsAlphabetically; 2 | 3 | function SortTagsAlphabetically() { 4 | console.log("re-ordering tags: alphabetical"); 5 | return { 6 | TagList: { 7 | leave(target) { 8 | target.sort((a, b) => { 9 | if (a.name < b.name) { 10 | return -1; 11 | } 12 | }); 13 | }, 14 | }, 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /custom-plugins/sorting/sorting.js: -------------------------------------------------------------------------------- 1 | const SortTagsAlphabetically = require("./sort-tags"); 2 | const SortEnumsAlphabetically = require("./sort-enums"); 3 | const SortMethods = require("./sort-methods"); 4 | const SortPropertiesAlphabetically = require("./sort-props-alpha"); 5 | const SortPropertiesRequiredFirst = require("./sort-props-required"); 6 | const RuleSortMethods = require("./rule-sort-methods"); 7 | const RuleSortProps = require("./rule-sort-props"); 8 | 9 | module.exports = function Sorting() { 10 | return { 11 | id: "sorting", 12 | rules: { 13 | oas3: { 14 | "method-sort": RuleSortMethods, 15 | "property-sort": RuleSortProps, 16 | }, 17 | }, 18 | decorators: { 19 | oas3: { 20 | "tags-alphabetical": SortTagsAlphabetically, 21 | "enums-alphabetical": SortEnumsAlphabetically, 22 | methods: SortMethods, 23 | "properties-alphabetical": SortPropertiesAlphabetically, 24 | "properties-required-first": SortPropertiesRequiredFirst, 25 | }, 26 | }, 27 | }; 28 | }; 29 | -------------------------------------------------------------------------------- /miscellaneous/reorder-bundled-description-properties/README.md: -------------------------------------------------------------------------------- 1 | # Reorder description properties 2 | 3 | Authors: 4 | 5 | - [`@tatomyr`](https://github.com/tatomyr) Andrew Tatomyr (Redocly) 6 | 7 | ## What this does and why 8 | 9 | Sometimes you might want to enforce a specific order of properties in your OpenAPI descriptions. Especially it could be useful after bundling the description, as the order of properties might change. 10 | 11 | ## Code 12 | 13 | For that purpose, you can write a simple `JavaScript` script code similar to the one below: 14 | 15 | ```javascript 16 | /* reorder.js */ 17 | 18 | // Import the necessary modules 19 | const fs = require("fs"); 20 | const { parseYaml, stringifyYaml } = require("@redocly/openapi-core"); 21 | 22 | // Define the function to reorder the properties 23 | const reorder = (root) => { 24 | const { components, openapi, ...rest } = root; 25 | return { 26 | // Here you can put the properties in the order you want 27 | components, 28 | ...rest, 29 | openapi, 30 | }; 31 | }; 32 | 33 | // Get the file name from the command line arguments 34 | const fileName = process.argv[2]; 35 | 36 | // Read the file content, reorder and print the output 37 | const content = fs.readFileSync(fileName, "utf8"); 38 | const reordered = reorder(parseYaml(content)); 39 | process.stdout.write(stringifyYaml(reordered)); 40 | ``` 41 | 42 | Please notice that `@redocly/openapi-core` has to be installed as a project dependency as it exposes the `parseYaml` and `stringifyYaml` functions. 43 | 44 | You can tweak the properties you need to put in the desired order in the `reorder` function. 45 | 46 | Then you can bundle the file with Redocly CLI (or just use any OpenAPI file) and then reorder the properties with the following command: 47 | 48 | ```bash 49 | node reorder.js 50 | ``` 51 | 52 | ## Examples 53 | 54 | Given an OpenAPI description: 55 | 56 | ```yaml 57 | openapi: 3.1.0 58 | paths: 59 | /test: 60 | get: 61 | responses: 62 | "200": 63 | content: 64 | application/json: 65 | schema: 66 | $ref: schema.yaml 67 | ``` 68 | 69 | which references a schema in a separate file `schema.yaml`: 70 | 71 | ```yaml 72 | type: number 73 | ``` 74 | 75 | bundling with `redocly bundle openapi.yaml -o bundled.yaml` will produce the following `bundled.yaml`: 76 | 77 | ```yaml 78 | openapi: 3.1.0 79 | paths: 80 | /test: 81 | get: 82 | responses: 83 | "200": 84 | content: 85 | application/json: 86 | schema: 87 | $ref: "#/components/schemas/schema" 88 | components: 89 | schemas: 90 | schema: 91 | type: number 92 | ``` 93 | 94 | But if we want to reorder the properties in the `bundled.yaml` file, we can run `node reorder.js bundled.yaml` which will result in the following output: 95 | 96 | ```yaml 97 | components: 98 | schemas: 99 | schema: 100 | type: number 101 | paths: 102 | /test: 103 | get: 104 | responses: 105 | "200": 106 | content: 107 | application/json: 108 | schema: 109 | $ref: "#/components/schemas/schema" 110 | openapi: 3.1.0 111 | ``` 112 | -------------------------------------------------------------------------------- /miscellaneous/reorder-bundled-description-properties/bundled.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | paths: 3 | /test: 4 | get: 5 | responses: 6 | '200': 7 | content: 8 | application/json: 9 | schema: 10 | $ref: '#/components/schemas/schema' 11 | components: 12 | schemas: 13 | schema: 14 | type: number 15 | -------------------------------------------------------------------------------- /miscellaneous/reorder-bundled-description-properties/openapi.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | paths: 3 | /test: 4 | get: 5 | responses: 6 | "200": 7 | content: 8 | application/json: 9 | schema: 10 | $ref: schema.yaml 11 | -------------------------------------------------------------------------------- /miscellaneous/reorder-bundled-description-properties/reorder.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const { parseYaml, stringifyYaml } = require("@redocly/openapi-core"); 3 | 4 | const reorder = (root) => { 5 | const { components, openapi, ...rest } = root; 6 | return { 7 | // Here you can put the properties in the order you want 8 | components, 9 | ...rest, 10 | openapi, 11 | }; 12 | }; 13 | 14 | const fileName = process.argv[2]; 15 | const content = fs.readFileSync(fileName, "utf8"); 16 | const reordered = reorder(parseYaml(content)); 17 | process.stdout.write(stringifyYaml(reordered)); 18 | -------------------------------------------------------------------------------- /miscellaneous/reorder-bundled-description-properties/schema.yaml: -------------------------------------------------------------------------------- 1 | type: number 2 | -------------------------------------------------------------------------------- /readme-template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Title (what is this?) 4 | 5 | Authors: 6 | - (github handle, optionally your name) 7 | 8 | 9 | ## What this does and why 10 | 11 | 12 | 13 | ## Code 14 | 15 | 16 | 17 | ## Examples 18 | 19 | 20 | 21 | ## References 22 | 23 | 24 | -------------------------------------------------------------------------------- /rulesets/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Redocly/redocly-cli-cookbook/d155441515902392de067b4a77a836c9ce5fdd6d/rulesets/.gitignore -------------------------------------------------------------------------------- /rulesets/common-mistakes/README.md: -------------------------------------------------------------------------------- 1 | # Spot common mistakes 2 | 3 | Authors: 4 | - `@CidTori` 5 | 6 | 7 | ## What this does and why 8 | 9 | This ruleset combines unopinionated rules to identify common mistakes and avoid oversights. The ruleset works well in combination with the [spec-compliant ruleset](../spec-compliant/). 10 | 11 | ## Code 12 | 13 | You can use it in your `redocly.yaml` with [`extends`](https://redocly.com/docs/cli/configuration/extends/), or you can copy its content directly: 14 | 15 | ```yaml 16 | rules: 17 | no-server-example.com: warn 18 | no-server-trailing-slash: warn 19 | no-ambiguous-paths: warn 20 | no-path-trailing-slash: warn 21 | operation-operationId: warn 22 | no-enum-type-mismatch: warn 23 | no-unused-components: warn 24 | ``` 25 | 26 | ## References 27 | 28 | Here is why each rule is included: 29 | 30 | - `no-server-example.com`: most likely a copy-paste error from some example in a documentation 31 | - `no-server-trailing-slash`: prevents double slash between the server and the endpoint path, no downside 32 | - `no-ambiguous-paths`: ambiguous paths may be hard to see without a linter, and preventing them is a good practice 33 | - `no-path-trailing-slash`: avoid confusion, no downside 34 | - `operation-operationId`: as the rule itself says: "This rule is unopinionated." 35 | - `no-enum-type-mismatch`: as the rule itself says: "Lack of compliance is most likely the result of a typo." 36 | - `no-unused-components`: unused components are hard to spot without a linter, and most of the time you just want to remove them 37 | 38 | Please, feel free to open issues or pull requests to suggest updates or additions to this ruleset. 39 | -------------------------------------------------------------------------------- /rulesets/common-mistakes/redocly.yaml: -------------------------------------------------------------------------------- 1 | extends: 2 | - https://raw.githubusercontent.com/Redocly/redocly-cli-cookbook/main/rulesets/spec-compliant/redocly.yaml 3 | 4 | rules: 5 | no-server-example.com: warn 6 | no-server-trailing-slash: warn 7 | no-ambiguous-paths: warn 8 | no-path-trailing-slash: warn 9 | operation-operationId: warn 10 | no-enum-type-mismatch: warn 11 | no-unused-components: warn 12 | -------------------------------------------------------------------------------- /rulesets/security/README.md: -------------------------------------------------------------------------------- 1 | # Security ruleset, a collection of security-conscious rules 2 | 3 | Authors: 4 | - [`@lornajane`](https://github.com/lornajane) Lorna Mitchell (Redocly) 5 | 6 | ## What this does and why 7 | 8 | A ruleset to pick up on some common API mistakes using linting. 9 | A security mindset means more than a few linting rules - but we hope that they help! 10 | 11 | Included in this set are a few defensive data type rules, plus a few others from the OWASP recommendations: 12 | 13 | - `rule/no-http-basic` - Don't use HTTP Basic auth 14 | - `rule/operation-security` - Security must be defined at the operation level 15 | - `rule/https-server-urls` - Server URLs must use https 16 | - `rule/limit-string-length` - Avoid overflow errors by setting a maximum length for string values 17 | - `rule/limit-array-length` - Avoid overflow errors by setting a maximum number of array items 18 | 19 | What else should be here? Open an issue - or a pull request. 20 | 21 | ## Code 22 | 23 | The following code snippet shows the rules to use: 24 | 25 | ```yaml 26 | rules: 27 | 28 | rule/no-http-basic: 29 | message: HTTP Basic should not be used. 30 | subject: 31 | type: SecurityRequirement 32 | property: scheme 33 | assertions: 34 | notPattern: /basic/i 35 | where: 36 | - subject: 37 | type: SecurityRequirement 38 | assertions: 39 | defined: true 40 | severity: error 41 | 42 | rule/operation-security: 43 | message: Security must be defined at the operation level. 44 | subject: 45 | type: Operation 46 | property: security 47 | assertions: 48 | defined: true 49 | severity: warn 50 | 51 | rule/https-server-urls: 52 | message: Server URLs must start with "https:". 53 | subject: 54 | type: Server 55 | property: url 56 | assertions: 57 | pattern: /^https:/ 58 | severity: error 59 | 60 | rule/limit-string-length: 61 | message: Strings must have maxLength defined, or be an enum/const 62 | subject: 63 | type: Schema 64 | assertions: 65 | requireAny: 66 | - maxLength 67 | - enum 68 | - const 69 | where: 70 | - subject: 71 | type: Schema 72 | property: type 73 | assertions: 74 | const: string 75 | defined: true 76 | severity: warn 77 | 78 | rule/limit-array-length: 79 | message: Arrays must have a maxItems property 80 | subject: 81 | type: Schema 82 | assertions: 83 | required: 84 | - maxItems 85 | where: 86 | - subject: 87 | type: Schema 88 | property: type 89 | assertions: 90 | const: array 91 | defined: true 92 | severity: warn 93 | ``` 94 | 95 | You can copy and paste this configuration into your own `redocly.yaml` file, and adjust the `severity` settings to suit your use case. 96 | 97 | Then run `redocly lint` to apply the linting rules to your API description. 98 | 99 | ## Examples 100 | 101 | Each rule has its own example. Run `redocly lint openapi.yaml` to lint your API description. 102 | 103 | ## HTTP Basic should not be used 104 | 105 | This rule picks up where a security scheme uses a scheme of "Basic" (or "basic"). 106 | Avoid using an example like the following: 107 | 108 | ```yaml 109 | components: 110 | securitySchemes: 111 | LegacyAuth: 112 | type: http 113 | scheme: basic 114 | ``` 115 | 116 | It is recommended to use a scheme such as bearer or digest in your APIs. 117 | 118 | ## Security must be defined at the operation level 119 | 120 | OpenAPI allows security to be defined at the top level of the document, but this rule adds a check that every endpoint has a security definition. 121 | By intentionally securing each endpoint with an appropriate configuration, security mistakes are less likely to occur. 122 | 123 | Each operation should look like the example below: 124 | 125 | ```yaml 126 | operationId: getMuseumHours 127 | security: 128 | summary: Get museum hours 129 | - ReaderAuth 130 | description: Get upcoming museum operating hours. 131 | ``` 132 | 133 | This approach also makes it easier to use tighter security for endpoints with side effects. 134 | 135 | ## Server URLs must start with "https:" 136 | 137 | It is good practice to use HTTPS endpoints to protect any credentials or important information during transit. 138 | This rule identifies any plain `http://` URLs in the server array, such as the following example: 139 | 140 | ```yaml 141 | servers: 142 | - url: "http://example.com/museum-api/" 143 | ``` 144 | 145 | Correct this problem by using `https://` URLs for all endpoints. 146 | 147 | ## Strings must have maxLength defined, or be an enum/const 148 | 149 | To avoid API endpoints receiving payloads that could cause overflow errors, set limits on the size of the fields that can be accepted or define specific values that can be sent. 150 | 151 | All of the following field examples are acceptable: 152 | 153 | ```yaml 154 | components: 155 | schemas: 156 | TicketType: 157 | description: Type of ticket being purchased. Use `general` for regular museum entry and `event` for tickets to special events. 158 | type: string 159 | enum: 160 | - event 161 | - general 162 | example: event 163 | Delivery: 164 | description: The method of ticket delivery for the purchased ticket. 165 | type: string 166 | const: Digital 167 | Email: 168 | description: Email address for ticket purchaser. 169 | type: string 170 | format: email 171 | maxLength: 120 172 | example: museum-lover@example.com 173 | ``` 174 | 175 | Note that the `const` keyword came in with the updated JSON Schema version in OpenAPI 3.1. 176 | For OpenAPI 3.0, use an `enum` field with a single option. 177 | 178 | ## Arrays must have a maxItems property 179 | 180 | Similar to the previous point about setting a maximum string field size, set a realistic limit for the size of the arrays that your endpoints accept. 181 | By setting a maximum array size to a sensible limit, you can avoid having your API endpoints try to process something so large that they cause problems. 182 | 183 | The following example shows an array field with a limit set: 184 | 185 | ```yaml 186 | EventDates: 187 | type: array 188 | maxItems: 4 189 | items: 190 | $ref: "#/components/schemas/Date" 191 | description: List of planned dates for the special event. 192 | ``` 193 | 194 | Pick a limit that's generous for the size of the data that you expect, but small enough that it can be handled without performance implications. 195 | 196 | ## References 197 | 198 | - [OWASP top ten](https://owasp.org/Top10/) 199 | - Published linting rulesets from [Spectral](https://blog.stoplight.io/spectral-owasp-api-2023-security-ruleset) and [Vacuum](https://quobix.com/vacuum/rules/owasp/) 200 | -------------------------------------------------------------------------------- /rulesets/security/redocly.yaml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | rule/no-http-basic: 4 | message: HTTP Basic should not be used. 5 | subject: 6 | type: SecurityRequirement 7 | property: scheme 8 | assertions: 9 | notPattern: /basic/i 10 | where: 11 | - subject: 12 | type: SecurityRequirement 13 | assertions: 14 | defined: true 15 | severity: error 16 | 17 | rule/operation-security: 18 | message: Security must be defined at the operation level. 19 | subject: 20 | type: Operation 21 | property: security 22 | assertions: 23 | defined: true 24 | severity: off 25 | 26 | rule/https-server-urls: 27 | message: Server URLs must start with "https:". 28 | subject: 29 | type: Server 30 | property: url 31 | assertions: 32 | pattern: /^https:/ 33 | severity: error 34 | 35 | rule/limit-string-length: 36 | message: Strings must have maxLength defined, or be an enum/const 37 | subject: 38 | type: Schema 39 | assertions: 40 | requireAny: 41 | - maxLength 42 | - enum 43 | - const 44 | where: 45 | - subject: 46 | type: Schema 47 | property: type 48 | assertions: 49 | const: string 50 | defined: true 51 | severity: off 52 | 53 | rule/limit-array-length: 54 | message: Arrays must have a maxItems property 55 | subject: 56 | type: Schema 57 | assertions: 58 | required: 59 | - maxItems 60 | where: 61 | - subject: 62 | type: Schema 63 | property: type 64 | assertions: 65 | const: array 66 | defined: true 67 | severity: off 68 | 69 | -------------------------------------------------------------------------------- /rulesets/spec-compliant/README.md: -------------------------------------------------------------------------------- 1 | # spec-compliant ruleset 2 | 3 | Authors: 4 | - `@CidTori` 5 | 6 | 7 | ## What this does and why 8 | 9 | This ruleset uses the current built-in rules to stick closely to the spec. 10 | 11 | It's an unopinionated good default. 12 | 13 | REQUIRED rules (in the spec) are set to `error`, RECOMMENDED rules to `warn`. 14 | 15 | ## Code 16 | 17 | You can use it in your `redocly.yaml` wih [`extends`](https://redocly.com/docs/cli/configuration/extends/), or you can copy its content directly: 18 | 19 | ```yaml 20 | rules: 21 | spec: error 22 | spec-strict-refs: error 23 | no-undefined-server-variable: error 24 | path-not-include-query: error 25 | path-declaration-must-exist: error 26 | no-identical-paths: error 27 | path-parameters-defined: error 28 | operation-operationId-url-safe: warn 29 | operation-operationId-unique: error 30 | operation-parameters-unique: error 31 | operation-2xx-response: warn 32 | no-invalid-parameter-examples: warn 33 | no-invalid-media-type-examples: warn 34 | no-invalid-schema-examples: warn 35 | no-example-value-and-externalValue: error 36 | no-unresolved-refs: error 37 | spec-components-invalid-map-name: error 38 | ``` 39 | 40 | ## References 41 | 42 | Here is why each rule is included: 43 | 44 | - `spec`: [obviously](https://redocly.com/docs/cli/rules/spec/#api-design-principles) 45 | - `spec-strict-refs`: ["strict adherence to the specifications"](https://redocly.com/docs/cli/rules/spec-strict-refs/#api-design-principles) 46 | - `no-undefined-server-variable`: ["it's an error with the specification"](https://redocly.com/docs/cli/rules/no-undefined-server-variable/#api-design-principles) 47 | - `path-declaration-must-exist`: ["This rule is for spec correctness"](https://redocly.com/docs/cli/rules/path-declaration-must-exist/#api-design-principles) 48 | - `no-identical-paths`: ["Templated paths with the same hierarchy but different templated names MUST NOT exist as they are identical."](https://spec.openapis.org/oas/latest.html#patterned-fields) 49 | - `path-not-include-query`: ["Each template expression in the path MUST correspond to a path parameter"](https://spec.openapis.org/oas/latest.html#path-templating) 50 | - `path-parameters-defined`: ["Each template expression in the path MUST correspond to a path parameter"](https://spec.openapis.org/oas/latest.html#path-templating) 51 | - `operation-operationId-url-safe` (debatable): ["it is RECOMMENDED to follow common programming naming conventions"](https://spec.openapis.org/oas/latest.html#fixed-fields-7) 52 | - `operation-operationId-unique`: ["The id MUST be unique among all operations described in the API."](https://spec.openapis.org/oas/latest.html#fixed-fields-7) 53 | - `operation-parameters-unique`: ["The list MUST NOT include duplicated parameters. A unique parameter is defined by a combination of a name and location."](https://spec.openapis.org/oas/latest.html#fixed-fields-7) 54 | - `operation-2xx-response` (debatable): ["The Responses Object MUST contain at least one response code, and if only one response code is provided it SHOULD be the response for a successful operation call."](https://spec.openapis.org/oas/latest.html#responses-object) 55 | - `no-invalid-parameter-examples`: ["The example SHOULD match the specified schema and encoding properties if present."](https://spec.openapis.org/oas/latest.html#fixed-fields-9) 56 | - `no-invalid-media-type-examples`: ["The example object SHOULD be in the correct format as specified by the media type."](https://spec.openapis.org/oas/latest.html#fixed-fields-11) 57 | - `no-invalid-schema-examples`: ["It is RECOMMENDED that these values be valid against the associated schema."](https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-00#section-9.5) 58 | - `no-example-value-and-externalValue`: ["The value field and externalValue field are mutually exclusive."](https://spec.openapis.org/oas/latest.html#fixed-fields-15) 59 | - `no-unresolved-refs` (debatable): ["The referenced structure MUST be in the form of a Path Item Object."](https://spec.openapis.org/oas/latest.html#fixed-fields-6) 60 | - `spec-components-invalid-map-name`: ["All the fixed fields declared above are objects that MUST use keys that match the regular expression: ^\[a-zA-Z0-9\.\-_\]+$."](https://spec.openapis.org/oas/latest.html#fixed-fields-5) 61 | 62 | Please, feel free to open issues or pull requests to suggest updates or additions to this ruleset. 63 | -------------------------------------------------------------------------------- /rulesets/spec-compliant/redocly.yaml: -------------------------------------------------------------------------------- 1 | rules: 2 | spec: error 3 | spec-strict-refs: error 4 | no-undefined-server-variable: error 5 | path-not-include-query: error 6 | path-declaration-must-exist: error 7 | no-identical-paths: error 8 | path-parameters-defined: error 9 | operation-operationId-url-safe: warn 10 | operation-operationId-unique: error 11 | operation-parameters-unique: error 12 | operation-2xx-response: warn 13 | no-invalid-parameter-examples: warn 14 | no-invalid-media-type-examples: warn 15 | no-invalid-schema-examples: warn 16 | no-example-value-and-externalValue: error 17 | no-unresolved-refs: error 18 | spec-components-invalid-map-name: error 19 | --------------------------------------------------------------------------------