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).
136 lines • 5.95 kB
JavaScript
import isNumber from 'lodash/isNumber.js';
import { AllOfSchema, AtomicSchemaObject, NotSchema, } from '../../atomic-schema/index.js';
import { MultipleOfAtomicSchema, TypeAtomicSchema } from '../type.js';
import { ConstAtomicSchema } from '../const.js';
import { findMultipleOfCardinality, findMultipleOfUnionCardinality, isInteger, isValidMultiple, multiRealLCM, realLCM, } from './multiple-of-utils/index.js';
import { getNegatedConstValuesFromNegatedAtomicSchemasByConstructor } from '../negated-const-helpers.js';
export class MinimumAtomicSchema extends AtomicSchemaObject {
minimum;
constructor(minimum) {
super();
this.minimum = minimum;
}
negate() {
return new AllOfSchema([
new MaximumAtomicSchema(this.minimum),
new NotSchema(new ConstAtomicSchema(this.minimum)),
new TypeAtomicSchema('number'),
]);
}
toJSONSchema() {
return { minimum: this.minimum };
}
}
export class MaximumAtomicSchema extends AtomicSchemaObject {
maximum;
constructor(maximum) {
super();
this.maximum = maximum;
}
negate() {
return new AllOfSchema([
new MinimumAtomicSchema(this.maximum),
new NotSchema(new ConstAtomicSchema(this.maximum)),
new TypeAtomicSchema('number'),
]);
}
toJSONSchema() {
return { maximum: this.maximum };
}
}
export const numberExtraction = {
extract: ({ schema }) => {
const schemas = [];
if (typeof schema.minimum === 'number') {
schemas.push(new MinimumAtomicSchema(schema.minimum));
}
if (typeof schema.maximum === 'number') {
schemas.push(new MaximumAtomicSchema(schema.maximum));
}
if (typeof schema.multipleOf === 'number') {
schemas.push(schema.multipleOf === 0
? // 0 would actually not be valid json schema, but just in case
new ConstAtomicSchema(0)
: new MultipleOfAtomicSchema(schema.multipleOf));
}
if (typeof schema.exclusiveMinimum === 'number') {
schemas.push(new AllOfSchema([
new MinimumAtomicSchema(schema.exclusiveMinimum),
new NotSchema(new ConstAtomicSchema(schema.exclusiveMinimum)),
]));
}
if (typeof schema.exclusiveMaximum === 'number') {
schemas.push(new AllOfSchema([
new MaximumAtomicSchema(schema.exclusiveMaximum),
new NotSchema(new ConstAtomicSchema(schema.exclusiveMaximum)),
]));
}
if (schemas.length === 0) {
return true;
}
return new AllOfSchema(schemas);
},
};
export const numberSimplification = {
appliesToJSONSchemaType: 'number',
mergeableKeywords: ['minimum', 'maximum', 'multipleOf'],
simplify({ atomicSchemasByConstructor, negatedAtomicSchemasByConstructor, validateConst, }) {
const minimum = Math.max(...atomicSchemasByConstructor
.get(MinimumAtomicSchema)
.map(({ minimum }) => minimum));
const maximum = Math.min(...atomicSchemasByConstructor
.get(MaximumAtomicSchema)
.map(({ maximum }) => maximum));
if (minimum > maximum) {
return false;
}
if (minimum === maximum) {
return validateConst(minimum);
}
const negatedConsts = getNegatedConstValuesFromNegatedAtomicSchemasByConstructor(negatedAtomicSchemasByConstructor, isNumber);
const negatedMultipleOfs = negatedAtomicSchemasByConstructor
.get(MultipleOfAtomicSchema)
.map(({ multipleOf }) => multipleOf);
const multipleOf = multiRealLCM(...atomicSchemasByConstructor
.get(MultipleOfAtomicSchema)
.map(({ multipleOf }) => multipleOf));
if (multipleOf !== undefined) {
if (negatedMultipleOfs.some((negatedMultipleOf) => isInteger(multipleOf / negatedMultipleOf))) {
return false;
}
const { cardinality: possibleValuesCount, singleElement } = findMultipleOfCardinality(multipleOf, minimum, maximum);
if (singleElement !== undefined) {
return validateConst(singleElement);
}
if (possibleValuesCount === 0) {
return false;
}
if (Number.isFinite(possibleValuesCount)) {
/**
* Applying de Morgan's theorem to {@link negatedMultipleOfs} and
* {@link negatedConsts} makes a negated union of multipleOfs or consts.
* If these (negated) multipeOfs or consts are intersected with
* {@link mulitpleOf} first, that union is a subset of the elements
* described by {@link mulitpleOf}. If {@link impossibleValuesCount} is
* the same as {@link possibleValuesCount}, there are no elements in this
* set => return true
*
*/
const impossibleValuesCount = findMultipleOfUnionCardinality(minimum, maximum, negatedMultipleOfs.map((negatedMultipleOf) => realLCM(negatedMultipleOf, multipleOf)), negatedConsts.filter(isValidMultiple(multipleOf, minimum, maximum)));
if (impossibleValuesCount >= possibleValuesCount) {
return false;
}
}
}
return {
...(minimum > -Infinity ? { minimum } : {}),
...(maximum < Infinity ? { maximum } : {}),
...(multipleOf !== undefined ? { multipleOf } : {}),
allOf: [
...negatedConsts.map((constValue) => ({ not: { const: constValue } })),
...negatedMultipleOfs.map((multipleOf) => ({ not: { multipleOf } })),
],
};
},
};
//# sourceMappingURL=number.js.map