lint: fix things
This commit is contained in:
parent
6aa1cb5365
commit
2bb48b0b87
|
@ -1,5 +1,6 @@
|
|||
import { Options, showCompletionScript, usage } from 'yargs';
|
||||
|
||||
import { RuleSelector } from '../rule';
|
||||
import { VERSION_INFO } from '../version';
|
||||
|
||||
export const CONFIG_ARGS_NAME = 'config-name';
|
||||
|
@ -16,25 +17,40 @@ export interface Args {
|
|||
mode: string;
|
||||
}
|
||||
|
||||
export interface ParsedArgs extends RuleSelector {
|
||||
[CONFIG_ARGS_NAME]: string;
|
||||
[CONFIG_ARGS_PATH]: string;
|
||||
coerce: boolean;
|
||||
count: boolean;
|
||||
defaults: boolean;
|
||||
dest: string;
|
||||
mode: string;
|
||||
rules: Array<string>;
|
||||
source: string;
|
||||
}
|
||||
|
||||
export interface ParseResults {
|
||||
args: ParsedArgs;
|
||||
mode: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap yargs to exit after completion.
|
||||
*
|
||||
* @TODO: fix it to use argv, not sure if yargs can do that
|
||||
*/
|
||||
export function parseArgs(argv: Array<string>) {
|
||||
export function parseArgs(argv: Array<string>): ParseResults {
|
||||
let mode = 'check';
|
||||
|
||||
const args = usage(`Usage: salty-dog <mode> [options]`)
|
||||
const parser = usage(`Usage: salty-dog <mode> [options]`)
|
||||
.command({
|
||||
command: ['check', '*'],
|
||||
describe: 'validate the source documents',
|
||||
handler: (argv: any) => {
|
||||
handler: (argi: any) => {
|
||||
mode = 'check';
|
||||
},
|
||||
})
|
||||
.command({
|
||||
command: ['fix'],
|
||||
describe: 'validate the source document and insert defaults',
|
||||
builder: (yargs: any) => {
|
||||
return yargs
|
||||
.option('coerce', {
|
||||
|
@ -46,21 +62,23 @@ export function parseArgs(argv: Array<string>) {
|
|||
type: 'boolean',
|
||||
});
|
||||
},
|
||||
handler: (argv: any) => {
|
||||
command: ['fix'],
|
||||
describe: 'validate the source document and insert defaults',
|
||||
handler: (argi: any) => {
|
||||
mode = 'fix';
|
||||
},
|
||||
})
|
||||
.command({
|
||||
command: ['list'],
|
||||
describe: 'list active rules',
|
||||
handler: (argv: any) => {
|
||||
handler: (argi: any) => {
|
||||
mode = 'list';
|
||||
},
|
||||
})
|
||||
.command({
|
||||
command: ['complete'],
|
||||
describe: 'generate tab completion script for bash or zsh',
|
||||
handler: (argv: any) => {
|
||||
handler: (argi: any) => {
|
||||
mode = 'complete';
|
||||
},
|
||||
})
|
||||
|
@ -112,8 +130,11 @@ export function parseArgs(argv: Array<string>) {
|
|||
})
|
||||
.help()
|
||||
.version(VERSION_INFO.app.version)
|
||||
.alias('version', 'v')
|
||||
.argv;
|
||||
.alias('version', 'v');
|
||||
|
||||
// @TODO: this should not need a cast but argv's type only has the last option (include-tag)
|
||||
// @tslint:disable-next-line:no-any
|
||||
const args = parser.argv as any;
|
||||
|
||||
if (mode === 'complete') {
|
||||
showCompletionScript();
|
||||
|
|
|
@ -1,14 +1,26 @@
|
|||
import { Stream } from 'bunyan';
|
||||
import { isNil, isString } from 'lodash';
|
||||
import { LogLevel } from 'noicejs';
|
||||
import { join } from 'path';
|
||||
|
||||
import { CONFIG_ENV, CONFIG_SCHEMA } from './schema';
|
||||
import { includeSchema } from './type/Include';
|
||||
import { NotFoundError } from '../error/NotFoundError';
|
||||
import { YamlParser } from '../parser/YamlParser';
|
||||
import { readFileSync } from '../source';
|
||||
import { CONFIG_ENV, CONFIG_SCHEMA } from './schema';
|
||||
import { includeSchema } from './type/Include';
|
||||
|
||||
includeSchema.schema = CONFIG_SCHEMA;
|
||||
|
||||
export interface ConfigData {
|
||||
data: {
|
||||
logger: {
|
||||
level: LogLevel;
|
||||
name: string;
|
||||
streams: Array<Stream>;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* With the given name, generate all potential config paths in their complete, absolute form.
|
||||
*
|
||||
|
@ -39,7 +51,7 @@ export function completePaths(name: string, extras: Array<string>): Array<string
|
|||
return paths;
|
||||
}
|
||||
|
||||
export async function loadConfig(name: string, ...extras: Array<string>): Promise<any> {
|
||||
export async function loadConfig(name: string, ...extras: Array<string>): Promise<ConfigData> {
|
||||
const paths = completePaths(name, extras);
|
||||
|
||||
for (const p of paths) {
|
||||
|
|
|
@ -20,4 +20,3 @@ export const streamType = new YamlType('!stream', {
|
|||
return Reflect.get(process, name);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ const MODES_LIST: Array<string> = [MODES.check, MODES.fix, MODES.list];
|
|||
|
||||
const STATUS_SUCCESS = 0;
|
||||
const STATUS_ERROR = 1;
|
||||
const STATUS_MAX = 255;
|
||||
|
||||
export async function main(argv: Array<string>): Promise<number> {
|
||||
const { args, mode } = parseArgs(argv);
|
||||
|
@ -43,7 +44,7 @@ export async function main(argv: Array<string>): Promise<number> {
|
|||
});
|
||||
|
||||
const rules = await loadRules(args.rules, ctx);
|
||||
const activeRules = await resolveRules(rules, args as any);
|
||||
const activeRules = await resolveRules(rules, args);
|
||||
|
||||
if (mode === 'list') {
|
||||
logger.info({ rules: activeRules }, 'listing active rules');
|
||||
|
@ -61,7 +62,7 @@ export async function main(argv: Array<string>): Promise<number> {
|
|||
if (ctx.errors.length > 0) {
|
||||
logger.error({ count: ctx.errors.length, errors: ctx.errors }, 'some rules failed');
|
||||
if (args.count) {
|
||||
return Math.min(ctx.errors.length, 255);
|
||||
return Math.min(ctx.errors.length, STATUS_MAX);
|
||||
} else {
|
||||
return STATUS_ERROR;
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import { CONFIG_SCHEMA } from '../config/schema';
|
|||
import { Parser } from '../parser';
|
||||
|
||||
export class YamlParser implements Parser {
|
||||
dump(...data: Array<any>): string {
|
||||
public dump(...data: Array<any>): string {
|
||||
const docs: Array<any> = [];
|
||||
for (const doc of data) {
|
||||
const part = safeDump(doc, {
|
||||
|
@ -15,7 +15,7 @@ export class YamlParser implements Parser {
|
|||
return docs.join('\n---\n\n');
|
||||
}
|
||||
|
||||
parse(body: string): Array<any> {
|
||||
public parse(body: string): Array<any> {
|
||||
const docs: Array<any> = [];
|
||||
safeLoadAll(body, (doc: any) => docs.push(doc), {
|
||||
schema: CONFIG_SCHEMA,
|
||||
|
|
157
src/rule.ts
157
src/rule.ts
|
@ -1,12 +1,12 @@
|
|||
import { ValidateFunction } from 'ajv';
|
||||
import { applyDiff, diff } from 'deep-diff';
|
||||
import { JSONPath } from 'jsonpath-plus';
|
||||
import { cloneDeep, Dictionary, intersection, isNil } from 'lodash';
|
||||
import { cloneDeep, defaultTo, Dictionary, intersection, isNil } from 'lodash';
|
||||
import { LogLevel } from 'noicejs';
|
||||
|
||||
import { YamlParser } from './parser/YamlParser';
|
||||
import { readFileSync } from './source';
|
||||
import { isNilOrEmpty } from './utils';
|
||||
import { ensureArray, isNilOrEmpty } from './utils';
|
||||
import { friendlyError } from './utils/ajv';
|
||||
import { Visitor } from './visitor';
|
||||
import { VisitorContext } from './visitor/VisitorContext';
|
||||
|
@ -40,11 +40,77 @@ export interface RuleSource {
|
|||
rules: Array<RuleData>;
|
||||
}
|
||||
|
||||
export function ensureArray<T>(val: Array<T> | undefined): Array<T> {
|
||||
if (isNil(val)) {
|
||||
return [];
|
||||
} else {
|
||||
return Array.from(val);
|
||||
export interface RuleResult extends VisitorResult {
|
||||
rule: Rule;
|
||||
}
|
||||
|
||||
export class Rule implements RuleData, Visitor<RuleResult> {
|
||||
public readonly check: ValidateFunction;
|
||||
public readonly desc: string;
|
||||
public readonly filter?: ValidateFunction;
|
||||
public readonly level: LogLevel;
|
||||
public readonly name: string;
|
||||
public readonly select: string;
|
||||
public readonly tags: Array<string>;
|
||||
|
||||
constructor(data: RuleData) {
|
||||
this.desc = data.desc;
|
||||
this.level = data.level;
|
||||
this.name = data.name;
|
||||
this.select = defaultTo(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);
|
||||
}
|
||||
}
|
||||
|
||||
public async pick(ctx: VisitorContext, root: any): Promise<Array<any>> {
|
||||
const scopes = JSONPath({
|
||||
json: root,
|
||||
path: this.select,
|
||||
});
|
||||
|
||||
if (isNil(scopes) || scopes.length === 0) {
|
||||
ctx.logger.debug('no data selected');
|
||||
return [];
|
||||
}
|
||||
|
||||
return scopes;
|
||||
}
|
||||
|
||||
public async visit(ctx: VisitorContext, node: any): Promise<RuleResult> {
|
||||
ctx.logger.debug({ item: node, rule: this }, 'visiting node');
|
||||
|
||||
const check = ctx.compile(this.check);
|
||||
const filter = this.compileFilter(ctx);
|
||||
const errors: Array<VisitorError> = [];
|
||||
const result: RuleResult = {
|
||||
changes: [],
|
||||
errors,
|
||||
rule: this,
|
||||
};
|
||||
|
||||
if (filter(node)) {
|
||||
ctx.logger.debug({ item: node }, 'checking item');
|
||||
if (!check(node) && !isNil(check.errors) && check.errors.length > 0) {
|
||||
ctx.error(...Array.from(check.errors).map(friendlyError));
|
||||
}
|
||||
} else {
|
||||
ctx.logger.debug({ errors: filter.errors, item: node }, 'skipping item');
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected compileFilter(ctx: VisitorContext): ValidateFunction {
|
||||
if (isNil(this.filter)) {
|
||||
return () => true;
|
||||
} else {
|
||||
return ctx.compile(this.filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -75,7 +141,7 @@ export async function loadRules(paths: Array<string>, ctx: VisitorContext): Prom
|
|||
ctx.addSchema(data.name, data.definitions);
|
||||
}
|
||||
|
||||
rules.push(...data.rules.map((data: RuleData) => new Rule(data)));
|
||||
rules.push(...data.rules.map((it: RuleData) => new Rule(it)));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -149,78 +215,3 @@ export async function visitRules(ctx: VisitorContext, rules: Array<Rule>, data:
|
|||
|
||||
return ctx;
|
||||
}
|
||||
|
||||
export interface RuleResult extends VisitorResult {
|
||||
rule: Rule;
|
||||
}
|
||||
|
||||
export class Rule implements RuleData, Visitor<RuleResult> {
|
||||
public readonly check: ValidateFunction;
|
||||
public readonly desc: string;
|
||||
public readonly filter?: ValidateFunction;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
public async pick(ctx: VisitorContext, root: any): Promise<Array<any>> {
|
||||
const scopes = JSONPath({
|
||||
json: root,
|
||||
path: this.select,
|
||||
});
|
||||
|
||||
if (isNil(scopes) || scopes.length === 0) {
|
||||
ctx.logger.debug('no data selected');
|
||||
return [];
|
||||
}
|
||||
|
||||
return scopes;
|
||||
}
|
||||
|
||||
public async visit(ctx: VisitorContext, node: any): Promise<RuleResult> {
|
||||
ctx.logger.debug({ item: node, rule: this }, 'visiting node');
|
||||
|
||||
const check = ctx.compile(this.check);
|
||||
const filter = this.compileFilter(ctx);
|
||||
const errors: Array<VisitorError> = [];
|
||||
const result: RuleResult = {
|
||||
changes: [],
|
||||
errors,
|
||||
rule: this,
|
||||
};
|
||||
|
||||
if (filter(node)) {
|
||||
ctx.logger.debug({ item: node }, 'checking item');
|
||||
if (!check(node) && check.errors && check.errors.length) {
|
||||
const errors = Array.from(check.errors);
|
||||
ctx.error(...errors.map(friendlyError));
|
||||
}
|
||||
} else {
|
||||
ctx.logger.debug({ errors: filter.errors, item: node }, 'skipping item');
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected compileFilter(ctx: VisitorContext): ValidateFunction {
|
||||
if (isNil(this.filter)) {
|
||||
return () => true;
|
||||
} else {
|
||||
return ctx.compile(this.filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { ErrorObject } from 'ajv';
|
||||
import { isNil } from 'lodash';
|
||||
|
||||
import { VisitorError } from '../../visitor/VisitorError';
|
||||
|
||||
|
@ -13,9 +14,9 @@ export function friendlyError(err: ErrorObject): VisitorError {
|
|||
}
|
||||
|
||||
export function friendlyErrorMessage(err: ErrorObject): string {
|
||||
if (err.message) {
|
||||
return `${err.dataPath} ${err.message}`;
|
||||
} else {
|
||||
if (isNil(err.message)) {
|
||||
return `${err.dataPath} ${err.keyword}`;
|
||||
} else {
|
||||
return `${err.dataPath} ${err.message}`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,13 @@
|
|||
import { isNil } from 'lodash';
|
||||
|
||||
export function isNilOrEmpty(val: Array<unknown> | null | undefined): val is Array<unknown> {
|
||||
return (Array.isArray(val) && val.length > 0);
|
||||
}
|
||||
|
||||
export function ensureArray<T>(val: Array<T> | undefined): Array<T> {
|
||||
if (isNil(val)) {
|
||||
return [];
|
||||
} else {
|
||||
return Array.from(val);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,20 +20,20 @@ export class VisitorContext implements VisitorContextOptions, VisitorResult {
|
|||
public readonly innerOptions: RuleOptions;
|
||||
|
||||
protected readonly ajv: Ajv.Ajv;
|
||||
protected readonly _changes: Array<any>;
|
||||
protected readonly _errors: Array<VisitorError>;
|
||||
protected readonly changeBuffer: Array<any>;
|
||||
protected readonly errorBuffer: Array<VisitorError>;
|
||||
|
||||
public get changes(): ReadonlyArray<any> {
|
||||
return this._changes;
|
||||
return this.changeBuffer;
|
||||
}
|
||||
|
||||
public get errors(): ReadonlyArray<VisitorError> {
|
||||
return this._errors;
|
||||
return this.errorBuffer;
|
||||
}
|
||||
|
||||
constructor(options: VisitorContextOptions) {
|
||||
this._changes = [];
|
||||
this._errors = [];
|
||||
this.changeBuffer = [];
|
||||
this.errorBuffer = [];
|
||||
|
||||
this.ajv = new Ajv({
|
||||
coerceTypes: options.innerOptions.coerce,
|
||||
|
@ -49,12 +49,12 @@ export class VisitorContext implements VisitorContextOptions, VisitorResult {
|
|||
logWithLevel(this.logger, err.level, err.data, err.msg);
|
||||
}
|
||||
|
||||
this._errors.push(...errors);
|
||||
this.errorBuffer.push(...errors);
|
||||
}
|
||||
|
||||
public mergeResult(other: VisitorResult): this {
|
||||
this._changes.push(...other.changes);
|
||||
this._errors.push(...other.errors);
|
||||
this.changeBuffer.push(...other.changes);
|
||||
this.errorBuffer.push(...other.errors);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -69,7 +69,7 @@ export class VisitorContext implements VisitorContextOptions, VisitorResult {
|
|||
});
|
||||
|
||||
this.ajv.addSchema({
|
||||
'$id': name,
|
||||
$id: name,
|
||||
definitions: schema,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -6,26 +6,26 @@ import { makeSelector, resolveRules, Rule, visitRules } from '../src/rule';
|
|||
import { VisitorContext } from '../src/visitor/VisitorContext';
|
||||
|
||||
const TEST_RULES = [new Rule({
|
||||
name: 'foo',
|
||||
check: {},
|
||||
desc: '',
|
||||
level: 'info',
|
||||
name: 'foo',
|
||||
select: '$',
|
||||
tags: ['all', 'foo'],
|
||||
check: {},
|
||||
select: '$',
|
||||
}), new Rule({
|
||||
check: {},
|
||||
desc: '',
|
||||
level: 'warn',
|
||||
name: 'bar',
|
||||
desc: '',
|
||||
level: 'warn',
|
||||
tags: ['all', 'test'],
|
||||
check: {},
|
||||
select: '$',
|
||||
tags: ['all', 'test'],
|
||||
}), new Rule({
|
||||
name: 'bin',
|
||||
check: {},
|
||||
desc: '',
|
||||
level: 'warn',
|
||||
tags: ['all', 'test'],
|
||||
check: {},
|
||||
name: 'bin',
|
||||
select: '$',
|
||||
tags: ['all', 'test'],
|
||||
})];
|
||||
|
||||
describe('rule resolver', () => {
|
||||
|
@ -102,21 +102,21 @@ describe('rule resolver', () => {
|
|||
describe('rule visitor', () => {
|
||||
it('should only call visit for selected items', async () => {
|
||||
const ctx = new VisitorContext({
|
||||
logger: new ConsoleLogger(),
|
||||
innerOptions: {
|
||||
coerce: false,
|
||||
defaults: false,
|
||||
mutate: false,
|
||||
}
|
||||
},
|
||||
logger: new ConsoleLogger(),
|
||||
});
|
||||
const data = {};
|
||||
const rule = new Rule({
|
||||
name: 'foo',
|
||||
check: {},
|
||||
desc: '',
|
||||
level: 'info',
|
||||
tags: [],
|
||||
name: 'foo',
|
||||
select: '$',
|
||||
check: {},
|
||||
tags: [],
|
||||
});
|
||||
|
||||
const mockRule = mock(rule);
|
||||
|
@ -143,12 +143,12 @@ describe('rule visitor', () => {
|
|||
});
|
||||
const data = {};
|
||||
const rule = new Rule({
|
||||
name: 'foo',
|
||||
check: {},
|
||||
desc: '',
|
||||
level: 'info',
|
||||
tags: [],
|
||||
name: 'foo',
|
||||
select: '$',
|
||||
check: {},
|
||||
tags: [],
|
||||
});
|
||||
|
||||
const mockRule = mock(rule);
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import sourceMapSupport from 'source-map-support'
|
||||
import sourceMapSupport from 'source-map-support';
|
||||
|
||||
sourceMapSupport.install()
|
||||
sourceMapSupport.install();
|
||||
|
|
|
@ -5,10 +5,10 @@ import { YamlParser } from '../../src/parser/YamlParser';
|
|||
describe('yaml parser', () => {
|
||||
describe('dump documents', () => {
|
||||
it('should dump multiple documents', () => {
|
||||
const parser = new YamlParser();
|
||||
const data = parser.dump({}, {});
|
||||
const parser = new YamlParser();
|
||||
const data = parser.dump({}, {});
|
||||
|
||||
expect(data).to.contain('---');
|
||||
expect(data).to.contain('---');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -20,7 +20,7 @@ foo: {}
|
|||
---
|
||||
bar: {}
|
||||
`);
|
||||
|
||||
|
||||
expect(Array.isArray(data)).to.equal(true);
|
||||
expect(data.length).to.equal(2);
|
||||
});
|
||||
|
|
|
@ -5,10 +5,10 @@ import { friendlyError } from '../../../src/utils/ajv';
|
|||
describe('friendly errors', () => {
|
||||
it('should have a message', () => {
|
||||
const err = friendlyError({
|
||||
keyword: 'test',
|
||||
dataPath: 'test-path',
|
||||
schemaPath: 'test-path',
|
||||
keyword: 'test',
|
||||
params: { /* ? */ },
|
||||
schemaPath: 'test-path',
|
||||
});
|
||||
expect(err.msg).to.not.equal('');
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue