feat(rules): add item index to rule error (fixes #116)
This commit is contained in:
parent
de5dd2833a
commit
f0b5109689
|
@ -56,7 +56,7 @@ export class SchemaRule implements Rule, RuleData, Visitor {
|
||||||
if (filter(node)) {
|
if (filter(node)) {
|
||||||
ctx.logger.debug({ item: node }, 'checking item');
|
ctx.logger.debug({ item: node }, 'checking item');
|
||||||
if (!check(node) && hasItems(check.errors)) {
|
if (!check(node) && hasItems(check.errors)) {
|
||||||
errors.push(...check.errors.map((err) => friendlyError(ctx, err, this)));
|
errors.push(...check.errors.map((err) => friendlyError(ctx, err)));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ctx.logger.debug({ errors: filter.errors, item: node }, 'skipping item');
|
ctx.logger.debug({ errors: filter.errors, item: node }, 'skipping item');
|
||||||
|
@ -74,21 +74,26 @@ export class SchemaRule implements Rule, RuleData, Visitor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function friendlyError(ctx: VisitorContext, err: ErrorObject, rule: SchemaRule): VisitorError {
|
export function friendlyError(ctx: VisitorContext, err: ErrorObject): VisitorError {
|
||||||
return {
|
return {
|
||||||
data: {
|
data: {
|
||||||
err,
|
err,
|
||||||
rule,
|
|
||||||
},
|
},
|
||||||
level: 'error',
|
level: 'error',
|
||||||
msg: friendlyErrorMessage(err, rule),
|
msg: friendlyErrorMessage(ctx, err),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function friendlyErrorMessage(err: ErrorObject, rule: SchemaRule): string {
|
export function friendlyErrorMessage(ctx: VisitorContext, err: ErrorObject): string {
|
||||||
|
const msg = [err.dataPath];
|
||||||
if (isNil(err.message)) {
|
if (isNil(err.message)) {
|
||||||
return `${err.dataPath} ${err.keyword} at ${rule.select} for ${rule.name}`;
|
msg.push(err.keyword);
|
||||||
} else {
|
} else {
|
||||||
return `${err.dataPath} ${err.message} at ${rule.select} for ${rule.name}`;
|
msg.push(err.message);
|
||||||
}
|
}
|
||||||
|
msg.push('at', 'item', ctx.visitData.itemIndex.toString());
|
||||||
|
msg.push('of', ctx.visitData.rule.select);
|
||||||
|
msg.push('for', ctx.visitData.rule.name);
|
||||||
|
|
||||||
|
return msg.join(' ');
|
||||||
}
|
}
|
||||||
|
|
|
@ -202,13 +202,18 @@ export async function resolveRules(rules: Array<Rule>, selector: RuleSelector):
|
||||||
export async function visitRules(ctx: VisitorContext, rules: Array<Rule>, data: any): Promise<VisitorContext> {
|
export async function visitRules(ctx: VisitorContext, rules: Array<Rule>, data: any): Promise<VisitorContext> {
|
||||||
for (const rule of rules) {
|
for (const rule of rules) {
|
||||||
const items = await rule.pick(ctx, data);
|
const items = await rule.pick(ctx, data);
|
||||||
|
let itemIndex = 0;
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
|
ctx.visitData = {
|
||||||
|
itemIndex,
|
||||||
|
rule,
|
||||||
|
};
|
||||||
const itemResult = cloneDeep(item);
|
const itemResult = cloneDeep(item);
|
||||||
const ruleResult = await rule.visit(ctx, itemResult);
|
const ruleResult = await rule.visit(ctx, itemResult);
|
||||||
|
|
||||||
if (hasItems(ruleResult.errors)) {
|
if (hasItems(ruleResult.errors)) {
|
||||||
ctx.logger.warn({ count: ruleResult.errors.length, rule }, 'rule failed');
|
ctx.logger.warn({ count: ruleResult.errors.length, rule }, 'rule failed');
|
||||||
ctx.mergeResult(ruleResult);
|
ctx.mergeResult(ruleResult, ctx.visitData);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -226,6 +231,8 @@ export async function visitRules(ctx: VisitorContext, rules: Array<Rule>, data:
|
||||||
} else {
|
} else {
|
||||||
ctx.logger.info({ rule: rule.name }, 'rule passed');
|
ctx.logger.info({ rule: rule.name }, 'rule passed');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
itemIndex += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ export class VisitorContext implements VisitorContextOptions, VisitorResult {
|
||||||
protected readonly ajv: Ajv.Ajv;
|
protected readonly ajv: Ajv.Ajv;
|
||||||
protected readonly changeBuffer: Array<any>;
|
protected readonly changeBuffer: Array<any>;
|
||||||
protected readonly errorBuffer: Array<VisitorError>;
|
protected readonly errorBuffer: Array<VisitorError>;
|
||||||
|
protected data: any;
|
||||||
|
|
||||||
public get changes(): ReadonlyArray<any> {
|
public get changes(): ReadonlyArray<any> {
|
||||||
return this.changeBuffer;
|
return this.changeBuffer;
|
||||||
|
@ -64,9 +65,17 @@ export class VisitorContext implements VisitorContextOptions, VisitorResult {
|
||||||
return this.ajv.compile(schema);
|
return this.ajv.compile(schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
public mergeResult(other: VisitorResult): this {
|
public mergeResult(other: VisitorResult, data: any = {}): this {
|
||||||
this.changeBuffer.push(...other.changes);
|
this.changeBuffer.push(...other.changes);
|
||||||
this.errorBuffer.push(...other.errors);
|
this.errorBuffer.push(...other.errors.map((err) => {
|
||||||
|
return {
|
||||||
|
...err,
|
||||||
|
data: {
|
||||||
|
...err.data,
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,4 +96,17 @@ export class VisitorContext implements VisitorContextOptions, VisitorResult {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* store some flash data. this is very much not the right way to do it.
|
||||||
|
*
|
||||||
|
* @TODO: fix this
|
||||||
|
*/
|
||||||
|
public get visitData(): any {
|
||||||
|
return this.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public set visitData(value: any) {
|
||||||
|
this.data = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { Diff } from 'deep-diff';
|
||||||
import { LogLevel } from 'noicejs';
|
import { LogLevel } from 'noicejs';
|
||||||
|
|
||||||
import { VisitorContext } from './VisitorContext';
|
import { VisitorContext } from './VisitorContext';
|
||||||
|
@ -12,8 +13,8 @@ export interface VisitorError {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VisitorResult {
|
export interface VisitorResult {
|
||||||
changes: ReadonlyArray<any>;
|
changes: ReadonlyArray<Diff<any, any>>;
|
||||||
errors: ReadonlyArray<any>;
|
errors: ReadonlyArray<VisitorError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Visitor<TResult extends VisitorResult = VisitorResult> {
|
export interface Visitor<TResult extends VisitorResult = VisitorResult> {
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
# test rules kubernetes
|
||||||
|
# test tags kubernetes
|
||||||
|
# test exit-status 1
|
||||||
|
|
||||||
|
metadata:
|
||||||
|
name: example
|
||||||
|
labels: {}
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: test
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpu: 4000m
|
||||||
|
memory: 5Gi
|
||||||
|
requests:
|
||||||
|
cpu: 4000m
|
||||||
|
memory: 5Gi
|
||||||
|
|
||||||
|
- name: other
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpu: 2000m
|
||||||
|
requests:
|
||||||
|
cpu: 2000m
|
||||||
|
|
|
@ -227,6 +227,7 @@ describeLeaks('rule visitor', async () => {
|
||||||
changes: [],
|
changes: [],
|
||||||
errors: [{
|
errors: [{
|
||||||
data: {},
|
data: {},
|
||||||
|
level: 'error',
|
||||||
msg: 'kaboom!',
|
msg: 'kaboom!',
|
||||||
}],
|
}],
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -160,26 +160,64 @@ describeLeaks('schema rule', async () => {
|
||||||
|
|
||||||
describe('friendly errors', () => {
|
describe('friendly errors', () => {
|
||||||
it('should have a message', () => {
|
it('should have a message', () => {
|
||||||
const err = friendlyError(new VisitorContext({
|
const rule = new SchemaRule({
|
||||||
innerOptions: {
|
|
||||||
coerce: false,
|
|
||||||
defaults: false,
|
|
||||||
mutate: false,
|
|
||||||
},
|
|
||||||
logger: NullLogger.global,
|
|
||||||
}), {
|
|
||||||
dataPath: 'test-path',
|
|
||||||
keyword: TEST_NAME,
|
|
||||||
params: { /* ? */ },
|
|
||||||
schemaPath: 'test-path',
|
|
||||||
}, new SchemaRule({
|
|
||||||
check: {},
|
check: {},
|
||||||
desc: TEST_NAME,
|
desc: TEST_NAME,
|
||||||
level: 'info',
|
level: 'info',
|
||||||
name: TEST_NAME,
|
name: TEST_NAME,
|
||||||
select: '',
|
select: '',
|
||||||
tags: [TEST_NAME],
|
tags: [TEST_NAME],
|
||||||
}));
|
});
|
||||||
expect(err.msg).to.not.equal('');
|
const ctx = new VisitorContext({
|
||||||
|
innerOptions: {
|
||||||
|
coerce: false,
|
||||||
|
defaults: false,
|
||||||
|
mutate: false,
|
||||||
|
},
|
||||||
|
logger: NullLogger.global,
|
||||||
|
});
|
||||||
|
ctx.visitData = {
|
||||||
|
itemIndex: 0,
|
||||||
|
rule,
|
||||||
|
};
|
||||||
|
const err = friendlyError(ctx, {
|
||||||
|
dataPath: 'test-path',
|
||||||
|
keyword: TEST_NAME,
|
||||||
|
params: { /* ? */ },
|
||||||
|
schemaPath: 'test-path',
|
||||||
|
});
|
||||||
|
expect(err.msg).to.include(TEST_NAME);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle errors with an existing message', () => {
|
||||||
|
const TEST_MESSAGE = 'test-message';
|
||||||
|
const rule = new SchemaRule({
|
||||||
|
check: {},
|
||||||
|
desc: TEST_NAME,
|
||||||
|
level: 'info',
|
||||||
|
name: TEST_NAME,
|
||||||
|
select: '',
|
||||||
|
tags: [TEST_NAME],
|
||||||
|
});
|
||||||
|
const ctx = new VisitorContext({
|
||||||
|
innerOptions: {
|
||||||
|
coerce: false,
|
||||||
|
defaults: false,
|
||||||
|
mutate: false,
|
||||||
|
},
|
||||||
|
logger: NullLogger.global,
|
||||||
|
});
|
||||||
|
ctx.visitData = {
|
||||||
|
itemIndex: 0,
|
||||||
|
rule,
|
||||||
|
};
|
||||||
|
const err = friendlyError(ctx, {
|
||||||
|
dataPath: 'test-path',
|
||||||
|
keyword: TEST_NAME,
|
||||||
|
message: TEST_MESSAGE,
|
||||||
|
params: { /* ? */ },
|
||||||
|
schemaPath: 'test-path',
|
||||||
|
});
|
||||||
|
expect(err.msg).to.include(TEST_MESSAGE);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,8 +15,17 @@ describe('visitor context', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const nextCtx = firstCtx.mergeResult({
|
const nextCtx = firstCtx.mergeResult({
|
||||||
changes: [{bar: 3}],
|
changes: [{
|
||||||
errors: [{foo: 2}],
|
kind: 'N',
|
||||||
|
rhs: {},
|
||||||
|
}],
|
||||||
|
errors: [{
|
||||||
|
data: {
|
||||||
|
foo: 2,
|
||||||
|
},
|
||||||
|
level: 'info',
|
||||||
|
msg: 'uh oh',
|
||||||
|
}],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(nextCtx).to.equal(firstCtx);
|
expect(nextCtx).to.equal(firstCtx);
|
||||||
|
|
Loading…
Reference in New Issue