From 487349a8686112f5d4986ffcab1e02a326fe5194 Mon Sep 17 00:00:00 2001 From: Robert Fidler Date: Mon, 9 Sep 2024 23:06:38 -0400 Subject: [PATCH 1/6] add custom formats --- common/reviews/api/node-core-library.api.md | 7 ++++ libraries/node-core-library/src/JsonSchema.ts | 32 +++++++++++++++++++ libraries/node-core-library/src/index.ts | 1 + .../src/test/JsonSchema.test.ts | 28 ++++++++++++++++ 4 files changed, 68 insertions(+) diff --git a/common/reviews/api/node-core-library.api.md b/common/reviews/api/node-core-library.api.md index 4c7786d3668..d759c3dcd74 100644 --- a/common/reviews/api/node-core-library.api.md +++ b/common/reviews/api/node-core-library.api.md @@ -423,6 +423,12 @@ export interface IJsonFileStringifyOptions extends IJsonFileParseOptions { prettyFormatting?: boolean; } +// @public +export interface IJsonSchemaCustomFormat { + type: T extends string ? "string" : T extends number ? "number" : never; + validate: (data: T) => boolean; +} + // @public export interface IJsonSchemaErrorInfo { details: string; @@ -436,6 +442,7 @@ export type IJsonSchemaFromObjectOptions = IJsonSchemaLoadOptions; // @public export interface IJsonSchemaLoadOptions { + customFormats?: Record | IJsonSchemaCustomFormat>; dependentSchemas?: JsonSchema[]; schemaVersion?: JsonSchemaVersion; } diff --git a/libraries/node-core-library/src/JsonSchema.ts b/libraries/node-core-library/src/JsonSchema.ts index fd68ad65302..5d9d614b702 100644 --- a/libraries/node-core-library/src/JsonSchema.ts +++ b/libraries/node-core-library/src/JsonSchema.ts @@ -25,6 +25,23 @@ interface ISchemaWithId { */ export type JsonSchemaVersion = 'draft-04' | 'draft-07'; +/** + * A definition for a custom format to consider during validation. + * @public + */ +export interface IJsonSchemaCustomFormat { + /** + * The base JSON type. + */ + type: T extends string ? 'string' : T extends number ? 'number' : never; + /** + * A validation function for the format. + * @param data - The raw field data to validate. + * @returns whether the data is valid according to the format. + */ + validate: (data: T) => boolean; +} + /** * Callback function arguments for {@link JsonSchema.validateObjectWithCallback} * @public @@ -94,6 +111,11 @@ export interface IJsonSchemaLoadOptions { * or does not match an expected URL, the default version will be used. */ schemaVersion?: JsonSchemaVersion; + + /** + * Any custom formats to consider during validation. + */ + customFormats?: Record | IJsonSchemaCustomFormat>; } /** @@ -141,6 +163,9 @@ export class JsonSchema { private _validator: ValidateFunction | undefined = undefined; private _schemaObject: JsonObject | undefined = undefined; private _schemaVersion: JsonSchemaVersion | undefined = undefined; + private _customFormats: + | Record | IJsonSchemaCustomFormat> + | undefined = undefined; private constructor() {} @@ -163,6 +188,7 @@ export class JsonSchema { if (options) { schema._dependentSchemas = options.dependentSchemas || []; schema._schemaVersion = options.schemaVersion; + schema._customFormats = options.customFormats; } return schema; @@ -181,6 +207,7 @@ export class JsonSchema { if (options) { schema._dependentSchemas = options.dependentSchemas || []; schema._schemaVersion = options.schemaVersion; + schema._customFormats = options.customFormats; } return schema; @@ -308,6 +335,11 @@ export class JsonSchema { // Enable json-schema format validation // https://ajv.js.org/packages/ajv-formats.html addFormats(validator); + if (this._customFormats) { + Object.entries(this._customFormats).forEach(([name, format]) => { + validator.addFormat(name, { ...format, async: false }); + }); + } const collectedSchemas: JsonSchema[] = []; const seenObjects: Set = new Set(); diff --git a/libraries/node-core-library/src/index.ts b/libraries/node-core-library/src/index.ts index 70396912fbc..54fb6509b7a 100644 --- a/libraries/node-core-library/src/index.ts +++ b/libraries/node-core-library/src/index.ts @@ -65,6 +65,7 @@ export { } from './JsonFile'; export { type IJsonSchemaErrorInfo, + type IJsonSchemaCustomFormat, type IJsonSchemaFromFileOptions, type IJsonSchemaFromObjectOptions, type IJsonSchemaLoadOptions, diff --git a/libraries/node-core-library/src/test/JsonSchema.test.ts b/libraries/node-core-library/src/test/JsonSchema.test.ts index b4a82de4576..e2d537056ae 100644 --- a/libraries/node-core-library/src/test/JsonSchema.test.ts +++ b/libraries/node-core-library/src/test/JsonSchema.test.ts @@ -119,4 +119,32 @@ describe(JsonSchema.name, () => { expect(errorDetails).toMatchSnapshot(); }); }); + + test('successfully applies custom formats', () => { + const schemaWithCustomFormat = JsonSchema.fromLoadedObject( + { + title: 'Test Custom Format', + type: 'object', + properties: { + exampleNumber: { + type: 'number', + format: 'uint8' + } + }, + additionalProperties: false, + required: ['exampleNumber'] + }, + { + schemaVersion: 'draft-07', + customFormats: { + uint8: { + type: 'number', + validate: (data) => data >= 0 && data <= 255 + } + } + } + ); + expect(() => schemaWithCustomFormat.validateObject({ exampleNumber: 10 }, '')).not.toThrow(); + expect(() => schemaWithCustomFormat.validateObject({ exampleNumber: 1000 }, '')).toThrow(); + }); }); From 967bb1d5f2bd4cbe66d4937ca670d50543ca5e03 Mon Sep 17 00:00:00 2001 From: Robert Fidler Date: Mon, 9 Sep 2024 23:16:02 -0400 Subject: [PATCH 2/6] changes --- ...jsonschema-add-custom-formats_2024-09-10-03-15.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 common/changes/@rushstack/node-core-library/rf-jsonschema-add-custom-formats_2024-09-10-03-15.json diff --git a/common/changes/@rushstack/node-core-library/rf-jsonschema-add-custom-formats_2024-09-10-03-15.json b/common/changes/@rushstack/node-core-library/rf-jsonschema-add-custom-formats_2024-09-10-03-15.json new file mode 100644 index 00000000000..339b3b8649d --- /dev/null +++ b/common/changes/@rushstack/node-core-library/rf-jsonschema-add-custom-formats_2024-09-10-03-15.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/node-core-library", + "comment": "Add a `customFormats` option to `JsonSchema`.", + "type": "minor" + } + ], + "packageName": "@rushstack/node-core-library" +} \ No newline at end of file From d583fa0cb530f5d10b2ba29ab4184f25a464782e Mon Sep 17 00:00:00 2001 From: Robert Fidler Date: Mon, 9 Sep 2024 23:23:47 -0400 Subject: [PATCH 3/6] formatting --- common/reviews/api/node-core-library.api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/reviews/api/node-core-library.api.md b/common/reviews/api/node-core-library.api.md index d759c3dcd74..842a74830bb 100644 --- a/common/reviews/api/node-core-library.api.md +++ b/common/reviews/api/node-core-library.api.md @@ -425,7 +425,7 @@ export interface IJsonFileStringifyOptions extends IJsonFileParseOptions { // @public export interface IJsonSchemaCustomFormat { - type: T extends string ? "string" : T extends number ? "number" : never; + type: T extends string ? 'string' : T extends number ? 'number' : never; validate: (data: T) => boolean; } From ab8562053a4a6d4137481ebad0866490233decb8 Mon Sep 17 00:00:00 2001 From: Robert Fidler Date: Tue, 10 Sep 2024 13:32:01 -0400 Subject: [PATCH 4/6] Update libraries/node-core-library/src/JsonSchema.ts Co-authored-by: Daniel <3473356+D4N14L@users.noreply.github.com> --- libraries/node-core-library/src/JsonSchema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/node-core-library/src/JsonSchema.ts b/libraries/node-core-library/src/JsonSchema.ts index 5d9d614b702..e5ef699a634 100644 --- a/libraries/node-core-library/src/JsonSchema.ts +++ b/libraries/node-core-library/src/JsonSchema.ts @@ -336,7 +336,7 @@ export class JsonSchema { // https://ajv.js.org/packages/ajv-formats.html addFormats(validator); if (this._customFormats) { - Object.entries(this._customFormats).forEach(([name, format]) => { + for (const [name, format] of Object.entries(this._customFormats)) { validator.addFormat(name, { ...format, async: false }); }); } From 74abd0409e42cb0722466bcebd2ae68e1aec9454 Mon Sep 17 00:00:00 2001 From: Robert Fidler Date: Tue, 10 Sep 2024 13:32:12 -0400 Subject: [PATCH 5/6] Update libraries/node-core-library/src/JsonSchema.ts Co-authored-by: Daniel <3473356+D4N14L@users.noreply.github.com> --- libraries/node-core-library/src/JsonSchema.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/node-core-library/src/JsonSchema.ts b/libraries/node-core-library/src/JsonSchema.ts index e5ef699a634..fb5024cacfe 100644 --- a/libraries/node-core-library/src/JsonSchema.ts +++ b/libraries/node-core-library/src/JsonSchema.ts @@ -34,6 +34,7 @@ export interface IJsonSchemaCustomFormat { * The base JSON type. */ type: T extends string ? 'string' : T extends number ? 'number' : never; + /** * A validation function for the format. * @param data - The raw field data to validate. From 40eb0bd0fff6eaf7201340857fffefe4b8cab0e2 Mon Sep 17 00:00:00 2001 From: Robert Fidler Date: Tue, 10 Sep 2024 13:55:43 -0400 Subject: [PATCH 6/6] docstring --- libraries/node-core-library/src/JsonSchema.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/libraries/node-core-library/src/JsonSchema.ts b/libraries/node-core-library/src/JsonSchema.ts index fb5024cacfe..dcbe8e81052 100644 --- a/libraries/node-core-library/src/JsonSchema.ts +++ b/libraries/node-core-library/src/JsonSchema.ts @@ -114,7 +114,9 @@ export interface IJsonSchemaLoadOptions { schemaVersion?: JsonSchemaVersion; /** - * Any custom formats to consider during validation. + * Any custom formats to consider during validation. Some standard formats are supported + * out-of-the-box (e.g. emails, uris), but additional formats can be defined here. You could + * for example define generic numeric formats (e.g. uint8) or domain-specific formats. */ customFormats?: Record | IJsonSchemaCustomFormat>; } @@ -339,7 +341,7 @@ export class JsonSchema { if (this._customFormats) { for (const [name, format] of Object.entries(this._customFormats)) { validator.addFormat(name, { ...format, async: false }); - }); + } } const collectedSchemas: JsonSchema[] = [];