1
0
Fork 0

feat: load rules from directories (#8) and modules (#6)

This commit is contained in:
ssube 2019-11-02 09:03:41 -05:00 committed by Sean Sube
parent 59e7c138c0
commit 9fbf7cc0c7
5 changed files with 131 additions and 35 deletions

View File

@ -1,8 +1,17 @@
import { Options, showCompletionScript, usage } from 'yargs'; import { Options, showCompletionScript, usage } from 'yargs';
import { RuleSelector } from '../rule'; import { RuleSelector, RuleSources } from '../rule';
import { VERSION_INFO } from '../version'; import { VERSION_INFO } from '../version';
export enum MODE {
check = 'check',
complete = 'complete',
fix = 'fix',
list = 'list',
}
export const VALID_MODES = new Set([MODE.check, MODE.fix, MODE.list]);
/* tslint:disable:no-any */ /* tslint:disable:no-any */
export const CONFIG_ARGS_NAME = 'config-name'; export const CONFIG_ARGS_NAME = 'config-name';
@ -19,7 +28,7 @@ export interface Args {
mode: string; mode: string;
} }
export interface ParsedArgs extends RuleSelector { export interface ParsedArgs extends RuleSelector, RuleSources {
[CONFIG_ARGS_NAME]: string; [CONFIG_ARGS_NAME]: string;
[CONFIG_ARGS_PATH]: string; [CONFIG_ARGS_PATH]: string;
coerce: boolean; coerce: boolean;
@ -33,7 +42,7 @@ export interface ParsedArgs extends RuleSelector {
export interface ParseResults { export interface ParseResults {
args: ParsedArgs; args: ParsedArgs;
mode: string; mode: MODE;
} }
/** /**
@ -42,14 +51,14 @@ export interface ParseResults {
* @TODO: fix it to use argv, not sure if yargs can do that * @TODO: fix it to use argv, not sure if yargs can do that
*/ */
export function parseArgs(argv: Array<string>): ParseResults { export function parseArgs(argv: Array<string>): ParseResults {
let mode = 'check'; let mode: MODE = MODE.check;
const parser = usage(`Usage: salty-dog <mode> [options]`) const parser = usage(`Usage: salty-dog <mode> [options]`)
.command({ .command({
command: ['check', '*'], command: ['check', '*'],
describe: 'validate the source documents', describe: 'validate the source documents',
handler: (argi: any) => { handler: (argi: any) => {
mode = 'check'; mode = MODE.check;
}, },
}) })
.command({ .command({
@ -67,21 +76,21 @@ export function parseArgs(argv: Array<string>): ParseResults {
command: ['fix'], command: ['fix'],
describe: 'validate the source document and insert defaults', describe: 'validate the source document and insert defaults',
handler: (argi: any) => { handler: (argi: any) => {
mode = 'fix'; mode = MODE.fix;
}, },
}) })
.command({ .command({
command: ['list'], command: ['list'],
describe: 'list active rules', describe: 'list active rules',
handler: (argi: any) => { handler: (argi: any) => {
mode = 'list'; mode = MODE.list;
}, },
}) })
.command({ .command({
command: ['complete'], command: ['complete'],
describe: 'generate tab completion script for bash or zsh', describe: 'generate tab completion script for bash or zsh',
handler: (argi: any) => { handler: (argi: any) => {
mode = 'complete'; mode = MODE.complete;
}, },
}) })
.option(CONFIG_ARGS_NAME, { .option(CONFIG_ARGS_NAME, {
@ -110,12 +119,24 @@ export function parseArgs(argv: Array<string>): ParseResults {
default: 'yaml', default: 'yaml',
type: 'string', type: 'string',
}) })
.option('rules', { .option('rule-file', {
alias: ['r'], alias: ['r', 'rule', 'rules'],
default: [], default: [],
desc: 'Rules file', desc: 'Rules file',
type: 'array', type: 'array',
}) })
.option('rule-module', {
alias: ['m'],
default: [],
desc: 'Rules module',
type: 'array',
})
.option('rule-path', {
alias: ['p'],
default: [],
desc: 'Rules path',
type: 'array',
})
.option('source', { .option('source', {
alias: ['s'], alias: ['s'],
default: '-', default: '-',
@ -138,7 +159,9 @@ export function parseArgs(argv: Array<string>): ParseResults {
// @tslint:disable-next-line:no-any // @tslint:disable-next-line:no-any
const args = parser.argv as any; const args = parser.argv as any;
if (mode === 'complete') { // this should not need a cast either, but something here allows TS to narrow MODE into
// MODE.check, which is much _too_ specific
if (mode as MODE === MODE.complete) {
showCompletionScript(); showCompletionScript();
process.exit(0); process.exit(0);
} }

View File

@ -1,21 +1,13 @@
import { createLogger } from 'bunyan'; import { createLogger } from 'bunyan';
import { loadConfig } from './config'; import { loadConfig } from './config';
import { CONFIG_ARGS_NAME, CONFIG_ARGS_PATH, parseArgs } from './config/args'; import { CONFIG_ARGS_NAME, CONFIG_ARGS_PATH, MODE, parseArgs, VALID_MODES } from './config/args';
import { YamlParser } from './parser/YamlParser'; import { YamlParser } from './parser/YamlParser';
import { createRuleSelector, loadRules, resolveRules, visitRules } from './rule'; import { createRuleSelector, createRuleSources, loadRules, resolveRules, visitRules } from './rule';
import { loadSource, writeSource } from './source'; import { loadSource, writeSource } from './source';
import { VERSION_INFO } from './version'; import { VERSION_INFO } from './version';
import { VisitorContext } from './visitor/VisitorContext'; import { VisitorContext } from './visitor/VisitorContext';
enum MODES {
check = 'check',
fix = 'fix',
list = 'list',
}
const MODES_LIST: Array<string> = [MODES.check, MODES.fix, MODES.list];
const STATUS_SUCCESS = 0; const STATUS_SUCCESS = 0;
const STATUS_ERROR = 1; const STATUS_ERROR = 1;
const STATUS_MAX = 255; const STATUS_MAX = 255;
@ -29,7 +21,7 @@ export async function main(argv: Array<string>): Promise<number> {
logger.info({ args }, 'main arguments'); logger.info({ args }, 'main arguments');
// check mode // check mode
if (!MODES_LIST.includes(mode)) { if (!VALID_MODES.has(mode)) {
logger.error({ mode }, 'unsupported mode'); logger.error({ mode }, 'unsupported mode');
return STATUS_ERROR; return STATUS_ERROR;
} }
@ -38,17 +30,19 @@ export async function main(argv: Array<string>): Promise<number> {
innerOptions: { innerOptions: {
coerce: args.coerce, coerce: args.coerce,
defaults: args.defaults, defaults: args.defaults,
mutate: mode === 'fix', mutate: mode === MODE.fix,
}, },
logger, logger,
}); });
const selector = createRuleSelector(args); const ruleSelector = createRuleSelector(args);
const loadedRules = await loadRules(args.rules, ctx); const ruleSources = createRuleSources(args);
const activeRules = await resolveRules(loadedRules, selector);
if (mode === 'list') { const loadedRules = await loadRules(ruleSources, ctx);
logger.info({ rules: activeRules, selector }, 'listing active rules'); const activeRules = await resolveRules(loadedRules, ruleSelector);
if (mode === MODE.list) {
logger.info({ activeRules, loadedRules, ruleSelector, ruleSources }, 'listing active rules');
return STATUS_SUCCESS; return STATUS_SUCCESS;
} }

View File

@ -1,9 +1,10 @@
import { applyDiff, diff } from 'deep-diff'; import { applyDiff, diff } from 'deep-diff';
import { cloneDeep, Dictionary, intersection, isNil } from 'lodash'; import { cloneDeep, Dictionary, intersection, isNil } from 'lodash';
import { LogLevel } from 'noicejs'; import { LogLevel } from 'noicejs';
import { join } from 'path';
import { YamlParser } from '../parser/YamlParser'; import { YamlParser } from '../parser/YamlParser';
import { readFile } from '../source'; import { readDir, readFile } from '../source';
import { ensureArray, hasItems } from '../utils'; import { ensureArray, hasItems } from '../utils';
import { VisitorContext } from '../visitor/VisitorContext'; import { VisitorContext } from '../visitor/VisitorContext';
import { SchemaRule } from './SchemaRule'; import { SchemaRule } from './SchemaRule';
@ -22,6 +23,11 @@ export interface RuleData {
select: string; select: string;
} }
/**
* Rule selector derived from arguments.
*
* The `excludeFoo`/`includeFoo`/ names match yargs output structure.
*/
export interface RuleSelector { export interface RuleSelector {
excludeLevel: Array<LogLevel>; excludeLevel: Array<LogLevel>;
excludeName: Array<string>; excludeName: Array<string>;
@ -31,13 +37,30 @@ export interface RuleSelector {
includeTag: Array<string>; includeTag: Array<string>;
} }
export interface RuleSource { /**
* Rule sources derived from arguments.
*
* The `ruleFoo` names match yargs output structure.
*/
export interface RuleSources {
ruleFile: Array<string>;
ruleModule: Array<string>;
rulePath: Array<string>;
}
export interface RuleSourceData {
definitions?: Dictionary<any>; definitions?: Dictionary<any>;
name: string; name: string;
rules: Array<RuleData>; rules: Array<RuleData>;
} }
export function createRuleSelector(options: Partial<RuleSelector>) { export interface RuleSourceModule {
definitions?: Dictionary<any>;
name: string;
rules: Array<SchemaRule>;
}
export function createRuleSelector(options: Partial<RuleSelector>): RuleSelector {
return { return {
excludeLevel: ensureArray(options.excludeLevel), excludeLevel: ensureArray(options.excludeLevel),
excludeName: ensureArray(options.excludeName), excludeName: ensureArray(options.excludeName),
@ -48,7 +71,15 @@ export function createRuleSelector(options: Partial<RuleSelector>) {
}; };
} }
export async function loadRules(paths: Array<string>, ctx: VisitorContext): Promise<Array<SchemaRule>> { export function createRuleSources(options: Partial<RuleSources>): RuleSources {
return {
ruleFile: ensureArray(options.ruleFile),
ruleModule: ensureArray(options.ruleModule),
rulePath: ensureArray(options.rulePath),
};
}
export async function loadRuleFiles(paths: Array<string>, ctx: VisitorContext): Promise<Array<SchemaRule>> {
const parser = new YamlParser(); const parser = new YamlParser();
const rules = []; const rules = [];
@ -57,7 +88,7 @@ export async function loadRules(paths: Array<string>, ctx: VisitorContext): Prom
encoding: 'utf-8', encoding: 'utf-8',
}); });
const docs = parser.parse(contents) as Array<RuleSource>; const docs = parser.parse(contents) as Array<RuleSourceData>;
for (const data of docs) { for (const data of docs) {
if (!isNil(data.definitions)) { if (!isNil(data.definitions)) {
@ -71,6 +102,52 @@ export async function loadRules(paths: Array<string>, ctx: VisitorContext): Prom
return rules; return rules;
} }
export async function loadRulePaths(paths: Array<string>, ctx: VisitorContext): Promise<Array<SchemaRule>> {
const rules = [];
for (const path of paths) {
const allFiles = await readDir(path);
const files = allFiles.filter((name) => {
// skip files that start with `.`, limit to yml
return name.match(/^[^\.].*\.ya?ml/);
}).map((name) => join(path, name));
const pathRules = await loadRuleFiles(files, ctx);
rules.push(...pathRules);
}
return rules;
}
export async function loadRuleModules(modules: Array<string>, ctx: VisitorContext): Promise<Array<SchemaRule>> {
const rules = [];
for (const name of modules) {
try {
const module: RuleSourceModule = require(name);
// TODO: ensure module has definitions, name, and rules
if (!isNil(module.definitions)) {
ctx.addSchema(module.name, module.definitions);
}
rules.push(...module.rules);
} catch (err) {
ctx.logger.error(err, 'error requiring rule module');
}
}
return rules;
}
export async function loadRules(sources: RuleSources, ctx: VisitorContext): Promise<Array<SchemaRule>> {
return [
...await loadRuleFiles(sources.ruleFile, ctx),
...await loadRulePaths(sources.rulePath, ctx),
...await loadRuleModules(sources.ruleModule, ctx),
];
}
export async function resolveRules(rules: Array<SchemaRule>, selector: RuleSelector): Promise<Array<SchemaRule>> { export async function resolveRules(rules: Array<SchemaRule>, selector: RuleSelector): Promise<Array<SchemaRule>> {
const activeRules = new Set<SchemaRule>(); const activeRules = new Set<SchemaRule>();

View File

@ -1,8 +1,10 @@
import { readFile as readBack, writeFile as writeBack } from 'fs'; import { readdir, readFile as readBack, writeFile as writeBack } from 'fs';
import { isNil } from 'lodash'; import { isNil } from 'lodash';
import { promisify } from 'util'; import { promisify } from 'util';
export const FILE_ENCODING = 'utf-8'; export const FILE_ENCODING = 'utf-8';
export const readDir = promisify(readdir);
export const readFile = promisify(readBack); export const readFile = promisify(readBack);
export const writeFile = promisify(writeBack); export const writeFile = promisify(writeBack);

View File

@ -1,6 +1,6 @@
import { isNil } from 'lodash'; import { isNil } from 'lodash';
export function hasItems(val: Array<unknown> | null | undefined): val is Array<unknown> { export function hasItems<T>(val: Array<T> | null | undefined): val is Array<T> {
return (Array.isArray(val) && val.length > 0); return (Array.isArray(val) && val.length > 0);
} }