Skip to content

Commit 32d5015

Browse files
CLOUDP-301224: Generate README with IPA rules (#451)
1 parent 982be01 commit 32d5015

File tree

8 files changed

+212
-30
lines changed

8 files changed

+212
-30
lines changed

.github/workflows/code-health-tools.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,14 @@ jobs:
8787
- name: Run ESLint on JS files
8888
run: |
8989
npm run lint-js
90+
- name: Check IPA docs up-to-date
91+
run: |
92+
npm run gen-ipa-docs
93+
if [[ -n $(git status --porcelain) ]]; then
94+
echo "IPA docs not up to date, please run 'npm run gen-ipa-docs' and commit the changes"
95+
exit 1
96+
fi
97+
exit 0
9098
- name: Install Go
9199
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34
92100
with:

package-lock.json

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

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"format": "npx prettier . --write",
77
"format-check": "npx prettier . --check",
88
"lint-js": "npx eslint **/*.js",
9+
"gen-ipa-docs": "node tools/spectral/ipa/scripts/generateRulesetReadme.js",
910
"ipa-validation": "spectral lint ./openapi/v2.yaml --ruleset=./tools/spectral/ipa/ipa-spectral.yaml",
1011
"test": "jest"
1112
},
@@ -28,6 +29,7 @@
2829
"apache-arrow": "^19.0.0",
2930
"dotenv": "^16.4.7",
3031
"eslint-plugin-jest": "^28.10.0",
32+
"markdown-table": "^3.0.4",
3133
"openapi-to-postmanv2": "4.25.0",
3234
"parquet-wasm": "^0.6.1"
3335
},

tools/spectral/ipa/metrics/metricCollection.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@ import {
33
extractTeamOwnership,
44
getSeverityPerRule,
55
loadCollectorResults,
6-
loadOpenAPIFile,
7-
loadRuleset,
86
merge,
97
} from './utils/metricCollectionUtils.js';
8+
import { loadJsonFile, loadRuleset } from '../utils.js';
109

1110
export async function runMetricCollectionJob(
1211
{
@@ -18,7 +17,7 @@ export async function runMetricCollectionJob(
1817
) {
1918
try {
2019
console.log(`Loading OpenAPI file: ${oasFilePath}`);
21-
const oasContent = loadOpenAPIFile(oasFilePath);
20+
const oasContent = loadJsonFile(oasFilePath);
2221

2322
console.log('Extracting team ownership data...');
2423
const ownershipData = extractTeamOwnership(oasContent);

tools/spectral/ipa/metrics/utils/metricCollectionUtils.js

Lines changed: 6 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,5 @@
1-
import fs from 'node:fs';
2-
import { bundleAndLoadRuleset } from '@stoplight/spectral-ruleset-bundler/with-loader';
31
import { EntryType } from '../collector.js';
4-
5-
export function loadOpenAPIFile(filePath) {
6-
try {
7-
const content = fs.readFileSync(filePath, 'utf8');
8-
return JSON.parse(content);
9-
} catch (error) {
10-
throw new Error(`Failed to load OpenAPI file: ${error.message}`);
11-
}
12-
}
2+
import { loadJsonFile } from '../../utils.js';
133

144
export function getSeverityPerRule(ruleset) {
155
const rules = ruleset.rules || {};
@@ -20,16 +10,6 @@ export function getSeverityPerRule(ruleset) {
2010
return map;
2111
}
2212

23-
export async function loadRuleset(rulesetPath, spectral) {
24-
try {
25-
const ruleset = await bundleAndLoadRuleset(rulesetPath, { fs, fetch });
26-
await spectral.setRuleset(ruleset);
27-
return ruleset;
28-
} catch (error) {
29-
throw new Error(`Failed to load ruleset: ${error.message}`);
30-
}
31-
}
32-
3313
export function extractTeamOwnership(oasContent) {
3414
const ownerTeams = {};
3515
const paths = oasContent.paths || {};
@@ -53,15 +33,14 @@ export function extractTeamOwnership(oasContent) {
5333

5434
export function loadCollectorResults(collectorResultsFilePath) {
5535
try {
56-
const content = fs.readFileSync(collectorResultsFilePath, 'utf8');
57-
const contentParsed = JSON.parse(content);
36+
const content = loadJsonFile(collectorResultsFilePath);
5837
return {
59-
[EntryType.VIOLATION]: contentParsed[EntryType.VIOLATION],
60-
[EntryType.ADOPTION]: contentParsed[EntryType.ADOPTION],
61-
[EntryType.EXCEPTION]: contentParsed[EntryType.EXCEPTION],
38+
[EntryType.VIOLATION]: content[EntryType.VIOLATION],
39+
[EntryType.ADOPTION]: content[EntryType.ADOPTION],
40+
[EntryType.EXCEPTION]: content[EntryType.EXCEPTION],
6241
};
6342
} catch (error) {
64-
throw new Error(`Failed to load Collector Results file: ${error.message}`);
43+
throw new Error(`Failed to parse Collector Results: ${error.message}`);
6544
}
6645
}
6746

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<!--- NOTE: This README file is generated, please see /scripts/generateRulesetReadme.js --->
2+
3+
# IPA Validation Rules
4+
5+
All Spectral rules used in the IPA validation are defined in rulesets grouped by IPA number (`IPA-XXX.yaml`). These rulesets are imported into the main IPA ruleset [ipa-spectral.yaml](https://github.com/mongodb/openapi/blob/main/tools/spectral/ipa/ipa-spectral.yaml) which is used for running the validation.
6+
7+
## Rulesets
8+
9+
The tables below lists all available rules, their descriptions and severity level.
10+
11+
### IPA-005
12+
13+
For rule definitions, see [IPA-005.yaml](https://github.com/mongodb/openapi/blob/main/tools/spectral/ipa/rulesets/IPA-005.yaml).
14+
15+
| Rule Name | Description | Severity |
16+
| --------------------------------------- | ------------------------------------------------------------------------ | -------- |
17+
| xgen-IPA-005-exception-extension-format | IPA exception extensions must follow the correct format. http://go/ipa/5 | warn |
18+
19+
### IPA-102
20+
21+
For rule definitions, see [IPA-102.yaml](https://github.com/mongodb/openapi/blob/main/tools/spectral/ipa/rulesets/IPA-102.yaml).
22+
23+
| Rule Name | Description | Severity |
24+
| ---------------------------------------------------- | -------------------------------------------------------------------------------- | -------- |
25+
| xgen-IPA-102-path-alternate-resource-name-path-param | Paths should alternate between resource names and path params. http://go/ipa/102 | warn |
26+
27+
### IPA-104
28+
29+
For rule definitions, see [IPA-104.yaml](https://github.com/mongodb/openapi/blob/main/tools/spectral/ipa/rulesets/IPA-104.yaml).
30+
31+
| Rule Name | Description | Severity |
32+
| ----------------------------- | --------------------------------------------------------------- | -------- |
33+
| xgen-IPA-104-resource-has-GET | APIs must provide a get method for resources. http://go/ipa/104 | warn |
34+
35+
### IPA-109
36+
37+
For rule definitions, see [IPA-109.yaml](https://github.com/mongodb/openapi/blob/main/tools/spectral/ipa/rulesets/IPA-109.yaml).
38+
39+
| Rule Name | Description | Severity |
40+
| ---------------------------------------------- | ------------------------------------------------------------------------- | -------- |
41+
| xgen-IPA-109-custom-method-must-be-GET-or-POST | The HTTP method for custom methods must be GET or POST. http://go/ipa/109 | warn |
42+
| xgen-IPA-109-custom-method-must-use-camel-case | The custom method must use camelCase format. http://go/ipa/109 | warn |
43+
44+
### IPA-113
45+
46+
For rule definitions, see [IPA-113.yaml](https://github.com/mongodb/openapi/blob/main/tools/spectral/ipa/rulesets/IPA-113.yaml).
47+
48+
| Rule Name | Description | Severity |
49+
| --------------------------------------- | ------------------------------------------------------------------------------------------- | -------- |
50+
| xgen-IPA-113-singleton-must-not-have-id | Singleton resources must not have a user-provided or system-generated ID. http://go/ipa/113 | warn |
51+
52+
### IPA-123
53+
54+
For rule definitions, see [IPA-123.yaml](https://github.com/mongodb/openapi/blob/main/tools/spectral/ipa/rulesets/IPA-123.yaml).
55+
56+
| Rule Name | Description | Severity |
57+
| ------------------------------------------------- | ------------------------------------------------------- | -------- |
58+
| xgen-IPA-123-enum-values-must-be-upper-snake-case | Enum values must be UPPER_SNAKE_CASE. http://go/ipa/123 | warn |
59+
60+
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import fs from 'node:fs';
2+
import path from 'path';
3+
import { fileURLToPath } from 'url';
4+
import spectral from '@stoplight/spectral-core';
5+
import { markdownTable } from 'markdown-table';
6+
import { loadRuleset } from '../utils.js';
7+
8+
const dirname = path.dirname(fileURLToPath(import.meta.url));
9+
const readmeFilePath = path.join(dirname, '../rulesets', 'README.md');
10+
11+
const rulesetsSection = await getRulesetsSection();
12+
13+
const fileContent =
14+
'<!--- NOTE: This README file is generated, please see /scripts/generateRulesetReadme.js --->\n\n' +
15+
'# IPA Validation Rules\n\n' +
16+
'All Spectral rules used in the IPA validation are defined in rulesets grouped by IPA number (`IPA-XXX.yaml`). These rulesets are imported into the main IPA ruleset [ipa-spectral.yaml](https://github.com/mongodb/openapi/blob/main/tools/spectral/ipa/ipa-spectral.yaml) which is used for running the validation.\n\n' +
17+
`${rulesetsSection}` +
18+
'\n';
19+
20+
fs.writeFile(readmeFilePath, fileContent, (error) => {
21+
if (error) {
22+
console.error('Error while generating the IPA rulesets README.md:', error);
23+
process.exit(1);
24+
}
25+
});
26+
27+
async function getRulesetsSection() {
28+
let content =
29+
'## Rulesets\n\n' + 'The tables below lists all available rules, their descriptions and severity level.\n\n';
30+
31+
const rules = await getAllRules();
32+
const ruleNames = Object.keys(rules);
33+
const ipaNumbers = getIpaNumbers(ruleNames);
34+
35+
ipaNumbers.forEach((ipaNumber) => {
36+
const ipaRules = filterRulesByIpaNumber(ipaNumber, rules);
37+
const table = generateRulesetTable(ipaRules);
38+
content +=
39+
`### ${ipaNumber}\n\n` + `For rule definitions, see ${getIpaRulesetUrl(ipaNumber)}.\n\n` + `${table}\n\n`;
40+
});
41+
42+
return content;
43+
}
44+
45+
function generateRulesetTable(rules) {
46+
const table = [['Rule Name', 'Description', 'Severity']];
47+
const tableRows = [];
48+
49+
const ruleNames = Object.keys(rules);
50+
ruleNames.forEach((ruleName) => {
51+
const rule = rules[ruleName];
52+
tableRows.push([ruleName, rule.description, rule.definition.severity]);
53+
});
54+
55+
tableRows.sort(sortBySeverity);
56+
tableRows.forEach((row) => table.push(row));
57+
return markdownTable(table);
58+
}
59+
60+
async function getAllRules() {
61+
const rulesetFilePath = path.join(dirname, '..', 'ipa-spectral.yaml');
62+
const { Spectral } = spectral;
63+
const ruleset = await loadRuleset(rulesetFilePath, new Spectral());
64+
return ruleset.rules;
65+
}
66+
67+
function getIpaNumbers(ruleNames) {
68+
const ipaNumbers = [];
69+
ruleNames.forEach((name) => {
70+
const ipaName = name.substring(5, 12);
71+
if (!ipaNumbers.includes(ipaName)) {
72+
ipaNumbers.push(ipaName);
73+
}
74+
});
75+
return ipaNumbers.sort();
76+
}
77+
78+
function getIpaRulesetUrl(ipaNumber) {
79+
return `[${ipaNumber}.yaml](https://github.com/mongodb/openapi/blob/main/tools/spectral/ipa/rulesets/${ipaNumber}.yaml)`;
80+
}
81+
82+
function filterRulesByIpaNumber(ipaNumber, rules) {
83+
return Object.keys(rules)
84+
.filter((key) => key.includes(ipaNumber))
85+
.reduce((obj, key) => {
86+
return {
87+
...obj,
88+
[key]: rules[key],
89+
};
90+
}, {});
91+
}
92+
93+
function sortBySeverity(a, b) {
94+
if (a[2] < b[2]) {
95+
return -1;
96+
} else if (a[2] > b[2]) {
97+
return 1;
98+
}
99+
return 0;
100+
}

tools/spectral/ipa/utils.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { bundleAndLoadRuleset } from '@stoplight/spectral-ruleset-bundler/with-loader';
2+
import fs from 'node:fs';
3+
4+
export async function loadRuleset(rulesetPath, spectral) {
5+
try {
6+
const ruleset = await bundleAndLoadRuleset(rulesetPath, { fs, fetch });
7+
await spectral.setRuleset(ruleset);
8+
return ruleset;
9+
} catch (error) {
10+
throw new Error(`Failed to load ruleset: ${error.message}`);
11+
}
12+
}
13+
14+
export function loadJsonFile(filePath) {
15+
try {
16+
const content = fs.readFileSync(filePath, 'utf8');
17+
return JSON.parse(content);
18+
} catch (error) {
19+
throw new Error(`Failed to load file: ${error.message}`);
20+
}
21+
}

0 commit comments

Comments
 (0)