feat: clean rules up with a bit of a visitor pattern
This commit is contained in:
parent
f61b5689ae
commit
9a25fb97a8
|
@ -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',
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
18
src/index.ts
18
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<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;
|
||||
}
|
||||
|
|
107
src/rule.ts
107
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<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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 === '-') {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import { VisitorContext } from 'src/visitor/context';
|
||||
|
||||
export interface Visitor {
|
||||
visit(ctx: VisitorContext, node: any): Promise<VisitorContext>;
|
||||
}
|
Loading…
Reference in New Issue