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

View File

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

View File

@ -1,5 +1,5 @@
import { createLogger } from 'bunyan'; import { createLogger } from 'bunyan';
import { diff } from 'deep-diff'; import { applyDiff, diff } from 'deep-diff';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import { Options, usage } from 'yargs'; 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); const activeRules = await resolveRules(rules, args as any);
for (const rule of activeRules) { for (const rule of activeRules) {
const workingCopy = cloneDeep(data); const items = await rule.pick(ctx, data);
const ruleErrors = await rule.visit(ctx, workingCopy); for (const item of items) {
const itemCopy = cloneDeep(item);
const itemResult = await rule.visit(ctx, itemCopy);
if (ruleErrors > 0) { if (itemResult.errors.length > 0) {
logger.warn({ rule }, 'rule failed'); logger.warn({ count: itemResult.errors.length, 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');
data = workingCopy; ctx.mergeResult(itemResult);
} else { } 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 { readFileSync } from 'src/source';
import { Visitor } from 'src/visitor'; import { Visitor } from 'src/visitor';
import { VisitorContext } from 'src/visitor/context'; import { VisitorContext } from 'src/visitor/context';
import { VisitorResult } from 'src/visitor/result';
export interface RuleData { export interface RuleData {
// metadata // metadata
@ -92,7 +93,11 @@ export async function resolveRules(rules: Array<Rule>, selector: RuleSelector):
return Array.from(activeRules); 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 check: any;
public readonly desc: string; public readonly desc: string;
public readonly filter?: any; public readonly filter?: any;
@ -115,40 +120,48 @@ export class Rule implements RuleData, Visitor {
} }
} }
public async visit(ctx: VisitorContext, node: any): Promise<number> { public async pick(ctx: VisitorContext, root: any): Promise<Array<any>> {
const check = ctx.ajv.compile(this.check);
const filter = this.compileFilter(ctx);
const scopes = JSONPath({ const scopes = JSONPath({
json: node, json: root,
path: this.select, path: this.select,
}); });
if (isNil(scopes) || scopes.length === 0) { if (isNil(scopes) || scopes.length === 0) {
ctx.logger.debug('no data selected'); ctx.logger.debug('no data selected');
return 0; return [];
} }
for (const item of scopes) { return scopes;
ctx.logger.debug({ item }, 'filtering item'); }
if (filter(item)) {
ctx.logger.debug({ item }, 'checking item') public async visit(ctx: VisitorContext, node: any): Promise<RuleResult> {
if (!check(item)) { ctx.logger.debug({ item: node, rule: this}, 'visiting node');
const errors = Array.from(check.errors);
ctx.logger.warn({ const check = ctx.ajv.compile(this.check);
errors, const filter = this.compileFilter(ctx);
name: this.name, const result: RuleResult = {
item, changes: [],
rule: this, errors: [],
}, 'rule failed on item'); rule: this,
ctx.errors.push(...errors); };
return errors.length;
} if (filter(node)) {
} else { ctx.logger.debug({ item: node }, 'checking item')
ctx.logger.debug({ errors: filter.errors, item }, 'skipping 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 { protected compileFilter(ctx: VisitorContext): any {
@ -158,4 +171,4 @@ export class Rule implements RuleData, Visitor {
return ctx.ajv.compile(this.filter); return ctx.ajv.compile(this.filter);
} }
} }
} }

View File

@ -1,13 +1,15 @@
import * as Ajv from 'ajv'; import * as Ajv from 'ajv';
import { Logger } from 'noicejs'; import { Logger } from 'noicejs';
import { VisitorResult } from 'src/visitor/result';
export interface VisitorContextOptions { export interface VisitorContextOptions {
coerce: boolean; coerce: boolean;
defaults: boolean; defaults: boolean;
logger: Logger; logger: Logger;
} }
export class VisitorContext { export class VisitorContext implements VisitorContextOptions, VisitorResult {
public readonly ajv: any; public readonly ajv: any;
public readonly changes: Array<any>; public readonly changes: Array<any>;
public readonly coerce: boolean; public readonly coerce: boolean;
@ -31,4 +33,10 @@ export class VisitorContext {
this.logger.error(options, msg); this.logger.error(options, msg);
this.errors.push(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 { VisitorContext } from 'src/visitor/context';
import { VisitorResult } from 'src/visitor/result';
export interface Visitor { export interface Visitor<TResult extends VisitorResult> {
visit(ctx: VisitorContext, node: any): Promise<number>; /**
} * 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>;
}