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 { RuleSelector } from '../rule';
import { RuleSelector, RuleSources } from '../rule';
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 */
export const CONFIG_ARGS_NAME = 'config-name';
@ -19,7 +28,7 @@ export interface Args {
mode: string;
}
export interface ParsedArgs extends RuleSelector {
export interface ParsedArgs extends RuleSelector, RuleSources {
[CONFIG_ARGS_NAME]: string;
[CONFIG_ARGS_PATH]: string;
coerce: boolean;
@ -33,7 +42,7 @@ export interface ParsedArgs extends RuleSelector {
export interface ParseResults {
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
*/
export function parseArgs(argv: Array<string>): ParseResults {
let mode = 'check';
let mode: MODE = MODE.check;
const parser = usage(`Usage: salty-dog <mode> [options]`)
.command({
command: ['check', '*'],
describe: 'validate the source documents',
handler: (argi: any) => {
mode = 'check';
mode = MODE.check;
},
})
.command({
@ -67,21 +76,21 @@ export function parseArgs(argv: Array<string>): ParseResults {
command: ['fix'],
describe: 'validate the source document and insert defaults',
handler: (argi: any) => {
mode = 'fix';
mode = MODE.fix;
},
})
.command({
command: ['list'],
describe: 'list active rules',
handler: (argi: any) => {
mode = 'list';
mode = MODE.list;
},
})
.command({
command: ['complete'],
describe: 'generate tab completion script for bash or zsh',
handler: (argi: any) => {
mode = 'complete';
mode = MODE.complete;
},
})
.option(CONFIG_ARGS_NAME, {
@ -110,12 +119,24 @@ export function parseArgs(argv: Array<string>): ParseResults {
default: 'yaml',
type: 'string',
})
.option('rules', {
alias: ['r'],
.option('rule-file', {
alias: ['r', 'rule', 'rules'],
default: [],
desc: 'Rules file',
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', {
alias: ['s'],
default: '-',
@ -138,7 +159,9 @@ export function parseArgs(argv: Array<string>): ParseResults {
// @tslint:disable-next-line:no-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();
process.exit(0);
}

View File

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

View File

@ -1,9 +1,10 @@
import { applyDiff, diff } from 'deep-diff';
import { cloneDeep, Dictionary, intersection, isNil } from 'lodash';
import { LogLevel } from 'noicejs';
import { join } from 'path';
import { YamlParser } from '../parser/YamlParser';
import { readFile } from '../source';
import { readDir, readFile } from '../source';
import { ensureArray, hasItems } from '../utils';
import { VisitorContext } from '../visitor/VisitorContext';
import { SchemaRule } from './SchemaRule';
@ -22,6 +23,11 @@ export interface RuleData {
select: string;
}
/**
* Rule selector derived from arguments.
*
* The `excludeFoo`/`includeFoo`/ names match yargs output structure.
*/
export interface RuleSelector {
excludeLevel: Array<LogLevel>;
excludeName: Array<string>;
@ -31,13 +37,30 @@ export interface RuleSelector {
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>;
name: string;
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 {
excludeLevel: ensureArray(options.excludeLevel),
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 rules = [];
@ -57,7 +88,7 @@ export async function loadRules(paths: Array<string>, ctx: VisitorContext): Prom
encoding: 'utf-8',
});
const docs = parser.parse(contents) as Array<RuleSource>;
const docs = parser.parse(contents) as Array<RuleSourceData>;
for (const data of docs) {
if (!isNil(data.definitions)) {
@ -71,6 +102,52 @@ export async function loadRules(paths: Array<string>, ctx: VisitorContext): Prom
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>> {
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 { promisify } from 'util';
export const FILE_ENCODING = 'utf-8';
export const readDir = promisify(readdir);
export const readFile = promisify(readBack);
export const writeFile = promisify(writeBack);

View File

@ -1,6 +1,6 @@
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);
}