diff --git a/src/app.ts b/src/app.ts index efbd60e9..9a2d3963 100644 --- a/src/app.ts +++ b/src/app.ts @@ -5,7 +5,7 @@ import { loadConfig } from './config'; import { CONFIG_ARGS_NAME, CONFIG_ARGS_PATH, MODE, parseArgs } from './config/args'; import { YamlParser } from './parser/YamlParser'; import { createRuleSelector, createRuleSources, loadRules, resolveRules, validateConfig } from './rule'; -import { RuleVisitor } from './rule/RuleVisitor'; +import { RuleVisitor, RuleVisitorData, RuleVisitorError } from './rule/RuleVisitor'; import { readSource, writeSource } from './source'; import { VERSION_INFO } from './version'; import { VisitorContext } from './visitor/VisitorContext'; @@ -29,7 +29,7 @@ export async function main(argv: Array): Promise { logger.info({ args, mode }, 'main arguments'); // load rules - const ctx = new VisitorContext({ + const ctx = new VisitorContext({ logger, schemaOptions: { coerce: args.coerce, diff --git a/src/rule/RuleVisitor.ts b/src/rule/RuleVisitor.ts index b9c292d4..c0e4a435 100644 --- a/src/rule/RuleVisitor.ts +++ b/src/rule/RuleVisitor.ts @@ -1,69 +1,132 @@ -import { applyDiff, diff } from 'deep-diff'; +import { applyDiff, Diff, diff } from 'deep-diff'; +import { EventEmitter } from 'events'; import { cloneDeep } from 'lodash'; import { Rule } from '.'; import { hasItems } from '../utils'; -import { Visitor } from '../visitor'; +import { Visitor, VisitorResult } from '../visitor'; import { VisitorContext } from '../visitor/VisitorContext'; /* eslint-disable @typescript-eslint/no-explicit-any */ +export interface RuleVisitorData { + item: unknown; + itemIndex: number; + rule: Rule; + ruleIndex: number; +} + +export interface RuleVisitorError extends RuleVisitorData { + key: string; + path: string; +} + export interface RuleVisitorOptions { rules: ReadonlyArray; } -export class RuleVisitor implements RuleVisitorOptions, Visitor { +export enum RuleVisitorEvents { + ITEM_DIFF = 'item-diff', + ITEM_ERROR = 'item-error', + ITEM_PASS = 'item-pass', + ITEM_VISIT = 'item-visit', + RULE_ERROR = 'rule-error', + RULE_PASS = 'rule-pass', + RULE_VISIT = 'rule-visit', +} + +export type RuleVisitorContext = VisitorContext; +export type RuleVisitorResult = VisitorResult; + +export class RuleVisitor extends EventEmitter implements RuleVisitorOptions, Visitor { public readonly rules: ReadonlyArray; constructor(options: RuleVisitorOptions) { + super(); + this.rules = Array.from(options.rules); } - public async pick(ctx: VisitorContext, root: any): Promise> { + public async pick(ctx: RuleVisitorContext, root: any): Promise> { return []; // TODO: why is this part of visitor rather than rule? } - public async visit(ctx: VisitorContext, root: any): Promise { + public async visit(ctx: RuleVisitorContext, root: any): Promise { + let ruleIndex = 0; for (const rule of this.rules) { - const items = await rule.pick(ctx, root); + const ruleData = { + rule, + }; + this.emit(RuleVisitorEvents.RULE_VISIT, ruleData); + let itemIndex = 0; + let ruleErrors = 0; + const items = await rule.pick(ctx, root); for (const item of items) { - ctx.visitData = { + const result = await this.visitItem(ctx, { + item, itemIndex, rule, - }; + ruleIndex, + }); + ctx.mergeResult(result); - await this.visitItem(ctx, item, itemIndex, rule); + ruleErrors += result.errors.length; itemIndex += 1; } + + if (ruleErrors > 0) { + ctx.logger.warn({ rule: rule.name }, 'rule failed'); + this.emit(RuleVisitorEvents.RULE_ERROR, ruleData); + } else { + ctx.logger.info({ rule: rule.name }, 'rule passed'); + this.emit(RuleVisitorEvents.RULE_PASS, ruleData); + } + + ruleIndex += 1; } return ctx; } - public async visitItem(ctx: VisitorContext, item: any, itemIndex: number, rule: Rule): Promise { - const itemResult = cloneDeep(item); - const ruleResult = await rule.visit(ctx, itemResult); + public async visitItem(ctx: RuleVisitorContext, data: RuleVisitorData): Promise { + ctx.visitData = data; + + const itemResult = cloneDeep(data.item); + const ruleResult = await data.rule.visit(ctx, itemResult); if (hasItems(ruleResult.errors)) { - ctx.logger.warn({ count: ruleResult.errors.length, rule }, 'rule failed'); - ctx.mergeResult(ruleResult, ctx.visitData); - return; + const errorData = { + ...data, + count: ruleResult.errors.length, + }; + ctx.logger.warn(errorData, 'item failed'); + this.emit(RuleVisitorEvents.ITEM_ERROR, errorData); + + return ruleResult; } - const itemDiff = diff(item, itemResult); + const itemDiff = diff(data.item, itemResult); if (hasItems(itemDiff)) { - ctx.logger.info({ - diff: itemDiff, - item, - rule: rule.name, - }, 'rule passed with modifications'); - - if (ctx.schemaOptions.mutate) { - applyDiff(item, itemResult); - } - } else { - ctx.logger.info({ rule: rule.name }, 'rule passed'); + await this.visitDiff(ctx, data, itemDiff, itemResult); + } + + ctx.logger.debug(data, 'item passed'); + this.emit(RuleVisitorEvents.ITEM_PASS, data); + + return ruleResult; + } + + public async visitDiff(ctx: RuleVisitorContext, data: RuleVisitorData, itemDiff: Array>, result: any): Promise { + const diffData = { + ...data, + diff: itemDiff, + }; + ctx.logger.info(diffData, 'item could pass rule with changes'); + this.emit(RuleVisitorEvents.ITEM_DIFF, diffData); + + if (ctx.schemaOptions.mutate) { + applyDiff(data.item, result); } } } diff --git a/src/rule/SchemaRule.ts b/src/rule/SchemaRule.ts index 8ae52492..9c60af1e 100644 --- a/src/rule/SchemaRule.ts +++ b/src/rule/SchemaRule.ts @@ -5,7 +5,7 @@ import { LogLevel } from 'noicejs'; import { Rule, RuleData } from '.'; import { doesExist, hasItems } from '../utils'; import { Visitor, VisitorError, VisitorResult } from '../visitor'; -import { VisitorContext } from '../visitor/VisitorContext'; +import { RuleVisitorContext } from './RuleVisitor'; /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/strict-boolean-expressions */ @@ -34,7 +34,7 @@ export class SchemaRule implements Rule, RuleData, Visitor { } } - public async pick(ctx: VisitorContext, root: any): Promise> { + public async pick(ctx: RuleVisitorContext, root: any): Promise> { const items = ctx.pick(this.select, root); if (items.length === 0) { @@ -44,7 +44,7 @@ export class SchemaRule implements Rule, RuleData, Visitor { return items; } - public async visit(ctx: VisitorContext, node: any): Promise { + public async visit(ctx: RuleVisitorContext, node: any): Promise { ctx.logger.debug({ item: node, rule: this }, 'visiting node'); const check = ctx.compile(this.check); @@ -67,7 +67,7 @@ export class SchemaRule implements Rule, RuleData, Visitor { return result; } - protected compileFilter(ctx: VisitorContext): ValidateFunction { + protected compileFilter(ctx: RuleVisitorContext): ValidateFunction { if (isNil(this.filter)) { return DEFAULT_FILTER; } else { @@ -76,7 +76,7 @@ export class SchemaRule implements Rule, RuleData, Visitor { } } -export function friendlyError(ctx: VisitorContext, err: ErrorObject): VisitorError { +export function friendlyError(ctx: RuleVisitorContext, err: ErrorObject): VisitorError { return { data: { err, @@ -86,7 +86,7 @@ export function friendlyError(ctx: VisitorContext, err: ErrorObject): VisitorErr }; } -export function friendlyErrorMessage(ctx: VisitorContext, err: ErrorObject): string { +export function friendlyErrorMessage(ctx: RuleVisitorContext, err: ErrorObject): string { const msg = [err.dataPath]; if (isNil(err.message)) { msg.push(err.keyword); diff --git a/src/rule/index.ts b/src/rule/index.ts index f64d913b..ffe708b4 100644 --- a/src/rule/index.ts +++ b/src/rule/index.ts @@ -10,6 +10,7 @@ import { readFile } from '../source'; import { doesExist, ensureArray } from '../utils'; import { VisitorResult } from '../visitor'; import { VisitorContext } from '../visitor/VisitorContext'; +import { RuleVisitorContext } from './RuleVisitor'; import { SchemaRule } from './SchemaRule'; /* eslint-disable @typescript-eslint/no-explicit-any */ @@ -27,6 +28,7 @@ export interface RuleData { /* tslint:enable:no-any */ export type Validator = ValidateFunction; + export interface Rule { check: Validator; desc?: string; @@ -36,8 +38,8 @@ export interface Rule { select: string; tags: Array; - pick(ctx: VisitorContext, root: any): Promise>; - visit(ctx: VisitorContext, item: any): Promise; + pick(ctx: RuleVisitorContext, root: any): Promise>; + visit(ctx: RuleVisitorContext, item: any): Promise; } /** @@ -100,7 +102,7 @@ export function isPOJSO(val: any): val is RuleData { return Reflect.getPrototypeOf(val) === Reflect.getPrototypeOf({}); } -export async function loadRuleSource(data: RuleSourceModule, ctx: VisitorContext): Promise> { +export async function loadRuleSource(data: RuleSourceModule, ctx: RuleVisitorContext): Promise> { if (doesExist(data.definitions)) { ctx.addSchema(data.name, data.definitions); } @@ -114,7 +116,7 @@ export async function loadRuleSource(data: RuleSourceModule, ctx: VisitorContext }); } -export async function loadRuleFiles(paths: Array, ctx: VisitorContext): Promise> { +export async function loadRuleFiles(paths: Array, ctx: RuleVisitorContext): Promise> { const parser = new YamlParser(); const rules = []; @@ -141,7 +143,7 @@ export async function loadRuleFiles(paths: Array, ctx: VisitorContext): return rules; } -export async function loadRulePaths(paths: Array, ctx: VisitorContext): Promise> { +export async function loadRulePaths(paths: Array, ctx: RuleVisitorContext): Promise> { const match = new Minimatch('**/*.+(json|yaml|yml)', { nocase: true, }); @@ -163,7 +165,7 @@ export async function loadRulePaths(paths: Array, ctx: VisitorContext): return rules; } -export async function loadRuleModules(modules: Array, ctx: VisitorContext, r = require): Promise> { +export async function loadRuleModules(modules: Array, ctx: RuleVisitorContext, r = require): Promise> { const rules = []; for (const name of modules) { @@ -186,7 +188,7 @@ export async function loadRuleModules(modules: Array, ctx: VisitorContex return rules; } -export async function loadRules(sources: RuleSources, ctx: VisitorContext): Promise> { +export async function loadRules(sources: RuleSources, ctx: RuleVisitorContext): Promise> { return [ ...await loadRuleFiles(sources.ruleFile, ctx), ...await loadRulePaths(sources.rulePath, ctx), @@ -218,32 +220,35 @@ export async function resolveRules(rules: Array, selector: RuleSelector): return Array.from(activeRules); } -export function validateRules(ctx: VisitorContext, root: any): boolean { +export function compileValidators(ctx: RuleVisitorContext) { const { definitions, name } = ruleSchemaData as any; const validCtx = new VisitorContext(ctx); validCtx.addSchema(name, definitions); - const ruleSchema = validCtx.compile(definitions.source); + const configSchema = validCtx.compile(definitions.config); + const sourceSchema = validCtx.compile(definitions.source); + + return { configSchema, sourceSchema }; +} + +export function validateRules(ctx: RuleVisitorContext, root: any): boolean { + const { sourceSchema } = compileValidators(ctx); - if (ruleSchema(root) === true) { + if (sourceSchema(root) === true) { return true; } else { - ctx.logger.error({ errors: ruleSchema.errors }, 'error validating rules'); + ctx.logger.error({ errors: sourceSchema.errors }, 'error validating rules'); return false; } } -export function validateConfig(ctx: VisitorContext, root: any): boolean { - const { definitions, name } = ruleSchemaData as any; - - const validCtx = new VisitorContext(ctx); - validCtx.addSchema(name, definitions); - const ruleSchema = validCtx.compile(definitions.config); +export function validateConfig(ctx: RuleVisitorContext, root: any): boolean { + const { configSchema } = compileValidators(ctx); - if (ruleSchema(root) === true) { + if (configSchema(root) === true) { return true; } else { - ctx.logger.error({ errors: ruleSchema.errors }, 'error validating rules'); + ctx.logger.error({ errors: configSchema.errors }, 'error validating config'); return false; } } diff --git a/src/utils/index.ts b/src/utils/index.ts index 4213c030..1bf26a9e 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,9 +1,18 @@ import { isNil } from 'lodash'; +import { NotFoundError } from '../error/NotFoundError'; + export function doesExist(val: T | null | undefined): val is T { return !isNil(val); } +export function mustExist(val: T | null | undefined): T { + if (isNil(val)) { + throw new NotFoundError(); + } + return val; +} + /** * Test if a value is an array with some items (length > 0). * diff --git a/src/visitor/VisitorContext.ts b/src/visitor/VisitorContext.ts index 2231afb5..ff6472ce 100644 --- a/src/visitor/VisitorContext.ts +++ b/src/visitor/VisitorContext.ts @@ -3,11 +3,11 @@ import { JSONPath } from 'jsonpath-plus'; import { Logger } from 'noicejs'; import { VisitorError, VisitorResult } from '.'; -import { doesExist, hasItems } from '../utils'; +import { doesExist, hasItems, mustExist } from '../utils'; /* eslint-disable @typescript-eslint/no-explicit-any */ -export interface RuleOptions { +export interface RuleSchemaOptions { coerce: boolean; defaults: boolean; mutate: boolean; @@ -15,17 +15,17 @@ export interface RuleOptions { export interface VisitorContextOptions { logger: Logger; - schemaOptions: RuleOptions; + schemaOptions: RuleSchemaOptions; } -export class VisitorContext implements VisitorContextOptions, VisitorResult { +export class VisitorContext implements VisitorContextOptions, VisitorResult { public readonly logger: Logger; - public readonly schemaOptions: RuleOptions; + public readonly schemaOptions: RuleSchemaOptions; protected readonly ajv: Ajv.Ajv; protected readonly changeBuffer: Array; protected readonly errorBuffer: Array; - protected data: any; + protected data?: TData; public get changes(): ReadonlyArray { return this.changeBuffer; @@ -96,15 +96,13 @@ export class VisitorContext implements VisitorContextOptions, VisitorResult { } /** - * store some flash data. this is very much not the right way to do it. - * - * @TODO: fix this + * Store some flash data about the item and rule being visited. */ - public get visitData(): any { - return this.data; + public get visitData(): TData { + return mustExist(this.data); } - public set visitData(value: any) { + public set visitData(value: TData) { this.data = value; } } diff --git a/src/visitor/index.ts b/src/visitor/index.ts index 95186c71..3742cbdd 100644 --- a/src/visitor/index.ts +++ b/src/visitor/index.ts @@ -6,25 +6,25 @@ import { VisitorContext } from './VisitorContext'; /** * This is a runtime error, not an exception. */ -export interface VisitorError { - data: any; +export interface VisitorError { + data: TData; level: LogLevel; msg: string; } -export interface VisitorResult { - changes: ReadonlyArray>; - errors: ReadonlyArray; +export interface VisitorResult { + changes: ReadonlyArray>; + errors: ReadonlyArray>; } -export interface Visitor { +export interface Visitor> { /** * Select nodes eligible to be visited. **/ - pick(ctx: VisitorContext, root: any): Promise>; + pick(ctx: VisitorContext, root: any): Promise>; /** * Visit a node. */ - visit(ctx: VisitorContext, node: any): Promise; + visit(ctx: VisitorContext, node: any): Promise; } diff --git a/test/helpers/context.ts b/test/helpers/context.ts new file mode 100644 index 00000000..b5f6c3c2 --- /dev/null +++ b/test/helpers/context.ts @@ -0,0 +1,16 @@ +import { NullLogger } from 'noicejs'; + +import { RuleVisitorData, RuleVisitorError } from '../../src/rule/RuleVisitor'; +import { VisitorContext } from '../../src/visitor/VisitorContext'; + +export function testContext() { + return new VisitorContext({ + logger: NullLogger.global, + schemaOptions: { + coerce: false, + defaults: false, + mutate: false, + }, + }); +} + diff --git a/test/rule/TestLoadRule.ts b/test/rule/TestLoadRule.ts index 669a6c4d..c913e9ff 100644 --- a/test/rule/TestLoadRule.ts +++ b/test/rule/TestLoadRule.ts @@ -1,12 +1,12 @@ import { expect } from 'chai'; import mockFS from 'mock-fs'; -import { LogLevel, NullLogger } from 'noicejs'; +import { LogLevel } from 'noicejs'; import { spy, stub } from 'sinon'; import { loadRuleFiles, loadRuleModules, loadRulePaths, loadRuleSource } from '../../src/rule'; import { SchemaRule } from '../../src/rule/SchemaRule'; -import { VisitorContext } from '../../src/visitor/VisitorContext'; import { describeLeaks, itLeaks } from '../helpers/async'; +import { testContext } from '../helpers/context'; const EXAMPLE_EMPTY = '{name: foo, definitions: {}, rules: []}'; const EXAMPLE_RULES = `{ @@ -21,31 +21,13 @@ const EXAMPLE_RULES = `{ }] }`; -function testContext() { - return new VisitorContext({ - logger: NullLogger.global, - schemaOptions: { - coerce: false, - defaults: false, - mutate: false, - }, - }); -} - describeLeaks('load rule file helper', async () => { itLeaks('should add schema', async () => { mockFS({ test: EXAMPLE_EMPTY, }); - const ctx = new VisitorContext({ - logger: NullLogger.global, - schemaOptions: { - coerce: false, - defaults: false, - mutate: false, - }, - }); + const ctx = testContext(); const schemaSpy = spy(ctx, 'addSchema'); const rules = await loadRuleFiles([ @@ -63,15 +45,7 @@ describeLeaks('load rule file helper', async () => { test: EXAMPLE_RULES, }); - const ctx = new VisitorContext({ - logger: NullLogger.global, - schemaOptions: { - coerce: false, - defaults: false, - mutate: false, - }, - }); - + const ctx = testContext(); const rules = await loadRuleFiles([ 'test', ], ctx); @@ -90,15 +64,7 @@ describeLeaks('load rule file helper', async () => { }` }); - const ctx = new VisitorContext({ - logger: NullLogger.global, - schemaOptions: { - coerce: false, - defaults: false, - mutate: false, - }, - }); - + const ctx = testContext(); const rules = await loadRuleFiles([ 'test', ], ctx); @@ -118,15 +84,7 @@ describeLeaks('load rule path helper', async () => { }, }); - const ctx = new VisitorContext({ - logger: NullLogger.global, - schemaOptions: { - coerce: false, - defaults: false, - mutate: false, - }, - }); - + const ctx = testContext(); const rules = await loadRulePaths([ 'test', ], ctx); @@ -149,15 +107,7 @@ describeLeaks('load rule path helper', async () => { }, }); - const ctx = new VisitorContext({ - logger: NullLogger.global, - schemaOptions: { - coerce: false, - defaults: false, - mutate: false, - }, - }); - + const ctx = testContext(); const rules = await loadRulePaths([ 'test', ], ctx); diff --git a/test/rule/TestRule.ts b/test/rule/TestRule.ts index 143ef5ef..96766085 100644 --- a/test/rule/TestRule.ts +++ b/test/rule/TestRule.ts @@ -1,10 +1,10 @@ import { expect } from 'chai'; -import { ConsoleLogger, LogLevel, NullLogger } from 'noicejs'; +import { LogLevel } from 'noicejs'; import { createRuleSelector, createRuleSources, resolveRules, validateRules } from '../../src/rule'; import { SchemaRule } from '../../src/rule/SchemaRule'; -import { VisitorContext } from '../../src/visitor/VisitorContext'; import { describeLeaks, itLeaks } from '../helpers/async'; +import { testContext } from '../helpers/context'; const TEST_RULES = [new SchemaRule({ check: {}, @@ -143,15 +143,7 @@ describe('create rule selector helper', () => { describeLeaks('validate rule helper', async () => { itLeaks('should accept valid modules', async () => { - const ctx = new VisitorContext({ - logger: ConsoleLogger.global, - schemaOptions: { - coerce: false, - defaults: false, - mutate: false, - }, - }); - + const ctx = testContext(); expect(validateRules(ctx, { name: 'test', rules: [], @@ -159,15 +151,7 @@ describeLeaks('validate rule helper', async () => { }); itLeaks('should reject partial modules', async () => { - const ctx = new VisitorContext({ - logger: NullLogger.global, - schemaOptions: { - coerce: false, - defaults: false, - mutate: false, - }, - }); - + const ctx = testContext(); expect(validateRules(ctx, {})).to.equal(false); expect(validateRules(ctx, { name: '', diff --git a/test/rule/TestRuleVisitor.ts b/test/rule/TestRuleVisitor.ts index 4f0c5577..489da584 100644 --- a/test/rule/TestRuleVisitor.ts +++ b/test/rule/TestRuleVisitor.ts @@ -1,22 +1,15 @@ import { expect } from 'chai'; -import { LogLevel, NullLogger } from 'noicejs'; +import { LogLevel } from 'noicejs'; import { mock, spy, stub } from 'sinon'; import { RuleVisitor } from '../../src/rule/RuleVisitor'; import { SchemaRule } from '../../src/rule/SchemaRule'; -import { VisitorContext } from '../../src/visitor/VisitorContext'; import { describeLeaks, itLeaks } from '../helpers/async'; +import { testContext } from '../helpers/context'; describeLeaks('rule visitor', async () => { itLeaks('should only call visit for selected items', async () => { - const ctx = new VisitorContext({ - logger: NullLogger.global, - schemaOptions: { - coerce: false, - defaults: false, - mutate: false, - }, - }); + const ctx = testContext(); const data = {}; const rule = new SchemaRule({ check: {}, @@ -44,14 +37,7 @@ describeLeaks('rule visitor', async () => { }); itLeaks('should call visit for each selected item', async () => { - const ctx = new VisitorContext({ - logger: NullLogger.global, - schemaOptions: { - coerce: false, - defaults: false, - mutate: false, - }, - }); + const ctx = testContext(); const data = {}; const rule = new SchemaRule({ check: {}, @@ -82,14 +68,7 @@ describeLeaks('rule visitor', async () => { }); itLeaks('should visit individual items', async () => { - const ctx = new VisitorContext({ - logger: NullLogger.global, - schemaOptions: { - coerce: false, - defaults: false, - mutate: false, - }, - }); + const ctx = testContext(); const data = { foo: [Math.random(), Math.random(), Math.random()], }; @@ -118,14 +97,7 @@ describeLeaks('rule visitor', async () => { }); itLeaks('should visit individual items', async () => { - const ctx = new VisitorContext({ - logger: NullLogger.global, - schemaOptions: { - coerce: false, - defaults: false, - mutate: false, - }, - }); + const ctx = testContext(); const data = { foo: [Math.random(), Math.random(), Math.random()], }; @@ -158,14 +130,7 @@ describeLeaks('rule visitor', async () => { }); itLeaks('should not pick items', async () => { - const ctx = new VisitorContext({ - logger: NullLogger.global, - schemaOptions: { - coerce: false, - defaults: false, - mutate: false, - }, - }); + const ctx = testContext(); const visitor = new RuleVisitor({ rules: [], }); diff --git a/test/rule/TestSchemaRule.ts b/test/rule/TestSchemaRule.ts index bbb566a3..600ebcf5 100644 --- a/test/rule/TestSchemaRule.ts +++ b/test/rule/TestSchemaRule.ts @@ -1,10 +1,10 @@ import { expect } from 'chai'; -import { LogLevel, NullLogger } from 'noicejs'; +import { LogLevel } from 'noicejs'; import { stub } from 'sinon'; import { friendlyError, SchemaRule } from '../../src/rule/SchemaRule'; -import { VisitorContext } from '../../src/visitor/VisitorContext'; import { describeLeaks, itLeaks } from '../helpers/async'; +import { testContext } from '../helpers/context'; /* eslint-disable @typescript-eslint/unbound-method */ @@ -12,14 +12,7 @@ const TEST_NAME = 'test-rule'; describeLeaks('schema rule', async () => { itLeaks('should pick items from the scope', async () => { - const ctx = new VisitorContext({ - logger: NullLogger.global, - schemaOptions: { - coerce: false, - defaults: false, - mutate: false, - }, - }); + const ctx = testContext(); const data = { foo: 3, }; @@ -37,14 +30,7 @@ describeLeaks('schema rule', async () => { }); itLeaks('should pick no items', async () => { - const ctx = new VisitorContext({ - logger: NullLogger.global, - schemaOptions: { - coerce: false, - defaults: false, - mutate: false, - }, - }); + const ctx = testContext(); const data = { bar: 3, }; @@ -62,15 +48,7 @@ describeLeaks('schema rule', async () => { }); itLeaks('should filter out items', async () => { - const ctx = new VisitorContext({ - logger: NullLogger.global, - schemaOptions: { - coerce: false, - defaults: false, - mutate: false, - }, - }); - + const ctx = testContext(); const data = { foo: 3, }; @@ -96,14 +74,7 @@ describeLeaks('schema rule', async () => { }); itLeaks('should pick items from the root', async () => { - const ctx = new VisitorContext({ - logger: NullLogger.global, - schemaOptions: { - coerce: false, - defaults: false, - mutate: false, - }, - }); + const ctx = testContext(); const rule = new SchemaRule({ check: undefined, desc: TEST_NAME, @@ -119,14 +90,7 @@ describeLeaks('schema rule', async () => { }); itLeaks('should visit selected items', async () => { - const ctx = new VisitorContext({ - logger: NullLogger.global, - schemaOptions: { - coerce: false, - defaults: false, - mutate: false, - }, - }); + const ctx = testContext(); const check = {}; const checkSpy = stub().returns(true); @@ -152,14 +116,7 @@ describeLeaks('schema rule', async () => { }); itLeaks('should skip filtered items', async () => { - const ctx = new VisitorContext({ - logger: NullLogger.global, - schemaOptions: { - coerce: false, - defaults: false, - mutate: false, - }, - }); + const ctx = testContext(); const checkSpy = stub().throws(new Error('check spy error')); const filterSpy = stub().returns(false); @@ -192,17 +149,12 @@ function createErrorContext() { select: '', tags: [TEST_NAME], }); - const ctx = new VisitorContext({ - logger: NullLogger.global, - schemaOptions: { - coerce: false, - defaults: false, - mutate: false, - }, - }); + const ctx = testContext(); ctx.visitData = { + item: {}, itemIndex: 0, rule, + ruleIndex: 0, }; return { ctx, rule }; diff --git a/test/utils/TestNil.ts b/test/utils/TestNil.ts new file mode 100644 index 00000000..9e0446f1 --- /dev/null +++ b/test/utils/TestNil.ts @@ -0,0 +1,21 @@ +import { expect } from 'chai'; + +import { NotFoundError } from '../../src/error/NotFoundError'; +import { mustExist } from '../../src/utils'; + +describe('nil helpers', () => { + describe('must exist helper', () => { + it('should return set values', () => { + const TEST_NUMBER = 3; + const TEST_STRING = '3'; + expect(mustExist(TEST_NUMBER)).to.equal(TEST_NUMBER); + expect(mustExist(TEST_STRING)).to.equal(TEST_STRING); + }); + + it('should throw on nil values', () => { + /* eslint-disable-next-line no-null/no-null */ + expect(() => mustExist(null)).to.throw(NotFoundError); + expect(() => mustExist(undefined)).to.throw(NotFoundError); + }); + }); +}); diff --git a/test/visitor/TestContext.ts b/test/visitor/TestContext.ts index a30adf34..4f1ff0c8 100644 --- a/test/visitor/TestContext.ts +++ b/test/visitor/TestContext.ts @@ -1,19 +1,11 @@ import { expect } from 'chai'; -import { LogLevel, NullLogger } from 'noicejs'; +import { LogLevel } from 'noicejs'; -import { VisitorContext } from '../../src/visitor/VisitorContext'; +import { testContext } from '../helpers/context'; describe('visitor context', () => { it('should merge results', () => { - const firstCtx = new VisitorContext({ - logger: NullLogger.global, - schemaOptions: { - coerce: false, - defaults: false, - mutate: false, - }, - }); - + const firstCtx = testContext(); const nextCtx = firstCtx.mergeResult({ changes: [{ kind: 'N',