Skip to content

Commit

Permalink
Beheer: voeg spectral linter toe aan standaard
Browse files Browse the repository at this point in the history
Op dit moment wordt deze linter configuratie gehost op
developer.overheid.nl. Echter zijn de beheerders van die
website niet verantwoordelijk voor de inhoudelijke
implementatie van de linter en de corresponderende
design rules.

Met deze commit voegen we de configuratie toe. Tevens
zijn er wat fixes gemaakt op basis van de huidige
COR API die wat incorrecte errors/warnings had. Sommige
errors/warnings waren valide en zullen in de COR API
zelf moeten worden opgelost.

Om ervoor te zorgen dat we inzicht hebben in wat het
effect van de linter/design rules zijn, voegen we ook de
COR API definitie met verwachte output toe. Deze kunnen
op CI worden getest telkens als de linter wordt gewijzigd.
  • Loading branch information
TimvdLippe committed Mar 3, 2025
1 parent 1603010 commit 8259ae7
Show file tree
Hide file tree
Showing 5 changed files with 1,407 additions and 0 deletions.
13 changes: 13 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,16 @@ jobs:
name: Publish (Logius)
uses: Logius-standaarden/Automatisering/.github/workflows/publish.yml@main
secrets: inherit
spectral_linter:
needs: build
name: Spectral linter test cases
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: run test suite
run: |
npm install -g @stoplight/spectral-cli
node linter/run-linter-tests.mjs
140 changes: 140 additions & 0 deletions linter/.spectral.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# spectral lint -r https://developer.overheid.nl/static/adr/ruleset.yaml $OAS_URL_OR_FILE
# curl https://developer.overheid.nl/static/adr/ruleset.yaml > .spectral.yml

extends: spectral:oas

rules:

#/core/doc-openapi
openapi3:
severity: error
given:
- "$.['openapi']"
then:
function: pattern
functionOptions:
match: "^3.0.*$"
message: "/core/doc-openapi: Use OpenAPI Specification for documentation: https://logius-standaarden.github.io/API-Design-Rules/#/core/doc-openapi"

#/core/version-header
missing-version-header:
severity: error
given: $..[responses][?(@property && @property.match(/(2|3)\d\d/))][headers]
then:
field: API-Version
function: truthy
message: "/core/version-header: Return the full version number in a response header: https://logius-standaarden.github.io/API-Design-Rules/#/core/version-header"

missing-header:
severity: error
given: $..[responses][?(@property && @property.match(/(2|3)\d\d/))]
then:
field: headers
function: truthy
message: "/core/version-header: Return the full version number in a response header: https://logius-standaarden.github.io/API-Design-Rules/#/core/version-header"

#/core/uri-version
include-major-version-in-uri:
severity: error
given:
- "$.servers[*]"
then:
function: pattern
functionOptions:
match: "\\/v[\\d+]"
field: url
message: "/core/uri-version: Include the major version number in the URI: https://logius-standaarden.github.io/API-Design-Rules/#/core/uri-version"

#/core/no-trailing-slash
paths-no-trailing-slash:
severity: error
given:
- "$.paths"
then:
function: pattern
functionOptions:
notMatch: "\\/$"
field: "@key"
message: "/core/no-trailing-slash: Leave off trailing slashes from URIs: https://logius-standaarden.github.io/API-Design-Rules/#/core/no-trailing-slash"

#/core/http-methods
http-methods:
severity: error
given:
- "$.paths[?(@property && @property.match(/(description|summary)/i))]"
then:
function: pattern
functionOptions:
match: "post|put|get|delete|patch|parameters"
field: "@key"
message: "/core/http-methods: Only apply standard HTTP methods: https://logius-standaarden.github.io/API-Design-Rules/#http-methods"

paths-kebab-case:
severity: warn
message: "{{property}} is not kebab-case."
given: $.paths[*]~
then:
function: pattern
functionOptions:
match: "^(\/[a-z0-9-.]+|\/{[a-zA-Z0-9_]+})+$"

schema-camel-case:
severity: warn
message: "Schema name should be CamelCase in {{path}}"
given: >-
$.components.schemas[*]~
then:
function: casing
functionOptions:
type: pascal
separator:
char: ""

servers-use-https:
severity: warn
message: "Server URL {{value}} {{error}}."
given:
- $.servers[*]
- $.paths..servers[*]
then:
field: url
function: pattern
functionOptions:
match: ^https://.*

use-problem-schema:
severity: warn
message: Your schema doesn't seem to match RFC7807. Are you sure it is ok? {{path}}
given: $..[responses][?(@property && @property.match(/^(4|5|default)/))][[schema]].properties
then:
function: schema
functionOptions:
schema:
anyOf:
- type: object
required:
- title
- status
- type: object
required:
- title
- type
- type: object
required:
- type
- status
- type: object
required:
- title
- detail

property-casing:
severity: warn
given:
- "$.*.schemas[*].properties.[?(@property && @property.match(/_links/i))]"
then:
function: casing
functionOptions:
type: camel
field: "@key"
message: Properties must be lowerCamelCase.
65 changes: 65 additions & 0 deletions linter/run-linter-tests.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import {exec} from 'child_process';
import utils from 'util';
import * as path from 'path';
import * as fs from 'fs';

const __dirname = import.meta.dirname;
const execute = utils.promisify(exec);
const readFile = utils.promisify(fs.readFile);
const readdir = utils.promisify(fs.readdir);

const SPECTRAL_RULESET_LOCATION = path.join(__dirname, '.spectral.yml');

function computeTestCommand(apiLocation) {
return `spectral lint -r ${SPECTRAL_RULESET_LOCATION} ${apiLocation}/openapi.json`
}

function removeProcessDir(output) {
return output.replaceAll(__dirname, '');
}

async function runCommand(apiLocation) {
try {
const {stdout, stderr} = await execute(computeTestCommand(apiLocation));

if (stderr) {
console.error('Found output on stderr:');
console.error(stderr);
process.exit(1);
}

if (!stdout) {
console.error('Did not find any output on stdout.');
process.exit(1);
}

return removeProcessDir(stdout);
} catch (e) {
console.error('Failed to run command');
console.error(e);
process.exit(1);
}
}

async function readExpectedOutput(apiLocation) {
return removeProcessDir(await readFile(path.join(apiLocation, 'expected-output.txt'), {encoding: 'utf-8'}));
}

async function obtainAllTestcases() {
const apiDirectories = await readdir(path.join(__dirname, 'testcases'), {withFileTypes: true});
return [...apiDirectories.map(dir => path.join(dir.parentPath, dir.name))];
}

for (const apiLocation of await obtainAllTestcases()) {
console.log(`Validating testcase ${apiLocation}`);
const actualOutput = await runCommand(apiLocation);
const expectedOutput = await readExpectedOutput(apiLocation);

if (actualOutput !== expectedOutput) {
console.error("Failing diff check. Expected:")
console.error(expectedOutput);
console.error("but got")
console.error(actualOutput);
process.exit(1);
}
}
6 changes: 6 additions & 0 deletions linter/testcases/cor-api/expected-output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

/testcases/cor-api/openapi.json
181:29 warning paths-kebab-case /laatsteWijziging is not kebab-case. paths./laatsteWijziging
774:30 warning use-problem-schema Your schema doesn't seem to match RFC7807. Are you sure it is ok? #/components/schemas/HealthCheckResponse/properties components.schemas.HealthCheckResponse.properties

✖ 2 problems (0 errors, 2 warnings, 0 infos, 0 hints)
Loading

0 comments on commit 8259ae7

Please sign in to comment.