Skip to content

Commit

Permalink
feat: clean rules up with a bit of a visitor pattern
Browse files Browse the repository at this point in the history
  • Loading branch information
ssube committed Jun 16, 2019
1 parent f61b568 commit 9a25fb9
Show file tree
Hide file tree
Showing 8 changed files with 106 additions and 57 deletions.
2 changes: 1 addition & 1 deletion config/rollup.js
Expand Up @@ -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',
Expand Down
9 changes: 9 additions & 0 deletions rules/salty-dog.yml
Expand Up @@ -22,17 +22,26 @@ 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:
type: string
pattern: "[-:a-z0-9]+"
select:
type: string
minLength: 1
filter:
type: object
check:
Expand Down
5 changes: 1 addition & 4 deletions 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], [
Expand All @@ -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.
*
Expand Down
18 changes: 9 additions & 9 deletions src/index.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
}
Expand Down
107 changes: 66 additions & 41 deletions 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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
});

if (isNil(scopes) || scopes.length === 0) {
logger.debug('no data selected');
return true;
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[];

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);

// copy schema objects
this.check = cloneDeep(data.check);
if (!isNil(data.filter)) {
this.filter = cloneDeep(data.filter);
}
}

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;
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,
});

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');
}
} else {
logger.debug({ errors: filter.errors, item }, 'skipping item');
}
}

return true;
}
return ctx;
}

export function compileFilter(rule: Rule, ajv: any): any {
if (isNil(rule.filter)) {
return () => true;
} else {
return ajv.compile(rule.filter);
protected compileFilter(ajv: any): any {
if (isNil(this.filter)) {
return () => true;
} else {
return ajv.compile(this.filter);
}
}
}
4 changes: 2 additions & 2 deletions 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<string> {
if (path === '-') {
Expand Down
13 changes: 13 additions & 0 deletions src/visitor/context.ts
@@ -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 changes: 5 additions & 0 deletions src/visitor/index.ts
@@ -0,0 +1,5 @@
import { VisitorContext } from 'src/visitor/context';

export interface Visitor {
visit(ctx: VisitorContext, node: any): Promise<VisitorContext>;
}

0 comments on commit 9a25fb9

Please sign in to comment.