diff --git a/src/config/args.ts b/src/config/args.ts index 45a7c95..cbb79fc 100644 --- a/src/config/args.ts +++ b/src/config/args.ts @@ -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; + 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) { +export function parseArgs(argv: Array): ParseResults { let mode = 'check'; - const args = usage(`Usage: salty-dog [options]`) + const parser = usage(`Usage: salty-dog [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) { 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) { }) .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(); diff --git a/src/config/index.ts b/src/config/index.ts index 539ef5c..16a4023 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -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; + }; + }; +} + /** * 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): Array): Promise { +export async function loadConfig(name: string, ...extras: Array): Promise { const paths = completePaths(name, extras); for (const p of paths) { diff --git a/src/config/type/Stream.ts b/src/config/type/Stream.ts index 07147cf..884a256 100644 --- a/src/config/type/Stream.ts +++ b/src/config/type/Stream.ts @@ -20,4 +20,3 @@ export const streamType = new YamlType('!stream', { return Reflect.get(process, name); }, }); - diff --git a/src/index.ts b/src/index.ts index e3f135f..d82fcf8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,6 +18,7 @@ const MODES_LIST: Array = [MODES.check, MODES.fix, MODES.list]; const STATUS_SUCCESS = 0; const STATUS_ERROR = 1; +const STATUS_MAX = 255; export async function main(argv: Array): Promise { const { args, mode } = parseArgs(argv); @@ -43,7 +44,7 @@ export async function main(argv: Array): Promise { }); 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): Promise { 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; } diff --git a/src/parser/YamlParser.ts b/src/parser/YamlParser.ts index a029fd4..be2435e 100644 --- a/src/parser/YamlParser.ts +++ b/src/parser/YamlParser.ts @@ -4,7 +4,7 @@ import { CONFIG_SCHEMA } from '../config/schema'; import { Parser } from '../parser'; export class YamlParser implements Parser { - dump(...data: Array): string { + public dump(...data: Array): string { const docs: Array = []; 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 { + public parse(body: string): Array { const docs: Array = []; safeLoadAll(body, (doc: any) => docs.push(doc), { schema: CONFIG_SCHEMA, diff --git a/src/rule.ts b/src/rule.ts index 80814c7..bbc6a3e 100644 --- a/src/rule.ts +++ b/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; } -export function ensureArray(val: Array | undefined): Array { - if (isNil(val)) { - return []; - } else { - return Array.from(val); +export interface RuleResult extends VisitorResult { + rule: Rule; +} + +export class Rule implements RuleData, Visitor { + 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; + + 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> { + 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 { + ctx.logger.debug({ item: node, rule: this }, 'visiting node'); + + const check = ctx.compile(this.check); + const filter = this.compileFilter(ctx); + const errors: Array = []; + 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, 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, data: return ctx; } - -export interface RuleResult extends VisitorResult { - rule: Rule; -} - -export class Rule implements RuleData, Visitor { - 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> { - 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 { - ctx.logger.debug({ item: node, rule: this }, 'visiting node'); - - const check = ctx.compile(this.check); - const filter = this.compileFilter(ctx); - const errors: Array = []; - 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); - } - } -} diff --git a/src/utils/ajv/index.ts b/src/utils/ajv/index.ts index 5296b89..ff02b66 100644 --- a/src/utils/ajv/index.ts +++ b/src/utils/ajv/index.ts @@ -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}`; } } diff --git a/src/utils/index.ts b/src/utils/index.ts index d2795a4..1364aed 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,3 +1,13 @@ +import { isNil } from 'lodash'; + export function isNilOrEmpty(val: Array | null | undefined): val is Array { return (Array.isArray(val) && val.length > 0); } + +export function ensureArray(val: Array | undefined): Array { + if (isNil(val)) { + return []; + } else { + return Array.from(val); + } +} diff --git a/src/visitor/VisitorContext.ts b/src/visitor/VisitorContext.ts index f3a4e31..0b7b797 100644 --- a/src/visitor/VisitorContext.ts +++ b/src/visitor/VisitorContext.ts @@ -20,20 +20,20 @@ export class VisitorContext implements VisitorContextOptions, VisitorResult { public readonly innerOptions: RuleOptions; protected readonly ajv: Ajv.Ajv; - protected readonly _changes: Array; - protected readonly _errors: Array; + protected readonly changeBuffer: Array; + protected readonly errorBuffer: Array; public get changes(): ReadonlyArray { - return this._changes; + return this.changeBuffer; } public get errors(): ReadonlyArray { - 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, }); } diff --git a/test/TestRule.ts b/test/TestRule.ts index b12f58b..681171d 100644 --- a/test/TestRule.ts +++ b/test/TestRule.ts @@ -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); diff --git a/test/harness.ts b/test/harness.ts index abc3b64..0b06326 100644 --- a/test/harness.ts +++ b/test/harness.ts @@ -1,3 +1,3 @@ -import sourceMapSupport from 'source-map-support' +import sourceMapSupport from 'source-map-support'; -sourceMapSupport.install() +sourceMapSupport.install(); diff --git a/test/parser/TestYamlParser.ts b/test/parser/TestYamlParser.ts index a64b380..1ff4097 100644 --- a/test/parser/TestYamlParser.ts +++ b/test/parser/TestYamlParser.ts @@ -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); }); diff --git a/test/utils/ajv/TestFriendlyErrors.ts b/test/utils/ajv/TestFriendlyErrors.ts index d74afa3..481c0b8 100644 --- a/test/utils/ajv/TestFriendlyErrors.ts +++ b/test/utils/ajv/TestFriendlyErrors.ts @@ -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(''); });