Skip to content

Commit

Permalink
Merge branch 'develop' into feat/expose-documentation-url
Browse files Browse the repository at this point in the history
  • Loading branch information
mnaumanali94 authored May 1, 2023
2 parents 38abb2c + 66f7690 commit 9200f3d
Show file tree
Hide file tree
Showing 40 changed files with 872 additions and 133 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ Stoplight has a set of Spectral rulesets that were created to help users get sta

- [OWASP Top 10](https://apistylebook.stoplight.io/docs/owasp-top-10) - Set of rules to enforce [OWASP security guidelines](https://owasp.org/www-project-api-security/).
- [URL Style Guidelines](https://apistylebook.stoplight.io/docs/url-guidelines) - Set of rules to help developers make better and consistent endpoints.
- [Documentation](https://github.com/stoplightio/spectral-documentation) - Scan an OpenAPI description to make sure you're leveraging enough of its features to help documentation tools like Stoplight Elements, ReDoc, and Swagger UI build the best quality API Reference Documentation possible.

There are also rulesets created by many companies to improve their APIs. You can use these as is to lint your OpenAPI descriptions, or use these as a reference to learn more about what rules you would want in your own ruleset:

Expand All @@ -99,7 +100,10 @@ There are also rulesets created by many companies to improve their APIs. You can
- [Tranascom](https://github.com/transcom/mymove/blob/master/swagger-def/.spectral.yml) - Don't even think about using anything other than `application/json`.
- [Zalando](https://apistylebook.stoplight.io/docs/zalando-restful-api-guidelines) - Based on [Zalando's RESTFUL API Guidelines](https://github.com/zalando/restful-api-guidelines), covers a wide-range of API topics such as versioning standards, property naming standards, the default format for request/response properties, and more.

Here are [more real-world examples](https://github.com/stoplightio/spectral-rulesets) of Spectral in action.
Check out some additional style guides here:

- [Spectral Rulesets by Stoplight](https://github.com/stoplightio/spectral-rulesets)
- [API Stylebook by Stoplight](https://apistylebook.stoplight.io)

## ⚙️ Integrations

Expand Down
2 changes: 2 additions & 0 deletions docs/guides/2-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ Here you can build a [custom ruleset](../getting-started/3-rulesets.md), or exte
- [OpenAPI ruleset](../reference/openapi-rules.md)
- [AsyncAPI ruleset](../reference/asyncapi-rules.md)

> If you use rules created or updated in a hosted [Stoplight API project](https://docs.stoplight.io/docs/platform/branches/pam-716-updated-landing-page/c433d678d027a-create-rules) with the Spectral CLI, you must publish the project from Stoplight before rule updates are used for linting.
## Error Results

Spectral has a few different error severities: `error`, `warn`, `info`, and `hint`, and they're in order from highest to lowest. By default, all results are shown regardless of severity, but since v5.0, only the presence of errors causes a failure status code of 1. Seeing results and getting a failure code for it are now two different things.
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@
"test.karma": "karma start",
"prepare": "husky install",
"prerelease": "patch-package",
"release": "yarn prerelease && yarn workspaces foreach run release"
"release": "yarn prerelease && yarn workspaces foreach run release",
"jest": "jest"
},
"workspaces": {
"packages": [
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
"nock": "^13.1.3",
"node-html-parser": "^4.1.5",
"pkg": "^5.8.0",
"xml2js": "^0.4.23"
"xml2js": "^0.5.0"
},
"pkg": {
"scripts": [
Expand Down
12 changes: 12 additions & 0 deletions packages/core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
# [@stoplight/spectral-core-v1.18.0](https://github.com/stoplightio/spectral/compare/@stoplight/spectral-core-v1.17.0...@stoplight/spectral-core-v1.18.0) (2023-04-25)


### Bug Fixes

* **core:** more accurate ruleset error paths ([66b3ca7](https://github.com/stoplightio/spectral/commit/66b3ca704136d5d8a34211e72e2d8a2c522261e4))


### Features

* **core:** relax formats validation ([#2151](https://github.com/stoplightio/spectral/issues/2151)) ([de16b4c](https://github.com/stoplightio/spectral/commit/de16b4cbd56cd9836609ab79487a6e3e06df964d))

# [@stoplight/spectral-core-v1.17.0](https://github.com/stoplightio/spectral/compare/@stoplight/spectral-core-v1.16.1...@stoplight/spectral-core-v1.17.0) (2023-03-23)


Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@stoplight/spectral-core",
"version": "1.17.0",
"version": "1.18.0",
"sideEffects": false,
"homepage": "https://github.com/stoplightio/spectral",
"bugs": "https://github.com/stoplightio/spectral/issues",
Expand Down
23 changes: 20 additions & 3 deletions packages/core/src/ruleset/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,27 @@ export class RulesetFunctionValidationError extends RulesetValidationError {
super(
'invalid-function-options',
RulesetFunctionValidationError.printMessage(fn, error),
error.instancePath.slice(1).split('/'),
RulesetFunctionValidationError.getPath(error),
);
}

private static getPath(error: ErrorObject): string[] {
const path: string[] = [
'functionOptions',
...(error.instancePath === '' ? [] : error.instancePath.slice(1).split('/')),
];

switch (error.keyword) {
case 'additionalProperties': {
const additionalProperty = (error as AdditionalPropertiesError).params.additionalProperty;
path.push(additionalProperty);
break;
}
}

return path;
}

private static printMessage(fn: string, error: ErrorObject): string {
switch (error.keyword) {
case 'type': {
Expand Down Expand Up @@ -157,7 +174,7 @@ export function createRulesetFunction<I, O>(
throw new RulesetValidationError(
'invalid-function-options',
`"${fn.name || '<unknown>'}" function does not accept any options`,
[],
['functionOptions'],
);
} else if (
'errors' in validateOptions &&
Expand All @@ -171,7 +188,7 @@ export function createRulesetFunction<I, O>(
throw new RulesetValidationError(
'invalid-function-options',
`"functionOptions" of "${fn.name || '<unknown>'}" function must be valid`,
[],
['functionOptions'],
);
}
};
Expand Down
17 changes: 1 addition & 16 deletions packages/core/src/ruleset/meta/json-extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,22 +36,7 @@
},
"Format": {
"$anchor": "format",
"enum": [
"oas2",
"oas3",
"oas3.0",
"oas3.1",
"asyncapi2",
"json-schema",
"json-schema-loose",
"json-schema-draft4",
"json-schema-draft6",
"json-schema-draft7",
"json-schema-draft-2019-09",
"json-schema-2019-09",
"json-schema-draft-2020-12",
"json-schema-2020-12"
],
"type": "string",
"errorMessage": "must be a valid format"
},
"Functions": {
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/ruleset/ruleset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,10 @@ export class Ruleset {
if (isPlainObject(maybeDefinition) && 'extends' in maybeDefinition) {
const { extends: _, ...def } = maybeDefinition;
// we don't want to validate extends - this is going to happen later on (line 29)
assertValidRuleset({ extends: [], ...def });
assertValidRuleset({ extends: [], ...def }, 'js');
definition = maybeDefinition as RulesetDefinition;
} else {
assertValidRuleset(maybeDefinition);
assertValidRuleset(maybeDefinition, 'js');
definition = maybeDefinition;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -773,7 +773,12 @@ describe('JS Ruleset Validation', () => {
}),
).toThrowAggregateError(
new AggregateError([
new RulesetValidationError('undefined-function', 'Function is not defined', ['rules', 'rule', 'then']),
new RulesetValidationError('undefined-function', 'Function is not defined', [
'rules',
'rule',
'then',
'function',
]),
]),
);
});
Expand Down Expand Up @@ -941,14 +946,14 @@ describe('JSON Ruleset Validation', () => {

it.each<[unknown, RulesetValidationError[]]>([
[
[2, 'a'],
[2, null],
[
new RulesetValidationError('invalid-format', 'must be a valid format', ['formats', '0']),
new RulesetValidationError('invalid-format', 'must be a valid format', ['formats', '1']),
],
],
[2, [new RulesetValidationError('invalid-ruleset-definition', 'must be an array of formats', ['formats'])]],
[[''], [new RulesetValidationError('invalid-format', 'must be a valid format', ['formats', '0'])]],
[[null], [new RulesetValidationError('invalid-format', 'must be a valid format', ['formats', '0'])]],
])('recognizes invalid ruleset %p formats syntax', (formats, errors) => {
expect(
assertValidRuleset.bind(
Expand Down Expand Up @@ -985,7 +990,7 @@ describe('JSON Ruleset Validation', () => {

it.each<[unknown, RulesetValidationError[]]>([
[
[2, 'a'],
[2, null],
[
new RulesetValidationError('invalid-format', 'must be a valid format', ['rules', 'rule', 'formats', '0']),
new RulesetValidationError('invalid-format', 'must be a valid format', ['rules', 'rule', 'formats', '1']),
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/ruleset/validation/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export type RulesetValidationErrorCode =
| 'undefined-alias';

interface IRulesetValidationSingleError extends Pick<IDiagnostic, 'message' | 'path'> {
code: RulesetValidationErrorCode;
readonly code: RulesetValidationErrorCode;
}

export class RulesetValidationError extends Error implements IRulesetValidationSingleError {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function validateFunction(
validator(opts);
} catch (ex) {
if (ex instanceof ReferenceError) {
return new RulesetValidationError('undefined-function', ex.message, toParsedPath(path));
return new RulesetValidationError('undefined-function', ex.message, [...toParsedPath(path), 'function']);
}

return wrapError(ex, path);
Expand Down
41 changes: 11 additions & 30 deletions packages/functions/src/__tests__/__helpers__/tester.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,8 @@ import {
IRuleResult,
RulesetFunction,
RulesetFunctionWithValidator,
RulesetValidationError,
} from '@stoplight/spectral-core';

import { isError } from 'lodash';

function isAggregateError(maybeAggregateError: unknown): maybeAggregateError is Error & { errors: unknown[] } {
return isError(maybeAggregateError) && maybeAggregateError.constructor.name === 'AggregateError';
}

export default async function <O = unknown>(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
fn: RulesetFunction<any, any> | RulesetFunctionWithValidator<any, any>,
Expand All @@ -23,31 +16,19 @@ export default async function <O = unknown>(
rule?: Partial<Omit<RuleDefinition, 'then'>> & { then?: Partial<RuleDefinition['then']> },
): Promise<Pick<IRuleResult, 'path' | 'message'>[]> {
const s = new Spectral();
try {
s.setRuleset({
rules: {
'my-rule': {
given: '$',
...rule,
then: {
...(rule?.then as Ruleset['rules']['then']),
function: fn,
functionOptions: opts,
},
s.setRuleset({
rules: {
'my-rule': {
given: '$',
...rule,
then: {
...(rule?.then as Ruleset['rules']['then']),
function: fn,
functionOptions: opts,
},
},
});
} catch (ex) {
if (isAggregateError(ex)) {
for (const e of ex.errors) {
if (e instanceof RulesetValidationError) {
e.path.length = 0;
}
}
}

throw ex;
}
},
});

const results = await s.run(input instanceof Document ? input : JSON.stringify(input));
return results
Expand Down
30 changes: 23 additions & 7 deletions packages/functions/src/__tests__/alphabetical.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,17 +132,33 @@ describe('Core Functions / Alphabetical', () => {
expect(await runAlphabetical([], opts)).toEqual([]);
});

it.each<[unknown, string]>([
[{ foo: true }, '"alphabetical" function does not support "foo" option'],
it.each<[unknown, RulesetValidationError]>([
[
{ foo: true },
new RulesetValidationError(
'invalid-function-options',
'"alphabetical" function does not support "foo" option',
['rules', 'my-rule', 'then', 'functionOptions', 'foo'],
),
],
[
2,
'"alphabetical" function has invalid options specified. Example valid options: null (no options), { "keyedBy": "my-key" }',
new RulesetValidationError(
'invalid-function-options',
'"alphabetical" function has invalid options specified. Example valid options: null (no options), { "keyedBy": "my-key" }',
['rules', 'my-rule', 'then', 'functionOptions'],
),
],
[
{ keyedBy: 2 },
new RulesetValidationError(
'invalid-function-options',
'"alphabetical" function and its "keyedBy" option accepts only the following types: string',
['rules', 'my-rule', 'then', 'functionOptions', 'keyedBy'],
),
],
[{ keyedBy: 2 }, '"alphabetical" function and its "keyedBy" option accepts only the following types: string'],
])('given invalid %p options, should throw', async (opts, error) => {
await expect(runAlphabetical([], opts)).rejects.toThrowAggregateError(
new AggregateError([new RulesetValidationError('invalid-function-options', error, [])]),
);
await expect(runAlphabetical([], opts)).rejects.toThrowAggregateError(new AggregateError([error]));
});
});
});
22 changes: 15 additions & 7 deletions packages/functions/src/__tests__/casing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,13 +381,21 @@ describe('Core Functions / Casing', () => {
new RulesetValidationError(
'invalid-function-options',
'"casing" function and its "type" option accept the following values: flat, camel, pascal, kebab, cobol, snake, macro',
[],
['rules', 'my-rule', 'then', 'functionOptions', 'type'],
),
],
],
[
{ type: 'macro', foo: true },
[new RulesetValidationError('invalid-function-options', '"casing" function does not support "foo" option', [])],
[
new RulesetValidationError('invalid-function-options', '"casing" function does not support "foo" option', [
'rules',
'my-rule',
'then',
'functionOptions',
'foo',
]),
],
],
[
{
Expand All @@ -399,7 +407,7 @@ describe('Core Functions / Casing', () => {
new RulesetValidationError(
'invalid-function-options',
'"casing" function is missing "separator.char" option',
[],
['rules', 'my-rule', 'then', 'functionOptions', 'separator'],
),
],
],
Expand All @@ -413,7 +421,7 @@ describe('Core Functions / Casing', () => {
new RulesetValidationError(
'invalid-function-options',
'"casing" function is missing "separator.char" option',
[],
['rules', 'my-rule', 'then', 'functionOptions', 'separator'],
),
],
],
Expand All @@ -423,7 +431,7 @@ describe('Core Functions / Casing', () => {
new RulesetValidationError(
'invalid-function-options',
'"casing" function does not support "separator.foo" option',
[],
['rules', 'my-rule', 'then', 'functionOptions', 'separator', 'foo'],
),
],
],
Expand All @@ -438,7 +446,7 @@ describe('Core Functions / Casing', () => {
new RulesetValidationError(
'invalid-function-options',
'"casing" function and its "separator.char" option accepts only char, i.e. "I" or "/"',
[],
['rules', 'my-rule', 'then', 'functionOptions', 'separator', 'char'],
),
],
],
Expand All @@ -453,7 +461,7 @@ describe('Core Functions / Casing', () => {
new RulesetValidationError(
'invalid-function-options',
'"casing" function and its "separator.char" option accepts only char, i.e. "I" or "/"',
[],
['rules', 'my-rule', 'then', 'functionOptions', 'separator', 'char'],
),
],
],
Expand Down
7 changes: 6 additions & 1 deletion packages/functions/src/__tests__/defined.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@ describe('Core Functions / Defined', () => {
it.each([{}, 2])('given invalid %p options, should throw', async opts => {
await expect(runDefined([], opts)).rejects.toThrowAggregateError(
new AggregateError([
new RulesetValidationError('invalid-function-options', '"defined" function does not accept any options', []),
new RulesetValidationError('invalid-function-options', '"defined" function does not accept any options', [
'rules',
'my-rule',
'then',
'functionOptions',
]),
]),
);
});
Expand Down
Loading

0 comments on commit 9200f3d

Please sign in to comment.