/* * Copyright 2021 Juan Cruz Viotti * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { strict as assert } from 'assert' import { cloneDeep } from 'lodash' import { JSONString } from '../json' import { Schema, ObjectSchema } from '../jsonschema' import { SimplificationRule } from './rule' import { RULES as RULES_SYNTAX_SUGAR } from './syntax-sugar' import { RULES as RULES_IMPLICITS } from './implicits' import { RULES as RULES_SIMPLIFICATION } from './simplification' import { RULES as RULES_SUPERFLUOUS } from './superfluous' import { RULES as RULES_HETEROGENEOUS } from './heterogeneous' import { RULES as RULES_UNRECOGNIZED } from './unrecognized' // The concatenation of all rules const SIMPLIFICATION_RULES: SimplificationRule[] = [ ...RULES_SYNTAX_SUGAR, ...RULES_IMPLICITS, ...RULES_SIMPLIFICATION, ...RULES_SUPERFLUOUS, ...RULES_HETEROGENEOUS, ...RULES_UNRECOGNIZED ] // Runtime validation to ensure that rules do not contain // duplicated identifiers. for (const [ index, rule ] of SIMPLIFICATION_RULES.entries()) { for (const [ subindex, subrule ] of SIMPLIFICATION_RULES.entries()) { assert.ok(index === subindex || rule.id !== subrule.id, `Duplicated rule id: ${rule.id}`) } } const applyRules = (schema: ObjectSchema, hits: Set): ObjectSchema => { for (const { id, condition, transform } of SIMPLIFICATION_RULES) { // We deep-clone to protect against rule implementations // that might accidentally mutate the schema if (condition(cloneDeep(schema))) { // Abort early on conditions that never stop matching // and cause the program to never terminate assert.ok(!hits.has(id), `Circular rule: ${id}`) hits.add(id) // Recurse until no more rules perform any change return applyRules(transform(cloneDeep(schema)), hits) } } /* * Recurse based on every possible applicator defined by JSON Schema */ // Keywords of type "schema" for (const keyword of [ 'items', 'contains', 'propertyNames', 'not', 'if', 'then', 'else', 'unevaluatedItems', 'unevaluatedProperties', 'contentSchema' ]) { if (typeof schema[keyword] !== 'undefined') { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions Reflect.set(schema, keyword, simplifySchema(schema[keyword] as Schema)) } } // Keywords of type "schema[]" for (const keyword of [ 'prefixItems', 'allOf', 'anyOf', 'oneOf' ]) { if (typeof schema[keyword] !== 'undefined') { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions Reflect.set(schema, keyword, (schema[keyword] as Schema[]).map(simplifySchema)) } } // TODO: We cannot handle additionalProperties yet as the rest of the codebase // assumes that additionalProperties is a boolean schema // Keywords of type "string => schema" for (const keyword of [ 'properties', 'patternProperties', 'dependentSchemas' ]) { if (typeof schema[keyword] !== 'undefined') { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions for (const [ key, value ] of Object.entries(schema[keyword] as Record)) { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions Reflect.set(schema[keyword] as Record, key, simplifySchema(value)) } } } return schema } export const simplifySchema = (schema: Schema): ObjectSchema => { // We avoid putting this particular rule with the others as // otherwise every rule inherits the "boolean" type on its // signature, which unnecessarily complicates things if (typeof schema === 'boolean') { return schema ? {} : { not: {} } } return applyRules(schema, new Set()) }