From fb08bee757c56b93bcf54bea8f676e6497bb7ab0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ro=C5=BCek?= Date: Fri, 10 Jul 2020 12:34:26 +0200 Subject: [PATCH] feat(ruleset): introduce documentationUrl property (#1242) * feat(ruleset): introduce documentationUrl property * chore: Update src/meta/ruleset.schema.json Co-authored-by: Phil Sturgeon * Update docs/getting-started/rulesets.md Co-authored-by: Phil Sturgeon * docs: example * chore: add documentationUrl * test: cover url format * docs: code example Co-authored-by: nulltoken * feat: expose documentationUrl Co-authored-by: Phil Sturgeon Co-authored-by: nulltoken --- docs/getting-started/rulesets.md | 17 +++++++++++++++ src/meta/ruleset.schema.json | 4 ++++ src/rulesets/__tests__/validation.test.ts | 25 +++++++++++++++++------ src/rulesets/asyncapi/index.json | 3 ++- src/rulesets/oas/index.json | 1 + src/rulesets/reader.ts | 5 +++++ src/types/ruleset.ts | 2 ++ 7 files changed, 50 insertions(+), 7 deletions(-) diff --git a/docs/getting-started/rulesets.md b/docs/getting-started/rulesets.md index 471107aa4..0f8f1d455 100644 --- a/docs/getting-started/rulesets.md +++ b/docs/getting-started/rulesets.md @@ -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. diff --git a/src/meta/ruleset.schema.json b/src/meta/ruleset.schema.json index a4ec140de..863ae4ba5 100644 --- a/src/meta/ruleset.schema.json +++ b/src/meta/ruleset.schema.json @@ -3,6 +3,10 @@ "$id": "http://stoplight.io/schemas/ruleset.schema.json", "type": "object", "properties": { + "documentationUrl": { + "type": "string", + "format": "url" + }, "rules": { "type": "object", "additionalProperties": { diff --git a/src/rulesets/__tests__/validation.test.ts b/src/rulesets/__tests__/validation.test.ts index 1bf0316cc..dd5afca25 100644 --- a/src/rulesets/__tests__/validation.test.ts +++ b/src/rulesets/__tests__/validation.test.ts @@ -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, { diff --git a/src/rulesets/asyncapi/index.json b/src/rulesets/asyncapi/index.json index 54e478637..c14da0c35 100644 --- a/src/rulesets/asyncapi/index.json +++ b/src/rulesets/asyncapi/index.json @@ -1,4 +1,5 @@ { + "documentationUrl": "https://stoplight.io/p/docs/gh/stoplightio/spectral/docs/reference/asyncapi-rules.md", "formats": [ "asyncapi2" ], @@ -398,4 +399,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/rulesets/oas/index.json b/src/rulesets/oas/index.json index c5dbfde77..17c07877e 100644 --- a/src/rulesets/oas/index.json +++ b/src/rulesets/oas/index.json @@ -1,4 +1,5 @@ { + "documentationUrl": "https://stoplight.io/p/docs/gh/stoplightio/spectral/docs/reference/openapi-rules.md", "formats": ["oas2", "oas3"], "functions": [ "oasDocumentSchema", diff --git a/src/rulesets/reader.ts b/src/rulesets/reader.ts index 6015d2ec3..3ed6b6928 100644 --- a/src/rulesets/reader.ts +++ b/src/rulesets/reader.ts @@ -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; @@ -86,6 +90,7 @@ const createRulesetProcessor = ( const functions = {}; const exceptions = {}; const newRuleset: IRuleset = { + ...('documentationUrl' in ruleset ? { documentationUrl: ruleset.documentationUrl } : null), rules, functions, exceptions, diff --git a/src/types/ruleset.ts b/src/types/ruleset.ts index 06d335029..1e1b5cbee 100644 --- a/src/types/ruleset.ts +++ b/src/types/ruleset.ts @@ -22,12 +22,14 @@ export type RulesetFunctionCollection = Dictionary; export interface IRuleset { + documentationUrl?: string; rules: RuleCollection; functions: RulesetFunctionCollection; exceptions: RulesetExceptionCollection; } export interface IRulesetFile { + documentationUrl?: string; extends?: Array; formats?: string[]; rules?: FileRuleCollection;