1
0
Fork 0

feat: clean rules up with a bit of a visitor pattern

This commit is contained in:
ssube 2019-06-16 13:30:04 -05:00
parent f61b5689ae
commit 9a25fb97a8
8 changed files with 106 additions and 57 deletions

View File

@ -35,7 +35,7 @@ export default {
}),
commonjs({
namedExports: {
'node_modules/lodash/lodash.js': ['intersection', 'isNil', 'isString'],
'node_modules/lodash/lodash.js': ['cloneDeep', 'intersection', 'isNil', 'isString'],
'node_modules/noicejs/out/main-bundle.js': ['BaseError'],
'node_modules/js-yaml/index.js': [
'DEFAULT_SAFE_SCHEMA',

View File

@ -22,10 +22,18 @@ rules:
properties:
name:
type: string
pattern: "[-a-z0-9]+"
desc:
type: string
minLength: 8
maxLength: 255
level:
type: string
enum:
- debug
- info
- warn
- error
tags:
type: array
items:
@ -33,6 +41,7 @@ rules:
pattern: "[-:a-z0-9]+"
select:
type: string
minLength: 1
filter:
type: object
check:

View File

@ -1,14 +1,13 @@
import { readFile } from 'fs';
import { DEFAULT_SAFE_SCHEMA, safeLoad, Schema } from 'js-yaml';
import { isNil, isString } from 'lodash';
import { join } from 'path';
import { promisify } from 'util';
import { envType } from 'src/config/type/Env';
import { includeSchema, includeType } from 'src/config/type/Include';
import { regexpType } from 'src/config/type/Regexp';
import { streamType } from 'src/config/type/Stream';
import { NotFoundError } from 'src/error/NotFoundError';
import { readFileSync } from 'src/source';
export const CONFIG_ENV = 'SALTY_HOME';
export const CONFIG_SCHEMA = Schema.create([DEFAULT_SAFE_SCHEMA], [
@ -20,8 +19,6 @@ export const CONFIG_SCHEMA = Schema.create([DEFAULT_SAFE_SCHEMA], [
includeSchema.schema = CONFIG_SCHEMA;
const readFileSync = promisify(readFile);
/**
* With the given name, generate all potential config paths in their complete, absolute form.
*

View File

@ -3,9 +3,10 @@ import { safeDump, safeLoad } from 'js-yaml';
import { detailed, Options } from 'yargs-parser';
import { CONFIG_SCHEMA, loadConfig } from 'src/config';
import { checkRule, loadRules, resolveRules } from 'src/rule';
import { loadRules, resolveRules } from 'src/rule';
import { loadSource, writeSource } from 'src/source';
import { VERSION_INFO } from 'src/version';
import { VisitorContext } from 'src/visitor/context';
const CONFIG_ARGS_NAME = 'config-name';
const CONFIG_ARGS_PATH = 'config-path';
@ -81,27 +82,26 @@ export async function main(argv: Array<string>): Promise<number> {
const activeRules = await resolveRules(rules, args.argv as any);
// run rules
let errors = 0;
const ctx = new VisitorContext(logger);
switch (args.argv.mode) {
case 'check':
for (const rule of activeRules) {
if (checkRule(rule, data, logger)) {
if (rule.visit(ctx, data)) {
logger.info({ rule }, 'passed rule');
} else {
logger.warn({ rule }, 'failed rule');
++errors;
}
}
break;
default:
logger.error({ mode: args.argv.mode }, 'unsupported mode');
++errors;
ctx.logger.error({ mode: args.argv.mode }, 'unsupported mode');
ctx.errors.push('unsupported mode');
}
if (errors > 0) {
logger.error({ errors }, 'some rules failed');
if (ctx.errors.length > 0) {
logger.error({ errors: ctx.errors }, 'some rules failed');
if (args.argv.count) {
return Math.min(errors, 255);
return Math.min(ctx.errors.length, 255);
} else {
return STATUS_ERROR;
}

View File

@ -1,16 +1,16 @@
import * as Ajv from 'ajv';
import { readFile } from 'fs';
import { safeLoad } from 'js-yaml';
import { JSONPath } from 'jsonpath-plus';
import { intersection, isNil } from 'lodash';
import { Logger, LogLevel } from 'noicejs';
import { promisify } from 'util';
import { cloneDeep, intersection, isNil } from 'lodash';
import { LogLevel } from 'noicejs';
import { CONFIG_SCHEMA } from './config';
import { CONFIG_SCHEMA } from 'src/config';
import { readFileSync } from 'src/source';
const readFileSync = promisify(readFile);
import { Visitor } from 'src/visitor';
import { VisitorContext } from 'src/visitor/context';
export interface Rule {
export interface RuleData {
// metadata
desc: string;
level: LogLevel;
@ -43,7 +43,7 @@ export async function loadRules(paths: Array<string>): Promise<Array<Rule>> {
schema: CONFIG_SCHEMA,
});
rules.push(...data.rules);
rules.push(...data.rules.map((data: any) => new Rule(data)));
}
return rules;
@ -83,44 +83,69 @@ export async function resolveRules(rules: Array<Rule>, selector: RuleSelector):
return Array.from(activeRules);
}
export function checkRule(rule: Rule, data: any, logger: Logger): boolean {
const ajv = new ((Ajv as any).default)()
const check = ajv.compile(rule.check);
const filter = compileFilter(rule, ajv);
const scopes = JSONPath({
json: data,
path: rule.select,
});
export class Rule implements RuleData, Visitor {
public readonly check: any;
public readonly desc: string;
public readonly filter?: any;
public readonly level: LogLevel;
public readonly name: string;
public readonly select: string;
public readonly tags: string[];
if (isNil(scopes) || scopes.length === 0) {
logger.debug('no data selected');
return true;
}
constructor(data: RuleData) {
this.desc = data.desc;
this.level = data.level;
this.name = data.name;
this.select = data.select;
this.tags = Array.from(data.tags);
for (const item of scopes) {
logger.debug({ item }, 'filtering item');
if (filter(item)) {
logger.debug({ item }, 'checking item')
if (!check(item)) {
logger.warn({
desc: rule.desc,
errors: check.errors,
item,
}, 'rule failed on item');
return false;
}
} else {
logger.debug({ errors: filter.errors, item }, 'skipping item');
// copy schema objects
this.check = cloneDeep(data.check);
if (!isNil(data.filter)) {
this.filter = cloneDeep(data.filter);
}
}
return true;
}
public async visit(ctx: VisitorContext, node: any): Promise<VisitorContext> {
const ajv = new ((Ajv as any).default)()
const check = ajv.compile(this.check);
const filter = this.compileFilter(ajv);
const scopes = JSONPath({
json: node,
path: this.select,
});
export function compileFilter(rule: Rule, ajv: any): any {
if (isNil(rule.filter)) {
return () => true;
} else {
return ajv.compile(rule.filter);
if (isNil(scopes) || scopes.length === 0) {
ctx.logger.debug('no data selected');
return ctx;
}
for (const item of scopes) {
ctx.logger.debug({ item }, 'filtering item');
if (filter(item)) {
ctx.logger.debug({ item }, 'checking item')
if (!check(item)) {
ctx.logger.warn({
desc: this.desc,
errors: check.errors,
item,
}, 'rule failed on item');
ctx.errors.push(...check.errors);
return ctx;
}
} else {
ctx.logger.debug({ errors: filter.errors, item }, 'skipping item');
}
}
return ctx;
}
protected compileFilter(ajv: any): any {
if (isNil(this.filter)) {
return () => true;
} else {
return ajv.compile(this.filter);
}
}
}

View File

@ -1,8 +1,8 @@
import { promisify } from 'util';
import { readFile, writeFile } from 'fs';
const readFileSync = promisify(readFile);
const writeFileSync = promisify(writeFile);
export const readFileSync = promisify(readFile);
export const writeFileSync = promisify(writeFile);
export async function loadSource(path: string): Promise<string> {
if (path === '-') {

13
src/visitor/context.ts Normal file
View File

@ -0,0 +1,13 @@
import { Logger } from 'noicejs';
export class VisitorContext {
public readonly changes: Array<any>;
public readonly errors: Array<any>;
public readonly logger: Logger;
constructor(logger: Logger) {
this.changes = [];
this.errors = [];
this.logger = logger;
}
}

5
src/visitor/index.ts Normal file
View File

@ -0,0 +1,5 @@
import { VisitorContext } from 'src/visitor/context';
export interface Visitor {
visit(ctx: VisitorContext, node: any): Promise<VisitorContext>;
}