Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add default resolvers #245

Merged
merged 26 commits into from
Jun 18, 2019
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
c4edd08
fix: Bump @stoplight/types from 5.1.2 to 7.0.1 (#224)
dependabot-preview[bot] May 23, 2019
d1c3849
fix: upgrade ref resolver (#225)
XVincentX May 23, 2019
d691915
chore: Bump yaml from 1.5.1 to 1.6.0
dependabot-support May 24, 2019
6590414
fix: invalid-ref error (#228)
P0lip May 24, 2019
c8d9e37
chore: [Security] Bump tar from 2.2.1 to 2.2.2
dependabot-preview[bot] May 24, 2019
90c1d84
chore: Bump @types/node from 12.0.2 to 12.0.3
dependabot-preview[bot] May 29, 2019
89acde2
chore: Bump typescript from 3.4.5 to 3.5.1
dependabot-preview[bot] May 30, 2019
d828d63
chore: Bump @stoplight/types from 7.0.1 to 7.1.0
dependabot-preview[bot] May 30, 2019
b7fe15a
chore: Bump @oclif/command from 1.5.13 to 1.5.14
dependabot-preview[bot] May 31, 2019
a8b0e5f
chore: Bump @types/lodash from 4.14.130 to 4.14.133
dependabot-preview[bot] May 31, 2019
53efc0e
feat: add default resolvers
marbemac Jun 4, 2019
5297c24
feat: support passing resolve option into run cmd
marbemac Jun 4, 2019
859965c
test: fix them
marbemac Jun 4, 2019
462ebbb
feat: resolve http and json refs in cli
marbemac Jun 4, 2019
6a1535e
fix: use the resolved value in schema function
marbemac Jun 4, 2019
26dcb3f
fix: missing function call
P0lip Jun 4, 2019
db6948c
fix: use actual schema object
P0lip Jun 4, 2019
e019e8f
test: update spectral.test.ts
P0lip Jun 4, 2019
cc91c50
fix: do not try to resolve valid uris
P0lip Jun 4, 2019
dc1df47
chore: rename authority to documentUri
marbemac Jun 4, 2019
56f3458
chore: use node-fetch
marbemac Jun 4, 2019
7589430
fix: parse remote yaml in resolver
marbemac Jun 5, 2019
a471244
Merge remote-tracking branch 'origin/develop' into feat/remote-refs
marbemac Jun 18, 2019
946d46b
chore: oops remove dummy fixture
marbemac Jun 18, 2019
ca6ae55
chore: use extname
marbemac Jun 18, 2019
7854e71
fix: resolve targetUri before run if its a file
marbemac Jun 18, 2019
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
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,16 @@
"@oclif/config": "^1.12.11",
"@oclif/plugin-help": "^2.0",
"@stoplight/json": "^2.1.0",
"@stoplight/json-ref-resolver": "1.x.x",
"@stoplight/types": "^5.1.0",
"@stoplight/json-ref-resolver": "^1.5.0",
"@stoplight/types": "^7.0.1",
"@stoplight/yaml": "^2.3.0",
"@types/urijs": "^1.19.1",
"ajv": "6.x.x",
"ajv-oai": "^1.1.1",
"chalk": "^2.4.2",
"jsonpath": "https://github.com/stoplightio/jsonpath#f1c0e9e634da2d45671b7639fa0a99afc807da17",
"lodash": ">=4.17.5",
"node-fetch": "2.x.x",
"node-fetch": "^2.6.0",
"strip-ansi": "^5.2.0",
"text-table": "^0.2.0",
"typescript-json-schema": "^0.38.0"
Expand Down Expand Up @@ -97,7 +98,7 @@
"ts-node": "^8.1.0",
"tslint": "5.16.0",
"tslint-config-stoplight": "1.3.0",
"typescript": "3.4.5",
"typescript": "3.5.1",
"yaml": "^1.5.1"
},
"lint-staged": {
Expand Down
8 changes: 8 additions & 0 deletions src/__tests__/__fixtures__/todos.invalid.oas2.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,14 @@
"completed": false
}
}
},
{
"name": "missing",
"in": "query",
"schema": {
"$ref": "#/parameters/missing",
"example": "test"
}
}
],
"responses": {
Expand Down
84 changes: 64 additions & 20 deletions src/__tests__/linter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const invalidSchema = fs.readFileSync(
path.join(__dirname, './__fixtures__/petstore.invalid-schema.oas3.yaml'),
'utf-8',
);
const todosInvalid = fs.readFileSync(path.join(__dirname, './__fixtures__/todos.invalid.oas2.json'), 'utf-8');

const fnName = 'fake';
const fnName2 = 'fake2';
Expand Down Expand Up @@ -205,26 +206,69 @@ responses:: !!foo

const result = await spectral.run(invalidSchema);

expect(result).toEqual([
expect.objectContaining({
code: 'oas3-schema',
message: 'should NOT have additional properties: type',
summary: 'should NOT have additional properties: type',
path: ['paths', '/pets', 'get', 'responses', '200', 'headers', 'header-1'],
}),
expect.objectContaining({
code: 'oas3-schema',
message: 'should match exactly one schema in oneOf',
summary: 'should match exactly one schema in oneOf',
path: ['paths', '/pets', 'get', 'responses', '200', 'headers', 'header-1'],
}),
expect.objectContaining({
code: 'oas3-schema',
message: "should have required property '$ref'",
summary: "should have required property '$ref'",
path: ['paths', '/pets', 'get', 'responses', '200', 'headers', 'header-1'],
}),
]);
expect(result).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: 'oas3-schema',
message: 'should NOT have additional properties: type',
summary: 'should NOT have additional properties: type',
path: ['paths', '/pets', 'get', 'responses', '200', 'headers', 'header-1'],
}),
expect.objectContaining({
code: 'oas3-schema',
message: 'should match exactly one schema in oneOf',
summary: 'should match exactly one schema in oneOf',
path: ['paths', '/pets', 'get', 'responses', '200', 'headers', 'header-1'],
}),
expect.objectContaining({
code: 'oas3-schema',
message: "should have required property '$ref'",
summary: "should have required property '$ref'",
path: ['paths', '/pets', 'get', 'responses', '200', 'headers', 'header-1'],
}),
]),
);
});

test('should report invalid schema $refs', async () => {
spectral.addRules(oas3Ruleset.rules as RuleCollection);
spectral.addFunctions(oas3Functions());

const result = await spectral.run(todosInvalid);

expect(result).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: 'valid-example',
message: '"schema" property can\'t resolve reference #/parameters/missing from id #',
path: ['paths', '/todos/{todoId}', 'put', 'parameters', 1, 'schema'],
}),
]),
);
});

test('should report invalid $refs', async () => {
spectral.addRules(oas3Ruleset.rules as RuleCollection);
spectral.addFunctions(oas3Functions());

const result = await spectral.run(invalidSchema);

expect(result).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: 'invalid-ref',
message: "No reader defined for scheme 'file' in ref file://models/pet.yaml",
path: ['paths', '/pets', 'get', 'responses', '200', 'content', 'application/json', 'schema', '$ref'],
severity: DiagnosticSeverity.Error,
}),
expect.objectContaining({
code: 'invalid-ref',
message: "No reader defined for scheme 'file' in ref file://../common/models/error.yaml",
path: ['paths', '/pets', 'get', 'responses', 'default', 'content', 'application/json', 'schema', '$ref'],
severity: DiagnosticSeverity.Error,
}),
]),
);
});

describe('functional tests for the given property', () => {
Expand Down
4 changes: 2 additions & 2 deletions src/__tests__/spectral.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,14 +104,14 @@ describe('spectral', () => {
};

const s = new Spectral({
resolver: fakeResolver,
resolver: fakeResolver as any,
});

const target = { foo: 'bar' };

await s.run(target);

expect(fakeResolver.resolve).toBeCalledWith(target);
expect(fakeResolver.resolve).toBeCalledWith(target, { authority: undefined });
});
});

Expand Down
29 changes: 25 additions & 4 deletions src/cli/commands/lint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import { getLocationForJsonPath } from '@stoplight/yaml';
import { writeFile } from 'fs';
import { isNil, omitBy } from 'lodash';
import { resolve } from 'path';
import * as URI from 'urijs';
import { promisify } from 'util';

import { IRuleResult } from '../..';
import { createEmptyConfig, getDefaultConfigFile, load as loadConfig } from '../../config/configLoader';
import { json, stylish } from '../../formatters';
import { readParsable } from '../../fs/reader';
import { httpAndFileResolver } from '../../resolvers/http-and-file';
import { oas2Functions, rules as oas2Rules } from '../../rulesets/oas2';
import { oas3Functions, rules as oas3Rules } from '../../rulesets/oas3';
import { readRulesFromRulesets } from '../../rulesets/reader';
Expand Down Expand Up @@ -124,8 +127,11 @@ async function lint(name: string, flags: any, command: Lint, rules?: RuleCollect
if (flags.verbose) {
command.log(`Linting ${name}`);
}
const spec: IParserResult = await readParsable(name, flags.encoding);
const spectral = new Spectral();

const targetUri = isValidURI(name) ? name : resolve(name);

const spec: IParserResult = await readParsable(targetUri, flags.encoding);
const spectral = new Spectral({ resolver: httpAndFileResolver });
marbemac marked this conversation as resolved.
Show resolved Hide resolved
if (parseInt(spec.data.swagger) === 2) {
command.log('Adding OpenAPI 2.0 (Swagger) functions');
spectral.addFunctions(oas2Functions());
Expand Down Expand Up @@ -163,12 +169,17 @@ async function lint(name: string, flags: any, command: Lint, rules?: RuleCollect
let results = [];
try {
const parsedResult: IParsedResult = {
source: resolve(process.cwd(), name),
source: targetUri,
parsed: spec,
getLocationForJsonPath,
};

results = await spectral.run(parsedResult);
results = await spectral.run(parsedResult, {
resolve: {
documentUri: targetUri,
},
});

if (results.length === 0) {
command.log('No errors or warnings found!');
return;
Expand Down Expand Up @@ -244,3 +255,13 @@ function mergeConfig(config: IConfig, flags: any): ILintConfig {
),
};
}

const isValidURI = (maybeUri: string) => {
try {
// tslint:disable-next-line:no-unused-expression
new URI(maybeUri);
return true;
} catch {
return false;
}
};
50 changes: 31 additions & 19 deletions src/functions/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,34 +55,46 @@ export const schema: IFunction<ISchemaOptions> = (targetVal, opts, paths) => {
},
];

// we already access a resolved object in src/functions/schema-path.ts
const { schema: schemaObj } = opts;

if (!ajv.validate(schemaObj, targetVal) && ajv.errors) {
// TODO: potential performance improvements (compile, etc)?
const collectedErrors: string[] = [];
try {
if (!ajv.validate(schemaObj, targetVal) && ajv.errors) {
// TODO: potential performance improvements (compile, etc)?
const collectedErrors: string[] = [];

for (const error of ajv.errors) {
if (collectedErrors.length > 0) {
const index = collectedErrors.indexOf(error.keyword);
if (index !== -1) {
if (mergeErrors(results[index], error)) continue;
for (const error of ajv.errors) {
if (collectedErrors.length > 0) {
const index = collectedErrors.indexOf(error.keyword);
if (index !== -1) {
if (mergeErrors(results[index], error)) continue;
}
}
}

let message = error.message || '';
let message = error.message || '';

if (
error.keyword === 'additionalProperties' &&
(error.params as AJV.AdditionalPropertiesParams).additionalProperty
) {
message += `: ${(error.params as AJV.AdditionalPropertiesParams).additionalProperty}`;
}
if (
error.keyword === 'additionalProperties' &&
(error.params as AJV.AdditionalPropertiesParams).additionalProperty
) {
message += `: ${(error.params as AJV.AdditionalPropertiesParams).additionalProperty}`;
}

collectedErrors.push(error.keyword);
collectedErrors.push(error.keyword);
results.push({
path: [...path, ...formatPath(error.dataPath)],
message,
});
}
}
} catch (ex) {
if (ex instanceof AJV.MissingRefError) {
results.push({
path: [...path, ...formatPath(error.dataPath)],
message,
message: ex.message,
path,
});
} else {
throw ex;
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/linter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ import * as jp from 'jsonpath';
import { get, has } from 'lodash';

import { message } from './rulesets/message';
import { IFunction, IGivenNode, IParsedResult, IRuleResult, IRunOpts, IRunRule, IThen } from './types';
import { IFunction, IGivenNode, IParsedResult, IRuleResult, IRunRule, IRunRuleOpts, IThen } from './types';

// TODO(SO-23): unit test but mock whatShouldBeLinted
export const lintNode = (
node: IGivenNode,
rule: IRunRule,
then: IThen<string, any>,
apply: IFunction,
opts: IRunOpts,
opts: IRunRuleOpts,
parsedResult: IParsedResult,
): IRuleResult[] => {
const givenPath = node.path[0] === '$' ? node.path.slice(1) : node.path;
Expand Down
23 changes: 23 additions & 0 deletions src/resolvers/http-and-file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Resolver } from '@stoplight/json-ref-resolver';
import * as fs from 'fs';

import { httpReader } from './http';

// resolves files, http and https $refs, and internal $refs
export const httpAndFileResolver = new Resolver({
readers: {
https: httpReader,
http: httpReader,
file: {
read(ref: any) {
return new Promise((resolve, reject) => {
const path = ref.path();
return fs.readFile(path, 'utf8', (err, data) => {
if (err) reject(err);
resolve(data);
});
});
},
},
},
});
17 changes: 17 additions & 0 deletions src/resolvers/http.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Resolver } from '@stoplight/json-ref-resolver';

const fetch = require('node-fetch');

export const httpReader = {
async read(ref: unknown) {
return (await fetch(String(ref))).text();
},
};

// resolves http and https $refs, and internal $refs
export const httpResolver = new Resolver({
readers: {
https: httpReader,
http: httpReader,
},
});
4 changes: 4 additions & 0 deletions src/resolvers/local.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { Resolver } from '@stoplight/json-ref-resolver';

// just resolves internal #/definitions/user type $refs
export const localResolver = new Resolver();

This file was deleted.

Loading