Skip to content

Commit

Permalink
feat(core): hook-up nimma v2 (#1785)
Browse files Browse the repository at this point in the history
  • Loading branch information
P0lip committed Sep 3, 2021
1 parent 0ced674 commit 3af8b69
Show file tree
Hide file tree
Showing 8 changed files with 100 additions and 133 deletions.
4 changes: 3 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ const projectDefault = {
preset: 'ts-jest',
moduleNameMapper: {
...mapValues(pathsToModuleNameMapper(compilerOptions.paths), v => path.join(__dirname, v)),
'@stoplight/spectral-test-utils': '<rootDir>/test-utils/node/index.ts',
'^@stoplight/spectral-test-utils$': '<rootDir>/test-utils/node/index.ts',
'^nimma/fallbacks$': '<rootDir>/node_modules/nimma/dist/cjs/fallbacks/index.js',
'^nimma/legacy$': '<rootDir>/node_modules/nimma/dist/legacy/cjs/index.js',
},
testEnvironment: 'node',
globals: {
Expand Down
3 changes: 3 additions & 0 deletions karma.conf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ module.exports = (config: Config): void => {
resolve: {
alias: {
'@stoplight/spectral-test-utils': require.resolve('./test-utils/browser/index.js'),
nimma: require.resolve('./node_modules/nimma/dist/legacy/cjs/index.js'),
'nimma/fallbacks': require.resolve('./node_modules/nimma/dist/legacy/cjs/fallbacks/index.js'),
'nimma/legacy': require.resolve('./node_modules/nimma/dist/legacy/cjs/index.js'),
'node-fetch': require.resolve('./__karma__/fetch'),
fs: require.resolve('./__karma__/fs'),
process: require.resolve('./__karma__/process'),
Expand Down
5 changes: 3 additions & 2 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,11 @@
"blueimp-md5": "2.18.0",
"expression-eval": "4.0.0",
"json-schema": "0.3.0",
"jsonpath-plus": "5.0.7",
"jsonpath-plus": "6.0.1",
"lodash": "~4.17.21",
"lodash.topath": "^4.5.2",
"minimatch": "3.0.4",
"nimma": "0.0.0",
"nimma": "0.1.1",
"tslib": "~2.3.0"
},
"devDependencies": {
Expand Down
42 changes: 0 additions & 42 deletions packages/core/src/ruleset/rule/rule.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { JsonPath, Optional } from '@stoplight/types';
import { JSONPathExpression } from 'nimma';
import { dirname, relative } from '@stoplight/path';
import { DiagnosticSeverity } from '@stoplight/types';
import { pathToPointer } from '@stoplight/json';

import { getDiagnosticSeverity, DEFAULT_SEVERITY_LEVEL } from '../utils/severity';
import { IGivenNode } from '../../types';
import { Ruleset } from '../ruleset';
import { Format } from '../format';
import { HumanReadableDiagnosticSeverity, IRuleThen, RuleDefinition } from '../types';
Expand Down Expand Up @@ -46,12 +44,6 @@ export class Rule implements IRule {
#then!: IRuleThen[];
#given!: string[];

public expressions?: JSONPathExpression[] | null;

public get isOptimized(): boolean {
return Array.isArray(this.expressions);
}

constructor(
public readonly name: string,
public readonly definition: RuleDefinition,
Expand Down Expand Up @@ -193,40 +185,10 @@ export class Rule implements IRule {
return false;
}

public optimize(): boolean {
if (this.expressions !== void 0) return this.isOptimized;

try {
this.expressions = this.given.map(given => {
const expr = new JSONPathExpression(given, stub, stub);
if (expr.matches === null) {
throw new Error(`Rule "${this.name}": cannot optimize ${given}`);
}

return expr;
});
} catch {
this.expressions = null;
}

return this.isOptimized;
}

public clone(): Rule {
return new Rule(this.name, this.definition, this.owner);
}

public hookup(cb: (rule: Rule, node: IGivenNode) => void): void {
for (const expr of this.expressions!) {
expr.onMatch = (value, path): void => {
cb(this, {
path,
value,
});
};
}
}

public toJSON(): StringifiedRule {
return {
name: this.name,
Expand All @@ -248,7 +210,3 @@ export class Rule implements IRule {
};
}
}

function stub(): void {
// nada
}
142 changes: 68 additions & 74 deletions packages/core/src/runner/runner.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,15 @@
import { JSONPath, JSONPathCallback } from 'jsonpath-plus';
import { isObject } from 'lodash';
import { JSONPathExpression, traverse } from 'nimma';

import { IDocument } from '../document';
import { DocumentInventory } from '../documentInventory';
import { IGivenNode, IRuleResult } from '../types';
import { IRuleResult } from '../types';
import { ComputeFingerprintFunc, prepareResults } from '../utils';
import { lintNode } from './lintNode';
import { RunnerRuntime } from './runtime';
import { IRunnerInternalContext } from './types';
import { Rule } from '../ruleset/rule/rule';
import { Ruleset } from '../ruleset/ruleset';
import { toPath } from 'lodash';

const runRule = (context: IRunnerInternalContext, rule: Rule): void => {
const target = rule.resolved ? context.documentInventory.resolved : context.documentInventory.unresolved;

for (const given of rule.given) {
// don't have to spend time running jsonpath if given is $ - can just use the root object
if (given === '$') {
lintNode(
context,
{
path: ['$'],
value: target,
},
rule,
);
} else if (isObject(target)) {
JSONPath({
path: given,
json: target,
resultType: 'all',
callback: (result => {
lintNode(
context,
{
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-call
path: toPath(result.path.slice(1)),
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
value: result.value,
},
rule,
);
}) as JSONPathCallback,
});
}
}
};
import Nimma, { Callback } from 'nimma/legacy'; // legacy = Node v12, nimma without /legacy supports only 14+
import { jsonPathPlus } from 'nimma/fallbacks';
import { isPlainObject } from '@stoplight/json';
import { isError } from 'lodash';

export class Runner {
public readonly results: IRuleResult[];
Expand Down Expand Up @@ -77,43 +39,35 @@ export class Runner {
promises: [],
};

const relevantRules = Object.values(rules).filter(
rule => rule.enabled && rule.matchesFormat(documentInventory.formats),
);

const optimizedRules: Rule[] = [];
const optimizedUnresolvedRules: Rule[] = [];
const unoptimizedRules: Rule[] = [];

const traverseCb = (rule: Rule, node: IGivenNode): void => {
lintNode(runnerContext, node, rule);
const enabledRules = Object.values(rules).filter(rule => rule.enabled);
const relevantRules = enabledRules.filter(rule => rule.matchesFormat(documentInventory.formats));
const callbacks: { resolved: Record<string, Callback[]>; unresolved: Record<string, Callback[]> } = {
resolved: {},
unresolved: {},
};

for (const rule of relevantRules) {
if (!rule.isOptimized) {
unoptimizedRules.push(rule);
continue;
}
for (const given of rule.given) {
const cb: Callback = (scope): void => {
lintNode(runnerContext, scope, rule);
};

if (rule.resolved) {
optimizedRules.push(rule);
} else {
optimizedUnresolvedRules.push(rule);
(callbacks[rule.resolved ? 'resolved' : 'unresolved'][given] ??= []).push(cb);
}

rule.hookup(traverseCb);
}

if (optimizedRules.length > 0) {
traverse(Object(runnerContext.documentInventory.resolved), optimizedRules.flatMap(pickExpressions));
}

if (optimizedUnresolvedRules.length > 0) {
traverse(Object(runnerContext.documentInventory.unresolved), optimizedUnresolvedRules.flatMap(pickExpressions));
}
execute(
runnerContext.documentInventory.resolved,
callbacks.resolved,
relevantRules.flatMap(r => (r.resolved ? r.given : [])),
);

for (const rule of unoptimizedRules) {
runRule(runnerContext, rule);
if (Object.keys(callbacks.unresolved).length > 0) {
execute(
runnerContext.documentInventory.unresolved,
callbacks.unresolved,
relevantRules.flatMap(r => (!r.resolved ? r.given : [])),
);
}

this.runtime.emit('beforeTeardown');
Expand All @@ -132,6 +86,46 @@ export class Runner {
}
}

function pickExpressions({ expressions }: Rule): JSONPathExpression[] {
return expressions!;
function execute(input: unknown, callbacks: Record<string, Callback[]>, jsonPathExpressions: string[]): void {
if (!isPlainObject(input) && !Array.isArray(input)) {
for (const cb of callbacks.$ ?? []) {
cb({
path: [],
value: input,
});
}

return;
}

try {
const nimma = new Nimma(jsonPathExpressions, {
fallback: jsonPathPlus,
unsafe: false,
output: 'auto',
});

nimma.query(
input,
Object.entries(callbacks).reduce<Record<string, Callback>>((mapped, [key, cbs]) => {
mapped[key] = scope => {
for (const cb of cbs) {
cb(scope);
}
};

return mapped;
}, {}),
);
} catch (e) {
if (isAggregateError(e) && e.errors.length === 1) {
throw e.errors[0];
} else {
throw e;
}
}
}

function isAggregateError(maybeAggregateError: unknown): maybeAggregateError is Error & { errors: unknown[] } {
return isError(maybeAggregateError) && maybeAggregateError.constructor.name === 'AggregateError';
}
1 change: 0 additions & 1 deletion packages/core/src/types/spectral.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import type { Resolver } from '@stoplight/spectral-ref-resolver';

export interface IConstructorOpts {
resolver?: Resolver;
useNimma?: boolean;
}

export interface IRunOpts {
Expand Down
5 changes: 4 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
"@stoplight/spectral-ref-resolver": ["packages/ref-resolver/src/index.ts"],
"@stoplight/spectral-runtime": ["packages/runtime/src/index.ts"],
"@stoplight/spectral-ruleset-migrator": ["packages/ruleset-migrator/src/index.ts"],
"@stoplight/spectral-rulesets": ["packages/rulesets/src/index.ts"]
"@stoplight/spectral-rulesets": ["packages/rulesets/src/index.ts"],

"nimma/fallbacks": ["node_modules/nimma/dist/cjs/fallbacks/"],
"nimma/legacy": ["node_modules/nimma/dist/legacy/cjs/"]
},
"moduleResolution": "node",
"target": "ES2019",
Expand Down
31 changes: 19 additions & 12 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2434,7 +2434,7 @@ astral-regex@^2.0.0:
resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==

astring@^1.4.3, astring@^1.7.5:
astring@^1.7.5:
version "1.7.5"
resolved "https://registry.yarnpkg.com/astring/-/astring-1.7.5.tgz#a7d47fceaf32b052d33a3d07c511efeec67447ca"
integrity sha512-lobf6RWXb8c4uZ7Mdq0U12efYmpD1UFnyOWVJPTa3ukqZrMopav+2hdNu0hgBF0JIBFK9QgrBDfwYvh3DFJDAA==
Expand Down Expand Up @@ -6311,7 +6311,7 @@ jsdom@^16.6.0:
ws "^7.4.5"
xml-name-validator "^3.0.0"

jsep@^0.3.0, jsep@^0.3.4:
jsep@^0.3.0:
version "0.3.5"
resolved "https://registry.yarnpkg.com/jsep/-/jsep-0.3.5.tgz#3fd79ebd92f6f434e4857d5272aaeef7d948264d"
integrity sha512-AoRLBDc6JNnKjNcmonituEABS5bcfqDhQAWWXNTFrqu6nVXBpBAGfcoTGZMFlIrh9FjmE1CQyX9CTNwZrXMMDA==
Expand Down Expand Up @@ -6446,10 +6446,10 @@ jsonparse@^1.2.0, jsonparse@^1.3.1:
resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=

jsonpath-plus@5.0.7:
version "5.0.7"
resolved "https://registry.yarnpkg.com/jsonpath-plus/-/jsonpath-plus-5.0.7.tgz#95fb437ebb69c67595208711a69c95735cbff45b"
integrity sha512-7TS6wsiw1s2UMK/A6nA4n0aUJuirCVhJ87nWX5je5MPOl0z5VTr2qs7nMP8NZ2ed3rlt6kePTqddgVPE9F0i0w==
jsonpath-plus@6.0.1, jsonpath-plus@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/jsonpath-plus/-/jsonpath-plus-6.0.1.tgz#9a3e16cedadfab07a3d8dc4e8cd5df4ed8f49c4d"
integrity sha512-EvGovdvau6FyLexFH2OeXfIITlgIbgZoAZe3usiySeaIDm5QS+A10DKNpaPBBqqRSZr2HN6HVNXxtwUAr2apEw==

jsonpointer@^4.0.1:
version "4.0.1"
Expand Down Expand Up @@ -6895,6 +6895,11 @@ lodash.toarray@^4.4.0:
resolved "https://registry.yarnpkg.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561"
integrity sha1-JMS/zWsvuji/0FlNsRedjptlZWE=

lodash.topath@^4.5.2:
version "4.5.2"
resolved "https://registry.yarnpkg.com/lodash.topath/-/lodash.topath-4.5.2.tgz#3616351f3bba61994a0931989660bd03254fd009"
integrity sha1-NhY1Hzu6YZlKCTGYlmC9AyVP0Ak=

lodash.truncate@^4.4.2:
version "4.4.2"
resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193"
Expand Down Expand Up @@ -7444,13 +7449,15 @@ nice-try@^1.0.4:
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==

nimma@0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/nimma/-/nimma-0.0.0.tgz#8fa61ab4ecdcb745c237bc70ddcc012c6cdf8127"
integrity sha512-if0VqyHpTMHKFORMiJ2WLWgoIF4xqwjybHZyvodQ/yCmiWag6RhLlMHeFukz4X31DanTBA26U+HwvXIrTaYQkQ==
nimma@0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/nimma/-/nimma-0.1.1.tgz#da96d3ec8802b133aed6cf8109c1613c648cc7a1"
integrity sha512-mYdfYSmKa9FoKfza0KtPxaAD/WF0DhitMEkr+4nq9scNSlwiBrn/a3aR7wHVgXGI20lfxyNUUVPHKasXYXLuyg==
dependencies:
astring "^1.4.3"
jsep "^0.3.4"
astring "^1.7.5"
optionalDependencies:
jsonpath-plus "^6.0.1"
lodash.topath "^4.5.2"

nock@^12.0.2:
version "12.0.3"
Expand Down

0 comments on commit 3af8b69

Please sign in to comment.