-
Notifications
You must be signed in to change notification settings - Fork 5
feat: add Spectral lint rules enforcing API design guidelines #317
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,175 @@ | ||
| extends: | ||
| - spectral:oas | ||
|
|
||
| rules: | ||
| # Disable built-in rules already covered by Redocly | ||
| info-contact: off | ||
| info-description: off | ||
| operation-tag-defined: off | ||
| oas3-api-servers: off | ||
|
|
||
| # ============================================================ | ||
| # Naming Conventions (README: Naming Conventions) | ||
| # ============================================================ | ||
|
|
||
| # Fields must be camelCase (excludes TestWebhookResponse which mirrors external format) | ||
| field-names-camelCase: | ||
| description: Schema property names must be camelCase | ||
| message: "Property '{{property}}' must be camelCase. See openapi/README.md#field-naming" | ||
| severity: error | ||
| given: "$.components.schemas[?(@property != 'TestWebhookResponse')].properties" | ||
| then: | ||
| field: "@key" | ||
| function: casing | ||
| functionOptions: | ||
| type: camel | ||
|
|
||
| # Enum values must be UPPER_SNAKE_CASE (dots allowed for webhook namespacing) | ||
| enum-values-upper-snake-case: | ||
| description: Enum values must be UPPER_SNAKE_CASE | ||
| message: "Enum value '{{value}}' must be UPPER_SNAKE_CASE. See openapi/README.md#field-naming" | ||
| severity: error | ||
| given: "$.components.schemas.*.enum[*]" | ||
| then: | ||
| function: pattern | ||
| functionOptions: | ||
| match: "^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*(\\.[A-Z][A-Z0-9]*(_[A-Z0-9]+)*)*$" | ||
|
|
||
| # Query parameters must be camelCase | ||
| query-params-camelCase: | ||
| description: Query parameter names must be camelCase | ||
| message: "Query parameter '{{value}}' must be camelCase. See openapi/README.md#field-naming" | ||
| severity: error | ||
| given: "$.paths.*.*.parameters[?(@.in=='query')].name" | ||
| then: | ||
| function: casing | ||
| functionOptions: | ||
| type: camel | ||
|
|
||
| # Path parameters must be camelCase | ||
| path-params-camelCase: | ||
| description: Path parameter names must be camelCase | ||
| message: "Path parameter '{{value}}' must be camelCase. See openapi/README.md#field-naming" | ||
| severity: error | ||
| given: "$.paths.*.*.parameters[?(@.in=='path')].name" | ||
| then: | ||
| function: casing | ||
| functionOptions: | ||
| type: camel | ||
greptile-apps[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| # ============================================================ | ||
| # Discriminators and Polymorphism (README: OpenAPI Best Practices) | ||
| # ============================================================ | ||
|
|
||
| # oneOf must include a discriminator | ||
| oneOf-must-have-discriminator: | ||
| description: oneOf schemas must include a discriminator | ||
| message: "oneOf without a discriminator. See openapi/README.md#discriminators-and-polymorphism" | ||
| severity: warn | ||
| given: "$.components.schemas[?(@.oneOf)]" | ||
| then: | ||
| field: discriminator | ||
| function: truthy | ||
|
|
||
| # ============================================================ | ||
| # No Inline Schemas (README: Avoid Inline Schemas) | ||
| # ============================================================ | ||
|
|
||
| # Request bodies must use $ref, not inline schemas. | ||
| # Note: Spectral resolves $refs in the bundled spec, so some component-level | ||
| # false positives appear — the real violations are on paths.* entries. | ||
| no-inline-request-schema: | ||
| description: Request body schemas must use $ref, not inline definitions | ||
| message: "Use $ref for request body schema instead of inline definition. See openapi/README.md#avoid-inline-schemas-in-request-and-response-definitions" | ||
| severity: error | ||
| given: "$.paths[*][get,post,put,patch,delete].requestBody.content[application/json].schema" | ||
| then: | ||
| field: "$ref" | ||
| function: truthy | ||
|
|
||
| # Response bodies must use $ref, not inline schemas | ||
| no-inline-response-schema: | ||
| description: Response body schemas must use $ref, not inline definitions | ||
| message: "Use $ref for response schema instead of inline definition. See openapi/README.md#avoid-inline-schemas-in-request-and-response-definitions" | ||
| severity: error | ||
| given: "$.paths[*][get,post,put,patch,delete].responses[*].content[application/json].schema" | ||
| then: | ||
| field: "$ref" | ||
| function: truthy | ||
greptile-apps[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| # ============================================================ | ||
| # Pagination (README: Pagination) | ||
| # ============================================================ | ||
|
|
||
| # GET list endpoints returning arrays should use pagination envelope | ||
| pagination-envelope-has-data: | ||
| description: Paginated responses must include a 'data' array field | ||
| message: "List response missing 'data' field. See openapi/README.md#pagination" | ||
| severity: warn | ||
| given: "$.paths.*.get.responses.200.content.application/json.schema.properties" | ||
| then: | ||
| field: data | ||
| function: truthy | ||
greptile-apps[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| pagination-envelope-has-hasMore: | ||
| description: Paginated responses must include a 'hasMore' boolean field | ||
| message: "List response missing 'hasMore' field. See openapi/README.md#pagination" | ||
| severity: warn | ||
| given: "$.paths.*.get.responses.200.content.application/json.schema.properties[?(@.data)]" | ||
| then: | ||
| field: hasMore | ||
| function: truthy | ||
greptile-apps[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| # ============================================================ | ||
| # HTTP Methods and Status Codes (README: HTTP Methods) | ||
| # ============================================================ | ||
|
|
||
| # DELETE operations should return 204 | ||
| delete-returns-204: | ||
| description: DELETE operations should return 204 No Content | ||
| message: "DELETE should return 204. See openapi/README.md#http-methods" | ||
| severity: warn | ||
| given: "$.paths.*.delete.responses" | ||
| then: | ||
| field: "204" | ||
| function: truthy | ||
|
|
||
| # ============================================================ | ||
| # Documentation (README: Documentation in OpenAPI) | ||
| # ============================================================ | ||
|
|
||
| # Schema properties should have descriptions | ||
| schema-properties-have-descriptions: | ||
| description: Schema properties should have descriptions | ||
| message: "Property '{{property}}' is missing a description. See openapi/README.md#documentation-in-openapi" | ||
| severity: warn | ||
| given: "$.components.schemas.*.properties.*" | ||
| then: | ||
| field: description | ||
| function: truthy | ||
|
|
||
| # Schemas should have examples where appropriate | ||
| schema-properties-have-examples: | ||
| description: String and number schema properties should have examples | ||
| message: "Property is missing an example. See openapi/README.md#documentation-in-openapi" | ||
| severity: info | ||
| given: "$.components.schemas.*.properties[?(@.type=='string' || @.type=='integer' || @.type=='number')]" | ||
| then: | ||
| field: example | ||
| function: truthy | ||
|
|
||
| # ============================================================ | ||
| # Paths (README: Resources) | ||
| # ============================================================ | ||
|
|
||
| # Paths should use kebab-case (path params in {camelCase} are allowed) | ||
| paths-kebab-case: | ||
| description: Path segments should use kebab-case | ||
| message: "Path should use kebab-case (e.g., /external-accounts not /externalAccounts). See openapi/README.md#resources" | ||
| severity: error | ||
| given: "$.paths" | ||
| then: | ||
| field: "@key" | ||
| function: pattern | ||
| functionOptions: | ||
| match: "^(\\/([a-z0-9]+(-[a-z0-9]+)*|\\{[a-zA-Z0-9]+\\}))+$" | ||
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,4 +1,4 @@ | ||||||||||||
| .PHONY: install build build-openapi mint lint lint-openapi lint-markdown cli-install cli-build cli | ||||||||||||
| .PHONY: install build build-openapi mint lint lint-openapi lint-spectral lint-markdown cli-install cli-build cli | ||||||||||||
|
|
||||||||||||
| install: | ||||||||||||
| npm install | ||||||||||||
|
|
@@ -21,11 +21,13 @@ mint: | |||||||||||
|
|
||||||||||||
| lint: | ||||||||||||
| npm run lint | ||||||||||||
|
Comment on lines
22
to
23
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The To enforce Spectral in CI,
Suggested change
Alternatively, add a Note also that Prompt To Fix With AIThis is a comment left during a code review.
Path: Makefile
Line: 22-23
Comment:
**Spectral not wired into the main `lint` target**
The `lint` Make target calls `npm run lint`, which maps to only `lint:openapi` (Redocly). The new `lint-spectral` target is entirely separate, so CI (`.github/workflows/lint.yml` runs `make lint`) will never execute Spectral. This means the rules added in this PR are effectively advisory — a PR introducing a camelCase violation, missing discriminator, or inline schema will pass CI without complaint.
To enforce Spectral in CI, `lint-spectral` needs to be added as a dependency of `lint` (or chained in):
```suggestion
lint:
npm run lint
$(MAKE) lint-spectral
```
Alternatively, add a `lint:spectral` script to `package.json` and extend the `lint` script: `"lint": "npm run lint:openapi && npx spectral lint openapi.yaml --fail-severity=error"`.
Note also that `.spectral.yaml` is absent from the `paths` trigger in `.github/workflows/lint.yml`, so changes to the rule file alone won't re-run CI.
How can I resolve this? If you propose a fix, please make it concise. |
||||||||||||
| cd mintlify && mint openapi-check openapi.yaml | ||||||||||||
|
|
||||||||||||
| lint-openapi: | ||||||||||||
| npm run lint:openapi | ||||||||||||
|
|
||||||||||||
| lint-spectral: | ||||||||||||
| npx spectral lint openapi.yaml --fail-severity=error | ||||||||||||
|
|
||||||||||||
greptile-apps[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
| lint-markdown: | ||||||||||||
| npm run lint:markdown | ||||||||||||
|
|
||||||||||||
|
|
@@ -36,4 +38,4 @@ cli-build: | |||||||||||
| cd cli && npm run build | ||||||||||||
|
|
||||||||||||
| cli: | ||||||||||||
| cd cli && npm run dev -- | ||||||||||||
| cd cli && npm run dev -- | ||||||||||||
Uh oh!
There was an error while loading. Please reload this page.