1
0
Fork 0

formalize visitor, results, etc

This commit is contained in:
ssube 2019-06-23 22:48:07 -05:00
parent aec3ea9e4e
commit eb1fdd3f30
7 changed files with 84 additions and 44 deletions

View File

@ -36,6 +36,7 @@ export default {
commonjs({
namedExports: {
'node_modules/deep-diff/index.js': [
'applyDiff',
'diff',
],
'node_modules/lodash/lodash.js': [
@ -63,4 +64,4 @@ export default {
rollupCommonJSResolveHack: true,
}),
],
};
};

View File

@ -56,7 +56,7 @@ rules:
limits:
type: object
properties:
cpu: &resources-cpu
cpu: *resources-cpu
# ensure the limits aren't *too* low
check:
@ -99,4 +99,4 @@ rules:
properties:
replica:
type: number
minimum: 1
minimum: 1

View File

@ -1,5 +1,5 @@
import { createLogger } from 'bunyan';
import { diff } from 'deep-diff';
import { applyDiff, diff } from 'deep-diff';
import { cloneDeep } from 'lodash';
import { Options, usage } from 'yargs';
@ -117,19 +117,24 @@ export async function main(argv: Array<string>): Promise<number> {
const activeRules = await resolveRules(rules, args as any);
for (const rule of activeRules) {
const workingCopy = cloneDeep(data);
const ruleErrors = await rule.visit(ctx, workingCopy);
const items = await rule.pick(ctx, data);
for (const item of items) {
const itemCopy = cloneDeep(item);
const itemResult = await rule.visit(ctx, itemCopy);
if (ruleErrors > 0) {
logger.warn({ rule }, 'rule failed');
} else {
const ruleDiff = diff(data, workingCopy);
if (Array.isArray(ruleDiff) && ruleDiff.length > 0) {
logger.info({ diff: ruleDiff, rule }, 'rule passed with modifications');
if (itemResult.errors.length > 0) {
logger.warn({ count: itemResult.errors.length, rule }, 'rule failed');
data = workingCopy;
ctx.mergeResult(itemResult);
} else {
logger.info({ rule }, 'rule passed');
const itemDiff = diff(item, itemCopy);
if (Array.isArray(itemDiff) && itemDiff.length > 0) {
logger.info({ diff: itemDiff, item, rule }, 'rule passed with modifications');
applyDiff(item, itemDiff);
} else {
logger.info({ rule }, 'rule passed');
}
}
}
}

View File

@ -6,6 +6,7 @@ import { YamlParser } from 'src/parser/YamlParser';
import { readFileSync } from 'src/source';
import { Visitor } from 'src/visitor';
import { VisitorContext } from 'src/visitor/context';
import { VisitorResult } from 'src/visitor/result';
export interface RuleData {
// metadata
@ -92,7 +93,11 @@ export async function resolveRules(rules: Array<Rule>, selector: RuleSelector):
return Array.from(activeRules);
}
export class Rule implements RuleData, Visitor {
export interface RuleResult extends VisitorResult {
rule: Rule;
}
export class Rule implements RuleData, Visitor<RuleResult> {
public readonly check: any;
public readonly desc: string;
public readonly filter?: any;
@ -115,40 +120,48 @@ export class Rule implements RuleData, Visitor {
}
}
public async visit(ctx: VisitorContext, node: any): Promise<number> {
const check = ctx.ajv.compile(this.check);
const filter = this.compileFilter(ctx);
public async pick(ctx: VisitorContext, root: any): Promise<Array<any>> {
const scopes = JSONPath({
json: node,
json: root,
path: this.select,
});
if (isNil(scopes) || scopes.length === 0) {
ctx.logger.debug('no data selected');
return 0;
return [];
}
for (const item of scopes) {
ctx.logger.debug({ item }, 'filtering item');
if (filter(item)) {
ctx.logger.debug({ item }, 'checking item')
if (!check(item)) {
const errors = Array.from(check.errors);
ctx.logger.warn({
errors,
name: this.name,
item,
rule: this,
}, 'rule failed on item');
ctx.errors.push(...errors);
return errors.length;
}
} else {
ctx.logger.debug({ errors: filter.errors, item }, 'skipping item');
return scopes;
}
public async visit(ctx: VisitorContext, node: any): Promise<RuleResult> {
ctx.logger.debug({ item: node, rule: this}, 'visiting node');
const check = ctx.ajv.compile(this.check);
const filter = this.compileFilter(ctx);
const result: RuleResult = {
changes: [],
errors: [],
rule: this,
};
if (filter(node)) {
ctx.logger.debug({ item: node }, 'checking item')
if (!check(node)) {
const errors = Array.from(check.errors);
ctx.logger.warn({
errors,
name: this.name,
item: node,
rule: this,
}, 'rule failed on item');
result.errors.push(...errors);
}
} else {
ctx.logger.debug({ errors: filter.errors, item: node }, 'skipping item');
}
return 0;
return result;
}
protected compileFilter(ctx: VisitorContext): any {
@ -158,4 +171,4 @@ export class Rule implements RuleData, Visitor {
return ctx.ajv.compile(this.filter);
}
}
}
}

View File

@ -1,13 +1,15 @@
import * as Ajv from 'ajv';
import { Logger } from 'noicejs';
import { VisitorResult } from 'src/visitor/result';
export interface VisitorContextOptions {
coerce: boolean;
defaults: boolean;
logger: Logger;
}
export class VisitorContext {
export class VisitorContext implements VisitorContextOptions, VisitorResult {
public readonly ajv: any;
public readonly changes: Array<any>;
public readonly coerce: boolean;
@ -31,4 +33,10 @@ export class VisitorContext {
this.logger.error(options, msg);
this.errors.push(options || msg);
}
}
public mergeResult(other: VisitorResult): this {
this.changes.push(...other.changes);
this.errors.push(...other.errors);
return this;
}
}

View File

@ -1,5 +1,14 @@
import { VisitorContext } from 'src/visitor/context';
import { VisitorResult } from 'src/visitor/result';
export interface Visitor {
visit(ctx: VisitorContext, node: any): Promise<number>;
}
export interface Visitor<TResult extends VisitorResult> {
/**
* Select nodes eligible to be visited.
**/
pick(ctx: VisitorContext, root: any): Promise<Array<any>>;
/**
* Visit a node.
*/
visit(ctx: VisitorContext, node: any): Promise<TResult>;
}

4
src/visitor/result.ts Normal file
View File

@ -0,0 +1,4 @@
export interface VisitorResult {
changes: Array<any>;
errors: Array<any>;
}