introduce formal rule visitor
This commit is contained in:
parent
4eec0dda70
commit
0ba6382253
11
src/app.ts
11
src/app.ts
|
@ -4,7 +4,8 @@ import { showCompletionScript } from 'yargs';
|
||||||
import { loadConfig } from './config';
|
import { loadConfig } from './config';
|
||||||
import { CONFIG_ARGS_NAME, CONFIG_ARGS_PATH, MODE, parseArgs } from './config/args';
|
import { CONFIG_ARGS_NAME, CONFIG_ARGS_PATH, MODE, parseArgs } from './config/args';
|
||||||
import { YamlParser } from './parser/YamlParser';
|
import { YamlParser } from './parser/YamlParser';
|
||||||
import { createRuleSelector, createRuleSources, loadRules, resolveRules, visitRules } from './rule';
|
import { createRuleSelector, createRuleSources, loadRules, resolveRules } from './rule';
|
||||||
|
import { RuleVisitor } from './rule/RuleVisitor';
|
||||||
import { readSource, writeSource } from './source';
|
import { readSource, writeSource } from './source';
|
||||||
import { VERSION_INFO } from './version';
|
import { VERSION_INFO } from './version';
|
||||||
import { VisitorContext } from './visitor/VisitorContext';
|
import { VisitorContext } from './visitor/VisitorContext';
|
||||||
|
@ -59,8 +60,12 @@ export async function main(argv: Array<string>): Promise<number> {
|
||||||
const source = await readSource(args.source);
|
const source = await readSource(args.source);
|
||||||
const docs = parser.parse(source);
|
const docs = parser.parse(source);
|
||||||
|
|
||||||
for (const data of docs) {
|
const visitor = new RuleVisitor({
|
||||||
await visitRules(ctx, activeRules, data);
|
rules: activeRules,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const root of docs) {
|
||||||
|
await visitor.visit(ctx, root);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ctx.errors.length === 0) {
|
if (ctx.errors.length === 0) {
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
import { applyDiff, diff } from 'deep-diff';
|
||||||
|
import { cloneDeep } from 'lodash';
|
||||||
|
|
||||||
|
import { Rule } from '.';
|
||||||
|
import { hasItems } from '../utils';
|
||||||
|
import { Visitor } from '../visitor';
|
||||||
|
import { VisitorContext } from '../visitor/VisitorContext';
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
|
export interface RuleVisitorOptions {
|
||||||
|
rules: ReadonlyArray<Rule>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RuleVisitor implements RuleVisitorOptions, Visitor {
|
||||||
|
public readonly rules: ReadonlyArray<Rule>;
|
||||||
|
|
||||||
|
constructor(options: RuleVisitorOptions) {
|
||||||
|
this.rules = Array.from(options.rules);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async pick(ctx: VisitorContext, root: any): Promise<Array<any>> {
|
||||||
|
return []; // why is this part of visitor rather than rule?
|
||||||
|
}
|
||||||
|
|
||||||
|
public async visit(ctx: VisitorContext, root: any): Promise<VisitorContext> {
|
||||||
|
for (const rule of this.rules) {
|
||||||
|
const items = await rule.pick(ctx, root);
|
||||||
|
let itemIndex = 0;
|
||||||
|
for (const item of items) {
|
||||||
|
ctx.visitData = {
|
||||||
|
itemIndex,
|
||||||
|
rule,
|
||||||
|
};
|
||||||
|
const itemResult = cloneDeep(item);
|
||||||
|
const ruleResult = await rule.visit(ctx, itemResult);
|
||||||
|
|
||||||
|
if (hasItems(ruleResult.errors)) {
|
||||||
|
ctx.logger.warn({ count: ruleResult.errors.length, rule }, 'rule failed');
|
||||||
|
ctx.mergeResult(ruleResult, ctx.visitData);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemDiff = diff(item, itemResult);
|
||||||
|
if (hasItems(itemDiff)) {
|
||||||
|
ctx.logger.info({
|
||||||
|
diff: itemDiff,
|
||||||
|
item,
|
||||||
|
rule: rule.name,
|
||||||
|
}, 'rule passed with modifications');
|
||||||
|
|
||||||
|
if (ctx.schemaOptions.mutate) {
|
||||||
|
applyDiff(item, itemResult);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ctx.logger.info({ rule: rule.name }, 'rule passed');
|
||||||
|
}
|
||||||
|
|
||||||
|
itemIndex += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,13 +1,12 @@
|
||||||
import { ValidateFunction } from 'ajv';
|
import { ValidateFunction } from 'ajv';
|
||||||
import { applyDiff, diff } from 'deep-diff';
|
import { Dictionary, intersection, isNil } from 'lodash';
|
||||||
import { cloneDeep, Dictionary, intersection, isNil } from 'lodash';
|
|
||||||
import { Minimatch } from 'minimatch';
|
import { Minimatch } from 'minimatch';
|
||||||
import { LogLevel } from 'noicejs';
|
import { LogLevel } from 'noicejs';
|
||||||
import recursive from 'recursive-readdir';
|
import recursive from 'recursive-readdir';
|
||||||
|
|
||||||
import { YamlParser } from '../parser/YamlParser';
|
import { YamlParser } from '../parser/YamlParser';
|
||||||
import { readFile } from '../source';
|
import { readFile } from '../source';
|
||||||
import { ensureArray, hasItems } from '../utils';
|
import { ensureArray } from '../utils';
|
||||||
import { VisitorResult } from '../visitor';
|
import { VisitorResult } from '../visitor';
|
||||||
import { VisitorContext } from '../visitor/VisitorContext';
|
import { VisitorContext } from '../visitor/VisitorContext';
|
||||||
import { SchemaRule } from './SchemaRule';
|
import { SchemaRule } from './SchemaRule';
|
||||||
|
@ -204,43 +203,3 @@ export async function resolveRules(rules: Array<Rule>, selector: RuleSelector):
|
||||||
|
|
||||||
return Array.from(activeRules);
|
return Array.from(activeRules);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function visitRules(ctx: VisitorContext, rules: Array<Rule>, data: any): Promise<VisitorContext> {
|
|
||||||
for (const rule of rules) {
|
|
||||||
const items = await rule.pick(ctx, data);
|
|
||||||
let itemIndex = 0;
|
|
||||||
for (const item of items) {
|
|
||||||
ctx.visitData = {
|
|
||||||
itemIndex,
|
|
||||||
rule,
|
|
||||||
};
|
|
||||||
const itemResult = cloneDeep(item);
|
|
||||||
const ruleResult = await rule.visit(ctx, itemResult);
|
|
||||||
|
|
||||||
if (hasItems(ruleResult.errors)) {
|
|
||||||
ctx.logger.warn({ count: ruleResult.errors.length, rule }, 'rule failed');
|
|
||||||
ctx.mergeResult(ruleResult, ctx.visitData);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const itemDiff = diff(item, itemResult);
|
|
||||||
if (hasItems(itemDiff)) {
|
|
||||||
ctx.logger.info({
|
|
||||||
diff: itemDiff,
|
|
||||||
item,
|
|
||||||
rule: rule.name,
|
|
||||||
}, 'rule passed with modifications');
|
|
||||||
|
|
||||||
if (ctx.schemaOptions.mutate) {
|
|
||||||
applyDiff(item, itemResult);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ctx.logger.info({ rule: rule.name }, 'rule passed');
|
|
||||||
}
|
|
||||||
|
|
||||||
itemIndex += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctx;
|
|
||||||
}
|
|
||||||
|
|
|
@ -2,7 +2,8 @@ import { expect } from 'chai';
|
||||||
import { LogLevel, NullLogger } from 'noicejs';
|
import { LogLevel, NullLogger } from 'noicejs';
|
||||||
import { mock, spy, stub } from 'sinon';
|
import { mock, spy, stub } from 'sinon';
|
||||||
|
|
||||||
import { createRuleSelector, createRuleSources, resolveRules, visitRules } from '../../src/rule';
|
import { createRuleSelector, createRuleSources, resolveRules } from '../../src/rule';
|
||||||
|
import { RuleVisitor } from '../../src/rule/RuleVisitor';
|
||||||
import { SchemaRule } from '../../src/rule/SchemaRule';
|
import { SchemaRule } from '../../src/rule/SchemaRule';
|
||||||
import { VisitorContext } from '../../src/visitor/VisitorContext';
|
import { VisitorContext } from '../../src/visitor/VisitorContext';
|
||||||
import { describeLeaks, itLeaks } from '../helpers/async';
|
import { describeLeaks, itLeaks } from '../helpers/async';
|
||||||
|
@ -128,7 +129,10 @@ describeLeaks('rule visitor', async () => {
|
||||||
pickStub.onFirstCall().returns(Promise.resolve([]));
|
pickStub.onFirstCall().returns(Promise.resolve([]));
|
||||||
pickStub.throws();
|
pickStub.throws();
|
||||||
|
|
||||||
await visitRules(ctx, [rule], {});
|
const visitor = new RuleVisitor({
|
||||||
|
rules: [rule],
|
||||||
|
});
|
||||||
|
await visitor.visit(ctx, {});
|
||||||
|
|
||||||
mockRule.verify();
|
mockRule.verify();
|
||||||
expect(ctx.errors.length).to.equal(0);
|
expect(ctx.errors.length).to.equal(0);
|
||||||
|
@ -163,7 +167,10 @@ describeLeaks('rule visitor', async () => {
|
||||||
visitStub.onFirstCall().returns(Promise.resolve(ctx));
|
visitStub.onFirstCall().returns(Promise.resolve(ctx));
|
||||||
visitStub.throws();
|
visitStub.throws();
|
||||||
|
|
||||||
await visitRules(ctx, [rule], {});
|
const visitor = new RuleVisitor({
|
||||||
|
rules: [rule],
|
||||||
|
});
|
||||||
|
await visitor.visit(ctx, {});
|
||||||
|
|
||||||
mockRule.verify();
|
mockRule.verify();
|
||||||
expect(ctx.errors.length).to.equal(0);
|
expect(ctx.errors.length).to.equal(0);
|
||||||
|
@ -196,7 +203,10 @@ describeLeaks('rule visitor', async () => {
|
||||||
errors: [],
|
errors: [],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
await visitRules(ctx, [rule], data);
|
const visitor = new RuleVisitor({
|
||||||
|
rules: [rule],
|
||||||
|
});
|
||||||
|
await visitor.visit(ctx, data);
|
||||||
|
|
||||||
expect(pickSpy).to.have.callCount(1).and.to.have.been.calledWithExactly(ctx, data);
|
expect(pickSpy).to.have.callCount(1).and.to.have.been.calledWithExactly(ctx, data);
|
||||||
expect(visitStub).to.have.callCount(3);
|
expect(visitStub).to.have.callCount(3);
|
||||||
|
@ -232,7 +242,10 @@ describeLeaks('rule visitor', async () => {
|
||||||
}],
|
}],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
await visitRules(ctx, [rule], data);
|
const visitor = new RuleVisitor({
|
||||||
|
rules: [rule],
|
||||||
|
});
|
||||||
|
await visitor.visit(ctx, data);
|
||||||
|
|
||||||
expect(visitStub).to.have.callCount(3);
|
expect(visitStub).to.have.callCount(3);
|
||||||
expect(ctx.errors.length).to.equal(3);
|
expect(ctx.errors.length).to.equal(3);
|
||||||
|
|
Loading…
Reference in New Issue