Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions tools/spectral/ipa/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,42 @@ overrides:
x-xgen-IPA-xxx-rule: 'off'
```

#### Consumer-Provided Word Lists

The casing rules `xgen-IPA-117-operation-summary-format` and `xgen-IPA-126-tag-names-should-use-title-case`
use two configurable lists:

- `ignoreList`: words allowed to keep their specific casing (e.g. acronyms such as `API`, `AWS`, `DNS`)
- `grammaticalWords`: common words allowed to stay lowercase in titles (e.g. `and`, `or`, `the`)

Both lists are passed to the rule functions through Spectral's native `functionOptions`. Consumers who
extend the ruleset can provide their own lists by redeclaring the rule in their `.spectral.yaml`, without
editing this package or waiting for a new release:

```yaml
extends:
- '@mongodb/ipa-validation-ruleset'

rules:
xgen-IPA-126-tag-names-should-use-title-case:
given: $.tags[?(@.name && @.name.length > 0)]
then:
function: IPA126TagNamesShouldUseTitleCase
functionOptions:
ignoreList:
- API
- AWS
# ...the words this repository needs
grammaticalWords:
- and
- or
```

Because Spectral requires the whole rule to be redeclared when overriding `functionOptions`, consumers own
the complete lists for their repository. The lists defined in `IPA-117.yaml` and `IPA-126.yaml` remain the
canonical package defaults and are the ones used by this repository's own CI, so validation here is
unaffected by any consumer overrides.

### CI/CD Integration

#### GitHub Actions Example
Expand Down
73 changes: 73 additions & 0 deletions tools/spectral/ipa/__tests__/ConsumerWordLists.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { describe, expect, it } from '@jest/globals';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { Spectral } from '@stoplight/spectral-core';
import { httpAndFileResolver } from '@stoplight/spectral-ref-resolver';
import { bundleAndLoadRuleset } from '@stoplight/spectral-ruleset-bundler/with-loader';

// Proof that consumers extending the ruleset can supply their own `ignoreList` and
// `grammaticalWords` by redeclaring the rule with their own `functionOptions` — the
// native Spectral mechanism — without editing this package. The lists in the shipped
// rulesets remain the canonical defaults used by this repository's own CI.
async function runRuleWithOptions(ruleName, rulesetFile, functionOptions, document) {
const rulesetPath = path.join(__dirname, '../rulesets', rulesetFile);
const s = new Spectral({ resolver: httpAndFileResolver });
const ruleset = Object(await bundleAndLoadRuleset(rulesetPath, { fs, fetch })).toJSON();
const definition = ruleset.rules[ruleName].definition;
if (functionOptions) {
definition.then.functionOptions = functionOptions;
}
const scopedRuleset = { rules: { [ruleName]: definition } };
if (ruleset.aliases) {
scopedRuleset.aliases = ruleset.aliases;
}
s.setRuleset(scopedRuleset);
return s.run(JSON.stringify(document));
}

describe('Consumer-provided word lists via functionOptions', () => {
describe('xgen-IPA-126-tag-names-should-use-title-case (ignoreList)', () => {
const document = { tags: [{ name: 'ACME Alerts' }] };

it('flags a consumer acronym that is not in the package ignoreList', async () => {
const errors = await runRuleWithOptions(
'xgen-IPA-126-tag-names-should-use-title-case',
'IPA-126.yaml',
null,
document
);
expect(errors).toHaveLength(1);
expect(errors[0].code).toEqual('xgen-IPA-126-tag-names-should-use-title-case');
});

it('accepts the consumer acronym once added to functionOptions', async () => {
const errors = await runRuleWithOptions(
'xgen-IPA-126-tag-names-should-use-title-case',
'IPA-126.yaml',
{ ignoreList: ['ACME'], grammaticalWords: [] },
document
);
expect(errors).toHaveLength(0);
});
});

describe('xgen-IPA-117-operation-summary-format (grammaticalWords)', () => {
const document = { paths: { '/resource': { get: { summary: 'Return One Resource per Project' } } } };

it('flags a consumer grammatical word that is not in the package list', async () => {
const errors = await runRuleWithOptions('xgen-IPA-117-operation-summary-format', 'IPA-117.yaml', null, document);
expect(errors).toHaveLength(1);
expect(errors[0].code).toEqual('xgen-IPA-117-operation-summary-format');
});

it('accepts the consumer grammatical word once added to functionOptions', async () => {
const errors = await runRuleWithOptions(
'xgen-IPA-117-operation-summary-format',
'IPA-117.yaml',
{ ignoreList: [], grammaticalWords: ['per'] },
document
);
expect(errors).toHaveLength(0);
});
});
});
Loading