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({ commonjs({
namedExports: { 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/noicejs/out/main-bundle.js': ['BaseError'],
'node_modules/js-yaml/index.js': [ 'node_modules/js-yaml/index.js': [
'DEFAULT_SAFE_SCHEMA', 'DEFAULT_SAFE_SCHEMA',

View File

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

View File

@ -1,14 +1,13 @@
import { readFile } from 'fs';
import { DEFAULT_SAFE_SCHEMA, safeLoad, Schema } from 'js-yaml'; import { DEFAULT_SAFE_SCHEMA, safeLoad, Schema } from 'js-yaml';
import { isNil, isString } from 'lodash'; import { isNil, isString } from 'lodash';
import { join } from 'path'; import { join } from 'path';
import { promisify } from 'util';
import { envType } from 'src/config/type/Env'; import { envType } from 'src/config/type/Env';
import { includeSchema, includeType } from 'src/config/type/Include'; import { includeSchema, includeType } from 'src/config/type/Include';
import { regexpType } from 'src/config/type/Regexp'; import { regexpType } from 'src/config/type/Regexp';
import { streamType } from 'src/config/type/Stream'; import { streamType } from 'src/config/type/Stream';
import { NotFoundError } from 'src/error/NotFoundError'; import { NotFoundError } from 'src/error/NotFoundError';
import { readFileSync } from 'src/source';
export const CONFIG_ENV = 'SALTY_HOME'; export const CONFIG_ENV = 'SALTY_HOME';
export const CONFIG_SCHEMA = Schema.create([DEFAULT_SAFE_SCHEMA], [ 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; includeSchema.schema = CONFIG_SCHEMA;
const readFileSync = promisify(readFile);
/** /**
* With the given name, generate all potential config paths in their complete, absolute form. * 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 { detailed, Options } from 'yargs-parser';
import { CONFIG_SCHEMA, loadConfig } from 'src/config'; 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 { loadSource, writeSource } from 'src/source';
import { VERSION_INFO } from 'src/version'; import { VERSION_INFO } from 'src/version';
import { VisitorContext } from 'src/visitor/context';
const CONFIG_ARGS_NAME = 'config-name'; const CONFIG_ARGS_NAME = 'config-name';
const CONFIG_ARGS_PATH = 'config-path'; 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); const activeRules = await resolveRules(rules, args.argv as any);
// run rules // run rules
let errors = 0; const ctx = new VisitorContext(logger);
switch (args.argv.mode) { switch (args.argv.mode) {
case 'check': case 'check':
for (const rule of activeRules) { for (const rule of activeRules) {
if (checkRule(rule, data, logger)) { if (rule.visit(ctx, data)) {
logger.info({ rule }, 'passed rule'); logger.info({ rule }, 'passed rule');
} else { } else {
logger.warn({ rule }, 'failed rule'); logger.warn({ rule }, 'failed rule');
++errors;
} }
} }
break; break;
default: default:
logger.error({ mode: args.argv.mode }, 'unsupported mode'); ctx.logger.error({ mode: args.argv.mode }, 'unsupported mode');
++errors; ctx.errors.push('unsupported mode');
} }
if (errors > 0) { if (ctx.errors.length > 0) {
logger.error({ errors }, 'some rules failed'); logger.error({ errors: ctx.errors }, 'some rules failed');
if (args.argv.count) { if (args.argv.count) {
return Math.min(errors, 255); return Math.min(ctx.errors.length, 255);
} else { } else {
return STATUS_ERROR; return STATUS_ERROR;
} }

View File

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