Skip to content

Commit 546f2a5

Browse files
clydinhansl
authored andcommitted
refactor: expose additional schema validation error data
1 parent 6a2ab67 commit 546f2a5

File tree

7 files changed

+93
-28
lines changed

7 files changed

+93
-28
lines changed

packages/angular_devkit/architect/src/architect_spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import { experimental, normalize } from '@angular-devkit/core';
9+
import { experimental, normalize, schema } from '@angular-devkit/core';
1010
import { NodeJsSyncHost } from '@angular-devkit/core/node';
1111
import { concatMap, tap, toArray } from 'rxjs/operators';
1212
import { BrowserTargetOptions } from '../test/browser';
@@ -115,7 +115,7 @@ describe('Architect', () => {
115115
const targetSpec = { project: 'app', target: 'badBrowser' };
116116
const builderConfig = architect.getBuilderConfiguration<BrowserTargetOptions>(targetSpec);
117117
architect.run(builderConfig).subscribe(undefined, (err: Error) => {
118-
expect(err).toEqual(jasmine.any(experimental.workspace.SchemaValidationException));
118+
expect(err).toEqual(jasmine.any(schema.SchemaValidationException));
119119
done();
120120
}, done.fail);
121121
});

packages/angular_devkit/core/src/json/schema/interface.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,49 @@ export type JsonPointer = string & {
1212
__PRIVATE_DEVKIT_JSON_POINTER: void;
1313
};
1414

15+
export type SchemaValidatorError =
16+
RefValidatorError |
17+
LimitValidatorError |
18+
AdditionalPropertiesValidatorError |
19+
FormatValidatorError |
20+
RequiredValidatorError;
21+
22+
export interface SchemaValidatorErrorBase {
23+
keyword: string;
24+
dataPath: string;
25+
message?: string;
26+
data?: JsonValue;
27+
}
28+
29+
export interface RefValidatorError extends SchemaValidatorErrorBase {
30+
keyword: '$ref';
31+
params: { ref: string };
32+
}
33+
34+
export interface LimitValidatorError extends SchemaValidatorErrorBase {
35+
keyword: 'maxItems' | 'minItems' | 'maxLength' | 'minLength' | 'maxProperties' | 'minProperties';
36+
params: { limit: number };
37+
}
38+
39+
export interface AdditionalPropertiesValidatorError extends SchemaValidatorErrorBase {
40+
keyword: 'additionalProperties';
41+
params: { additionalProperty: string };
42+
}
43+
44+
export interface FormatValidatorError extends SchemaValidatorErrorBase {
45+
keyword: 'format';
46+
params: { format: string };
47+
}
48+
49+
export interface RequiredValidatorError extends SchemaValidatorErrorBase {
50+
keyword: 'required';
51+
params: { missingProperty: string };
52+
}
53+
1554
export interface SchemaValidatorResult {
1655
data: JsonValue;
1756
success: boolean;
18-
errors?: string[];
57+
errors?: SchemaValidatorError[];
1958
}
2059

2160
export interface SchemaValidator {

packages/angular_devkit/core/src/json/schema/registry.ts

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@ import * as ajv from 'ajv';
99
import * as http from 'http';
1010
import { Observable, from, of as observableOf } from 'rxjs';
1111
import { concatMap, map, switchMap, tap } from 'rxjs/operators';
12+
import { BaseException } from '../../exception/exception';
1213
import { PartiallyOrderedSet, isObservable } from '../../utils';
1314
import { JsonObject, JsonValue } from '../interface';
1415
import {
1516
SchemaFormat,
1617
SchemaFormatter,
1718
SchemaRegistry,
1819
SchemaValidator,
20+
SchemaValidatorError,
1921
SchemaValidatorResult,
2022
SmartDefaultProvider,
2123
} from './interface';
@@ -31,6 +33,39 @@ interface AjvValidationError {
3133
validation: true;
3234
}
3335

36+
export class SchemaValidationException extends BaseException {
37+
public readonly errors: SchemaValidatorError[];
38+
39+
constructor(errors?: SchemaValidatorError[]) {
40+
if (!errors || errors.length === 0) {
41+
super('Schema validation failed.');
42+
43+
return;
44+
}
45+
46+
const messages = SchemaValidationException.createMessages(errors);
47+
super(`Schema validation failed with the following errors:\n ${messages.join('\n ')}`);
48+
this.errors = errors;
49+
}
50+
51+
public static createMessages(errors?: SchemaValidatorError[]): string[] {
52+
if (!errors || errors.length === 0) {
53+
return [];
54+
}
55+
56+
const messages = errors.map((err) => {
57+
let message = `Data path ${JSON.stringify(err.dataPath)} ${err.message}`;
58+
if (err.keyword === 'additionalProperties') {
59+
message += `(${err.params.additionalProperty})`;
60+
}
61+
62+
return message + '.';
63+
});
64+
65+
return messages;
66+
}
67+
}
68+
3469
export class CoreSchemaRegistry implements SchemaRegistry {
3570
private _ajv: ajv.Ajv;
3671
private _uriCache = new Map<string, JsonObject>();
@@ -214,13 +249,7 @@ export class CoreSchemaRegistry implements SchemaRegistry {
214249
return {
215250
data,
216251
success: false,
217-
errors: (validate.errors || [])
218-
.map((err) => `Data path ${JSON.stringify(err.dataPath)} ${err.message}${
219-
err.keyword === 'additionalProperties' && err.params
220-
// tslint:disable-next-line:no-any
221-
? ` (${(err.params as any)['additionalProperty']}).`
222-
: '.'
223-
}`),
252+
errors: (validate.errors || []),
224253
} as SchemaValidatorResult;
225254
}),
226255
);

packages/angular_devkit/core/src/json/schema/registry_spec.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,8 @@ describe('CoreSchemaRegistry', () => {
121121
mergeMap(validator => validator(data)),
122122
map(result => {
123123
expect(result.success).toBe(false);
124-
expect(result.errors).toContain(
125-
'Data path "" should NOT have additional properties (notNum).');
124+
expect(result.errors && result.errors[0].message).toContain(
125+
'should NOT have additional properties');
126126
}),
127127
)
128128
.subscribe(done, done.fail);
@@ -143,8 +143,9 @@ describe('CoreSchemaRegistry', () => {
143143
mergeMap(validator => validator(data)),
144144
map(result => {
145145
expect(result.success).toBe(false);
146-
expect(result.errors).toContain(
147-
'Data path "" should NOT have additional properties (notNum).');
146+
expect(result.errors && result.errors[0].message).toContain(
147+
'should NOT have additional properties');
148+
expect(result.errors && result.errors[0].keyword).toBe('additionalProperties');
148149
}),
149150
)
150151
.subscribe(done, done.fail);
@@ -268,8 +269,10 @@ describe('CoreSchemaRegistry', () => {
268269
mergeMap(validator => validator(data)),
269270
map(result => {
270271
expect(result.success).toBe(false);
271-
expect(result.errors && result.errors[0]).toBe(
272-
'Data path ".banana" should match format "is-hotdog".');
272+
expect(result.errors && result.errors[0]).toBeTruthy();
273+
expect(result.errors && result.errors[0].keyword).toBe('format');
274+
expect(result.errors && result.errors[0].dataPath).toBe('.banana');
275+
expect(result.errors && (result.errors[0].params as any).format).toBe('is-hotdog');
273276
}),
274277
)
275278
.subscribe(done, done.fail);

packages/angular_devkit/core/src/workspace/workspace.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,6 @@ export class ProjectToolNotFoundException extends BaseException {
4242
}
4343
}
4444

45-
export class SchemaValidationException extends BaseException {
46-
constructor(errors: string[]) {
47-
super(`Schema validation failed with the following errors:\n ${errors.join('\n ')}`);
48-
}
49-
}
50-
5145
export class WorkspaceNotYetLoadedException extends BaseException {
5246
constructor() { super(`Workspace needs to be loaded before it is used.`); }
5347
}
@@ -222,7 +216,7 @@ export class Workspace {
222216
if (validatorResult.success) {
223217
return of(contentJsonCopy as T);
224218
} else {
225-
return throwError(new SchemaValidationException(validatorResult.errors as string[]));
219+
return throwError(new schema.SchemaValidationException(validatorResult.errors));
226220
}
227221
}),
228222
);

packages/angular_devkit/core/src/workspace/workspace_spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@
77
*/
88

99
import { tap } from 'rxjs/operators';
10+
import { schema } from '..';
1011
import { NodeJsSyncHost } from '../../node';
1112
import { join, normalize } from '../virtual-fs';
1213
import {
1314
ProjectNotFoundException,
14-
SchemaValidationException,
1515
Workspace,
1616
WorkspaceNotYetLoadedException,
1717
WorkspaceProject,
@@ -118,7 +118,7 @@ describe('Workspace', () => {
118118
const workspace = new Workspace(root, host);
119119
workspace.loadWorkspaceFromJson({ foo: 'bar' })
120120
.subscribe(undefined, (err) => {
121-
expect(err).toEqual(jasmine.any(SchemaValidationException));
121+
expect(err).toEqual(jasmine.any(schema.SchemaValidationException));
122122
done();
123123
}, done.fail);
124124
});

packages/angular_devkit/schematics/tools/schema-option-transform.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ export type SchematicDesc =
2020

2121
export class InvalidInputOptions extends BaseException {
2222
// tslint:disable-next-line:no-any
23-
constructor(options: any, errors: string[]) {
23+
constructor(options: any, public readonly errors: schema.SchemaValidatorError[]) {
2424
super(`Schematic input does not validate against the Schema: ${JSON.stringify(options)}\n`
25-
+ `Errors:\n ${errors.join('\n ')}`);
25+
+ `Errors:\n ${schema.SchemaValidationException.createMessages(errors).join('\n ')}`);
2626
}
2727
}
2828

@@ -59,7 +59,7 @@ export function validateOptionsWithSchema(registry: schema.SchemaRegistry) {
5959
first(),
6060
map(result => {
6161
if (!result.success) {
62-
throw new InvalidInputOptions(options, result.errors || ['Unknown reason.']);
62+
throw new InvalidInputOptions(options, result.errors || []);
6363
}
6464

6565
return options;

0 commit comments

Comments
 (0)