From 08ba0bcbe8216b8165e4ed9ad66c930e6c4b03fd Mon Sep 17 00:00:00 2001 From: David Weber Date: Wed, 29 Mar 2023 23:49:55 +0200 Subject: [PATCH] feat(core): add documentUrl to JS api and cli formatters --- docs/guides/2-cli.md | 1 + .../cli/src/commands/__tests__/lint.test.ts | 10 +- packages/cli/src/commands/lint.ts | 15 ++ packages/cli/src/services/config.ts | 1 + packages/core/src/runner/lintNode.ts | 1 + packages/core/src/types/spectral.ts | 1 + .../formatters/src/__tests__/html.test.ts | 6 + packages/formatters/src/github-actions.ts | 4 +- .../src/html/html-template-message.html | 1 + packages/formatters/src/html/index.ts | 1 + packages/formatters/src/json.ts | 7 + packages/formatters/src/junit.ts | 3 + packages/formatters/src/pretty.ts | 5 + packages/formatters/src/sarif.ts | 2 +- packages/formatters/src/stylish.ts | 1 + packages/formatters/src/teamcity.ts | 3 +- packages/formatters/src/text.ts | 3 +- .../__tests__/asyncApi2DocumentSchema.test.ts | 4 + .../results-default-output.scenario | 63 ++++++ .../results-format-github-actions.scenario | 59 ++++++ .../results-format-html.scenario | 200 ++++++++++++++++++ .../results-format-json.scenario | 115 ++++++++++ .../results-format-junit.scenario | 62 ++++++ .../results-format-sarif.scenario | 175 +++++++++++++++ .../results-format-stylish.scenario | 63 ++++++ .../results-format-teamcity.scenario | 61 ++++++ .../formats/results-format-html.scenario | 3 + .../formats/results-format-sarif.scenario | 175 +++++++++++++++ .../formats/too-few-outputs.scenario | 1 + .../formats/too-many-outputs.scenario | 1 + .../formats/unmatched-outputs.scenario | 1 + .../scenarios/help-no-document.scenario | 1 + .../scenarios/strict-options.scenario | 1 + 33 files changed, 1044 insertions(+), 6 deletions(-) create mode 100644 test-harness/scenarios/documentation-url/results-default-output.scenario create mode 100644 test-harness/scenarios/documentation-url/results-format-github-actions.scenario create mode 100644 test-harness/scenarios/documentation-url/results-format-html.scenario create mode 100644 test-harness/scenarios/documentation-url/results-format-json.scenario create mode 100644 test-harness/scenarios/documentation-url/results-format-junit.scenario create mode 100644 test-harness/scenarios/documentation-url/results-format-sarif.scenario create mode 100644 test-harness/scenarios/documentation-url/results-format-stylish.scenario create mode 100644 test-harness/scenarios/documentation-url/results-format-teamcity.scenario create mode 100644 test-harness/scenarios/formats/results-format-sarif.scenario diff --git a/docs/guides/2-cli.md b/docs/guides/2-cli.md index 96879bc29..e2e1c1e3f 100644 --- a/docs/guides/2-cli.md +++ b/docs/guides/2-cli.md @@ -45,6 +45,7 @@ Other options include: -D, --display-only-failures only output results equal to or greater than --fail-severity [boolean] [default: false] --ignore-unknown-format do not warn about unmatched formats [boolean] [default: false] --fail-on-unmatched-globs fail on unmatched glob patterns [boolean] [default: false] + --show-documentation-url show documentation url in output result [boolean] [default: false] -v, --verbose increase verbosity [boolean] -q, --quiet no logging - output only [boolean] ``` diff --git a/packages/cli/src/commands/__tests__/lint.test.ts b/packages/cli/src/commands/__tests__/lint.test.ts index 69d59fb82..2390d1b0d 100644 --- a/packages/cli/src/commands/__tests__/lint.test.ts +++ b/packages/cli/src/commands/__tests__/lint.test.ts @@ -81,6 +81,7 @@ describe('lint', () => { output: { stylish: '' }, ignoreUnknownFormat: false, failOnUnmatchedGlobs: false, + showDocumentationUrl: false, }); }); }); @@ -94,6 +95,7 @@ describe('lint', () => { output: { stylish: '' }, ignoreUnknownFormat: false, failOnUnmatchedGlobs: false, + showDocumentationUrl: false, }); }); @@ -106,6 +108,7 @@ describe('lint', () => { output: { stylish: '' }, ignoreUnknownFormat: false, failOnUnmatchedGlobs: false, + showDocumentationUrl: false, }); }); @@ -118,6 +121,7 @@ describe('lint', () => { output: { json: '' }, ignoreUnknownFormat: false, failOnUnmatchedGlobs: false, + showDocumentationUrl: false, }); }); @@ -184,6 +188,7 @@ describe('lint', () => { output: { stylish: '' }, ignoreUnknownFormat: true, failOnUnmatchedGlobs: false, + showDocumentationUrl: false, }); }); @@ -195,6 +200,7 @@ describe('lint', () => { output: { stylish: '' }, ignoreUnknownFormat: false, failOnUnmatchedGlobs: true, + showDocumentationUrl: false, }); }); @@ -244,13 +250,13 @@ describe('lint', () => { expect(process.stderr.write).nthCalledWith(2, `Error #1: ${chalk.red('some unhandled exception')}\n`); expect(process.stderr.write).nthCalledWith( 3, - expect.stringContaining(`packages/cli/src/commands/__tests__/lint.test.ts:236`), + expect.stringContaining(`packages/cli/src/commands/__tests__/lint.test.ts:242`), ); expect(process.stderr.write).nthCalledWith(4, `Error #2: ${chalk.red('another one')}\n`); expect(process.stderr.write).nthCalledWith( 5, - expect.stringContaining(`packages/cli/src/commands/__tests__/lint.test.ts:237`), + expect.stringContaining(`packages/cli/src/commands/__tests__/lint.test.ts:243`), ); expect(process.stderr.write).nthCalledWith(6, `Error #3: ${chalk.red('original exception')}\n`); diff --git a/packages/cli/src/commands/lint.ts b/packages/cli/src/commands/lint.ts index dffc1f3bb..d84c6f89e 100644 --- a/packages/cli/src/commands/lint.ts +++ b/packages/cli/src/commands/lint.ts @@ -151,6 +151,11 @@ const lintCommand: CommandModule = { type: 'boolean', default: false, }, + 'show-documentation-url': { + description: 'show documentation url in output result', + type: 'boolean', + default: false, + }, verbose: { alias: 'v', description: 'increase verbosity', @@ -175,6 +180,7 @@ const lintCommand: CommandModule = { encoding, ignoreUnknownFormat, failOnUnmatchedGlobs, + showDocumentationUrl, ...config } = args as unknown as ILintConfig & { documents: Array; @@ -189,6 +195,7 @@ const lintCommand: CommandModule = { encoding, ignoreUnknownFormat, failOnUnmatchedGlobs, + showDocumentationUrl, ruleset, stdinFilepath, ...pick, keyof ILintConfig>(config, ['verbose', 'quiet', 'resolver']), @@ -198,6 +205,10 @@ const lintCommand: CommandModule = { linterResult.results = filterResultsBySeverity(linterResult.results, failSeverity); } + if (!showDocumentationUrl) { + linterResult.results = removeDocumentationUrlFromResults(linterResult.results); + } + await Promise.all( format.map(f => { const formattedOutput = formatOutput( @@ -279,6 +290,10 @@ const filterResultsBySeverity = (results: IRuleResult[], failSeverity: FailSever return results.filter(r => r.severity <= diagnosticSeverity); }; +const removeDocumentationUrlFromResults = (results: IRuleResult[]): IRuleResult[] => { + return results.map(r => ({ ...r, documentationUrl: undefined })); +}; + export const severeEnoughToFail = (results: IRuleResult[], failSeverity: FailSeverity): boolean => { const diagnosticSeverity = getDiagnosticSeverity(failSeverity); return results.some(r => r.severity <= diagnosticSeverity); diff --git a/packages/cli/src/services/config.ts b/packages/cli/src/services/config.ts index b0ec0213c..d2dcf5849 100644 --- a/packages/cli/src/services/config.ts +++ b/packages/cli/src/services/config.ts @@ -24,6 +24,7 @@ export interface ILintConfig { stdinFilepath?: string; ignoreUnknownFormat: boolean; failOnUnmatchedGlobs: boolean; + showDocumentationUrl: boolean; verbose?: boolean; quiet?: boolean; } diff --git a/packages/core/src/runner/lintNode.ts b/packages/core/src/runner/lintNode.ts index 2dcf931c2..6070c6811 100644 --- a/packages/core/src/runner/lintNode.ts +++ b/packages/core/src/runner/lintNode.ts @@ -99,6 +99,7 @@ function processTargetResults( severity, ...(source !== null ? { source } : null), range, + documentationUrl: rule.documentationUrl ?? undefined, }); } } diff --git a/packages/core/src/types/spectral.ts b/packages/core/src/types/spectral.ts index 6b30d739e..a6ae3a1a2 100644 --- a/packages/core/src/types/spectral.ts +++ b/packages/core/src/types/spectral.ts @@ -13,6 +13,7 @@ export interface IRunOpts { export interface ISpectralDiagnostic extends IDiagnostic { path: JsonPath; code: string | number; + documentationUrl?: string; } export type IRuleResult = ISpectralDiagnostic; diff --git a/packages/formatters/src/__tests__/html.test.ts b/packages/formatters/src/__tests__/html.test.ts index 8db9acb7e..c9d7c4564 100644 --- a/packages/formatters/src/__tests__/html.test.ts +++ b/packages/formatters/src/__tests__/html.test.ts @@ -18,36 +18,42 @@ describe('HTML formatter', () => { 3:10 hint Info object should contain \`contact\` object. + 3:10 warning OpenAPI object info \`description\` must be present and non-empty string. + 5:14 error Info must contain Stoplight + 17:13 information Operation \`description\` must be present and non-empty string. + 64:14 information Operation \`description\` must be present and non-empty string. + 86:13 information Operation \`description\` must be present and non-empty string. + `); }); }); diff --git a/packages/formatters/src/github-actions.ts b/packages/formatters/src/github-actions.ts index f09b43adf..bca7f05c6 100644 --- a/packages/formatters/src/github-actions.ts +++ b/packages/formatters/src/github-actions.ts @@ -41,7 +41,9 @@ export const githubActions: Formatter = results => { // FIXME: Use replaceAll instead after removing Node.js 14 support. const message = result.message.replace(/\n/g, '%0A'); - return `::${OUTPUT_TYPES[result.severity]} ${paramsString}::${message}`; + return `::${OUTPUT_TYPES[result.severity]} ${paramsString}::${message}${ + result.documentationUrl ? `::${result.documentationUrl}` : '' + }`; }) .join('\n'); }; diff --git a/packages/formatters/src/html/html-template-message.html b/packages/formatters/src/html/html-template-message.html index 73f5ba9d1..74ac39f98 100644 --- a/packages/formatters/src/html/html-template-message.html +++ b/packages/formatters/src/html/html-template-message.html @@ -2,4 +2,5 @@ <%= line %>:<%= character %> <%= severity %> <%- message %> + <% if(documentationUrl) { %>documentation<% } %> diff --git a/packages/formatters/src/html/index.ts b/packages/formatters/src/html/index.ts index f68f9d561..33bda5257 100644 --- a/packages/formatters/src/html/index.ts +++ b/packages/formatters/src/html/index.ts @@ -50,6 +50,7 @@ function renderMessages(messages: IRuleResult[], parentIndex: number): string { severity: getSeverityName(message.severity), message: message.message, code: message.code, + documentationUrl: message.documentationUrl, }); }) .join('\n'); diff --git a/packages/formatters/src/json.ts b/packages/formatters/src/json.ts index 4ff9fbce9..0f57fc2f0 100644 --- a/packages/formatters/src/json.ts +++ b/packages/formatters/src/json.ts @@ -2,6 +2,12 @@ import { Formatter } from './types'; export const json: Formatter = results => { const outputJson = results.map(result => { + let documentationUrlObject = {}; + if (result.documentationUrl) { + documentationUrlObject = { + documentationUrl: result.documentationUrl, + }; + } return { code: result.code, path: result.path, @@ -9,6 +15,7 @@ export const json: Formatter = results => { severity: result.severity, range: result.range, source: result.source, + ...documentationUrlObject, }; }); return JSON.stringify(outputJson, null, '\t'); diff --git a/packages/formatters/src/junit.ts b/packages/formatters/src/junit.ts index 646341af5..60eebf2c3 100644 --- a/packages/formatters/src/junit.ts +++ b/packages/formatters/src/junit.ts @@ -62,6 +62,9 @@ export const junit: Formatter = (results, { failSeverity }) => { output += `line ${result.range.start.line + 1}, col ${result.range.start.character + 1}, `; output += `${prepareForCdata(result.message)} (${result.code}) `; output += `at path ${prepareForCdata(path)}`; + if (result.documentationUrl) { + output += `, ${result.documentationUrl}`; + } output += ']]>'; output += ``; output += '\n'; diff --git a/packages/formatters/src/pretty.ts b/packages/formatters/src/pretty.ts index 3d1a40403..a4fd63906 100644 --- a/packages/formatters/src/pretty.ts +++ b/packages/formatters/src/pretty.ts @@ -74,6 +74,11 @@ export const pretty: Formatter = results => { { text: chalk[color].bold(result.code), padding: PAD_TOP0_LEFT2, width: COLUMNS[2] }, { text: chalk.gray(result.message), padding: PAD_TOP0_LEFT2, width: COLUMNS[3] }, { text: chalk.cyan(printPath(result.path, PrintStyle.Dot)), padding: PAD_TOP0_LEFT2 }, + { + text: chalk.gray(result.documentationUrl ?? ''), + padding: PAD_TOP0_LEFT2, + width: result.documentationUrl ? undefined : 0.1, + }, ); ui.div(); }); diff --git a/packages/formatters/src/sarif.ts b/packages/formatters/src/sarif.ts index 76c9f74eb..fb7d69103 100644 --- a/packages/formatters/src/sarif.ts +++ b/packages/formatters/src/sarif.ts @@ -44,7 +44,7 @@ export const sarif: Formatter = (results, _, ctx) => { const severity: DiagnosticSeverity = result.severity || DiagnosticSeverity.Error; sarifResultBuilder.initSimple({ level: OUTPUT_TYPES[severity] || 'error', - messageText: result.message, + messageText: result.documentationUrl ? `${result.message} -- ${result.documentationUrl}` : result.message, ruleId: result.code.toString(), fileUri: relative(process.cwd(), result.source ?? '').replace(/\\/g, '/'), startLine: result.range.start.line + 1, diff --git a/packages/formatters/src/stylish.ts b/packages/formatters/src/stylish.ts index 7f0aecf34..013eb6439 100644 --- a/packages/formatters/src/stylish.ts +++ b/packages/formatters/src/stylish.ts @@ -72,6 +72,7 @@ export const stylish: Formatter = results => { result.code ?? '', result.message, printPath(result.path, PrintStyle.Dot), + result.documentationUrl ?? '', ]); output += `${table(pathTableData, { diff --git a/packages/formatters/src/teamcity.ts b/packages/formatters/src/teamcity.ts index 8b3a53e32..c69bf5df5 100644 --- a/packages/formatters/src/teamcity.ts +++ b/packages/formatters/src/teamcity.ts @@ -23,7 +23,8 @@ function inspectionType(result: IRuleResult & { source: string }): string { const code = escapeString(result.code); const severity = getSeverityName(result.severity); const message = escapeString(result.message); - return `##teamcity[inspectionType category='openapi' id='${code}' name='${code}' description='${severity} -- ${message}']`; + const documentationUrl = result.documentationUrl ? ` -- ${escapeString(result.documentationUrl)}` : ''; + return `##teamcity[inspectionType category='openapi' id='${code}' name='${code}' description='${severity} -- ${message}${documentationUrl}']`; } function inspection(result: IRuleResult & { source: string }): string { diff --git a/packages/formatters/src/text.ts b/packages/formatters/src/text.ts index 12638bf0c..54705272c 100644 --- a/packages/formatters/src/text.ts +++ b/packages/formatters/src/text.ts @@ -12,7 +12,8 @@ function renderResults(results: IRuleResult[]): string { const line = result.range.start.line + 1; const character = result.range.start.character + 1; const severity = getSeverityName(result.severity); - return `${result.source}:${line}:${character} ${severity} ${result.code} "${result.message}"`; + const documentationUrl = result.documentationUrl ? ` ${result.documentationUrl}` : ''; + return `${result.source}:${line}:${character} ${severity} ${result.code} "${result.message}"${documentationUrl}`; }) .join('\n'); } diff --git a/packages/rulesets/src/asyncapi/functions/__tests__/asyncApi2DocumentSchema.test.ts b/packages/rulesets/src/asyncapi/functions/__tests__/asyncApi2DocumentSchema.test.ts index d530b9930..11c9ebc9a 100644 --- a/packages/rulesets/src/asyncapi/functions/__tests__/asyncApi2DocumentSchema.test.ts +++ b/packages/rulesets/src/asyncapi/functions/__tests__/asyncApi2DocumentSchema.test.ts @@ -43,6 +43,7 @@ describe('asyncApi2DocumentSchema', () => { ).toEqual([ { code: 'asyncapi-schema', + documentationUrl: 'https://meta.stoplight.io/docs/spectral/docs/reference/asyncapi-rules.md#asyncapi-schema', message: '"info" property must have required property "title"', path: ['info'], severity: DiagnosticSeverity.Error, @@ -131,6 +132,7 @@ describe('asyncApi2DocumentSchema', () => { ).toEqual([ { code: 'asyncapi-schema', + documentationUrl: 'https://meta.stoplight.io/docs/spectral/docs/reference/asyncapi-rules.md#asyncapi-schema', message: '"0" property type must be string', path: ['channels', '/user/signedup', 'servers', '0'], severity: DiagnosticSeverity.Error, @@ -138,6 +140,7 @@ describe('asyncApi2DocumentSchema', () => { }, { code: 'asyncapi-schema', + documentationUrl: 'https://meta.stoplight.io/docs/spectral/docs/reference/asyncapi-rules.md#asyncapi-schema', message: '"2" property type must be string', path: ['channels', '/user/signedup', 'servers', '2'], severity: DiagnosticSeverity.Error, @@ -184,6 +187,7 @@ describe('asyncApi2DocumentSchema', () => { ).toEqual([ { code: 'asyncapi-schema', + documentationUrl: 'https://meta.stoplight.io/docs/spectral/docs/reference/asyncapi-rules.md#asyncapi-schema', message: '"kafka" property must have required property "url"', path: ['components', 'servers', 'kafka'], severity: DiagnosticSeverity.Error, diff --git a/test-harness/scenarios/documentation-url/results-default-output.scenario b/test-harness/scenarios/documentation-url/results-default-output.scenario new file mode 100644 index 000000000..858e837fd --- /dev/null +++ b/test-harness/scenarios/documentation-url/results-default-output.scenario @@ -0,0 +1,63 @@ +====test==== +Invalid document --output to a file, will show all the contents when the file is read +====document==== +--- +info: + version: 1.0.0 + title: Stoplight +====asset:ruleset.json==== +{ + "rules": { + "api-servers": { + "documentationUrl": "https://www.example.com/docs/api-servers.md", + "description": "\"servers\" must be present and non-empty array.", + "recommended": true, + "given": "$", + "then": { + "field": "servers", + "function": "schema", + "functionOptions": { + "dialect": "draft7", + "schema": { + "items": { + "type": "object", + }, + "minItems": 1, + "type": "array" + } + } + } + }, + "info-contact": { + "description": "Info object must have a \"contact\" object.", + "recommended": true, + "type": "style", + "given": "$", + "then": { + "field": "info.contact", + "function": "truthy", + } + }, + "info-description": { + "documentationUrl": "https://www.example.com/docs/info-description.md", + "description": "Info \"description\" must be present and non-empty string.", + "recommended": true, + "type": "style", + "given": "$", + "then": { + "field": "info.description", + "function": "truthy" + } + } + } +} +====command-nix==== +{bin} lint {document} --ruleset "{asset:ruleset.json}" --format=text --output={asset:output.txt} --show-documentation-url > /dev/null; cat {asset:output.txt} +====command-win==== +{bin} lint {document} --ruleset "{asset:ruleset.json}" --format=text --output={asset:output.txt} --show-documentation-url | Out-Null; cat {asset:output.txt} +====asset:output.txt==== +====stdout==== +{document}:1:1 warning api-servers ""servers" must be present and non-empty array." https://www.example.com/docs/api-servers.md +{document}:2:6 warning info-contact "Info object must have a "contact" object." +{document}:2:6 warning info-description "Info "description" must be present and non-empty string." https://www.example.com/docs/info-description.md + diff --git a/test-harness/scenarios/documentation-url/results-format-github-actions.scenario b/test-harness/scenarios/documentation-url/results-format-github-actions.scenario new file mode 100644 index 000000000..b18f2e207 --- /dev/null +++ b/test-harness/scenarios/documentation-url/results-format-github-actions.scenario @@ -0,0 +1,59 @@ +====test==== +Invalid document outputs results when --format=github-actions +====document==== +--- +info: + version: 1.0.0 + title: Stoplight +====asset:ruleset.json==== +{ + "rules": { + "api-servers": { + "documentationUrl": "https://www.example.com/docs/api-servers.md", + "description": "\"servers\" must be present and non-empty array.", + "recommended": true, + "given": "$", + "then": { + "field": "servers", + "function": "schema", + "functionOptions": { + "dialect": "draft7", + "schema": { + "items": { + "type": "object", + }, + "minItems": 1, + "type": "array" + } + } + } + }, + "info-contact": { + "description": "Info object must have a \"contact\" object.", + "recommended": true, + "type": "style", + "given": "$", + "then": { + "field": "info.contact", + "function": "truthy", + } + }, + "info-description": { + "documentationUrl": "https://www.example.com/docs/info-description.md", + "description": "Info \"description\" must be present and non-empty string.", + "recommended": true, + "type": "style", + "given": "$", + "then": { + "field": "info.description", + "function": "truthy" + } + } + } +} +====command==== +{bin} lint {document} --format=github-actions --ruleset "{asset:ruleset.json}" --show-documentation-url +====stdout==== +::warning title=api-servers,file=document,col=1,endColumn=19,line=1,endLine=4::"servers" must be present and non-empty array.::https://www.example.com/docs/api-servers.md +::warning title=info-contact,file=document,col=6,endColumn=19,line=2,endLine=4::Info object must have a "contact" object. +::warning title=info-description,file=document,col=6,endColumn=19,line=2,endLine=4::Info "description" must be present and non-empty string.::https://www.example.com/docs/info-description.md \ No newline at end of file diff --git a/test-harness/scenarios/documentation-url/results-format-html.scenario b/test-harness/scenarios/documentation-url/results-format-html.scenario new file mode 100644 index 000000000..4fa05cb70 --- /dev/null +++ b/test-harness/scenarios/documentation-url/results-format-html.scenario @@ -0,0 +1,200 @@ +====test==== +Invalid document outputs results when --format=html +====document==== +--- +info: + version: 1.0.0 + title: Stoplight +====asset:ruleset.json==== +{ + "rules": { + "api-servers": { + "documentationUrl": "https://www.example.com/docs/api-servers.md", + "description": "\"servers\" must be present and non-empty array.", + "recommended": true, + "given": "$", + "then": { + "field": "servers", + "function": "schema", + "functionOptions": { + "dialect": "draft7", + "schema": { + "items": { + "type": "object", + }, + "minItems": 1, + "type": "array" + } + } + } + }, + "info-contact": { + "description": "Info object must have a \"contact\" object.", + "recommended": true, + "type": "style", + "given": "$", + "then": { + "field": "info.contact", + "function": "truthy", + } + }, + "info-description": { + "documentationUrl": "https://www.example.com/docs/info-description.md", + "description": "Info \"description\" must be present and non-empty string.", + "recommended": true, + "type": "style", + "given": "$", + "then": { + "field": "info.description", + "function": "truthy" + } + } + } +} +====command==== +{bin} lint {document} --format=html --ruleset "{asset:ruleset.json}" --show-documentation-url +====stdout==== + + + + + + Spectral Report + + +
+

Spectral Report

+
+ 3 problems (0 errors, 3 warnings, 0 infos, 0 hints) - Generated on {date} +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ [+] {document} + 3 problems (0 errors, 3 warnings, 0 infos, 0 hints) +
+ + + diff --git a/test-harness/scenarios/documentation-url/results-format-json.scenario b/test-harness/scenarios/documentation-url/results-format-json.scenario new file mode 100644 index 000000000..0488022c0 --- /dev/null +++ b/test-harness/scenarios/documentation-url/results-format-json.scenario @@ -0,0 +1,115 @@ +====test==== +Invalid document outputs results --format=json +====document==== +--- +info: + version: 1.0.0 + title: Stoplight +====asset:ruleset.json==== +{ + "rules": { + "api-servers": { + "documentationUrl": "https://www.example.com/docs/api-servers.md", + "description": "\"servers\" must be present and non-empty array.", + "recommended": true, + "given": "$", + "then": { + "field": "servers", + "function": "schema", + "functionOptions": { + "dialect": "draft7", + "schema": { + "items": { + "type": "object", + }, + "minItems": 1, + "type": "array" + } + } + } + }, + "info-contact": { + "description": "Info object must have a \"contact\" object.", + "recommended": true, + "type": "style", + "given": "$", + "then": { + "field": "info.contact", + "function": "truthy", + } + }, + "info-description": { + "documentationUrl": "https://www.example.com/docs/info-description.md", + "description": "Info \"description\" must be present and non-empty string.", + "recommended": true, + "type": "style", + "given": "$", + "then": { + "field": "info.description", + "function": "truthy" + } + } + } +} +====command==== +{bin} lint {document} --format=json --ruleset "{asset:ruleset.json}" --show-documentation-url +====stdout==== +[ + { + "code": "api-servers", + "path": [], + "message": "\"servers\" must be present and non-empty array.", + "severity": 1, + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 3, + "character": 18 + } + }, + "source": "{document}", + "documentationUrl": "https://www.example.com/docs/api-servers.md" + }, + { + "code": "info-contact", + "path": [ + "info" + ], + "message": "Info object must have a \"contact\" object.", + "severity": 1, + "range": { + "start": { + "line": 1, + "character": 5 + }, + "end": { + "line": 3, + "character": 18 + } + }, + "source": "{document}" + }, + { + "code": "info-description", + "path": [ + "info" + ], + "message": "Info \"description\" must be present and non-empty string.", + "severity": 1, + "range": { + "start": { + "line": 1, + "character": 5 + }, + "end": { + "line": 3, + "character": 18 + } + }, + "source": "{document}", + "documentationUrl": "https://www.example.com/docs/info-description.md" + } +] diff --git a/test-harness/scenarios/documentation-url/results-format-junit.scenario b/test-harness/scenarios/documentation-url/results-format-junit.scenario new file mode 100644 index 000000000..5a6f26960 --- /dev/null +++ b/test-harness/scenarios/documentation-url/results-format-junit.scenario @@ -0,0 +1,62 @@ +====test==== +Invalid document outputs results when --format=junit +====document==== +--- +info: + version: 1.0.0 + title: Stoplight +====asset:ruleset.json==== +{ + "rules": { + "api-servers": { + "documentationUrl": "https://www.example.com/docs/api-servers.md", + "description": "\"servers\" must be present and non-empty array.", + "severity": "error", + "given": "$", + "then": { + "field": "servers", + "function": "schema", + "functionOptions": { + "dialect": "draft7", + "schema": { + "items": { + "type": "object", + }, + "minItems": 1, + "type": "array" + } + } + } + }, + "info-contact": { + "description": "Info object must have a \"contact\" object.", + "recommended": true, + "type": "style", + "given": "$", + "then": { + "field": "info.contact", + "function": "truthy", + } + }, + "info-description": { + "documentationUrl": "https://www.example.com/docs/info-description.md", + "description": "Info \"description\" must be present and non-empty string.", + "recommended": true, + "type": "style", + "given": "$", + "then": { + "field": "info.description", + "function": "truthy" + } + } + } +} +====command==== +{bin} lint {document} --format=junit --ruleset "{asset:ruleset.json}" --show-documentation-url +====stdout==== + + + + + + diff --git a/test-harness/scenarios/documentation-url/results-format-sarif.scenario b/test-harness/scenarios/documentation-url/results-format-sarif.scenario new file mode 100644 index 000000000..a976a3be9 --- /dev/null +++ b/test-harness/scenarios/documentation-url/results-format-sarif.scenario @@ -0,0 +1,175 @@ +====test==== +Invalid document outputs results when --format=sarif +====document==== +--- +info: + version: 1.0.0 + title: Stoplight +====asset:ruleset.json==== +{ + "rules": { + "api-servers": { + "documentationUrl": "https://www.example.com/docs/api-servers.md", + "description": "\"servers\" must be present and non-empty array.", + "recommended": true, + "given": "$", + "then": { + "field": "servers", + "function": "schema", + "functionOptions": { + "dialect": "draft7", + "schema": { + "items": { + "type": "object", + }, + "minItems": 1, + "type": "array" + } + } + } + }, + "info-contact": { + "description": "Info object must have a \"contact\" object.", + "recommended": true, + "type": "style", + "given": "$", + "then": { + "field": "info.contact", + "function": "truthy", + } + }, + "info-description": { + "documentationUrl": "https://www.example.com/docs/info-description.md", + "description": "Info \"description\" must be present and non-empty string.", + "recommended": true, + "type": "style", + "given": "$", + "then": { + "field": "info.description", + "function": "truthy" + } + } + } +} +====command==== +{bin} lint {document} --format=sarif --ruleset "{asset:ruleset.json}" --show-documentation-url +====stdout==== +{ + "$schema": "http://json.schemastore.org/sarif-2.1.0-rtm.6.json", + "version": "2.1.0", + "runs": [ + { + "tool": { + "driver": { + "name": "spectral", + "rules": [ + { + "id": "api-servers", + "shortDescription": { + "text": "\"servers\" must be present and non-empty array." + }, + "helpUri": "https://www.example.com/docs/api-servers.md" + }, + { + "id": "info-contact", + "shortDescription": { + "text": "Info object must have a \"contact\" object." + } + }, + { + "id": "info-description", + "shortDescription": { + "text": "Info \"description\" must be present and non-empty string." + }, + "helpUri": "https://www.example.com/docs/info-description.md" + } + ], + "version": "6.11.1", + "informationUri": "https://github.com/stoplightio/spectral" + } + }, + "results": [ + { + "level": "warning", + "message": { + "text": "\"servers\" must be present and non-empty array. -- https://www.example.com/docs/api-servers.md" + }, + "ruleId": "api-servers", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "document", + "index": 0 + }, + "region": { + "startLine": 1, + "startColumn": 1, + "endLine": 4, + "endColumn": 19 + } + } + } + ], + "ruleIndex": 0 + }, + { + "level": "warning", + "message": { + "text": "Info object must have a \"contact\" object." + }, + "ruleId": "info-contact", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "document", + "index": 0 + }, + "region": { + "startLine": 2, + "startColumn": 6, + "endLine": 4, + "endColumn": 19 + } + } + } + ], + "ruleIndex": 1 + }, + { + "level": "warning", + "message": { + "text": "Info \"description\" must be present and non-empty string. -- https://www.example.com/docs/info-description.md" + }, + "ruleId": "info-description", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "document", + "index": 0 + }, + "region": { + "startLine": 2, + "startColumn": 6, + "endLine": 4, + "endColumn": 19 + } + } + } + ], + "ruleIndex": 2 + } + ], + "artifacts": [ + { + "sourceLanguage": "unknown", + "location": { + "uri": "document" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/test-harness/scenarios/documentation-url/results-format-stylish.scenario b/test-harness/scenarios/documentation-url/results-format-stylish.scenario new file mode 100644 index 000000000..29103576e --- /dev/null +++ b/test-harness/scenarios/documentation-url/results-format-stylish.scenario @@ -0,0 +1,63 @@ +====test==== +Invalid document outputs results when --format=stylish +====document==== +--- +info: + version: 1.0.0 + title: Stoplight +paths: {} +====asset:ruleset.json==== +{ + "rules": { + "api-servers": { + "documentationUrl": "https://www.example.com/docs/api-servers.md", + "description": "\"servers\" must be present and non-empty array.", + "recommended": true, + "given": "$", + "then": { + "field": "servers", + "function": "schema", + "functionOptions": { + "dialect": "draft7", + "schema": { + "items": { + "type": "object", + }, + "minItems": 1, + "type": "array" + } + } + } + }, + "info-contact": { + "description": "Info object must have a \"contact\" object.", + "recommended": true, + "type": "style", + "given": "$", + "then": { + "field": "info.contact", + "function": "truthy", + } + }, + "info-description": { + "documentationUrl": "https://www.example.com/docs/info-description.md", + "description": "Info \"description\" must be present and non-empty string.", + "recommended": true, + "type": "style", + "given": "$", + "then": { + "field": "info.description", + "function": "truthy" + } + } + } +} +====command==== +{bin} lint {document} --format=stylish --ruleset "{asset:ruleset.json}" --show-documentation-url +====stdout==== +{document} + 1:1 warning api-servers "servers" must be present and non-empty array. https://www.example.com/docs/api-servers.md + 2:6 warning info-contact Info object must have a "contact" object. info + 2:6 warning info-description Info "description" must be present and non-empty string. info https://www.example.com/docs/info-description.md + +✖ 3 problems (0 errors, 3 warnings, 0 infos, 0 hints) diff --git a/test-harness/scenarios/documentation-url/results-format-teamcity.scenario b/test-harness/scenarios/documentation-url/results-format-teamcity.scenario new file mode 100644 index 000000000..6fc9cd445 --- /dev/null +++ b/test-harness/scenarios/documentation-url/results-format-teamcity.scenario @@ -0,0 +1,61 @@ +====test==== +Invalid document outputs results when --format=teamcity +====document==== +--- +info: + version: 1.0.0 + title: Stoplight +====asset:ruleset.json==== +{ + "rules": { + "api-servers": { + "documentationUrl": "https://www.example.com/docs/api-servers.md", + "description": "\"servers\" must be present and non-empty array.", + "recommended": true, + "given": "$", + "then": { + "field": "servers", + "function": "schema", + "functionOptions": { + "dialect": "draft7", + "schema": { + "items": { + "type": "object", + }, + "minItems": 1, + "type": "array" + } + } + } + }, + "info-contact": { + "description": "Info object must have a \"contact\" object.", + "recommended": true, + "type": "style", + "given": "$", + "then": { + "field": "info.contact", + "function": "truthy", + } + }, + "info-description": { + "documentationUrl": "https://www.example.com/docs/info-description.md", + "description": "Info \"description\" must be present and non-empty string.", + "recommended": true, + "type": "style", + "given": "$", + "then": { + "field": "info.description", + "function": "truthy" + } + } + } +} +====command==== +{bin} lint {document} --format=teamcity --ruleset "{asset:ruleset.json}" --show-documentation-url +====stdout==== +##teamcity[inspectionType category='openapi' id='api-servers' name='api-servers' description='warning -- "servers" must be present and non-empty array. -- https://www.example.com/docs/api-servers.md']##teamcity[inspection typeId='api-servers' file='{document}' line='1' message='warning -- "servers" must be present and non-empty array.'] +##teamcity[inspectionType category='openapi' id='info-contact' name='info-contact' description='warning -- Info object must have a "contact" object.'] +##teamcity[inspection typeId='info-contact' file='{document}' line='2' message='warning -- Info object must have a "contact" object.'] +##teamcity[inspectionType category='openapi' id='info-description' name='info-description' description='warning -- Info "description" must be present and non-empty string. -- https://www.example.com/docs/info-description.md'] +##teamcity[inspection typeId='info-description' file='{document}' line='2' message='warning -- Info "description" must be present and non-empty string.'] \ No newline at end of file diff --git a/test-harness/scenarios/formats/results-format-html.scenario b/test-harness/scenarios/formats/results-format-html.scenario index 63ae922d7..75342e396 100644 --- a/test-harness/scenarios/formats/results-format-html.scenario +++ b/test-harness/scenarios/formats/results-format-html.scenario @@ -163,18 +163,21 @@ info: 1:1 warning "servers" must be present and non-empty array. + 2:6 warning Info object must have a "contact" object. + 2:6 warning Info "description" must be present and non-empty string. + diff --git a/test-harness/scenarios/formats/results-format-sarif.scenario b/test-harness/scenarios/formats/results-format-sarif.scenario new file mode 100644 index 000000000..0740276c0 --- /dev/null +++ b/test-harness/scenarios/formats/results-format-sarif.scenario @@ -0,0 +1,175 @@ +====test==== +Invalid document outputs results when --format=sarif +====document==== +--- +info: + version: 1.0.0 + title: Stoplight +====asset:ruleset.json==== +{ + "rules": { + "api-servers": { + "documentationUrl": "https://www.example.com/docs/api-servers.md", + "description": "\"servers\" must be present and non-empty array.", + "recommended": true, + "given": "$", + "then": { + "field": "servers", + "function": "schema", + "functionOptions": { + "dialect": "draft7", + "schema": { + "items": { + "type": "object", + }, + "minItems": 1, + "type": "array" + } + } + } + }, + "info-contact": { + "description": "Info object must have a \"contact\" object.", + "recommended": true, + "type": "style", + "given": "$", + "then": { + "field": "info.contact", + "function": "truthy", + } + }, + "info-description": { + "documentationUrl": "https://www.example.com/docs/info-description.md", + "description": "Info \"description\" must be present and non-empty string.", + "recommended": true, + "type": "style", + "given": "$", + "then": { + "field": "info.description", + "function": "truthy" + } + } + } +} +====command==== +{bin} lint {document} --format=sarif --ruleset "{asset:ruleset.json}" +====stdout==== +{ + "$schema": "http://json.schemastore.org/sarif-2.1.0-rtm.6.json", + "version": "2.1.0", + "runs": [ + { + "tool": { + "driver": { + "name": "spectral", + "rules": [ + { + "id": "api-servers", + "shortDescription": { + "text": "\"servers\" must be present and non-empty array." + }, + "helpUri": "https://www.example.com/docs/api-servers.md" + }, + { + "id": "info-contact", + "shortDescription": { + "text": "Info object must have a \"contact\" object." + } + }, + { + "id": "info-description", + "shortDescription": { + "text": "Info \"description\" must be present and non-empty string." + }, + "helpUri": "https://www.example.com/docs/info-description.md" + } + ], + "version": "6.11.1", + "informationUri": "https://github.com/stoplightio/spectral" + } + }, + "results": [ + { + "level": "warning", + "message": { + "text": "\"servers\" must be present and non-empty array." + }, + "ruleId": "api-servers", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "document", + "index": 0 + }, + "region": { + "startLine": 1, + "startColumn": 1, + "endLine": 4, + "endColumn": 19 + } + } + } + ], + "ruleIndex": 0 + }, + { + "level": "warning", + "message": { + "text": "Info object must have a \"contact\" object." + }, + "ruleId": "info-contact", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "document", + "index": 0 + }, + "region": { + "startLine": 2, + "startColumn": 6, + "endLine": 4, + "endColumn": 19 + } + } + } + ], + "ruleIndex": 1 + }, + { + "level": "warning", + "message": { + "text": "Info \"description\" must be present and non-empty string." + }, + "ruleId": "info-description", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "document", + "index": 0 + }, + "region": { + "startLine": 2, + "startColumn": 6, + "endLine": 4, + "endColumn": 19 + } + } + } + ], + "ruleIndex": 2 + } + ], + "artifacts": [ + { + "sourceLanguage": "unknown", + "location": { + "uri": "document" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/test-harness/scenarios/formats/too-few-outputs.scenario b/test-harness/scenarios/formats/too-few-outputs.scenario index 733e54185..22acc57e2 100644 --- a/test-harness/scenarios/formats/too-few-outputs.scenario +++ b/test-harness/scenarios/formats/too-few-outputs.scenario @@ -28,6 +28,7 @@ Options: -D, --display-only-failures only output results equal to or greater than --fail-severity [boolean] [default: false] --ignore-unknown-format do not warn about unmatched formats [boolean] [default: false] --fail-on-unmatched-globs fail on unmatched glob patterns [boolean] [default: false] + --show-documentation-url show documentation url in output result [boolean] [default: false] -v, --verbose increase verbosity [boolean] -q, --quiet no logging - output only [boolean] diff --git a/test-harness/scenarios/formats/too-many-outputs.scenario b/test-harness/scenarios/formats/too-many-outputs.scenario index c127e994a..5355283d5 100644 --- a/test-harness/scenarios/formats/too-many-outputs.scenario +++ b/test-harness/scenarios/formats/too-many-outputs.scenario @@ -28,6 +28,7 @@ Options: -D, --display-only-failures only output results equal to or greater than --fail-severity [boolean] [default: false] --ignore-unknown-format do not warn about unmatched formats [boolean] [default: false] --fail-on-unmatched-globs fail on unmatched glob patterns [boolean] [default: false] + --show-documentation-url show documentation url in output result [boolean] [default: false] -v, --verbose increase verbosity [boolean] -q, --quiet no logging - output only [boolean] diff --git a/test-harness/scenarios/formats/unmatched-outputs.scenario b/test-harness/scenarios/formats/unmatched-outputs.scenario index 69f7f1fc5..84828a5a7 100644 --- a/test-harness/scenarios/formats/unmatched-outputs.scenario +++ b/test-harness/scenarios/formats/unmatched-outputs.scenario @@ -28,6 +28,7 @@ Options: -D, --display-only-failures only output results equal to or greater than --fail-severity [boolean] [default: false] --ignore-unknown-format do not warn about unmatched formats [boolean] [default: false] --fail-on-unmatched-globs fail on unmatched glob patterns [boolean] [default: false] + --show-documentation-url show documentation url in output result [boolean] [default: false] -v, --verbose increase verbosity [boolean] -q, --quiet no logging - output only [boolean] diff --git a/test-harness/scenarios/help-no-document.scenario b/test-harness/scenarios/help-no-document.scenario index bdb524bf2..4cc5687f7 100644 --- a/test-harness/scenarios/help-no-document.scenario +++ b/test-harness/scenarios/help-no-document.scenario @@ -29,6 +29,7 @@ Options: -D, --display-only-failures only output results equal to or greater than --fail-severity [boolean] [default: false] --ignore-unknown-format do not warn about unmatched formats [boolean] [default: false] --fail-on-unmatched-globs fail on unmatched glob patterns [boolean] [default: false] + --show-documentation-url show documentation url in output result [boolean] [default: false] -v, --verbose increase verbosity [boolean] -q, --quiet no logging - output only [boolean] diff --git a/test-harness/scenarios/strict-options.scenario b/test-harness/scenarios/strict-options.scenario index 949d5379f..b8567bc93 100644 --- a/test-harness/scenarios/strict-options.scenario +++ b/test-harness/scenarios/strict-options.scenario @@ -29,6 +29,7 @@ Options: -D, --display-only-failures only output results equal to or greater than --fail-severity [boolean] [default: false] --ignore-unknown-format do not warn about unmatched formats [boolean] [default: false] --fail-on-unmatched-globs fail on unmatched glob patterns [boolean] [default: false] + --show-documentation-url show documentation url in output result [boolean] [default: false] -v, --verbose increase verbosity [boolean] -q, --quiet no logging - output only [boolean]