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).
594 lines (593 loc) • 19.8 kB
TypeScript
import { type JSONSchema } from '../json-schema/index.js';
import type { RawDNF, ConjunctionOfLiterals } from '../atomic-schema/to-raw-dnf/index.js';
import type { JSONSchemaType } from '../json-schema-type/index.js';
import { type InternalOptions, type Options, type OptionsSimplificationPlugin } from '../options/index.js';
import { type NonConstConjunctionSchemaObject, type ConjunctionSchemaObject, type SimplificationPlugin, type SimplificationResultSchemaByType, type SimplificationResultSchemaForType, type ValidateConst } from '../plugin/index.js';
import type { InstancesByConstructor } from '../utils/instances-by-constructor/index.js';
import type { BuiltInSimplificationPlugin } from '../built-in-plugins/index.js';
export declare function createValidateConst(options: InternalOptions, schema: JSONSchema): ValidateConst;
/**
* Brings the given disjunct of a {@link RawDNF} to a simpler canonical form by
* summarizing keywords. Returns `false` (which is a valid JSON Schema) if there
* is a contradiction in the disjunct.
*
*/
export declare function simplifyDisjunct<Type extends JSONSchemaType, SimplificationPlugin_ extends SimplificationPlugin>({ type, atomicSchemasByConstructor, negatedAtomicSchemasByConstructor, options, validateConst, }: {
type: Type;
atomicSchemasByConstructor: InstancesByConstructor;
negatedAtomicSchemasByConstructor: InstancesByConstructor;
options: InternalOptions<SimplificationPlugin_>;
validateConst: ValidateConst;
}): SimplificationResultSchemaForType<SimplificationPlugin_, Type>;
export type JSONSchemaTypeForDNF = Exclude<JSONSchemaType, 'null' | 'boolean'>;
export declare function isJSONSchemaTypeForDNF(type: JSONSchemaType): type is JSONSchemaTypeForDNF;
/**
* @param rawDisjunct Represents multiple disjuncts, because there might be
* multiple types
*
*/
export declare function simplifyRawDisjunct<SimplificationPlugin_ extends SimplificationPlugin>(rawDisjunct: ConjunctionOfLiterals, validateConst: ValidateConst, options: InternalOptions<SimplificationPlugin_>): SimplificationResultSchemaByType<SimplificationPlugin_, JSONSchemaTypeForDNF>[];
export type Disjunct<SimplificationPlugin_ extends SimplificationPlugin> = Exclude<SimplificationResultSchemaByType<SimplificationPlugin_, JSONSchemaTypeForDNF>, boolean>;
export type DNF<SimplificationPlugin_ extends SimplificationPlugin> = boolean | {
anyOf: Disjunct<SimplificationPlugin_>[];
};
/**
* This is equivalent to
*
* ```ts
* type GeneralDNFSpelledOut =
* | boolean
* | {
* anyOf: (
* | { const: unknown }
* | {
* [mergeableKeyword: string]: unknown
* type: 'string' | 'number' | 'object' | 'array'
* allOf?: JSONSchema[]
* const?: never
* anyOf?: never
* not?: never
* }
* )[]
* }
* ```
*
*/
export type GeneralDNF = DNF<SimplificationPlugin>;
/**
* Is equivalent to {@link GeneralDNF}
*
*/
export type GeneralDNFSpelledOut = boolean | {
anyOf: ({
const: unknown;
} | {
[mergeableKeyword: string]: unknown;
type: 'string' | 'number' | 'object' | 'array';
allOf?: JSONSchema[];
const?: never;
anyOf?: never;
not?: never;
})[];
};
/**
* This is equivalent to
*
* ```ts
* type DefaultDNFSpelledOut =
* | boolean
* | {
* anyOf: (
* | { const: unknown }
* | {
* type: 'number'
* maximum?: number
* minimum?: number
* multipleOf?: number
* allOf?: (
* | { not: { const: number } }
* | { not: { multipleOf: number } }
* | { $ref: string }
* | { not: { $ref: string } }
* )[]
* const?: never
* anyOf?: never
* not?: never
* }
* | {
* type: 'string'
* maxLength?: number
* minLength?: number
* allOf?: (
* | { not: { const: string } }
* | { pattern: string }
* | { not: { pattern: string } }
* | { $ref: string }
* | { not: { $ref: string } }
* )[]
* const?: never
* anyOf?: never
* not?: never
* }
* | {
* type: 'object'
* maxProperties?: number
* minProperties?: number
* patternProperties?: Record<string, JSONSchema>
* properties?: Record<string, JSONSchema>
* propertyNames?: JSONSchema
* required?: string[]
* allOf?: (
* | { not: { const: Record<string, unknown> } }
* | {
* additionalProperties: JSONSchema
* properties?: Record<string, true>
* patternProperties?: Record<string, true>
* }
* | { not: { patternProperties: Record<string, JSONSchema> } }
* | {
* not: {
* additionalProperties: JSONSchema
* properties?: Record<string, true>
* patternProperties?: Record<string, true>
* }
* }
* | { not: { propertyNames: JSONSchema } }
* | { $ref: string }
* | { not: { $ref: string } }
* )[]
* const?: never
* anyOf?: never
* not?: never
* }
* | {
* type: 'array'
* items?: JSONSchema
* maxItems?: number
* minItems?: number
* prefixItems?: JSONSchema[]
* uniqueItems?: boolean
* allOf?: (
* | { not: { const: unknown[] } }
* | {
* contains: JSONSchema
* minContains?: number
* maxContains?: number
* }
* | {
* not: { uniqueItems?: boolean }
* }
* | {
* not: {
* prefixItems?: true[]
* items?: JSONSchema
* }
* }
* | { $ref: string }
* | { not: { $ref: string } }
* )[]
* const?: never
* anyOf?: never
* not?: never
* }
* )[]
* }
* ```
*
*/
export type DefaultDNF = DNF<BuiltInSimplificationPlugin>;
/**
* Is equivalent to {@link DefaultDNF}
*
*/
export type DefaultDNFSpelledOut = boolean | {
anyOf: ({
const: unknown;
} | {
type: 'number';
maximum?: number;
minimum?: number;
multipleOf?: number;
allOf?: ({
not: {
const: number;
};
} | {
not: {
multipleOf: number;
};
} | {
$ref: string;
} | {
not: {
$ref: string;
};
})[];
const?: never;
anyOf?: never;
not?: never;
} | {
type: 'string';
maxLength?: number;
minLength?: number;
allOf?: ({
not: {
const: string;
};
} | {
pattern: string;
} | {
not: {
pattern: string;
};
} | {
$ref: string;
} | {
not: {
$ref: string;
};
})[];
const?: never;
anyOf?: never;
not?: never;
} | {
type: 'object';
maxProperties?: number;
minProperties?: number;
patternProperties?: Record<string, JSONSchema>;
properties?: Record<string, JSONSchema>;
propertyNames?: JSONSchema;
required?: string[];
allOf?: ({
not: {
const: Record<string, unknown>;
};
} | {
additionalProperties: JSONSchema;
properties?: Record<string, true>;
patternProperties?: Record<string, true>;
} | {
not: {
patternProperties: Record<string, JSONSchema>;
};
} | {
not: {
additionalProperties: JSONSchema;
properties?: Record<string, true>;
patternProperties?: Record<string, true>;
};
} | {
not: {
propertyNames: JSONSchema;
};
} | {
$ref: string;
} | {
not: {
$ref: string;
};
})[];
const?: never;
anyOf?: never;
not?: never;
} | {
type: 'array';
items?: JSONSchema;
maxItems?: number;
minItems?: number;
prefixItems?: JSONSchema[];
uniqueItems?: boolean;
allOf?: ({
not: {
const: unknown[];
};
} | {
contains: JSONSchema;
minContains?: number;
maxContains?: number;
} | {
not: {
uniqueItems?: boolean;
};
} | {
not: {
prefixItems?: true[];
items?: JSONSchema;
};
} | {
$ref: string;
} | {
not: {
$ref: string;
};
})[];
const?: never;
anyOf?: never;
not?: never;
})[];
};
/**
* Sort const schemas to the start of the array (mainly for aesthetic reasons)
*
*/
export declare function sortCompareDisjuncts(...disjuncts: [
ConjunctionSchemaObject<NonConstConjunctionSchemaObject>,
ConjunctionSchemaObject<NonConstConjunctionSchemaObject>
]): -1 | 0 | 1;
/**
* Removes unnecessary disjuncts (if they are subsets of other disjuncts)
*
*/
export declare function removeSubsetDisjuncts<Disjunct_ extends JSONSchema>(disjuncts: Disjunct_[], schemaDescribesSubset: (potentialSubset: JSONSchema, potentialSuperset: JSONSchema) => boolean | null): Disjunct_[];
/**
* Removes unnecessary conjuncts (if they are supersets of other conjuncts)
*
*/
export declare function removeSupersetConjuncts<Conjunct_ extends JSONSchema>(conjuncts: Conjunct_[], schemaDescribesSubset: (potentialSubset: JSONSchema, potentialSuperset: JSONSchema) => boolean | null): Conjunct_[];
export type DNFFromOptions<Options_ extends Options | undefined> = DNF<BuiltInSimplificationPlugin | OptionsSimplificationPlugin<Options_>>;
/**
* Transforms the given schema to a [disjunctive normal
* form](https://en.wikipedia.org/wiki/Disjunctive_normal_form) similar to the
* one utilized by {@link schemaDescribesEmptySet}.
*
* @returns The resulting dnf schema will be equivalent to the provided schema (meaning
* that it will accept the same data values) but all
* [boolean combinations](https://json-schema.org/understanding-json-schema/reference/combining)
* will be restructured.
*
* Subschemas that represent property values of a JSON object or elements of a
* JSON array do not represent boolean combinations. They are currently
* considered atomic for that purpose.
*
* The resulting dnf schema will be simplified so that disjuncts that were determined
* to be unsatisfiable are already eliminated. If each disjunct was determined
* to be unsatisfiable the return value is `false`.
*
* The return type's most general form (without specified
* {@link Options.plugins | plugin} types, for example returned by
* `toDNF<Options>(...)`) is equivalent to:
*
* ```ts
* type GeneralDNFSpelledOut =
* | boolean
* | {
* anyOf: (
* | { const: unknown }
* | {
* [mergeableKeyword: string]: unknown
* type: 'string' | 'number' | 'object' | 'array'
* allOf?: JSONSchema[]
* const?: never
* anyOf?: never
* not?: never
* }
* )[]
* }
* ```
*
* If the provided option's type does not contain any custom
* {@link Options.plugins | plugins}, the default return type (for example
* returned by `toDNF(schema)` (without options) or by
* `toDNF<{ plugins: [] }>(...)`) is equivalent to:
*
* ```ts
* type DefaultDNFSpelledOut =
* | boolean
* | {
* anyOf: (
* | { const: unknown }
* | {
* type: 'number'
* maximum?: number
* minimum?: number
* multipleOf?: number
* allOf?: (
* | { not: { const: number } }
* | { not: { multipleOf: number } }
* | { $ref: string }
* | { not: { $ref: string } }
* )[]
* const?: never
* anyOf?: never
* not?: never
* }
* | {
* type: 'string'
* maxLength?: number
* minLength?: number
* allOf?: (
* | { not: { const: string } }
* | { pattern: string }
* | { not: { pattern: string } }
* | { $ref: string }
* | { not: { $ref: string } }
* )[]
* const?: never
* anyOf?: never
* not?: never
* }
* | {
* type: 'object'
* maxProperties?: number
* minProperties?: number
* patternProperties?: Record<string, JSONSchema>
* properties?: Record<string, JSONSchema>
* propertyNames?: JSONSchema
* required?: string[]
* allOf?: (
* | { not: { const: Record<string, unknown> } }
* | {
* additionalProperties: JSONSchema
* properties?: Record<string, true>
* patternProperties?: Record<string, true>
* }
* | { not: { patternProperties: Record<string, JSONSchema> } }
* | {
* not: {
* additionalProperties: JSONSchema
* properties?: Record<string, true>
* patternProperties?: Record<string, true>
* }
* }
* | { not: { propertyNames: JSONSchema } }
* | { $ref: string }
* | { not: { $ref: string } }
* )[]
* const?: never
* anyOf?: never
* not?: never
* }
* | {
* type: 'array'
* items?: JSONSchema
* maxItems?: number
* minItems?: number
* prefixItems?: JSONSchema[]
* uniqueItems?: boolean
* allOf?: (
* | { not: { const: unknown[] } }
* | {
* contains: JSONSchema
* minContains?: number
* maxContains?: number
* }
* | {
* not: { uniqueItems?: boolean }
* }
* | {
* not: {
* prefixItems?: true[]
* items?: JSONSchema
* }
* }
* | { $ref: string }
* | { not: { $ref: string } }
* )[]
* const?: never
* anyOf?: never
* not?: never
* }
* )[]
* }
* ```
*
* The return type will adjust according to the (explicit or inferred) type of
* the property `plugins` of the provided `options`.
*
* @example ```typescript
* import { toDNF } from 'json-schema-describes-subset'
*
* console.log(
* toDNF({
* anyOf: [{ minimum: 2 }, { exclusiveMinimum: 1 }],
* }),
* )
* ```
*
* logs:
*
* ```json
* {
* "anyOf": [
* { "const": null },
* { "const": true },
* { "const": false },
* { "type": "number", "minimum": 1, "allOf": [{ "not": { "const": 1 } }] },
* { "type": "string" },
* { "type": "array" },
* { "type": "object" }
* ]
* }
* ```
*
* ***
*
* ```typescript
* import { toDNF } from 'json-schema-describes-subset'
*
* console.log(
* toDNF({
* anyOf: [{ multipleOf: 2 }, { multipleOf: 3 }, { multipleOf: 4 }],
* }),
* )
* ```
*
* logs:
*
* ```json
* {
* "anyOf": [
* { "const": null },
* { "const": true },
* { "const": false },
* { "type": "number", "multipleOf": 2 },
* { "type": "number", "multipleOf": 3 },
* { "type": "string" },
* { "type": "array" },
* { "type": "object" }
* ]
* }
* ```
*
* @remarks Use cases
*
* This function was created mainly for demonstration purposes, but might also
* have some real world use cases. For example when creating a data mocking
* tool, that generates example data for a given schema, it might be easier to
* generate that data for one of the logically flat disjuncts instead of a
* complex schema which is logically deeply nested.
*
*/
export declare function toDNF<const Options_ extends Options | undefined = undefined>(schema: JSONSchema, options?: Options_): DNFFromOptions<Options_>;
export declare function rawDisjunctDescribesEmptySet<SimplificationPlugin_ extends SimplificationPlugin>(rawDisjunct: ConjunctionOfLiterals, validateConst: ValidateConst, options: InternalOptions<SimplificationPlugin_>): boolean | null;
export declare function rawDNFDescribesEmptySet(rawDNF: RawDNF, validateConst: ValidateConst, options: InternalOptions): boolean | null;
/**
* Tries to determine whether the provided JSON Schema is unsatisfiable and
* therefore describes the empty set. In that case, the schema would be
* equivalent to the `false` schema.
*
* @returns Returns `true` if it does find a reason why the schema will not accept any
* value.
*
* If such a reason cannot be found, usually `null` is returned to indicate the
* possibility of false negatives.
*
* The true positive `false` return value is currently only returned if an
* example data value that satisfies the schema can be trivially found.
* See [Limitations](https://github.com/jobohner/json-schema-describes-subset/blob/v0.4.0/src/dnf/dnf.ts#limitations) for more details.
*
* @example ```ts
* import { schemaDescribesEmptySet } from 'json-schema-describes-subset'
*
* console.log(schemaDescribesEmptySet(false)) // logs: `true`
*
* console.log(
* schemaDescribesEmptySet(
* // this schema will accept anything that is not a number
* { minimum: 2, maximum: 1 },
* ),
* ) // logs: `false`
*
* console.log(
* schemaDescribesEmptySet({
* type: 'number',
* minimum: 2,
* maximum: 1,
* }),
* ) // logs: `true`
* ```
*
* @remarks How does this work?
*
* The provided schema is first transformed to a [disjunctive normal
* form](https://en.wikipedia.org/wiki/Disjunctive_normal_form) similar to the
* one returned by {@link toDNF}. Then each disjunct is checked for
* contradictions which would make it unsatisfiable. If a contradiction is found
* for each disjunct, the complete schema is unsatisfiable and `true` is
* returned.
*
*/
export declare function schemaDescribesEmptySet(schema: JSONSchema, options?: Options | undefined): boolean | null;
export type SchemaDescribesEmptySet = typeof schemaDescribesEmptySet;
export declare function schemaDescribesEmptySetInternal(schema: JSONSchema, options: InternalOptions): boolean | null;
export type SchemaDescribesEmptySetInternal = typeof schemaDescribesEmptySetInternal;