Skip to content
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

feat: validate openapi #12

Merged
merged 10 commits into from
Mar 4, 2025
2 changes: 1 addition & 1 deletion .github/workflows/deploy-preview.yaml
Original file line number Diff line number Diff line change
@@ -41,7 +41,7 @@ jobs:
run: pnpm build

- name: validate openapi
run: pnpm run validate-openapi
run: pnpm run lint:openapi

- name: lint
run: pnpm run lint
2 changes: 1 addition & 1 deletion .github/workflows/deploy.yaml
Original file line number Diff line number Diff line change
@@ -40,7 +40,7 @@ jobs:
run: pnpm build

- name: validate openapi
run: pnpm run validate-openapi
run: pnpm run lint:openapi

- name: lint
run: pnpm run lint
199 changes: 199 additions & 0 deletions .spectral.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
extends: [[spectral:oas, all], [spectral:asyncapi, all]]
rules:
operation-tags: off
operation-operationId: off
operation-success-response: error

# ----------------------------#
# Mojis OAS v2.0, v3.0 rules #
# ----------------------------#

mojis-paths-kebab-case:
description: All YAML/JSON paths MUST follow kebab-case
severity: warn
recommended: true
message: "{{property}} is not kebab-case: {{error}}"
given: $.paths[*]~
then:
function: pattern
functionOptions:
match: "^\/([a-z0-9]+(-[a-z0-9]+)*)?(\/[a-z0-9]+(-[a-z0-9]+)*|\/{.+})*$" # doesn't allow /asasd{asdas}sadas pattern or not closed braces

mojis-path-parameters-camelCase-alphanumeric:
description: Path parameters MUST follow camelCase
severity: warn
recommended: true
message: "{{property}} path parameter is not camelCase: {{error}}"
given: $..parameters[?(@.in == 'path')].name
then:
function: pattern
functionOptions:
match: "^[a-z][a-zA-Z0-9]+$"

mojis-definitions-camelCase-alphanumeric:
description: All YAML/JSON definitions MUST follow fields-camelCase and be ASCII alphanumeric characters or `_` or `$`.
severity: error
recommended: true
message: "{{property}} MUST follow camelCase and be ASCII alphanumeric characters or `_` or `$`."
given: $.definitions[*]~
then:
function: pattern
functionOptions:
match: "/^[a-z$_]{1}[A-Z09$_]*/"

mojis-properties-camelCase-alphanumeric:
description: All JSON Schema properties MUST follow fields-camelCase and be ASCII alphanumeric characters or `_` or `$`.
severity: error
recommended: true
message: "{{property}} MUST follow camelCase and be ASCII alphanumeric characters or `_` or `$`."
given: $.definitions..properties[*]~
then:
function: pattern
functionOptions:
match: "/^[a-z$_]{1}[A-Z09$_]*/"

mojis-request-GET-no-body:
description: "A 'GET' request MUST NOT accept a 'body` parameter"
severity: error
given: $.paths..get.parameters..in
then:
function: pattern
functionOptions:
notMatch: /^body$/

mojis-headers-no-x-headers:
description: "All 'HTTP' headers SHOULD NOT include 'X-' headers (https://tools.ietf.org/html/rfc6648)."
severity: warn
given: "$..parameters[?(@.in == 'header')].name"
message: "HTTP headers SHOULD NOT include 'X-' prefix."
recommended: true
type: style
then:
function: pattern
functionOptions:
notMatch: "/^(x|X)-/"

mojis-headers-hyphenated-pascal-case:
description: All `HTTP` headers MUST use `Hyphenated-Pascal-Case` notation
severity: error
given: "$..parameters[?(@.in == 'header')].name"
message: "'HTTP' headers MUST follow 'Hyphenated-Pascal-Case' notation"
recommended: true
type: style
then:
function: pattern
functionOptions:
match: "/^([A-Z][a-z0-9]-)*([A-Z][a-z0-9])+/"

# ----------------------#
# Mojis OAS v2.0 rules #
# ----------------------#

mojis-oas2-protocol-https-only:
description: ALL requests MUST go through `https` protocol only
formats:
- oas2
recommended: true
severity: error
type: style
message: Schemes MUST be https and no other value is allowed.
given: $
then:
field: schemes
function: schema
functionOptions:
schema:
type: array
items:
type: string
enum: [https]
maxItems: 1

mojis-oas2-request-support-json:
description: Every request SHOULD support `application/json` media type
formats:
- oas2
severity: warn
message: "{{description}}: {{error}}"
recommended: true
given: $..consumes
then:
function: schema
functionOptions:
schema:
type: array
contains:
type: string
enum:
- application/json

mojis-oas2-example-exists-in-parameters:
description: All models MUST have a valid example.
severity: error
recommended: true
formats:
- oas2
message: "{{ property }} MUST have a valid example."
given: "$..parameters..[?(@.in == 'body' && (@.example || @.schema.$ref))]"
then:
function: truthy

mojis-oas2-response-error-problem: # schemas and/or produces
description: All error responses MUST be of media type `application/problem+json`
severity: error
formats:
- oas2
given: $.paths..responses[?( @property >= 400 && @property < 600 )]
recommended: true
type: style
message: "Error response document MUST follow application/problem+json: {{error}}"
then:
field: schema.example
function: schema
functionOptions:
schema:
title: Problem Details for HTTP APIs
description: Definition of [RFC7807](https://tools.ietf.org/html/rfc7807) problem detail
type: object
properties:
type:
type: string
title:
type: string
status:
type: number
detail:
type: string
instance:
type: string
required:
- title
- detail

# ----------------------#
# Mojis OAS v3.0 rules #
# ----------------------#

mojis-oas3-request-support-json:
description: Every request MUST support `application/json` media type
formats:
- oas3
recommended: true
severity: error
message: "{{description}}: {{error}}"
given: $.paths.[*].requestBody.content[?(@property.indexOf('json') === -1)]^
then:
function: falsy

mojis-oas3-protocol-https-only:
description: ALL requests MUST go through `https` protocol only
formats:
- oas3
recommended: true
severity: error
message: Servers MUST be https and no other protocol is allowed.
given: $.servers..url
then:
function: pattern
functionOptions:
match: "/^https:/"
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -6,10 +6,12 @@
"scripts": {
"dev": "wrangler dev",
"build": "wrangler deploy --dry-run --outdir=dist",
"build:openapi": "tsx ./scripts/build-openapi",
"deploy": "wrangler deploy",
"test": "pnpm vitest --run",
"test:watch": "pnpm vitest",
"lint": "eslint .",
"lint:openapi": "pnpm run build:openapi && spectral lint ./node_modules/.openapi/openapi.json",
"typecheck": "tsc --noEmit"
},
"dependencies": {
@@ -23,8 +25,10 @@
"@cloudflare/vitest-pool-workers": "^0.7.4",
"@cloudflare/workers-types": "^4.20250224.0",
"@luxass/eslint-config": "^4.15.0",
"@stoplight/spectral-cli": "^6.14.2",
"eslint": "^9.21.0",
"eslint-plugin-format": "^1.0.1",
"tsx": "^4.19.3",
"typescript": "^5.8.2",
"vitest": "^3.0.7",
"wrangler": "^3.111.0"
Loading