From 9a25fb97a83b202b96bc2792c8d26349f2009bd7 Mon Sep 17 00:00:00 2001 From: ssube Date: Sun, 16 Jun 2019 13:30:04 -0500 Subject: [PATCH] feat: clean rules up with a bit of a visitor pattern --- config/rollup.js | 2 +- rules/salty-dog.yml | 9 ++++ src/config/index.ts | 5 +- src/index.ts | 18 +++---- src/rule.ts | 107 +++++++++++++++++++++++++---------------- src/source.ts | 4 +- src/visitor/context.ts | 13 +++++ src/visitor/index.ts | 5 ++ 8 files changed, 106 insertions(+), 57 deletions(-) create mode 100644 src/visitor/context.ts create mode 100644 src/visitor/index.ts diff --git a/config/rollup.js b/config/rollup.js index 304e0de..c1114c5 100644 --- a/config/rollup.js +++ b/config/rollup.js @@ -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', diff --git a/rules/salty-dog.yml b/rules/salty-dog.yml index fe68d85..f68e2f8 100644 --- a/rules/salty-dog.yml +++ b/rules/salty-dog.yml @@ -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: diff --git a/src/config/index.ts b/src/config/index.ts index c73f035..c6a319b 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -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. * diff --git a/src/index.ts b/src/index.ts index 3845dcd..fd464ea 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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): Promise { 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; } diff --git a/src/rule.ts b/src/rule.ts index 160ce5f..9f3d67e 100644 --- a/src/rule.ts +++ b/src/rule.ts @@ -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): Promise> { 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, 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 { + 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); + } } } \ No newline at end of file diff --git a/src/source.ts b/src/source.ts index f4ae3da..33faa5c 100644 --- a/src/source.ts +++ b/src/source.ts @@ -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 { if (path === '-') { diff --git a/src/visitor/context.ts b/src/visitor/context.ts new file mode 100644 index 0000000..ec885f8 --- /dev/null +++ b/src/visitor/context.ts @@ -0,0 +1,13 @@ +import { Logger } from 'noicejs'; + +export class VisitorContext { + public readonly changes: Array; + public readonly errors: Array; + public readonly logger: Logger; + + constructor(logger: Logger) { + this.changes = []; + this.errors = []; + this.logger = logger; + } +} \ No newline at end of file diff --git a/src/visitor/index.ts b/src/visitor/index.ts new file mode 100644 index 0000000..0fb9343 --- /dev/null +++ b/src/visitor/index.ts @@ -0,0 +1,5 @@ +import { VisitorContext } from 'src/visitor/context'; + +export interface Visitor { + visit(ctx: VisitorContext, node: any): Promise; +} \ No newline at end of file