parent
59e7c138c0
commit
9fbf7cc0c7
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
28
src/index.ts
28
src/index.ts
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>();
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue