json-schema-describes-subset
Version:
Tools for static JSON schema analysis, including functions to determine if one schema describes a subset of another or if a schema describes the empty set or to convert a schema to its disjunctive normal form (DNF).
156 lines • 4.86 kB
JavaScript
import _addFormats, {} from 'ajv-formats';
import intersection from 'lodash/intersection.js';
import { AllOfSchema, AtomicSchemaObject, NotSchema, } from '../../atomic-schema/index.js';
import { MultipleOfAtomicSchema, TypeAtomicSchema, } from '../../built-in-plugins/type.js';
import { MaximumAtomicSchema, MinimumAtomicSchema, } from '../../built-in-plugins/index.js';
// workaround (https://github.com/microsoft/TypeScript/issues/50058#issuecomment-1297806160)
const addFormats = _addFormats;
export const stringFormatNames = [
'date',
'time',
'date-time',
'iso-time',
'iso-date-time',
'duration',
'uri-reference',
'uri-template',
'uri',
'email',
'hostname',
'ipv4',
'ipv6',
'regex',
'uuid',
'json-pointer',
'relative-json-pointer',
'byte',
/* there are no checks for 'password' or 'binary' => no need to add them */
// 'password',
// 'binary',
];
export class StringFormatAtomicSchema extends AtomicSchemaObject {
format;
constructor(format) {
super();
this.format = format;
}
negate() {
return new AllOfSchema([
new NotSchema(this),
/* actually redundant, but makes checking for contradictions easier */
new TypeAtomicSchema('string'),
]);
}
toJSONSchema() {
return { format: this.format };
}
}
// TODO: derive further atomic schemas from format, e. g. `minLength`
export const stringFormatLogicalCombinations = Object.fromEntries([
...stringFormatNames.map((formatName) => [
formatName,
new StringFormatAtomicSchema(formatName),
]),
]);
export const numberFormatLogicalCombinations = {
// number formats according to the checks in ajv-formats => no need to add
// `'float'` or `'double'` as format
int32: new AllOfSchema([
new MultipleOfAtomicSchema(1),
new MinimumAtomicSchema(-(2 ** 31)),
new MaximumAtomicSchema(2 ** 31 - 1),
]),
int64: new MultipleOfAtomicSchema(1),
};
const formatLogicalCombinations = {
...stringFormatLogicalCombinations,
...numberFormatLogicalCombinations,
};
export const formatExtractionPlugin = {
extract: ({ schema }) => {
return (typeof schema.format !== 'string' ||
(formatLogicalCombinations[schema.format] ?? true));
},
};
export const formatValidationPlugin = {
modifyAjv(ajv) {
addFormats(ajv, {
formats: Object.keys(formatLogicalCombinations),
mode: 'full',
keywords: false, // currently no support for ranges
});
},
};
export const formatSimplificationPlugin = {
appliesToJSONSchemaType: 'string',
mergeableKeywords: [],
simplify({ atomicSchemasByConstructor, negatedAtomicSchemasByConstructor, }) {
const formats = atomicSchemasByConstructor
.get(StringFormatAtomicSchema)
.map(({ format }) => format);
const negatedFormats = negatedAtomicSchemasByConstructor
.get(StringFormatAtomicSchema)
.map(({ format }) => format);
if (intersection(formats, negatedFormats).length > 0) {
return false;
}
return {
allOf: [
...formats.map((format) => ({ format })),
...negatedFormats.map((format) => ({ not: { format } })),
],
};
},
};
/**
* Adds support for the `format` values provided by
* [`ajv-formats`](https://ajv.js.org/packages/ajv-formats.html).
*
* `format`s that apply to strings are only compared for equality, so that only
* schemas like
*
* ```json
* {
* "allOf": [
* { "format": "email" },
* { "not": { "format": "email" } }
* ]
* }
* ```
*
* include a [contradiction](https://github.com/jobohner/json-schema-describes-subset/blob/v0.4.0/docs/README.md#contradictions).
*
* `format`s that apply to numbers are transformed to equivalent
* {@link AtomicSchemaObject}s like {@link MultipleOfAtomicSchema},
* {@link MinimumAtomicSchema} or {@link MaximumAtomicSchema}.
*
* @example ```ts
* import { schemaDescribesSubset } from 'json-schema-describes-subset'
* import { formatPlugin } from 'json-schema-describes-subset/custom-plugins/format-plugin'
*
* console.log(
* schemaDescribesSubset(
* { format: 'email' },
* { format: 'date-time' },
* { plugins: [formatPlugin] },
* ),
* ) // logs: `null`
*
* console.log(
* schemaDescribesSubset(
* { type: 'integer', minimum: 0, maximum: 10 },
* { format: 'int32' },
* { plugins: [formatPlugin] },
* ),
* ) // logs: `true`
* ```
*
* @privateRemarks TODO: return type of {@link toDNF}
*
*/
export const formatPlugin = [
formatValidationPlugin,
formatExtractionPlugin,
formatSimplificationPlugin,
];
//# sourceMappingURL=format-plugin.js.map