Skip to content

Commit

Permalink
Add custom formats to JsonSchema (#4916)
Browse files Browse the repository at this point in the history
* add custom formats

* changes

* formatting

* Update libraries/node-core-library/src/JsonSchema.ts

Co-authored-by: Daniel <[email protected]>

* Update libraries/node-core-library/src/JsonSchema.ts

Co-authored-by: Daniel <[email protected]>

* docstring

---------

Co-authored-by: Daniel <[email protected]>
  • Loading branch information
robertf224 and D4N14L authored Sep 10, 2024
1 parent 6a5ac8a commit 39425b9
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@rushstack/node-core-library",
"comment": "Add a `customFormats` option to `JsonSchema`.",
"type": "minor"
}
],
"packageName": "@rushstack/node-core-library"
}
7 changes: 7 additions & 0 deletions common/reviews/api/node-core-library.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,12 @@ export interface IJsonFileStringifyOptions extends IJsonFileParseOptions {
prettyFormatting?: boolean;
}

// @public
export interface IJsonSchemaCustomFormat<T extends string | number> {
type: T extends string ? 'string' : T extends number ? 'number' : never;
validate: (data: T) => boolean;
}

// @public
export interface IJsonSchemaErrorInfo {
details: string;
Expand All @@ -436,6 +442,7 @@ export type IJsonSchemaFromObjectOptions = IJsonSchemaLoadOptions;

// @public
export interface IJsonSchemaLoadOptions {
customFormats?: Record<string, IJsonSchemaCustomFormat<string> | IJsonSchemaCustomFormat<number>>;
dependentSchemas?: JsonSchema[];
schemaVersion?: JsonSchemaVersion;
}
Expand Down
35 changes: 35 additions & 0 deletions libraries/node-core-library/src/JsonSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,24 @@ interface ISchemaWithId {
*/
export type JsonSchemaVersion = 'draft-04' | 'draft-07';

/**
* A definition for a custom format to consider during validation.
* @public
*/
export interface IJsonSchemaCustomFormat<T extends string | number> {
/**
* 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
Expand Down Expand Up @@ -94,6 +112,13 @@ 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. 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<string, IJsonSchemaCustomFormat<string> | IJsonSchemaCustomFormat<number>>;
}

/**
Expand Down Expand Up @@ -141,6 +166,9 @@ export class JsonSchema {
private _validator: ValidateFunction | undefined = undefined;
private _schemaObject: JsonObject | undefined = undefined;
private _schemaVersion: JsonSchemaVersion | undefined = undefined;
private _customFormats:
| Record<string, IJsonSchemaCustomFormat<string> | IJsonSchemaCustomFormat<number>>
| undefined = undefined;

private constructor() {}

Expand All @@ -163,6 +191,7 @@ export class JsonSchema {
if (options) {
schema._dependentSchemas = options.dependentSchemas || [];
schema._schemaVersion = options.schemaVersion;
schema._customFormats = options.customFormats;
}

return schema;
Expand All @@ -181,6 +210,7 @@ export class JsonSchema {
if (options) {
schema._dependentSchemas = options.dependentSchemas || [];
schema._schemaVersion = options.schemaVersion;
schema._customFormats = options.customFormats;
}

return schema;
Expand Down Expand Up @@ -308,6 +338,11 @@ export class JsonSchema {
// Enable json-schema format validation
// https://ajv.js.org/packages/ajv-formats.html
addFormats(validator);
if (this._customFormats) {
for (const [name, format] of Object.entries(this._customFormats)) {
validator.addFormat(name, { ...format, async: false });
}
}

const collectedSchemas: JsonSchema[] = [];
const seenObjects: Set<JsonSchema> = new Set<JsonSchema>();
Expand Down
1 change: 1 addition & 0 deletions libraries/node-core-library/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export {
} from './JsonFile';
export {
type IJsonSchemaErrorInfo,
type IJsonSchemaCustomFormat,
type IJsonSchemaFromFileOptions,
type IJsonSchemaFromObjectOptions,
type IJsonSchemaLoadOptions,
Expand Down
28 changes: 28 additions & 0 deletions libraries/node-core-library/src/test/JsonSchema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});

0 comments on commit 39425b9

Please sign in to comment.