Skip to content

Commit a03356b

Browse files
CLOUDP-285964: Adds IPA rule 104 - Resource has get
1 parent bb3fdb5 commit a03356b

File tree

13 files changed

+8254
-724
lines changed

13 files changed

+8254
-724
lines changed

babel.config.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"presets": ["@babel/preset-env"]
3+
}

eslint.config.mjs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import globals from 'globals';
22
import pluginJs from '@eslint/js';
3+
import pluginJest from 'eslint-plugin-jest';
4+
import jest from 'eslint-plugin-jest';
35

46
/** @type {import('eslint').Linter.Config[]} */
57
export default [
6-
{ languageOptions: { globals: globals.browser } },
8+
{
9+
plugins: { jest: pluginJest },
10+
languageOptions: { globals: globals.node },
11+
},
712
pluginJs.configs.recommended,
813
{
914
languageOptions: {
@@ -14,4 +19,8 @@ export default [
1419
{
1520
ignores: ['node-modules'],
1621
},
22+
{
23+
files: ['**/*.test.js'],
24+
...jest.configs['flat/recommended'],
25+
},
1726
];

package-lock.json

Lines changed: 7883 additions & 721 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,36 @@
44
"scripts": {
55
"format": "npx prettier . --write",
66
"format-check": "npx prettier . --check",
7-
"lint-js": "npx eslint **/*.js"
7+
"lint-js": "npx eslint **/*.js",
8+
"ipa-validation": "spectral lint ./openapi/v2.yaml --ruleset=./tools/ipa/ipa-spectral.yaml",
9+
"test": "jest"
10+
},
11+
"jest": {
12+
"transform": {
13+
"^.+\\.[t|j]sx?$": "babel-jest"
14+
},
15+
"testPathIgnorePatterns": [
16+
"__helpers__"
17+
]
818
},
919
"dependencies": {
20+
"@stoplight/spectral-cli": "^6.14.2",
21+
"@stoplight/spectral-core": "^1.19.4",
22+
"@stoplight/spectral-ref-resolver": "^1.0.5",
23+
"@stoplight/spectral-ruleset-bundler": "^1.6.1",
24+
"@stoplight/spectral-ruleset-migrator": "^1.11.1",
25+
"@stoplight/spectral-runtime": "^1.1.3",
26+
"@stoplight/types": "^14.1.1",
27+
"eslint-plugin-jest": "^28.9.0",
1028
"openapi-to-postmanv2": "4.24.0"
1129
},
1230
"devDependencies": {
31+
"@babel/preset-env": "^7.26.0",
1332
"@eslint/js": "^9.16.0",
14-
"eslint": "^9.15.0",
33+
"@jest/globals": "^29.7.0",
34+
"eslint": "^9.16.0",
1535
"globals": "^15.13.0",
36+
"jest": "^29.7.0",
1637
"prettier": "3.4.1"
1738
}
1839
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { pattern, truthy } from '@stoplight/spectral-functions';
2+
import eachResourceHasGetMethod from '/Users/lovisa.berggren/openapi/tools/ipa/rulesets/functions/eachResourceHasGetMethod.js';
3+
export default {
4+
extends: [
5+
{
6+
rules: {
7+
'xgen-IPA-104-resource-has-GET': {
8+
description: 'APIs must provide a get method for resources. http://go/ipa/104',
9+
message: '{{error}} http://go/ipa/117',
10+
severity: 'warn',
11+
given: '$.paths',
12+
then: {
13+
field: '@key',
14+
function: eachResourceHasGetMethod,
15+
},
16+
},
17+
},
18+
},
19+
{
20+
aliases: {
21+
PathItem: ['$.paths[*]'],
22+
OperationObject: ['#PathItem[get,put,post,delete,options,head,patch,trace]'],
23+
DescribableObjects: [
24+
'$.info',
25+
'$.tags[*]',
26+
'#OperationObject',
27+
'#PathItem.parameters[?(@ && @.in)]',
28+
'#OperationObject.parameters[?(@ && @.in)]',
29+
'$.components.schemas[*].properties[?(@ && @.type)]',
30+
],
31+
},
32+
rules: {
33+
'xgen-description': {
34+
description:
35+
'Use this field to describe the action performed by each specific API endpoint or property, to provide context for how to use it to the users of the API.',
36+
message: 'Property must have a description.',
37+
severity: 'error',
38+
given: '#DescribableObjects',
39+
then: [
40+
{
41+
field: 'description',
42+
function: truthy,
43+
},
44+
{
45+
field: 'description',
46+
function: pattern,
47+
functionOptions: {
48+
match: '/^[A-Z]/',
49+
},
50+
},
51+
{
52+
field: 'description',
53+
function: pattern,
54+
functionOptions: {
55+
match: '\\.|',
56+
},
57+
},
58+
],
59+
},
60+
},
61+
},
62+
],
63+
};
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import * as fs from 'node:fs';
2+
import * as path from 'node:path';
3+
import { describe, expect, it } from '@jest/globals';
4+
import { Spectral, Document } from '@stoplight/spectral-core';
5+
import { httpAndFileResolver } from '@stoplight/spectral-ref-resolver';
6+
import { bundleAndLoadRuleset } from '@stoplight/spectral-ruleset-bundler/with-loader';
7+
8+
const rulesetPath = path.join(__dirname, '../..', 'ipa-spectral.yaml');
9+
10+
export default (ruleName, tests) => {
11+
describe(`Rule ${ruleName}`, () => {
12+
for (const testCase of tests) {
13+
it.concurrent(testCase.name, async () => {
14+
const s = await createSpectral();
15+
const doc = testCase.document instanceof Document ? testCase.document : JSON.stringify(testCase.document);
16+
const allErrors = await s.run(doc);
17+
18+
const errors = allErrors.filter((e) => {
19+
if (testCase.errors[0]) {
20+
return e.code === testCase.errors[0].code;
21+
}
22+
return false;
23+
});
24+
25+
expect(errors.length).toEqual(testCase.errors.length);
26+
27+
errors.forEach((error, index) => {
28+
expect(error.code).toEqual(testCase.errors[index].code);
29+
expect(error.message).toEqual(testCase.errors[index].message);
30+
expect(error.path).toEqual(testCase.errors[index].path);
31+
});
32+
});
33+
}
34+
});
35+
};
36+
37+
async function createSpectral() {
38+
const s = new Spectral({ resolver: httpAndFileResolver });
39+
40+
s.setRuleset(await bundleAndLoadRuleset(rulesetPath, { fs, fetch }));
41+
42+
return s;
43+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import testRule from './__helpers__/testRule';
2+
import { DiagnosticSeverity } from '@stoplight/types';
3+
4+
testRule('xgen-description', [
5+
{
6+
name: 'valid standard method',
7+
document: {
8+
paths: {
9+
'/example': {
10+
get: {
11+
description: 'Test',
12+
},
13+
},
14+
},
15+
},
16+
errors: [],
17+
},
18+
{
19+
name: 'invalid standard method',
20+
document: {
21+
paths: {
22+
'/example': {
23+
get: {},
24+
},
25+
},
26+
},
27+
errors: [
28+
{
29+
code: 'xgen-description',
30+
message: 'Property must have a description.',
31+
path: ['paths', '/example', 'get'],
32+
severity: DiagnosticSeverity.Warning,
33+
},
34+
],
35+
},
36+
]);
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import testRule from './__helpers__/testRule';
2+
import { DiagnosticSeverity } from '@stoplight/types';
3+
4+
testRule('xgen-IPA-104-resource-has-GET', [
5+
{
6+
name: 'valid standard method',
7+
document: {
8+
paths: {
9+
'/example': {
10+
post: {},
11+
get: {},
12+
},
13+
'/example/{exampleId}': {
14+
get: {},
15+
patch: {},
16+
delete: {},
17+
},
18+
},
19+
},
20+
errors: [],
21+
},
22+
{
23+
name: 'invalid standard method',
24+
document: {
25+
paths: {
26+
'/example': {
27+
post: {},
28+
get: {},
29+
},
30+
'/example/{exampleId}': {
31+
patch: {},
32+
delete: {},
33+
},
34+
},
35+
},
36+
errors: [
37+
{
38+
code: 'xgen-IPA-104-resource-has-GET',
39+
message: 'APIs must provide a get method for resources. http://go/ipa/117',
40+
path: ['paths', '/example'],
41+
severity: DiagnosticSeverity.Warning,
42+
},
43+
],
44+
},
45+
]);

tools/ipa/ipa-spectral.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
extends:
2+
- ./rulesets/IPA-104.yaml
3+
- ./rulesets/IPA-xxx.yaml

tools/ipa/rulesets/IPA-104.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# IPA-104: Get
2+
# http://go/ipa/104
3+
4+
functions:
5+
- eachResourceHasGetMethod
6+
7+
rules:
8+
xgen-IPA-104-resource-has-GET:
9+
description: "APIs must provide a get method for resources. http://go/ipa/104"
10+
message: "{{error}} http://go/ipa/117"
11+
severity: warn
12+
given: "$.paths"
13+
then:
14+
field: "@key"
15+
function: "eachResourceHasGetMethod"

0 commit comments

Comments
 (0)