UNPKG

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
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>;