Skip to content
This repository was archived by the owner on Apr 4, 2025. It is now read-only.

Commit eb87ca4

Browse files
filipesilvahansl
authored andcommitted
feat(@angular-devkit/core): support schema custom formats
1 parent 1ac7ede commit eb87ca4

File tree

4 files changed

+149
-10
lines changed

4 files changed

+149
-10
lines changed

packages/angular_devkit/core/README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,34 @@ export interface SchemaValidator {
2323
}
2424
```
2525

26+
### SchemaFormatter
27+
28+
```
29+
export interface SchemaFormatter {
30+
readonly async: boolean;
31+
validate(data: any): boolean | Observable<boolean>;
32+
}
33+
```
34+
2635
### SchemaRegistry
2736

2837
```
2938
export interface SchemaRegistry {
3039
compile(schema: Object): Observable<SchemaValidator>;
40+
addFormat(name: string, formatter: SchemaFormatter): void;
3141
}
3242
```
3343

3444
### CoreSchemaRegistry
3545

3646
`SchemaRegistry` implementation using https://github.com/epoberezkin/ajv.
47+
Constructor accepts object containing `SchemaFormatter` that will be added automatically.
48+
49+
```
50+
export class CoreSchemaRegistry implements SchemaRegistry {
51+
constructor(formats: { [name: string]: SchemaFormatter} = {}) {}
52+
}
53+
```
3754

3855
# Logger
3956

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,23 @@ export interface SchemaValidatorResult {
1313
errors?: string[];
1414
}
1515

16-
1716
export interface SchemaValidator {
1817
// tslint:disable-next-line:no-any
1918
(data: any): Observable<SchemaValidatorResult>;
2019
}
2120

21+
export interface SchemaFormatter {
22+
readonly async: boolean;
23+
// tslint:disable-next-line:no-any
24+
validate(data: any): boolean | Observable<boolean>;
25+
}
26+
27+
export interface SchemaFormat {
28+
name: string;
29+
formatter: SchemaFormatter;
30+
}
2231

2332
export interface SchemaRegistry {
2433
compile(schema: Object): Observable<SchemaValidator>;
34+
addFormat(format: SchemaFormat): void;
2535
}

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

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,13 @@ import 'rxjs/add/observable/of';
1212
import { fromPromise } from 'rxjs/observable/fromPromise';
1313
import { map } from 'rxjs/operators/map';
1414
import { JsonArray, JsonObject } from '../interface';
15-
import { SchemaRegistry, SchemaValidator, SchemaValidatorResult } from './interface';
15+
import {
16+
SchemaFormat,
17+
SchemaFormatter,
18+
SchemaRegistry,
19+
SchemaValidator,
20+
SchemaValidatorResult,
21+
} from './interface';
1622

1723

1824
function _parseJsonPointer(pointer: string): string[] {
@@ -101,13 +107,21 @@ export class CoreSchemaRegistry implements SchemaRegistry {
101107
private _ajv: ajv.Ajv;
102108
private _uriCache = new Map<string, JsonObject>();
103109

104-
constructor() {
110+
constructor(formats: SchemaFormat[] = []) {
105111
/**
106112
* Build an AJV instance that will be used to validate schemas.
107113
*/
114+
115+
const formatsObj: { [name: string]: SchemaFormatter } = {};
116+
117+
for (const format of formats) {
118+
formatsObj[format.name] = format.formatter;
119+
}
120+
108121
this._ajv = ajv({
109122
removeAdditional: 'all',
110123
useDefaults: true,
124+
formats: formatsObj,
111125
loadSchema: (uri: string) => this._fetch(uri) as ajv.Thenable<object>,
112126
});
113127

@@ -265,11 +279,28 @@ export class CoreSchemaRegistry implements SchemaRegistry {
265279

266280
return {
267281
success: false,
268-
errors: (validate.errors || []).map((err: ajv.ErrorObject) => err.message),
282+
errors: (validate.errors || [])
283+
.map((err: ajv.ErrorObject) => `${err.dataPath} ${err.message}`),
269284
} as SchemaValidatorResult;
270285
}),
271286
);
272287
}),
273288
);
274289
}
290+
291+
addFormat(format: SchemaFormat): void {
292+
// tslint:disable-next-line:no-any
293+
const validate = (data: any) => {
294+
const result = format.formatter.validate(data);
295+
296+
return result instanceof Observable ? result.toPromise() : result;
297+
};
298+
299+
this._ajv.addFormat(format.name, {
300+
async: format.formatter.async,
301+
validate,
302+
// AJV typings list `compare` as required, but it is optional.
303+
// tslint:disable-next-line:no-any
304+
} as any);
305+
}
275306
}

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

Lines changed: 87 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@
55
* Use of this source code is governed by an MIT-style license that can be
66
* found in the LICENSE file at https://angular.io/license
77
*/
8-
// tslint:disable:no-any
8+
import { Observable } from 'rxjs/Observable';
99
import 'rxjs/add/operator/mergeMap';
1010
import { CoreSchemaRegistry } from './registry';
1111

1212

1313
describe('CoreSchemaRegistry', () => {
1414
it('works asynchronously', done => {
15-
const registry = new CoreSchemaRegistry();
16-
const data: any = {}; // tslint:disable:no-any
15+
const registry = new CoreSchemaRegistry();
16+
const data: any = {}; // tslint:disable-line:no-any
1717

1818
registry
1919
.compile({
@@ -38,14 +38,14 @@ describe('CoreSchemaRegistry', () => {
3838
expect(data.tslint).not.toBeUndefined();
3939
})
4040
.subscribe(done, done.fail);
41-
});
41+
});
4242

4343
// Synchronous failure is only used internally.
4444
// If it's meant to be used externally then this test should change to truly be synchronous
4545
// (i.e. not relyign on the observable).
4646
it('works synchronously', done => {
47-
const registry = new CoreSchemaRegistry();
48-
const data: any = {}; // tslint:disable:no-any
47+
const registry = new CoreSchemaRegistry();
48+
const data: any = {}; // tslint:disable-line:no-any
4949
let isDone = false;
5050

5151
registry
@@ -73,4 +73,85 @@ describe('CoreSchemaRegistry', () => {
7373
expect(isDone).toBe(true);
7474
done();
7575
});
76+
77+
it('supports sync format', done => {
78+
const registry = new CoreSchemaRegistry();
79+
const data = { str: 'hotdog' };
80+
const format = {
81+
name: 'is-hotdog',
82+
formatter: {
83+
async: false,
84+
validate: (str: string) => str === 'hotdog',
85+
},
86+
};
87+
88+
registry.addFormat(format);
89+
90+
registry
91+
.compile({
92+
properties: {
93+
str: { type: 'string', format: 'is-hotdog' },
94+
},
95+
})
96+
.mergeMap(validator => validator(data))
97+
.map(result => {
98+
expect(result.success).toBe(true);
99+
})
100+
.subscribe(done, done.fail);
101+
});
102+
103+
it('supports async format', done => {
104+
const registry = new CoreSchemaRegistry();
105+
const data = { str: 'hotdog' };
106+
const format = {
107+
name: 'is-hotdog',
108+
formatter: {
109+
async: true,
110+
validate: (str: string) => Observable.of(str === 'hotdog'),
111+
},
112+
};
113+
114+
registry.addFormat(format);
115+
116+
registry
117+
.compile({
118+
$async: true,
119+
properties: {
120+
str: { type: 'string', format: 'is-hotdog' },
121+
},
122+
})
123+
.mergeMap(validator => validator(data))
124+
.map(result => {
125+
expect(result.success).toBe(true);
126+
})
127+
.subscribe(done, done.fail);
128+
});
129+
130+
it('shows dataPath and message on error', done => {
131+
const registry = new CoreSchemaRegistry();
132+
const data = { hotdot: 'hotdog', banana: 'banana' };
133+
const format = {
134+
name: 'is-hotdog',
135+
formatter: {
136+
async: false,
137+
validate: (str: string) => str === 'hotdog',
138+
},
139+
};
140+
141+
registry.addFormat(format);
142+
143+
registry
144+
.compile({
145+
properties: {
146+
hotdot: { type: 'string', format: 'is-hotdog' },
147+
banana: { type: 'string', format: 'is-hotdog' },
148+
},
149+
})
150+
.mergeMap(validator => validator(data))
151+
.map(result => {
152+
expect(result.success).toBe(false);
153+
expect(result.errors && result.errors[0]).toBe('.banana should match format "is-hotdog"');
154+
})
155+
.subscribe(done, done.fail);
156+
});
76157
});

0 commit comments

Comments
 (0)