jsonbinpack
Version:
A space-efficient schema-driven binary JSON serialization format based on JSON Schema
152 lines (128 loc) • 4.32 kB
text/typescript
/*
* 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<string>): 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<JSONString, Schema>)) {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
Reflect.set(schema[keyword] as Record<JSONString, Schema>, 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())
}