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).
250 lines (249 loc) • 11.8 kB
TypeScript
import { Ajv2020 as Ajv } from 'ajv/dist/2020.js';
import type { JSONSchema, JSONSchemaObject } from '../json-schema/index.js';
import type { LogicalCombination } from '../atomic-schema/index.js';
import type { RawDNF } from '../atomic-schema/to-raw-dnf/index.js';
import type { InstancesByConstructor } from '../utils/instances-by-constructor/index.js';
import type { // eslint-disable-line @typescript-eslint/no-unused-vars
InternalOptions } from '../options/index.js';
import type { JSONSchemaType } from '../json-schema-type/index.js';
import type { Prettify } from '../utils/type-helpers/index.js';
import type { // eslint-disable-line @typescript-eslint/no-unused-vars
BuiltInExtractionPluginId, BuiltInSimplificationPluginId } from '../built-in-plugins/index.js';
/**
* Can be used to modify how schemas are interpreted, for example by regarding
* additional keywords.
*
* There are three kinds of plugins:
*
* * {@link ValidationPlugin},
* * {@link ExtractionPlugin}, and
* * {@link SimplificationPlugin}.
*
* A plugin added to {@link Options.plugins} is an array of one or more of
* these.
*
* Usually a plugin consists of
*
* * a {@link ValidationPlugin} that modifies how
* data values are validated against a JSON Schema and
* * one or more {@link ExtractionPlugin | Extraction-} and/or
* {@link SimplificationPlugin}s that adjust, how schemas are transformed into
* their respective simplified disjunctive normal form.
*
* Examples for plugins are the internal
* {@link "built-in-plugins-collection" | built in plugins}
* and the predefined custom {@link formatPlugin}.
*
*/
export type Plugin = (ValidationPlugin | ExtractionPlugin | SimplificationPlugin)[];
/**
* In order to transform a schema into its disjunctive normal form (DNF), at
* first {@link ExtractionPlugin}s are used to extract
* {@link LogicalCombination}s of {@link AtomicSchema}s.
*
*/
export interface ExtractionPlugin {
/**
* Use this to extract a {@link LogicalCombination} of {@link AtomicSchema}s
* from the keywords of the provided schema.
*
*/
extract: (args: {
/**
* The schema to extract from. This schema is the one that is to be
* transformed into its DNF.
*
*/
schema: JSONSchemaObject;
/**
* Use this to recursively split "logical" subschemas (like the ones defined
* by the `not`, `anyOf` or `allOf` keywords (among others)).
*
*/
split: (schema: JSONSchema) => LogicalCombination;
}) => LogicalCombination;
/**
* Instead of adding functionality, an {@link ExtractionPlugin} can also
* replace one of the {@link builtInExtractionPlugins}, by setting
* {@link ExtractionPlugin.overrides} to that built in plugin's id.
*
*/
overrides?: BuiltInExtractionPluginId | undefined;
/** @hidden */
simplify?: undefined;
}
export type ExtractFunctionArguments = Parameters<ExtractionPlugin['extract']>[0];
export type NonConstConjunctionSchemaObject = {
type?: JSONSchemaType;
const?: never;
anyOf?: never;
not?: never;
allOf?: JSONSchema[];
[key: string]: unknown;
};
export type AllOfElement<Conjunction extends {
allOf?: unknown[];
}> = Conjunction extends {
allOf?: (infer Element)[];
} ? Element : never;
export type ConjunctionSchemaObject<Conjunction extends NonConstConjunctionSchemaObject> = {
const: unknown;
} | Conjunction;
export type ConjunctionSchema<Conjunction extends NonConstConjunctionSchemaObject> = boolean | ConjunctionSchemaObject<Conjunction>;
export declare function isConstSchema(value: unknown): value is {
const: unknown;
};
export type ValidateConst = (constValue: unknown) => false | {
const: unknown;
};
export interface SimplificationPluginArguments {
/**
* The value of the `type` keyword of the currently processed disjunct.
*
* @expandType JSONSchemaType
*
*/
type: JSONSchemaType;
/**
* The merged result of all previously applied {@link SimplificationPlugin}s.
*
*/
previousSimplificationResultSchema: Readonly<NonConstConjunctionSchemaObject>;
/**
* Use this to retrieve {@link AtomicSchema}s that are (a not negated) part of
* the currently processed disjunct.
*
*/
atomicSchemasByConstructor: InstancesByConstructor;
/**
* Use this to retrieve {@link AtomicSchema}s that are a negated part of the
* currently processed disjunct.
*
*/
negatedAtomicSchemasByConstructor: InstancesByConstructor;
/**
* Use this instead of importing {@link schemaDescribesEmptySet}.
*
*/
schemaDescribesEmptySet: (schema: JSONSchema) => boolean | null;
/**
* Use this to split subschemas like the ones defined with keywords like
* `items`. (Subschemas referenced by "logical" keywords like `anyOf`, `allOf`
* or `not` are already taken care of and do not need to be split again.)
*
*/
splitToRawDNF: (schema: JSONSchema) => RawDNF;
options: InternalOptions;
/**
* If the disjunct was narrowed down to a single possible value, this function
* can be used to create a `{ const: ... }` schema from it or `false` if that
* value does not satisfy the original schema. The returned value can then be
* returned by {@link SimplificationPlugin.simplify}.
*
*/
validateConst: ValidateConst;
}
/**
* After a schema has been transformed to its DNF, each disjunct is
* simplified using {@link SimplificationPlugin}s, merging or omitting keywords
* if possible or to `false` if a contradiction is detected.
*
*/
export interface SimplificationPlugin<AppliesToJSONSchemaType extends JSONSchemaType = JSONSchemaType, NonConstConjunctionSchemaObjectSimplifyReturnType extends NonConstConjunctionSchemaObject = NonConstConjunctionSchemaObject, MergeableKeyword extends keyof NonConstConjunctionSchemaObjectSimplifyReturnType = keyof NonConstConjunctionSchemaObjectSimplifyReturnType> {
/**
* Only the disjuncts with a `type` that is also specified here, are
* simplified using this plugin.
*
* If this is not set, or set to `undefined` the plugin is applied to any
* disjunct regardless of their type.
*
*/
appliesToJSONSchemaType?: AppliesToJSONSchemaType | AppliesToJSONSchemaType[] | undefined;
/**
* For some keywords, multiple values can be merged into one. (For example
* multiple `minimum` values can be merged). The result of {@link toDNF} will
* contain these keywords (if present) as direct properties as opposed to
* other keywords which cannot be merged and end up as elements of the
* simplified disjunct's `allOf` array.
*
*/
mergeableKeywords: MergeableKeyword[];
/**
* @returns A {@link ConjunctionSchema} that will be merged into the
* simplified disjuncts. Return `false` if a contradiction is detected.
*
* The returned {@link ConjunctionSchema} must not contain the `type` keyword
* because it will be already defined and will be added to the result later.
*
*/
simplify: (args: SimplificationPluginArguments) => ConjunctionSchema<NonConstConjunctionSchemaObjectSimplifyReturnType> & {
type?: never;
};
/**
* Instead of adding functionality, a {@link SimplificationPlugin} can also
* replace one of the {@link builtInSimplificationPlugins}, by setting
* {@link SimplificationPlugin.overrides} to that built in plugin's id.
*
*/
overrides?: BuiltInSimplificationPluginId | undefined;
/** @hidden */
extract?: undefined;
}
export type SimplificationPluginType<SimplificationPlugin_ extends SimplificationPlugin> = SimplificationPlugin_ extends SimplificationPlugin<infer AppliesToJSONSchemaType> ? AppliesToJSONSchemaType : JSONSchemaType;
export type NonConstConjunctionSchemaObjectSimplifyReturnType<SimplificationPlugin_ extends SimplificationPlugin> = Extract<ReturnType<SimplificationPlugin_['simplify']>, NonConstConjunctionSchemaObject>;
/**
* Without `type`, which is technically also a mergeable keyword, but is added
* separately
*
*/
export type SimplificationPluginMergeableKeyword<SimplificationPlugin_ extends SimplificationPlugin> = Exclude<SimplificationPlugin_['mergeableKeywords'][number], 'const' | 'anyOf' | 'not' | 'allOf' | 'type'> & string;
export type SimplificationPluginWithMergeableKeyword<Plugin extends SimplificationPlugin, Keyword extends string> = Plugin extends SimplificationPlugin ? Keyword extends Plugin['mergeableKeywords'][number] ? Plugin : never : never;
export type SimplificationPluginWithType<Plugin extends SimplificationPlugin, Type extends JSONSchemaType> = Plugin extends SimplificationPlugin ? Type extends SimplificationPluginType<Plugin> ? Plugin : never : never;
export type SimplificationResultNonConstConjunctionSchemaObjectForType<Plugin extends SimplificationPlugin, Type extends JSONSchemaType> = {
type: Type;
} & {
[Key in SimplificationPluginMergeableKeyword<SimplificationPluginWithType<Plugin, Type>>]?: NonConstConjunctionSchemaObjectSimplifyReturnType<SimplificationPluginWithMergeableKeyword<SimplificationPluginWithType<Plugin, Type>, Key>>[Key];
} & {
allOf?: AllOfElement<NonConstConjunctionSchemaObjectSimplifyReturnType<SimplificationPluginWithType<Plugin, Type>>>[];
const?: never;
anyOf?: never;
not?: never;
};
export type SimplificationResultSchemaForType<SimplificationPlugin_ extends SimplificationPlugin, Type extends JSONSchemaType> = Prettify<ConjunctionSchema<SimplificationResultNonConstConjunctionSchemaObjectForType<SimplificationPlugin_, Type>>>;
export type SimplificationResultSchemaByType<SimplificationPlugin_ extends SimplificationPlugin, Type extends JSONSchemaType = JSONSchemaType> = Type extends unknown ? SimplificationResultSchemaForType<SimplificationPlugin_, Type> : never;
/**
* Customizes the internally used
* {@link <https://ajv.js.org/json-schema.html#draft-2020-12> | Ajv (draft 2020-12)}
* instance in order to support e. g. the `format` keyword or non-standard
* keywords.
*
* Ajv is used internally e. g. to check whether a `const` property satisfies a
* schema or if a property name satisfies `propertyNames`.
*
* A custom plugin which is added to {@link Options.plugins} would usually
* contain a {@link ValidationPlugin}, because the purpose of these custom
* plugins is to interpret a schema in a different way than it would be
* interpreted by default.
*
* As opposed to {@link ExtractionPlugin}s and {@link SimplificationPlugin}s,
* which are used internally as built in plugins, there are no internal built in
* {@link ValidationPlugin}s. This is because the default behavior is supported
* out of the box.
*
*/
export interface ValidationPlugin {
/**
* Can be used to modify `ajv`, for example by using `ajv.addKeyword(...)`.
*
*/
modifyAjv: (ajv: Ajv) => void;
/** @hidden */
simplify?: undefined;
/** @hidden */
extract?: undefined;
/** @hidden */
appliesToJSONSchemaType?: undefined;
}
export declare function pluginIsExtractionPlugin<Plugin_ extends Plugin[number]>(plugin: Plugin_): plugin is Extract<Plugin_, ExtractionPlugin>;
export declare function pluginIsSimplificationPlugin<Plugin_ extends Plugin[number]>(plugin: Plugin_): plugin is Extract<Plugin_, SimplificationPlugin>;
export declare function pluginIsValidationPlugin<Plugin_ extends Plugin[number]>(plugin: Plugin_): plugin is Extract<Plugin_, ValidationPlugin>;