Skip to content

Commit

Permalink
feat(ruleset): introduce documentationUrl property (#1242)
Browse files Browse the repository at this point in the history
* feat(ruleset): introduce documentationUrl property

* chore: Update src/meta/ruleset.schema.json

Co-authored-by: Phil Sturgeon <phil@stoplight.io>

* Update docs/getting-started/rulesets.md

Co-authored-by: Phil Sturgeon <phil@stoplight.io>

* docs: example

* chore: add documentationUrl

* test: cover url format

* docs: code example

Co-authored-by: nulltoken <emeric.fermas@gmail.com>

* feat: expose documentationUrl

Co-authored-by: Phil Sturgeon <phil@stoplight.io>
Co-authored-by: nulltoken <emeric.fermas@gmail.com>
  • Loading branch information
3 people authored Jul 10, 2020
1 parent 89f1daa commit fb08bee
Show file tree
Hide file tree
Showing 7 changed files with 50 additions and 7 deletions.
17 changes: 17 additions & 0 deletions docs/getting-started/rulesets.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,20 @@ Spectral comes with two rulesets included:
- `spectral:asyncapi` - AsyncAPI v2 rules

You can also make your own: read more about [Custom Rulesets](../guides/4-custom-rulesets.md).

## Documentation URL

Optionally provide a documentation URL to your ruleset in order to help end-users find more information about various warnings. Result messages will sometimes be more than enough to explain what the problem is, but it can also be beneficial to explain _why_ a message exists, and this is a great place to do that.

Whatever you link you provide, the rule name will be appended as an anchor.

Given the following `documentationUrl` [`https://stoplight.io/p/docs/gh/stoplightio/spectral/docs/reference/openapi-rules.md`](https://stoplight.io/p/docs/gh/stoplightio/spectral/docs/reference/openapi-rules.md), an example URL for `info-contact` rule would look as follows [`https://stoplight.io/p/docs/gh/stoplightio/spectral/docs/reference/openapi-rules.md#info-contact`](https://stoplight.io/p/docs/gh/stoplightio/spectral/docs/reference/openapi-rules.md#info-contact).

```yaml
documentationUrl: https://www.example.com/docs/api-ruleset.md

rules:
# ...
```

If no `documentationUrl` is provided, no links will show up, and users will just have to rely on the error messages to figure out how the errors can be fixed.
4 changes: 4 additions & 0 deletions src/meta/ruleset.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
"$id": "http://stoplight.io/schemas/ruleset.schema.json",
"type": "object",
"properties": {
"documentationUrl": {
"type": "string",
"format": "url"
},
"rules": {
"type": "object",
"additionalProperties": {
Expand Down
25 changes: 19 additions & 6 deletions src/rulesets/__tests__/validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,46 @@ const invalidRuleset = require('./__fixtures__/invalid-ruleset.json');
const validRuleset = require('./__fixtures__/valid-flat-ruleset.json');

describe('Ruleset Validation', () => {
it('given primitive type should throw', () => {
it('given primitive type, throws', () => {
expect(assertValidRuleset.bind(null, null)).toThrow('Provided ruleset is not an object');
expect(assertValidRuleset.bind(null, 2)).toThrow('Provided ruleset is not an object');
expect(assertValidRuleset.bind(null, 'true')).toThrow('Provided ruleset is not an object');
});

it('given object with no rules and no extends properties should throw', () => {
it('given object with no rules and no extends properties, throws', () => {
expect(assertValidRuleset.bind(null, {})).toThrow('Ruleset must have rules or extends property');
expect(assertValidRuleset.bind(null, { rule: {} })).toThrow('Ruleset must have rules or extends property');
});

it('given object with extends property only should emit no errors', () => {
it('given object with extends property only, emits no errors', () => {
expect(assertValidRuleset.bind(null, { extends: [] })).not.toThrow();
});

it('given object with rules property only should emit no errors', () => {
it('given object with rules property only, emits no errors', () => {
expect(assertValidRuleset.bind(null, { rules: {} })).not.toThrow();
});

it('given invalid ruleset should throw', () => {
it('given invalid ruleset, throws', () => {
expect(assertValidRuleset.bind(null, invalidRuleset)).toThrow(ValidationError);
});

it('given valid ruleset should emit no errors', () => {
it('given valid ruleset should, emits no errors', () => {
expect(assertValidRuleset.bind(null, validRuleset)).not.toThrow();
});

it.each([false, 2, null, 'foo', '12.foo.com'])('given invalid %s documentationUrl, throws', documentationUrl => {
expect(assertValidRuleset.bind(null, { documentationUrl, rules: {} })).toThrow(ValidationError);
});

it('recognizes valid documentationUrl', () => {
expect(
assertValidRuleset.bind(null, {
documentationUrl: 'https://stoplight.io/p/docs/gh/stoplightio/spectral/docs/reference/openapi-rules.md',
extends: ['spectral:oas'],
}),
).not.toThrow();
});

it.each(['error', 'warn', 'info', 'hint', 'off'])('recognizes human-readable %s severity', severity => {
expect(
assertValidRuleset.bind(null, {
Expand Down
3 changes: 2 additions & 1 deletion src/rulesets/asyncapi/index.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"documentationUrl": "https://stoplight.io/p/docs/gh/stoplightio/spectral/docs/reference/asyncapi-rules.md",
"formats": [
"asyncapi2"
],
Expand Down Expand Up @@ -398,4 +399,4 @@
}
}
}
}
}
1 change: 1 addition & 0 deletions src/rulesets/oas/index.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"documentationUrl": "https://stoplight.io/p/docs/gh/stoplightio/spectral/docs/reference/openapi-rules.md",
"formats": ["oas2", "oas3"],
"functions": [
"oasDocumentSchema",
Expand Down
5 changes: 5 additions & 0 deletions src/rulesets/reader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ export async function readRuleset(uris: string | string[], opts?: IRulesetReadOp
Object.assign(base.rules, resolvedRuleset.rules);
Object.assign(base.functions, resolvedRuleset.functions);
Object.assign(base.exceptions, resolvedRuleset.exceptions);

if (resolvedRuleset.documentationUrl !== void 0 && !('documentationUrl' in base)) {
base.documentationUrl = resolvedRuleset.documentationUrl;
}
}

return base;
Expand Down Expand Up @@ -86,6 +90,7 @@ const createRulesetProcessor = (
const functions = {};
const exceptions = {};
const newRuleset: IRuleset = {
...('documentationUrl' in ruleset ? { documentationUrl: ruleset.documentationUrl } : null),
rules,
functions,
exceptions,
Expand Down
2 changes: 2 additions & 0 deletions src/types/ruleset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@ export type RulesetFunctionCollection = Dictionary<IRulesetFunctionDefinition, s
export type RulesetExceptionCollection = Dictionary<string[], string>;

export interface IRuleset {
documentationUrl?: string;
rules: RuleCollection;
functions: RulesetFunctionCollection;
exceptions: RulesetExceptionCollection;
}

export interface IRulesetFile {
documentationUrl?: string;
extends?: Array<string | [string, FileRulesetSeverity]>;
formats?: string[];
rules?: FileRuleCollection;
Expand Down

0 comments on commit fb08bee

Please sign in to comment.