diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..83beb31 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Set default behavior to automatically convert line endings +* text=auto eol=lf \ No newline at end of file diff --git a/.github/test.yml b/.github/test.yml new file mode 100644 index 0000000..fce3203 --- /dev/null +++ b/.github/test.yml @@ -0,0 +1,32 @@ +name: Run Tests + +on: [push, pull_request] + +permissions: + contents: read + +jobs: + test: + name: Test + runs-on: ${{ matrix.os }} + + strategy: + matrix: + node-version: [18.x, 20.x] + os: [ubuntu-latest, windows-latest, macOS-latest] + + steps: + - uses: actions/checkout@v3 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + + - name: Install + run: | + npm install + + - name: Test + run: | + npm run test diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..9cf9495 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..8c37d71 --- /dev/null +++ b/README.md @@ -0,0 +1,115 @@ +# merge-json-schema + +__merge-json-schema__ is a javascript library that build a logical product (AND) for multiple [JSON schemas](https://json-schema.org/draft/2020-12/json-schema-core#name-introduction). + +- [Installation](#installation) +- [Usage](#usage) +- [API](#api) + - [mergeSchemas(schemas, options)](#mergeschemasschemas-options) + - [resolvers](#resolvers) + - [defaultResolver](#defaultresolver) +- [License](#license) + + + +## Installation + +```bash +npm install merge-json-schema +``` + + + +## Usage + +```javascript +const assert = require('node:assert') +const { mergeSchemas } = require('merge-json-schema') + +const schema1 = { + $id: 'schema1', + type: 'object', + properties: { + foo: { type: 'string', enum: ['foo1', 'foo2'] }, + bar: { type: 'string', minLength: 3 } + } +} + +const schema2 = { + $id: 'schema1', + type: 'object', + properties: { + foo: { type: 'string', enum: ['foo1', 'foo3'] }, + bar: { type: 'string', minLength: 5 } + }, + required: ['foo'] +} + +const mergedSchema = mergeSchemas([schema1, schema2]) +assert.deepStrictEqual(mergedSchema, { + $id: 'schema1', + type: 'object', + properties: { + foo: { type: 'string', enum: ['foo1'] }, + bar: { type: 'string', minLength: 5 } + }, + required: ['foo'] +}) +``` + + + +## API + + + +#### mergeSchemas(schemas, options) + +Builds a logical conjunction (AND) of multiple [JSON schemas](https://json-schema.org/draft/2020-12/json-schema-core#name-introduction). + +- `schemas` __\__ - list of JSON schemas to merge. +- `options` __\__ - optional options. + - `resolvers` __\__ - custom resolvers for JSON schema keywords. Each key is the name of a JSON schema keyword. Each value is a resolver function. See [keywordResolver](#keywordresolver-keyword-values-mergedschema-parentschemas-options). + - `defaultResolver` __\__ - custom default resolver for JSON schema keywords. See [keywordResolver](#keywordresolver-keyword-values-mergedschema-parentschemas-options). + - `onConflict` __\__ - action to take when a conflict is found. Used by the default `defaultResolver`. Default is `throw`. Possible values are: + - `throw` - throws an error if found a multiple different schemas for the same keyword. + - `ignore` - do nothing if found a multiple different schemas for the same keyword. + - `first` - use the value of the first schema if found a multiple different schemas for the same keyword. + +#### resolvers + +A list of default resolvers that __merge-json-schema__ uses to merge JSON schemas. You can override the default resolvers by passing a list of custom resolvers in the `options` argument of `mergeSchemas`. See [keywordResolver](#keywordresolver-keyword-values-mergedschema-parentschemas-options). + +#### defaultResolver + +A default resolver that __merge-json-schema__ uses to merge JSON schemas. Default resolver is used when no custom resolver is defined for a JSON schema keyword. By default, the default resolver works as follows: + +- If only one schema contains the keyword, the value of the keyword is used as the merged value. +- If multiple schemas contain the exact same value for the keyword, the value of the keyword is used as the merged value. +- If multiple schemas contain different values for the keyword, it throws an error. + +#### keywordResolver (keyword, values, mergedSchema, parentSchemas, options) + +__merge-json-schema__ uses a set of resolvers to merge JSON schemas. Each resolver is associated with a JSON schema keyword. The resolver is called when the keyword is found in the schemas to merge. The resolver is called with the following arguments: + +- `keyword` __\__ - the name of the keyword to merge. +- `values` __\__ - the values of the keyword to merge. The length of the array is equal to the number of schemas to merge. If a schema does not contain the keyword, the value is `undefined`. +- `mergedSchema` __\__ - an instance of the merged schema. +- `parentSchemas` __\__ - the list of parent schemas. +- `options` __\__ - the options passed to `mergeSchemas`. + +The resolver must set the merged value of the `keyword` in the `mergedSchema` object. + +__Example:__ resolver for the `minNumber` keyword. + +```javascript +function minNumberResolver (keyword, values, mergedSchema) { + mergedSchema[keyword] = Math.min(...values) +} +``` + + + +## License + +MIT diff --git a/index.js b/index.js new file mode 100644 index 0000000..7c04b15 --- /dev/null +++ b/index.js @@ -0,0 +1,357 @@ +'use strict' + +const deepEqual = require('fast-deep-equal') +const resolvers = require('./lib/resolvers') +const errors = require('./lib/errors') + +const keywordsResolvers = { + $id: resolvers.skip, + type: resolvers.hybridArraysIntersection, + enum: resolvers.arraysIntersection, + minLength: resolvers.maxNumber, + maxLength: resolvers.minNumber, + minimum: resolvers.maxNumber, + maximum: resolvers.minNumber, + multipleOf: resolvers.commonMultiple, + exclusiveMinimum: resolvers.maxNumber, + exclusiveMaximum: resolvers.minNumber, + minItems: resolvers.maxNumber, + maxItems: resolvers.minNumber, + maxProperties: resolvers.minNumber, + minProperties: resolvers.maxNumber, + const: resolvers.allEqual, + default: resolvers.allEqual, + format: resolvers.allEqual, + required: resolvers.arraysUnion, + properties: mergeProperties, + patternProperties: mergeObjects, + additionalProperties: mergeSchemasResolver, + items: mergeItems, + additionalItems: mergeAdditionalItems, + definitions: mergeObjects, + $defs: mergeObjects, + nullable: resolvers.booleanAnd, + oneOf: mergeOneOf, + anyOf: mergeOneOf, + allOf: resolvers.arraysUnion, + not: mergeSchemasResolver, + if: mergeIfThenElseSchemas, + then: resolvers.skip, + else: resolvers.skip, + dependencies: mergeDependencies, + dependentRequired: mergeDependencies, + dependentSchemas: mergeObjects, + propertyNames: mergeSchemasResolver, + uniqueItems: resolvers.booleanOr, + contains: mergeSchemasResolver +} + +function mergeSchemasResolver (keyword, values, mergedSchema, schemas, options) { + mergedSchema[keyword] = _mergeSchemas(values, options) +} + +function cartesianProduct (arrays) { + let result = [[]] + + for (const array of arrays) { + const temp = [] + for (const x of result) { + for (const y of array) { + temp.push([...x, y]) + } + } + result = temp + } + + return result +} + +function mergeOneOf (keyword, values, mergedSchema, schemas, options) { + if (values.length === 1) { + mergedSchema[keyword] = values[0] + return + } + + const product = cartesianProduct(values) + const mergedOneOf = [] + for (const combination of product) { + try { + const mergedSchema = _mergeSchemas(combination, options) + if (mergedSchema !== undefined) { + mergedOneOf.push(mergedSchema) + } + } catch (error) { + // If this combination is not valid, we can ignore it. + if (error instanceof errors.MergeError) continue + throw error + } + } + mergedSchema[keyword] = mergedOneOf +} + +function getSchemaForItem (schema, index) { + const { items, additionalItems } = schema + + if (Array.isArray(items)) { + if (index < items.length) { + return items[index] + } + return additionalItems + } + + if (items !== undefined) { + return items + } + + return additionalItems +} + +function mergeItems (keyword, values, mergedSchema, schemas, options) { + let maxArrayItemsLength = 0 + for (const itemsSchema of values) { + if (Array.isArray(itemsSchema)) { + maxArrayItemsLength = Math.max(maxArrayItemsLength, itemsSchema.length) + } + } + + if (maxArrayItemsLength === 0) { + mergedSchema[keyword] = _mergeSchemas(values, options) + return + } + + const mergedItemsSchemas = [] + for (let i = 0; i < maxArrayItemsLength; i++) { + const indexItemSchemas = [] + for (const schema of schemas) { + const itemSchema = getSchemaForItem(schema, i) + if (itemSchema !== undefined) { + indexItemSchemas.push(itemSchema) + } + } + mergedItemsSchemas[i] = _mergeSchemas(indexItemSchemas, options) + } + mergedSchema[keyword] = mergedItemsSchemas +} + +function mergeAdditionalItems (keyword, values, mergedSchema, schemas, options) { + let hasArrayItems = false + for (const schema of schemas) { + if (Array.isArray(schema.items)) { + hasArrayItems = true + break + } + } + + if (!hasArrayItems) { + mergedSchema[keyword] = _mergeSchemas(values, options) + return + } + + const mergedAdditionalItemsSchemas = [] + for (const schema of schemas) { + let additionalItemsSchema = schema.additionalItems + if ( + additionalItemsSchema === undefined && + !Array.isArray(schema.items) + ) { + additionalItemsSchema = schema.items + } + if (additionalItemsSchema !== undefined) { + mergedAdditionalItemsSchemas.push(additionalItemsSchema) + } + } + + mergedSchema[keyword] = _mergeSchemas(mergedAdditionalItemsSchemas, options) +} + +function getSchemaForProperty (schema, propertyName) { + const { properties, patternProperties, additionalProperties } = schema + + if (properties?.[propertyName] !== undefined) { + return properties[propertyName] + } + + for (const pattern of Object.keys(patternProperties ?? {})) { + const regexp = new RegExp(pattern) + if (regexp.test(propertyName)) { + return patternProperties[pattern] + } + } + + return additionalProperties +} + +function mergeProperties (keyword, values, mergedSchema, schemas, options) { + const foundProperties = {} + for (const currentSchema of schemas) { + const properties = currentSchema.properties ?? {} + for (const propertyName of Object.keys(properties)) { + if (foundProperties[propertyName] !== undefined) continue + + const propertySchema = properties[propertyName] + foundProperties[propertyName] = [propertySchema] + + for (const anotherSchema of schemas) { + if (currentSchema === anotherSchema) continue + + const propertySchema = getSchemaForProperty(anotherSchema, propertyName) + if (propertySchema !== undefined) { + foundProperties[propertyName].push(propertySchema) + } + } + } + } + + const mergedProperties = {} + for (const property of Object.keys(foundProperties)) { + const propertySchemas = foundProperties[property] + mergedProperties[property] = _mergeSchemas(propertySchemas, options) + } + mergedSchema[keyword] = mergedProperties +} + +function mergeObjects (keyword, values, mergedSchema, schemas, options) { + const objectsProperties = {} + + for (const properties of values) { + for (const propertyName of Object.keys(properties)) { + if (objectsProperties[propertyName] === undefined) { + objectsProperties[propertyName] = [] + } + objectsProperties[propertyName].push(properties[propertyName]) + } + } + + const mergedProperties = {} + for (const propertyName of Object.keys(objectsProperties)) { + const propertySchemas = objectsProperties[propertyName] + const mergedPropertySchema = _mergeSchemas(propertySchemas, options) + mergedProperties[propertyName] = mergedPropertySchema + } + + mergedSchema[keyword] = mergedProperties +} + +function mergeIfThenElseSchemas (keyword, values, mergedSchema, schemas, options) { + for (let i = 0; i < schemas.length; i++) { + const subSchema = { + if: schemas[i].if, + then: schemas[i].then, + else: schemas[i].else + } + + if (subSchema.if === undefined) continue + + if (mergedSchema.if === undefined) { + mergedSchema.if = subSchema.if + if (subSchema.then !== undefined) { + mergedSchema.then = subSchema.then + } + if (subSchema.else !== undefined) { + mergedSchema.else = subSchema.else + } + continue + } + + if (mergedSchema.then !== undefined) { + mergedSchema.then = _mergeSchemas([mergedSchema.then, subSchema], options) + } + if (mergedSchema.else !== undefined) { + mergedSchema.else = _mergeSchemas([mergedSchema.else, subSchema], options) + } + } +} + +function mergeDependencies (keyword, values, mergedSchema) { + const mergedDependencies = {} + for (const dependencies of values) { + for (const propertyName of Object.keys(dependencies)) { + if (mergedDependencies[propertyName] === undefined) { + mergedDependencies[propertyName] = [] + } + const mergedPropertyDependencies = mergedDependencies[propertyName] + for (const propertyDependency of dependencies[propertyName]) { + if (!mergedPropertyDependencies.includes(propertyDependency)) { + mergedPropertyDependencies.push(propertyDependency) + } + } + } + } + mergedSchema[keyword] = mergedDependencies +} + +function _mergeSchemas (schemas, options) { + if (schemas.length === 0) return {} + if (schemas.length === 1) return schemas[0] + + const mergedSchema = {} + const keywords = {} + + let allSchemasAreTrue = true + + for (const schema of schemas) { + if (schema === false) return false + if (schema === true) continue + allSchemasAreTrue = false + + for (const keyword of Object.keys(schema)) { + if (keywords[keyword] === undefined) { + keywords[keyword] = [] + } + keywords[keyword].push(schema[keyword]) + } + } + + if (allSchemasAreTrue) return true + + for (const keyword of Object.keys(keywords)) { + const keywordValues = keywords[keyword] + const resolver = options.resolvers[keyword] ?? options.defaultResolver + resolver(keyword, keywordValues, mergedSchema, schemas, options) + } + + return mergedSchema +} + +function defaultResolver (keyword, values, mergedSchema, schemas, options) { + const onConflict = options.onConflict ?? 'throw' + + if (values.length === 1 || onConflict === 'first') { + mergedSchema[keyword] = values[0] + return + } + + let allValuesEqual = true + for (let i = 1; i < values.length; i++) { + if (!deepEqual(values[i], values[0])) { + allValuesEqual = false + break + } + } + + if (allValuesEqual) { + mergedSchema[keyword] = values[0] + return + } + + if (onConflict === 'throw') { + throw new errors.ResolverNotFoundError(keyword, values) + } + if (onConflict === 'skip') { + return + } + throw new errors.InvalidOnConflictOptionError(onConflict) +} + +function mergeSchemas (schemas, options = {}) { + if (options.defaultResolver === undefined) { + options.defaultResolver = defaultResolver + } + + options.resolvers = { ...keywordsResolvers, ...options.resolvers } + + const mergedSchema = _mergeSchemas(schemas, options) + return mergedSchema +} + +module.exports = { mergeSchemas, keywordsResolvers, defaultResolver, ...errors } diff --git a/lib/errors.js b/lib/errors.js new file mode 100644 index 0000000..0a14792 --- /dev/null +++ b/lib/errors.js @@ -0,0 +1,36 @@ +'use strict' + +class MergeError extends Error { + constructor (keyword, schemas) { + super() + this.name = 'JsonSchemaMergeError' + this.code = 'JSON_SCHEMA_MERGE_ERROR' + this.message = `Failed to merge "${keyword}" keyword schemas.` + this.schemas = schemas + } +} + +class ResolverNotFoundError extends Error { + constructor (keyword, schemas) { + super() + this.name = 'JsonSchemaMergeError' + this.code = 'JSON_SCHEMA_MERGE_ERROR' + this.message = `Resolver for "${keyword}" keyword not found.` + this.schemas = schemas + } +} + +class InvalidOnConflictOptionError extends Error { + constructor (onConflict) { + super() + this.name = 'JsonSchemaMergeError' + this.code = 'JSON_SCHEMA_MERGE_ERROR' + this.message = `Invalid "onConflict" option: "${onConflict}".` + } +} + +module.exports = { + MergeError, + ResolverNotFoundError, + InvalidOnConflictOptionError +} diff --git a/lib/resolvers.js b/lib/resolvers.js new file mode 100644 index 0000000..59dc843 --- /dev/null +++ b/lib/resolvers.js @@ -0,0 +1,127 @@ +'use strict' + +const deepEqual = require('fast-deep-equal') +const { MergeError } = require('./errors') + +function _arraysIntersection (arrays) { + let intersection = arrays[0] + for (let i = 1; i < arrays.length; i++) { + intersection = intersection.filter( + value => arrays[i].includes(value) + ) + } + return intersection +} + +function arraysIntersection (keyword, values, mergedSchema) { + const intersection = _arraysIntersection(values) + if (intersection.length === 0) { + throw new MergeError(keyword, values) + } + mergedSchema[keyword] = intersection +} + +function hybridArraysIntersection (keyword, values, mergedSchema) { + for (let i = 0; i < values.length; i++) { + if (!Array.isArray(values[i])) { + values[i] = [values[i]] + } + } + + const intersection = _arraysIntersection(values) + if (intersection.length === 0) { + throw new MergeError(keyword, values) + } + + if (intersection.length === 1) { + mergedSchema[keyword] = intersection[0] + } else { + mergedSchema[keyword] = intersection + } +} + +function arraysUnion (keyword, values, mergedSchema) { + const union = [] + + for (const array of values) { + for (const value of array) { + if (!union.includes(value)) { + union.push(value) + } + } + } + + mergedSchema[keyword] = union +} + +function minNumber (keyword, values, mergedSchema) { + mergedSchema[keyword] = Math.min(...values) +} + +function maxNumber (keyword, values, mergedSchema) { + mergedSchema[keyword] = Math.max(...values) +} + +function commonMultiple (keyword, values, mergedSchema) { + const gcd = (a, b) => (!b ? a : gcd(b, a % b)) + const lcm = (a, b) => (a * b) / gcd(a, b) + + let scale = 1 + for (const value of values) { + while (value * scale % 1 !== 0) { + scale *= 10 + } + } + + let multiple = values[0] * scale + for (const value of values) { + multiple = lcm(multiple, value * scale) + } + + mergedSchema[keyword] = multiple / scale +} + +function allEqual (keyword, values, mergedSchema) { + const firstValue = values[0] + for (let i = 1; i < values.length; i++) { + if (!deepEqual(values[i], firstValue)) { + throw new MergeError(keyword, values) + } + } + mergedSchema[keyword] = firstValue +} + +function skip () {} + +function booleanAnd (keyword, values, mergedSchema) { + for (const value of values) { + if (value === false) { + mergedSchema[keyword] = false + return + } + } + mergedSchema[keyword] = true +} + +function booleanOr (keyword, values, mergedSchema) { + for (const value of values) { + if (value === true) { + mergedSchema[keyword] = true + return + } + } + mergedSchema[keyword] = false +} + +module.exports = { + arraysIntersection, + hybridArraysIntersection, + arraysUnion, + minNumber, + maxNumber, + commonMultiple, + allEqual, + booleanAnd, + booleanOr, + skip +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..94fbee9 --- /dev/null +++ b/package.json @@ -0,0 +1,39 @@ +{ + "name": "merge-json-schemas", + "version": "0.1.0", + "description": "Builds a logical conjunction (AND) of multiple JSON schemas", + "main": "index.js", + "type": "commonjs", + "types": "types/index.d.ts", + "scripts": { + "lint": "standard", + "lint:fix": "standard --fix", + "test:unit": "c8 --100 node --test", + "test:types": "tsd", + "test": "npm run lint && npm run test:unit && npm run test:types" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/fastify/merge-json-schemas.git" + }, + "keywords": [ + "json", + "schema", + "merge", + "allOf" + ], + "author": "Ivan Tymoshenko ", + "license": "MIT", + "bugs": { + "url": "https://github.com/fastify/merge-json-schemas/issues" + }, + "homepage": "https://github.com/fastify/merge-json-schemas#readme", + "devDependencies": { + "c8": "^8.0.1", + "standard": "^17.1.0", + "tsd": "^0.30.3" + }, + "dependencies": { + "fast-deep-equal": "^3.1.3" + } +} diff --git a/test/additional-items.test.js b/test/additional-items.test.js new file mode 100644 index 0000000..707450f --- /dev/null +++ b/test/additional-items.test.js @@ -0,0 +1,164 @@ +'use strict' + +const assert = require('node:assert/strict') +const { test } = require('node:test') +const { mergeSchemas } = require('../index') +const { defaultResolver } = require('./utils') + +test('should merge empty schema and additionalItems = false keyword', () => { + const schema1 = { type: 'array' } + const schema2 = { + type: 'array', + additionalItems: false + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + type: 'array', + additionalItems: false + }) +}) + +test('should merge two schemas with boolean additionalItems', () => { + const schema1 = { + type: 'array', + additionalItems: true + } + const schema2 = { + type: 'array', + additionalItems: false + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + type: 'array', + additionalItems: false + }) +}) + +test('should merge additionalItems schema with false value', () => { + const schema1 = { + type: 'array', + additionalItems: { + type: 'string' + } + } + const schema2 = { + type: 'array', + additionalItems: false + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + type: 'array', + additionalItems: false + }) +}) + +test('should merge additionalItems schema with true value', () => { + const schema1 = { + type: 'array', + additionalItems: { + type: 'string' + } + } + const schema2 = { + type: 'array', + additionalItems: true + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + type: 'array', + additionalItems: { + type: 'string' + } + }) +}) + +test('should merge two additionalItems schemas', () => { + const schema1 = { + type: 'array', + additionalItems: { + type: 'string' + } + } + const schema2 = { + type: 'array', + additionalItems: { + type: 'string', minLength: 1 + } + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + type: 'array', + additionalItems: { + type: 'string', minLength: 1 + } + }) +}) + +test('should merge additionalItems with items array', () => { + const schema1 = { + type: 'array', + items: [ + { type: 'string', const: 'foo1' }, + { type: 'string', const: 'foo2' }, + { type: 'string', const: 'foo3' } + ] + } + const schema2 = { + type: 'array', + additionalItems: { + type: 'string', minLength: 42 + } + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + type: 'array', + items: [ + { type: 'string', const: 'foo1', minLength: 42 }, + { type: 'string', const: 'foo2', minLength: 42 }, + { type: 'string', const: 'foo3', minLength: 42 } + ], + additionalItems: { + type: 'string', minLength: 42 + } + }) +}) + +test('should merge items array and additionalItems with items array', () => { + const schema1 = { + type: 'array', + items: [ + { type: 'string', const: 'foo1' }, + { type: 'string', const: 'foo2' }, + { type: 'string', const: 'foo3' } + ] + } + const schema2 = { + type: 'array', + items: [ + { type: 'string', minLength: 1 }, + { type: 'string', minLength: 2 } + ], + additionalItems: { + type: 'string', minLength: 3 + } + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + type: 'array', + items: [ + { type: 'string', const: 'foo1', minLength: 1 }, + { type: 'string', const: 'foo2', minLength: 2 }, + { type: 'string', const: 'foo3', minLength: 3 } + ], + additionalItems: { + type: 'string', minLength: 3 + } + }) +}) diff --git a/test/additional-properties.test.js b/test/additional-properties.test.js new file mode 100644 index 0000000..e4304ac --- /dev/null +++ b/test/additional-properties.test.js @@ -0,0 +1,129 @@ +'use strict' + +const assert = require('node:assert/strict') +const { test } = require('node:test') +const { mergeSchemas } = require('../index') +const { defaultResolver } = require('./utils') + +test('should merge empty schema and additionalProperties=false keyword', () => { + const schema1 = { type: 'object' } + const schema2 = { + type: 'object', + additionalProperties: false + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + type: 'object', + additionalProperties: false + }) +}) + +test('should merge two schemas with boolean additionalProperties', () => { + const schema1 = { + type: 'object', + additionalProperties: true + } + const schema2 = { + type: 'object', + additionalProperties: false + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + type: 'object', + additionalProperties: false + }) +}) + +test('should merge additionalProperties schema with false value', () => { + const schema1 = { + type: 'object', + additionalProperties: { + type: 'string' + } + } + const schema2 = { + type: 'object', + additionalProperties: false + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + type: 'object', + additionalProperties: false + }) +}) + +test('should merge additionalProperties schema with true value', () => { + const schema1 = { + type: 'object', + additionalProperties: { + type: 'string' + } + } + const schema2 = { + type: 'object', + additionalProperties: true + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + type: 'object', + additionalProperties: { + type: 'string' + } + }) +}) + +test('should merge two additionalProperties schemas', () => { + const schema1 = { + type: 'object', + additionalProperties: { + type: 'string' + } + } + const schema2 = { + type: 'object', + additionalProperties: { + type: 'string', minLength: 1 + } + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + type: 'object', + additionalProperties: { + type: 'string', minLength: 1 + } + }) +}) + +test('should merge two additionalProperties and properties schemas', () => { + const schema1 = { + type: 'object', + additionalProperties: { + type: 'string' + } + } + const schema2 = { + type: 'object', + properties: { + foo: { type: ['string', 'number'] } + }, + additionalProperties: { + type: 'string', minLength: 1 + } + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + type: 'object', + properties: { + foo: { type: 'string' } + }, + additionalProperties: { + type: 'string', minLength: 1 + } + }) +}) diff --git a/test/all-of.test.js b/test/all-of.test.js new file mode 100644 index 0000000..2b2acef --- /dev/null +++ b/test/all-of.test.js @@ -0,0 +1,43 @@ +'use strict' + +const assert = require('node:assert/strict') +const { test } = require('node:test') +const { mergeSchemas } = require('../index') +const { defaultResolver } = require('./utils') + +test('should merge empty schema and allOf keyword', () => { + const schema1 = {} + const schema2 = { + allOf: [ + { type: 'string', const: 'foo' } + ] + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + allOf: [ + { type: 'string', const: 'foo' } + ] + }) +}) + +test('should merge schemas with allOfs schemas', () => { + const schema1 = { + allOf: [ + { type: 'number', minimum: 0 } + ] + } + const schema2 = { + allOf: [ + { type: 'string', const: 'foo' } + ] + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + allOf: [ + { type: 'number', minimum: 0 }, + { type: 'string', const: 'foo' } + ] + }) +}) diff --git a/test/any-of.test.js b/test/any-of.test.js new file mode 100644 index 0000000..98b3a21 --- /dev/null +++ b/test/any-of.test.js @@ -0,0 +1,81 @@ +'use strict' + +const assert = require('node:assert/strict') +const { test } = require('node:test') +const { mergeSchemas } = require('../index') +const { defaultResolver } = require('./utils') + +test('should merge empty schema and anyOf keyword', () => { + const schema1 = {} + const schema2 = { + anyOf: [ + { type: 'string', const: 'foo' } + ] + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + anyOf: [ + { type: 'string', const: 'foo' } + ] + }) +}) + +test('should merge two schemas with anyOfs schemas', () => { + const schema1 = { + anyOf: [ + { type: 'string', enum: ['foo1', 'foo2', 'foo3'] }, + { type: 'string', enum: ['foo3', 'foo4', 'foo5'] } + ] + } + const schema2 = { + anyOf: [ + { type: 'string', enum: ['foo2', 'foo3', 'foo4'] }, + { type: 'string', enum: ['foo3', 'foo6', 'foo7'] } + ] + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + anyOf: [ + { type: 'string', enum: ['foo2', 'foo3'] }, + { type: 'string', enum: ['foo3'] }, + { type: 'string', enum: ['foo3', 'foo4'] }, + { type: 'string', enum: ['foo3'] } + ] + }) +}) + +test('should merge three schemas with anyOfs schemas', () => { + const schema1 = { + anyOf: [ + { type: 'string', enum: ['foo1', 'foo2', 'foo3', 'foo4'] }, + { type: 'string', enum: ['foo3', 'foo4', 'foo5', 'foo7'] } + ] + } + const schema2 = { + anyOf: [ + { type: 'string', enum: ['foo2', 'foo3', 'foo4', 'foo5'] }, + { type: 'string', enum: ['foo3', 'foo6', 'foo7', 'foo8'] } + ] + } + + const schema3 = { + anyOf: [ + { type: 'string', enum: ['foo1', 'foo3', 'foo5', 'foo7'] }, + { type: 'string', enum: ['foo2', 'foo4', 'foo6', 'foo8'] } + ] + } + + const mergedSchema = mergeSchemas([schema1, schema2, schema3], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + anyOf: [ + { type: 'string', enum: ['foo3'] }, + { type: 'string', enum: ['foo2', 'foo4'] }, + { type: 'string', enum: ['foo3'] }, + { type: 'string', enum: ['foo3', 'foo5'] }, + { type: 'string', enum: ['foo4'] }, + { type: 'string', enum: ['foo3', 'foo7'] } + ] + }) +}) diff --git a/test/const.test.js b/test/const.test.js new file mode 100644 index 0000000..1b8cfe0 --- /dev/null +++ b/test/const.test.js @@ -0,0 +1,58 @@ +'use strict' + +const assert = require('node:assert/strict') +const { test } = require('node:test') +const { mergeSchemas } = require('../index') +const { defaultResolver } = require('./utils') + +test('should merge empty schema and string const keyword', () => { + const schema1 = { type: 'string' } + const schema2 = { type: 'string', const: 'foo' } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'string', const: 'foo' }) +}) + +test('should merge equal string const keywords', () => { + const schema1 = { type: 'string', const: 'foo' } + const schema2 = { type: 'string', const: 'foo' } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'string', const: 'foo' }) +}) + +test('should merge equal object const keywords', () => { + const schema1 = { type: 'string', const: { foo: 'bar' } } + const schema2 = { type: 'string', const: { foo: 'bar' } } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'string', const: { foo: 'bar' } }) +}) + +test('should throw an error if const string values are different', () => { + const schema1 = { type: 'string', const: 'foo' } + const schema2 = { type: 'string', const: 'bar' } + + assert.throws(() => { + mergeSchemas([schema1, schema2], { defaultResolver }) + }, { + name: 'JsonSchemaMergeError', + code: 'JSON_SCHEMA_MERGE_ERROR', + message: 'Failed to merge "const" keyword schemas.', + schemas: ['foo', 'bar'] + }) +}) + +test('should throw an error if const object values are different', () => { + const schema1 = { type: 'object', const: { foo: 'bar' } } + const schema2 = { type: 'object', const: { foo: 'baz' } } + + assert.throws(() => { + mergeSchemas([schema1, schema2], { defaultResolver }) + }, { + name: 'JsonSchemaMergeError', + code: 'JSON_SCHEMA_MERGE_ERROR', + message: 'Failed to merge "const" keyword schemas.', + schemas: [{ foo: 'bar' }, { foo: 'baz' }] + }) +}) diff --git a/test/contains.test.js b/test/contains.test.js new file mode 100644 index 0000000..66efb05 --- /dev/null +++ b/test/contains.test.js @@ -0,0 +1,55 @@ +'use strict' + +const assert = require('node:assert/strict') +const { test } = require('node:test') +const { mergeSchemas } = require('../index') +const { defaultResolver } = require('./utils') + +test('should merge empty schema and contains keyword', () => { + const schema1 = {} + const schema2 = { + type: 'array', + contains: { + type: 'integer', + minimum: 5 + } + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + type: 'array', + contains: { + type: 'integer', + minimum: 5 + } + }) +}) + +test('should merge two contains keyword schemas', () => { + const schema1 = { + type: 'array', + contains: { + type: 'integer', + minimum: 5, + maximum: 14 + } + } + const schema2 = { + type: 'array', + contains: { + type: 'integer', + minimum: 9, + maximum: 10 + } + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + type: 'array', + contains: { + type: 'integer', + minimum: 9, + maximum: 10 + } + }) +}) diff --git a/test/custom-resolvers.test.js b/test/custom-resolvers.test.js new file mode 100644 index 0000000..2079188 --- /dev/null +++ b/test/custom-resolvers.test.js @@ -0,0 +1,50 @@ +'use strict' + +const assert = require('node:assert/strict') +const { test } = require('node:test') +const { mergeSchemas } = require('../index') +const { defaultResolver } = require('./utils') + +test('should use a custom resolver instead of default one', () => { + const schema1 = { type: 'string' } + const schema2 = { type: 'number' } + + const mergedSchema = mergeSchemas( + [schema1, schema2], + { + resolvers: { + type: (keyword, values, mergedSchema, schemas) => { + assert.strictEqual(keyword, 'type') + assert.deepStrictEqual(values, ['string', 'number']) + assert.deepStrictEqual(schemas, [schema1, schema2]) + + mergedSchema[keyword] = 'custom-type' + } + }, + defaultResolver + } + ) + assert.deepStrictEqual(mergedSchema, { type: 'custom-type' }) +}) + +test('should use a custom resolver for unknown keyword', () => { + const schema1 = { customKeyword: 'string' } + const schema2 = { customKeyword: 'number' } + + const mergedSchema = mergeSchemas( + [schema1, schema2], + { + resolvers: { + customKeyword: (keyword, values, mergedSchema, schemas) => { + assert.strictEqual(keyword, 'customKeyword') + assert.deepStrictEqual(values, ['string', 'number']) + assert.deepStrictEqual(schemas, [schema1, schema2]) + + mergedSchema[keyword] = 'custom-type' + } + }, + defaultResolver + } + ) + assert.deepStrictEqual(mergedSchema, { customKeyword: 'custom-type' }) +}) diff --git a/test/default-resolver.test.js b/test/default-resolver.test.js new file mode 100644 index 0000000..186a13a --- /dev/null +++ b/test/default-resolver.test.js @@ -0,0 +1,111 @@ +'use strict' + +const assert = require('node:assert/strict') +const { test } = require('node:test') +const { mergeSchemas } = require('../index') + +test('should merge an unknown keyword with an empty schema', () => { + const schema1 = {} + const schema2 = { customKeyword: 42 } + + const mergedSchema = mergeSchemas([schema1, schema2]) + assert.deepStrictEqual(mergedSchema, { customKeyword: 42 }) +}) + +test('should merge two equal unknown keywords', () => { + const schema1 = { customKeyword: 42 } + const schema2 = { customKeyword: 42 } + + const mergedSchema = mergeSchemas([schema1, schema2]) + assert.deepStrictEqual(mergedSchema, { customKeyword: 42 }) +}) + +test('should merge two equal unknown object keywords', () => { + const schema1 = { type: 'string', customKeyword: { foo: 'bar' } } + const schema2 = { type: 'string', customKeyword: { foo: 'bar' } } + + const mergedSchema = mergeSchemas([schema1, schema2]) + assert.deepStrictEqual(mergedSchema, { + type: 'string', + customKeyword: { foo: 'bar' } + }) +}) + +test('should use custom defaultResolver if passed', () => { + const schema1 = { type: 'string', customKeyword: 42 } + const schema2 = { type: 'string', customKeyword: 43 } + + const mergedSchema = mergeSchemas( + [schema1, schema2], + { + defaultResolver: (keyword, values, mergedSchema, schemas) => { + assert.strictEqual(keyword, 'customKeyword') + assert.deepStrictEqual(values, [42, 43]) + assert.deepStrictEqual(schemas, [schema1, schema2]) + + mergedSchema.customKeyword = 'custom-value-42' + } + } + ) + assert.deepStrictEqual(mergedSchema, { + type: 'string', + customKeyword: 'custom-value-42' + }) +}) + +test('should trow an error when merging two different unknown keywords', () => { + const schema1 = { customKeyword: 42 } + const schema2 = { customKeyword: 43 } + + assert.throws(() => { + mergeSchemas([schema1, schema2]) + }, { + name: 'JsonSchemaMergeError', + code: 'JSON_SCHEMA_MERGE_ERROR', + message: 'Resolver for "customKeyword" keyword not found.', + schemas: [42, 43] + }) +}) + +test('should trow an error when merging two different unknown keywords with onConflict = throw', () => { + const schema1 = { customKeyword: 42 } + const schema2 = { customKeyword: 43 } + + assert.throws(() => { + mergeSchemas([schema1, schema2], { onConflict: 'throw' }) + }, { + name: 'JsonSchemaMergeError', + code: 'JSON_SCHEMA_MERGE_ERROR', + message: 'Resolver for "customKeyword" keyword not found.', + schemas: [42, 43] + }) +}) + +test('should skip the keyword schemas if onConflict = skip', () => { + const schema1 = { customKeyword: 42 } + const schema2 = { customKeyword: 43 } + + const mergedSchema = mergeSchemas([schema1, schema2], { onConflict: 'skip' }) + assert.deepStrictEqual(mergedSchema, {}) +}) + +test('should pick first schema if onConflict = first', () => { + const schema1 = { customKeyword: 42 } + const schema2 = { customKeyword: 43 } + + const mergedSchema = mergeSchemas([schema1, schema2], { onConflict: 'first' }) + assert.deepStrictEqual(mergedSchema, { customKeyword: 42 }) +}) + +test('should throw an error if pass wrong onConflict value', () => { + const schema1 = { customKeyword: 42 } + const schema2 = { customKeyword: 43 } + + assert.throws(() => { + mergeSchemas([schema1, schema2], { onConflict: 'foo' }) + }, { + name: 'JsonSchemaMergeError', + code: 'JSON_SCHEMA_MERGE_ERROR', + message: 'Invalid "onConflict" option: "foo".' + }) +}) diff --git a/test/default.test.js b/test/default.test.js new file mode 100644 index 0000000..a87cbcc --- /dev/null +++ b/test/default.test.js @@ -0,0 +1,50 @@ +'use strict' + +const assert = require('node:assert/strict') +const { test } = require('node:test') +const { mergeSchemas } = require('../index') +const { defaultResolver } = require('./utils') + +test('should merge empty schema and string default keyword', () => { + const schema1 = { type: 'string' } + const schema2 = { type: 'string', default: 'foo' } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'string', default: 'foo' }) +}) + +test('should merge equal string default keywords', () => { + const schema1 = { type: 'string', default: 'foo' } + const schema2 = { type: 'string', default: 'foo' } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'string', default: 'foo' }) +}) + +test('should throw an error if default string values are different', () => { + const schema1 = { type: 'string', default: 'foo' } + const schema2 = { type: 'string', default: 'bar' } + + assert.throws(() => { + mergeSchemas([schema1, schema2], { defaultResolver }) + }, { + name: 'JsonSchemaMergeError', + code: 'JSON_SCHEMA_MERGE_ERROR', + message: 'Failed to merge "default" keyword schemas.', + schemas: ['foo', 'bar'] + }) +}) + +test('should throw an error if default object values are different', () => { + const schema1 = { type: 'object', default: { foo: 'bar' } } + const schema2 = { type: 'object', default: { foo: 'baz' } } + + assert.throws(() => { + mergeSchemas([schema1, schema2], { defaultResolver }) + }, { + name: 'JsonSchemaMergeError', + code: 'JSON_SCHEMA_MERGE_ERROR', + message: 'Failed to merge "default" keyword schemas.', + schemas: [{ foo: 'bar' }, { foo: 'baz' }] + }) +}) diff --git a/test/definitions.test.js b/test/definitions.test.js new file mode 100644 index 0000000..2f90280 --- /dev/null +++ b/test/definitions.test.js @@ -0,0 +1,46 @@ +'use strict' + +const assert = require('node:assert/strict') +const { test } = require('node:test') +const { mergeSchemas } = require('../index') +const { defaultResolver } = require('./utils') + +test('should merge empty schema and definitions keyword', () => { + const schema1 = {} + const schema2 = { + definitions: { + foo: { type: 'string', const: 'foo' } + } + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + definitions: { + foo: { type: 'string', const: 'foo' } + } + }) +}) + +test('should merge two definition schemas', () => { + const schema1 = { + definitions: { + foo: { type: 'string', enum: ['foo', 'bar'] }, + bar: { type: 'string', enum: ['foo', 'bar'] } + } + } + const schema2 = { + definitions: { + foo: { type: 'string', enum: ['foo'] }, + baz: { type: 'string' } + } + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + definitions: { + foo: { type: 'string', enum: ['foo'] }, + bar: { type: 'string', enum: ['foo', 'bar'] }, + baz: { type: 'string' } + } + }) +}) diff --git a/test/defs.test.js b/test/defs.test.js new file mode 100644 index 0000000..e5433ab --- /dev/null +++ b/test/defs.test.js @@ -0,0 +1,46 @@ +'use strict' + +const assert = require('node:assert/strict') +const { test } = require('node:test') +const { mergeSchemas } = require('../index') +const { defaultResolver } = require('./utils') + +test('should merge empty schema and $defs keyword', () => { + const schema1 = {} + const schema2 = { + $defs: { + foo: { type: 'string', const: 'foo' } + } + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + $defs: { + foo: { type: 'string', const: 'foo' } + } + }) +}) + +test('should merge two definition schemas', () => { + const schema1 = { + $defs: { + foo: { type: 'string', enum: ['foo', 'bar'] }, + bar: { type: 'string', enum: ['foo', 'bar'] } + } + } + const schema2 = { + $defs: { + foo: { type: 'string', enum: ['foo'] }, + baz: { type: 'string' } + } + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + $defs: { + foo: { type: 'string', enum: ['foo'] }, + bar: { type: 'string', enum: ['foo', 'bar'] }, + baz: { type: 'string' } + } + }) +}) diff --git a/test/dependencies.test.js b/test/dependencies.test.js new file mode 100644 index 0000000..3afb0cf --- /dev/null +++ b/test/dependencies.test.js @@ -0,0 +1,75 @@ +'use strict' + +const assert = require('node:assert/strict') +const { test } = require('node:test') +const { mergeSchemas } = require('../index') +const { defaultResolver } = require('./utils') + +test('should merge empty schema and dependencies keyword', () => { + const schema1 = {} + const schema2 = { + type: 'object', + properties: { + foo: { type: 'string' }, + bar: { type: 'string' } + }, + dependencies: { + foo: ['bar'] + } + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + type: 'object', + properties: { + foo: { type: 'string' }, + bar: { type: 'string' } + }, + dependencies: { + foo: ['bar'] + } + }) +}) + +test('should merge two dependencies keyword schemas', () => { + const schema1 = { + type: 'object', + properties: { + foo: { type: 'string' }, + bar: { type: 'string' }, + que: { type: 'string' } + }, + dependencies: { + foo: ['bar', 'que'], + bar: ['que'] + } + } + const schema2 = { + type: 'object', + properties: { + foo: { type: 'string' }, + bar: { type: 'string' }, + baz: { type: 'string' } + }, + dependencies: { + foo: ['baz'], + baz: ['foo'] + } + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + type: 'object', + properties: { + foo: { type: 'string' }, + bar: { type: 'string' }, + que: { type: 'string' }, + baz: { type: 'string' } + }, + dependencies: { + foo: ['bar', 'que', 'baz'], + bar: ['que'], + baz: ['foo'] + } + }) +}) diff --git a/test/dependent-required.test.js b/test/dependent-required.test.js new file mode 100644 index 0000000..7729b95 --- /dev/null +++ b/test/dependent-required.test.js @@ -0,0 +1,75 @@ +'use strict' + +const assert = require('node:assert/strict') +const { test } = require('node:test') +const { mergeSchemas } = require('../index') +const { defaultResolver } = require('./utils') + +test('should merge empty schema and dependentRequired keyword', () => { + const schema1 = {} + const schema2 = { + type: 'object', + properties: { + foo: { type: 'string' }, + bar: { type: 'string' } + }, + dependentRequired: { + foo: ['bar'] + } + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + type: 'object', + properties: { + foo: { type: 'string' }, + bar: { type: 'string' } + }, + dependentRequired: { + foo: ['bar'] + } + }) +}) + +test('should merge two dependentRequired keyword schemas', () => { + const schema1 = { + type: 'object', + properties: { + foo: { type: 'string' }, + bar: { type: 'string' }, + que: { type: 'string' } + }, + dependentRequired: { + foo: ['bar', 'que'], + bar: ['que'] + } + } + const schema2 = { + type: 'object', + properties: { + foo: { type: 'string' }, + bar: { type: 'string' }, + baz: { type: 'string' } + }, + dependentRequired: { + foo: ['baz'], + baz: ['foo'] + } + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + type: 'object', + properties: { + foo: { type: 'string' }, + bar: { type: 'string' }, + que: { type: 'string' }, + baz: { type: 'string' } + }, + dependentRequired: { + foo: ['bar', 'que', 'baz'], + bar: ['que'], + baz: ['foo'] + } + }) +}) diff --git a/test/dependent-schemas.test.js b/test/dependent-schemas.test.js new file mode 100644 index 0000000..76e0435 --- /dev/null +++ b/test/dependent-schemas.test.js @@ -0,0 +1,76 @@ +'use strict' + +const assert = require('node:assert/strict') +const { test } = require('node:test') +const { mergeSchemas } = require('../index') +const { defaultResolver } = require('./utils') + +test('should merge empty schema and dependentRequired keyword', () => { + const schema1 = {} + const schema2 = { + type: 'object', + properties: { + foo: { type: 'string' }, + bar: { type: 'string' } + }, + dependentSchemas: { + foo: { required: ['bar'] } + } + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + type: 'object', + properties: { + foo: { type: 'string' }, + bar: { type: 'string' } + }, + dependentSchemas: { + foo: { required: ['bar'] } + } + }) +}) + +test('should merge two dependentRequired keyword schemas', () => { + const schema1 = { + type: 'object', + properties: { + foo: { type: 'string' }, + bar: { type: 'string' }, + que: { type: 'string' } + }, + dependentSchemas: { + foo: { required: ['bar', 'que'] }, + bar: { required: ['que'] } + } + } + + const schema2 = { + type: 'object', + properties: { + foo: { type: 'string' }, + bar: { type: 'string' }, + baz: { type: 'string' } + }, + dependentSchemas: { + foo: { required: ['baz'] }, + baz: { required: ['foo'] } + } + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + type: 'object', + properties: { + foo: { type: 'string' }, + bar: { type: 'string' }, + que: { type: 'string' }, + baz: { type: 'string' } + }, + dependentSchemas: { + foo: { required: ['bar', 'que', 'baz'] }, + bar: { required: ['que'] }, + baz: { required: ['foo'] } + } + }) +}) diff --git a/test/enum.test.js b/test/enum.test.js new file mode 100644 index 0000000..8a0dd13 --- /dev/null +++ b/test/enum.test.js @@ -0,0 +1,44 @@ +'use strict' + +const assert = require('node:assert/strict') +const { test } = require('node:test') +const { mergeSchemas } = require('../index') +const { defaultResolver } = require('./utils') + +test('should merge empty schema and string enum values', () => { + const schema1 = { type: 'string' } + const schema2 = { type: 'string', enum: ['foo', 'bar'] } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'string', enum: ['foo', 'bar'] }) +}) + +test('should merge equal string enum values', () => { + const schema1 = { type: 'string', enum: ['foo', 'bar'] } + const schema2 = { type: 'string', enum: ['foo', 'bar'] } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'string', enum: ['foo', 'bar'] }) +}) + +test('should merge different string enum values', () => { + const schema1 = { type: 'string', enum: ['foo', 'bar'] } + const schema2 = { type: 'string', enum: ['foo', 'baz'] } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'string', enum: ['foo'] }) +}) + +test('should throw an error if can not merge enum values', () => { + const schema1 = { type: 'string', enum: ['foo', 'bar'] } + const schema2 = { type: 'string', enum: ['baz', 'qux'] } + + assert.throws(() => { + mergeSchemas([schema1, schema2], { defaultResolver }) + }, { + name: 'JsonSchemaMergeError', + code: 'JSON_SCHEMA_MERGE_ERROR', + message: 'Failed to merge "enum" keyword schemas.', + schemas: [['foo', 'bar'], ['baz', 'qux']] + }) +}) diff --git a/test/exclusive-maximum.test.js b/test/exclusive-maximum.test.js new file mode 100644 index 0000000..9481bb5 --- /dev/null +++ b/test/exclusive-maximum.test.js @@ -0,0 +1,30 @@ +'use strict' + +const assert = require('node:assert/strict') +const { test } = require('node:test') +const { mergeSchemas } = require('../index') +const { defaultResolver } = require('./utils') + +test('should merge empty schema and exclusiveMaximum keyword', () => { + const schema1 = { type: 'number' } + const schema2 = { type: 'number', exclusiveMaximum: 42 } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'number', exclusiveMaximum: 42 }) +}) + +test('should merge equal exclusiveMaximum values', () => { + const schema1 = { type: 'number', exclusiveMaximum: 42 } + const schema2 = { type: 'number', exclusiveMaximum: 42 } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'number', exclusiveMaximum: 42 }) +}) + +test('should merge different exclusiveMaximum values', () => { + const schema1 = { type: 'integer', exclusiveMaximum: 42 } + const schema2 = { type: 'integer', exclusiveMaximum: 43 } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'integer', exclusiveMaximum: 42 }) +}) diff --git a/test/exclusive-minimum.test.js b/test/exclusive-minimum.test.js new file mode 100644 index 0000000..d2de258 --- /dev/null +++ b/test/exclusive-minimum.test.js @@ -0,0 +1,30 @@ +'use strict' + +const assert = require('node:assert/strict') +const { test } = require('node:test') +const { mergeSchemas } = require('../index') +const { defaultResolver } = require('./utils') + +test('should merge empty schema and exclusiveMinimum keyword', () => { + const schema1 = { type: 'number' } + const schema2 = { type: 'number', exclusiveMinimum: 42 } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'number', exclusiveMinimum: 42 }) +}) + +test('should merge equal exclusiveMinimum values', () => { + const schema1 = { type: 'number', exclusiveMinimum: 42 } + const schema2 = { type: 'number', exclusiveMinimum: 42 } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'number', exclusiveMinimum: 42 }) +}) + +test('should merge different exclusiveMinimum values', () => { + const schema1 = { type: 'integer', exclusiveMinimum: 42 } + const schema2 = { type: 'integer', exclusiveMinimum: 43 } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'integer', exclusiveMinimum: 43 }) +}) diff --git a/test/format.test.js b/test/format.test.js new file mode 100644 index 0000000..e79e1c8 --- /dev/null +++ b/test/format.test.js @@ -0,0 +1,36 @@ +'use strict' + +const assert = require('node:assert/strict') +const { test } = require('node:test') +const { mergeSchemas } = require('../index') +const { defaultResolver } = require('./utils') + +test('should merge empty schema and string format keyword', () => { + const schema1 = { type: 'string' } + const schema2 = { type: 'string', format: 'date-time' } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'string', format: 'date-time' }) +}) + +test('should merge equal string format keywords', () => { + const schema1 = { type: 'string', format: 'date-time' } + const schema2 = { type: 'string', format: 'date-time' } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'string', format: 'date-time' }) +}) + +test('should throw an error if format keyword values are different', () => { + const schema1 = { type: 'string', format: 'date-time' } + const schema2 = { type: 'string', format: 'date' } + + assert.throws(() => { + mergeSchemas([schema1, schema2], { defaultResolver }) + }, { + name: 'JsonSchemaMergeError', + code: 'JSON_SCHEMA_MERGE_ERROR', + message: 'Failed to merge "format" keyword schemas.', + schemas: ['date-time', 'date'] + }) +}) diff --git a/test/id.test.js b/test/id.test.js new file mode 100644 index 0000000..9221738 --- /dev/null +++ b/test/id.test.js @@ -0,0 +1,22 @@ +'use strict' + +const assert = require('node:assert/strict') +const { test } = require('node:test') +const { mergeSchemas } = require('../index') +const { defaultResolver } = require('./utils') + +test('should skip $id keyword if they are equal', () => { + const schema1 = { $id: 'foo', type: 'string' } + const schema2 = { $id: 'foo', type: 'string' } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'string' }) +}) + +test('should skip $id keyword if they are different', () => { + const schema1 = { $id: 'foo', type: 'string' } + const schema2 = { $id: 'bar', type: 'string' } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'string' }) +}) diff --git a/test/if-then-else.test.js b/test/if-then-else.test.js new file mode 100644 index 0000000..2135611 --- /dev/null +++ b/test/if-then-else.test.js @@ -0,0 +1,550 @@ +'use strict' + +const assert = require('node:assert/strict') +const { test } = require('node:test') +const { mergeSchemas } = require('../index') +const { defaultResolver } = require('./utils') + +test('should merge empty schema and if/then/else keywords', () => { + const schema1 = {} + const schema2 = { + if: { + type: 'string', + const: 'foo' + }, + then: { + type: 'string', + const: 'bar' + }, + else: { + type: 'string', + const: 'baz' + } + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + if: { + type: 'string', + const: 'foo' + }, + then: { + type: 'string', + const: 'bar' + }, + else: { + type: 'string', + const: 'baz' + } + }) +}) + +test('should merge if/then/else schema with an empty schema', () => { + const schema1 = { + if: { + type: 'string', + const: 'foo' + }, + then: { + type: 'string', + const: 'bar' + }, + else: { + type: 'string', + const: 'baz' + } + } + const schema2 = {} + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + if: { + type: 'string', + const: 'foo' + }, + then: { + type: 'string', + const: 'bar' + }, + else: { + type: 'string', + const: 'baz' + } + }) +}) + +test('should merge two if/then/else schemas', () => { + const schema1 = { + type: 'object', + if: { + properties: { + foo1: { type: 'string', const: 'foo1' } + } + }, + then: { + properties: { + bar1: { type: 'string', const: 'bar1' } + } + }, + else: { + properties: { + baz1: { type: 'string', const: 'baz1' } + } + } + } + const schema2 = { + type: 'object', + if: { + properties: { + foo2: { type: 'string', const: 'foo2' } + } + }, + then: { + properties: { + bar2: { type: 'string', const: 'bar2' } + } + }, + else: { + properties: { + baz2: { type: 'string', const: 'baz2' } + } + } + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + type: 'object', + if: { + properties: { + foo1: { type: 'string', const: 'foo1' } + } + }, + then: { + properties: { + bar1: { type: 'string', const: 'bar1' } + }, + if: { + properties: { + foo2: { type: 'string', const: 'foo2' } + } + }, + then: { + properties: { + bar2: { type: 'string', const: 'bar2' } + } + }, + else: { + properties: { + baz2: { type: 'string', const: 'baz2' } + } + } + }, + else: { + properties: { + baz1: { type: 'string', const: 'baz1' } + }, + if: { + properties: { + foo2: { type: 'string', const: 'foo2' } + } + }, + then: { + properties: { + bar2: { type: 'string', const: 'bar2' } + } + }, + else: { + properties: { + baz2: { type: 'string', const: 'baz2' } + } + } + } + }) +}) + +test('should merge three if/then/else schemas', () => { + const schema1 = { + type: 'object', + if: { + properties: { + foo1: { type: 'string', const: 'foo1' } + } + }, + then: { + properties: { + bar1: { type: 'string', const: 'bar1' } + } + }, + else: { + properties: { + baz1: { type: 'string', const: 'baz1' } + } + } + } + const schema2 = { + type: 'object', + if: { + properties: { + foo2: { type: 'string', const: 'foo2' } + } + }, + then: { + properties: { + bar2: { type: 'string', const: 'bar2' } + } + }, + else: { + properties: { + baz2: { type: 'string', const: 'baz2' } + } + } + } + const schema3 = { + type: 'object', + if: { + properties: { + foo3: { type: 'string', const: 'foo3' } + } + }, + then: { + properties: { + bar3: { type: 'string', const: 'bar3' } + } + }, + else: { + properties: { + baz3: { type: 'string', const: 'baz3' } + } + } + } + + const mergedSchema = mergeSchemas([schema1, schema2, schema3], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + type: 'object', + if: { + properties: { + foo1: { type: 'string', const: 'foo1' } + } + }, + then: { + properties: { + bar1: { type: 'string', const: 'bar1' } + }, + if: { + properties: { + foo2: { type: 'string', const: 'foo2' } + } + }, + then: { + properties: { + bar2: { type: 'string', const: 'bar2' } + }, + if: { + properties: { + foo3: { type: 'string', const: 'foo3' } + } + }, + then: { + properties: { + bar3: { type: 'string', const: 'bar3' } + } + }, + else: { + properties: { + baz3: { type: 'string', const: 'baz3' } + } + } + }, + else: { + properties: { + baz2: { type: 'string', const: 'baz2' } + }, + if: { + properties: { + foo3: { type: 'string', const: 'foo3' } + } + }, + then: { + properties: { + bar3: { type: 'string', const: 'bar3' } + } + }, + else: { + properties: { + baz3: { type: 'string', const: 'baz3' } + } + } + } + }, + else: { + properties: { + baz1: { type: 'string', const: 'baz1' } + }, + if: { + properties: { + foo2: { type: 'string', const: 'foo2' } + } + }, + then: { + properties: { + bar2: { type: 'string', const: 'bar2' } + }, + if: { + properties: { + foo3: { type: 'string', const: 'foo3' } + } + }, + then: { + properties: { + bar3: { type: 'string', const: 'bar3' } + } + }, + else: { + properties: { + baz3: { type: 'string', const: 'baz3' } + } + } + }, + else: { + properties: { + baz2: { type: 'string', const: 'baz2' } + }, + if: { + properties: { + foo3: { type: 'string', const: 'foo3' } + } + }, + then: { + properties: { + bar3: { type: 'string', const: 'bar3' } + } + }, + else: { + properties: { + baz3: { type: 'string', const: 'baz3' } + } + } + } + } + }) +}) + +test('should two if/then keyword schemas', () => { + const schema1 = { + type: 'object', + if: { + properties: { + foo1: { type: 'string', const: 'foo1' } + } + }, + then: { + properties: { + bar1: { type: 'string', const: 'bar1' } + } + } + } + + const schema2 = { + type: 'object', + if: { + properties: { + foo2: { type: 'string', const: 'foo2' } + } + }, + then: { + properties: { + bar2: { type: 'string', const: 'bar2' } + } + } + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + type: 'object', + if: { + properties: { + foo1: { type: 'string', const: 'foo1' } + } + }, + then: { + properties: { + bar1: { type: 'string', const: 'bar1' } + }, + if: { + properties: { + foo2: { type: 'string', const: 'foo2' } + } + }, + then: { + properties: { + bar2: { type: 'string', const: 'bar2' } + } + } + } + }) +}) + +test('should two if/else keyword schemas', () => { + const schema1 = { + type: 'object', + if: { + properties: { + foo1: { type: 'string', const: 'foo1' } + } + }, + else: { + properties: { + bar1: { type: 'string', const: 'bar1' } + } + } + } + + const schema2 = { + type: 'object', + if: { + properties: { + foo2: { type: 'string', const: 'foo2' } + } + }, + else: { + properties: { + bar2: { type: 'string', const: 'bar2' } + } + } + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + type: 'object', + if: { + properties: { + foo1: { type: 'string', const: 'foo1' } + } + }, + else: { + properties: { + bar1: { type: 'string', const: 'bar1' } + }, + if: { + properties: { + foo2: { type: 'string', const: 'foo2' } + } + }, + else: { + properties: { + bar2: { type: 'string', const: 'bar2' } + } + } + } + }) +}) + +test('should two if/then and if/else keyword schemas', () => { + const schema1 = { + type: 'object', + if: { + properties: { + foo1: { type: 'string', const: 'foo1' } + } + }, + then: { + properties: { + bar1: { type: 'string', const: 'bar1' } + } + } + } + + const schema2 = { + type: 'object', + if: { + properties: { + foo2: { type: 'string', const: 'foo2' } + } + }, + else: { + properties: { + bar2: { type: 'string', const: 'bar2' } + } + } + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + type: 'object', + if: { + properties: { + foo1: { type: 'string', const: 'foo1' } + } + }, + then: { + properties: { + bar1: { type: 'string', const: 'bar1' } + }, + if: { + properties: { + foo2: { type: 'string', const: 'foo2' } + } + }, + else: { + properties: { + bar2: { type: 'string', const: 'bar2' } + } + } + } + }) +}) + +test('should two if/else and if/then keyword schemas', () => { + const schema1 = { + type: 'object', + if: { + properties: { + foo1: { type: 'string', const: 'foo1' } + } + }, + else: { + properties: { + bar1: { type: 'string', const: 'bar1' } + } + } + } + + const schema2 = { + type: 'object', + if: { + properties: { + foo2: { type: 'string', const: 'foo2' } + } + }, + then: { + properties: { + bar2: { type: 'string', const: 'bar2' } + } + } + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + type: 'object', + if: { + properties: { + foo1: { type: 'string', const: 'foo1' } + } + }, + else: { + properties: { + bar1: { type: 'string', const: 'bar1' } + }, + if: { + properties: { + foo2: { type: 'string', const: 'foo2' } + } + }, + then: { + properties: { + bar2: { type: 'string', const: 'bar2' } + } + } + } + }) +}) diff --git a/test/items.test.js b/test/items.test.js new file mode 100644 index 0000000..b780530 --- /dev/null +++ b/test/items.test.js @@ -0,0 +1,152 @@ +'use strict' + +const assert = require('node:assert/strict') +const { test } = require('node:test') +const { mergeSchemas } = require('../index') +const { defaultResolver } = require('./utils') + +test('should merge empty schema and items keyword', () => { + const schema1 = { type: 'array' } + const schema2 = { + type: 'array', + items: { + type: 'object', + properties: { + foo: { type: 'string' } + } + } + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + type: 'array', + items: { + type: 'object', + properties: { + foo: { type: 'string' } + } + } + }) +}) + +test('should merge two equal item schemas', () => { + const schema1 = { + type: 'array', + items: { + type: 'object', + properties: { + foo: { type: 'string' } + } + } + } + + const schema2 = { + type: 'array', + items: { + type: 'object', + properties: { + foo: { type: 'string' } + } + } + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + type: 'array', + items: { + type: 'object', + properties: { + foo: { type: 'string' } + } + } + }) +}) + +test('should merge two different sets of item schemas', () => { + const schema1 = { + type: 'array', + items: { + type: 'object', + properties: { + foo: { type: 'string' }, + bar: { type: 'number' } + } + } + } + + const schema2 = { + type: 'array', + items: { + type: 'object', + properties: { + foo: { type: 'string' }, + baz: { type: 'boolean' } + } + } + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + type: 'array', + items: { + type: 'object', + properties: { + foo: { type: 'string' }, + bar: { type: 'number' }, + baz: { type: 'boolean' } + } + } + }) +}) + +test('should merge two different sets of item schemas with additionalItems', () => { + const schema1 = { + type: 'array', + items: [ + { + type: 'object', + properties: { + foo: { type: 'string', const: 'foo' } + } + } + ], + additionalItems: { + type: 'object', + properties: { + baz: { type: 'string', const: 'baz' } + } + } + } + + const schema2 = { + type: 'array', + items: { + type: 'object', + properties: { + foo: { type: 'string' }, + baz: { type: 'string' } + } + } + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + type: 'array', + items: [ + { + type: 'object', + properties: { + foo: { type: 'string', const: 'foo' }, + baz: { type: 'string' } + } + } + ], + additionalItems: { + type: 'object', + properties: { + foo: { type: 'string' }, + baz: { type: 'string', const: 'baz' } + } + } + }) +}) diff --git a/test/max-items.test.js b/test/max-items.test.js new file mode 100644 index 0000000..939cf29 --- /dev/null +++ b/test/max-items.test.js @@ -0,0 +1,30 @@ +'use strict' + +const assert = require('node:assert/strict') +const { test } = require('node:test') +const { mergeSchemas } = require('../index') +const { defaultResolver } = require('./utils') + +test('should merge empty schema and maxItems keyword', () => { + const schema1 = { type: 'array' } + const schema2 = { type: 'array', maxItems: 42 } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'array', maxItems: 42 }) +}) + +test('should merge equal maxItems values', () => { + const schema1 = { type: 'array', maxItems: 42 } + const schema2 = { type: 'array', maxItems: 42 } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'array', maxItems: 42 }) +}) + +test('should merge different maxItems values', () => { + const schema1 = { type: 'array', maxItems: 42 } + const schema2 = { type: 'array', maxItems: 43 } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'array', maxItems: 42 }) +}) diff --git a/test/max-length.test.js b/test/max-length.test.js new file mode 100644 index 0000000..c97002f --- /dev/null +++ b/test/max-length.test.js @@ -0,0 +1,30 @@ +'use strict' + +const assert = require('node:assert/strict') +const { test } = require('node:test') +const { mergeSchemas } = require('../index') +const { defaultResolver } = require('./utils') + +test('should merge empty schema and maxLength keyword', () => { + const schema1 = { type: 'string' } + const schema2 = { type: 'string', maxLength: 42 } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'string', maxLength: 42 }) +}) + +test('should merge equal maxLength values', () => { + const schema1 = { type: 'string', maxLength: 42 } + const schema2 = { type: 'string', maxLength: 42 } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'string', maxLength: 42 }) +}) + +test('should merge different maxLength values', () => { + const schema1 = { type: 'string', maxLength: 42 } + const schema2 = { type: 'string', maxLength: 43 } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'string', maxLength: 42 }) +}) diff --git a/test/max-properties.test.js b/test/max-properties.test.js new file mode 100644 index 0000000..b5b3949 --- /dev/null +++ b/test/max-properties.test.js @@ -0,0 +1,30 @@ +'use strict' + +const assert = require('node:assert/strict') +const { test } = require('node:test') +const { mergeSchemas } = require('../index') +const { defaultResolver } = require('./utils') + +test('should merge empty schema and maxProperties keyword', () => { + const schema1 = { type: 'object' } + const schema2 = { type: 'object', maxProperties: 42 } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'object', maxProperties: 42 }) +}) + +test('should merge equal maxProperties values', () => { + const schema1 = { type: 'object', maxProperties: 42 } + const schema2 = { type: 'object', maxProperties: 42 } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'object', maxProperties: 42 }) +}) + +test('should merge different maxProperties values', () => { + const schema1 = { type: 'object', maxProperties: 42 } + const schema2 = { type: 'object', maxProperties: 43 } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'object', maxProperties: 42 }) +}) diff --git a/test/maximum.test.js b/test/maximum.test.js new file mode 100644 index 0000000..4a0c982 --- /dev/null +++ b/test/maximum.test.js @@ -0,0 +1,30 @@ +'use strict' + +const assert = require('node:assert/strict') +const { test } = require('node:test') +const { mergeSchemas } = require('../index') +const { defaultResolver } = require('./utils') + +test('should merge empty schema and maximum keyword', () => { + const schema1 = { type: 'number' } + const schema2 = { type: 'number', maximum: 42 } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'number', maximum: 42 }) +}) + +test('should merge equal maximum values', () => { + const schema1 = { type: 'number', maximum: 42 } + const schema2 = { type: 'number', maximum: 42 } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'number', maximum: 42 }) +}) + +test('should merge different maximum values', () => { + const schema1 = { type: 'integer', maximum: 42 } + const schema2 = { type: 'integer', maximum: 43 } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'integer', maximum: 42 }) +}) diff --git a/test/merge-schema.test.js b/test/merge-schema.test.js new file mode 100644 index 0000000..a41c3a9 --- /dev/null +++ b/test/merge-schema.test.js @@ -0,0 +1,29 @@ +'use strict' + +const assert = require('node:assert/strict') +const { test } = require('node:test') +const { mergeSchemas } = require('../index') +const { defaultResolver } = require('./utils') + +test('should return an empty schema if passing an empty array', () => { + const mergedSchema = mergeSchemas([], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, {}) +}) + +test('should return true if passing all true values', () => { + const mergedSchema = mergeSchemas([true, true, true], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, true) +}) + +test('should return true if passing all false values', () => { + const mergedSchema = mergeSchemas([false, false, false], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, false) +}) + +test('should return true if passing at least one false schema', () => { + const schema1 = { type: 'string' } + const schema2 = { type: 'number' } + + const mergedSchema = mergeSchemas([schema1, schema2, false], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, false) +}) diff --git a/test/min-items.test.js b/test/min-items.test.js new file mode 100644 index 0000000..b5d5073 --- /dev/null +++ b/test/min-items.test.js @@ -0,0 +1,30 @@ +'use strict' + +const assert = require('node:assert/strict') +const { test } = require('node:test') +const { mergeSchemas } = require('../index') +const { defaultResolver } = require('./utils') + +test('should merge empty schema and minItems keyword', () => { + const schema1 = { type: 'array' } + const schema2 = { type: 'array', minItems: 42 } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'array', minItems: 42 }) +}) + +test('should merge equal minItems values', () => { + const schema1 = { type: 'array', minItems: 42 } + const schema2 = { type: 'array', minItems: 42 } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'array', minItems: 42 }) +}) + +test('should merge different minItems values', () => { + const schema1 = { type: 'array', minItems: 42 } + const schema2 = { type: 'array', minItems: 43 } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'array', minItems: 43 }) +}) diff --git a/test/min-length.test.js b/test/min-length.test.js new file mode 100644 index 0000000..64a2c44 --- /dev/null +++ b/test/min-length.test.js @@ -0,0 +1,30 @@ +'use strict' + +const assert = require('node:assert/strict') +const { test } = require('node:test') +const { mergeSchemas } = require('../index') +const { defaultResolver } = require('./utils') + +test('should merge empty schema and minLength keyword', () => { + const schema1 = { type: 'string' } + const schema2 = { type: 'string', minLength: 42 } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'string', minLength: 42 }) +}) + +test('should merge equal minLength values', () => { + const schema1 = { type: 'string', minLength: 42 } + const schema2 = { type: 'string', minLength: 42 } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'string', minLength: 42 }) +}) + +test('should merge different minLength values', () => { + const schema1 = { type: 'string', minLength: 42 } + const schema2 = { type: 'string', minLength: 43 } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'string', minLength: 43 }) +}) diff --git a/test/min-properties.test.js b/test/min-properties.test.js new file mode 100644 index 0000000..bfe4599 --- /dev/null +++ b/test/min-properties.test.js @@ -0,0 +1,30 @@ +'use strict' + +const assert = require('node:assert/strict') +const { test } = require('node:test') +const { mergeSchemas } = require('../index') +const { defaultResolver } = require('./utils') + +test('should merge empty schema and minProperties keyword', () => { + const schema1 = { type: 'object' } + const schema2 = { type: 'object', minProperties: 42 } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'object', minProperties: 42 }) +}) + +test('should merge equal minItems values', () => { + const schema1 = { type: 'object', minProperties: 42 } + const schema2 = { type: 'object', minProperties: 42 } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'object', minProperties: 42 }) +}) + +test('should merge different minItems values', () => { + const schema1 = { type: 'object', minProperties: 42 } + const schema2 = { type: 'object', minProperties: 43 } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'object', minProperties: 43 }) +}) diff --git a/test/minimum.test.js b/test/minimum.test.js new file mode 100644 index 0000000..f0dd105 --- /dev/null +++ b/test/minimum.test.js @@ -0,0 +1,30 @@ +'use strict' + +const assert = require('node:assert/strict') +const { test } = require('node:test') +const { mergeSchemas } = require('../index') +const { defaultResolver } = require('./utils') + +test('should merge empty schema and minimum keyword', () => { + const schema1 = { type: 'number' } + const schema2 = { type: 'number', minimum: 42 } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'number', minimum: 42 }) +}) + +test('should merge equal minimum values', () => { + const schema1 = { type: 'number', minimum: 42 } + const schema2 = { type: 'number', minimum: 42 } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'number', minimum: 42 }) +}) + +test('should merge different minimum values', () => { + const schema1 = { type: 'integer', minimum: 42 } + const schema2 = { type: 'integer', minimum: 43 } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'integer', minimum: 43 }) +}) diff --git a/test/multiple-of.test.js b/test/multiple-of.test.js new file mode 100644 index 0000000..f07a32b --- /dev/null +++ b/test/multiple-of.test.js @@ -0,0 +1,36 @@ +'use strict' + +const assert = require('node:assert/strict') +const { test } = require('node:test') +const { mergeSchemas } = require('../index') +const { defaultResolver } = require('./utils') + +test('should merge empty schema and multipleOf keyword', () => { + const schema1 = { type: 'number' } + const schema2 = { type: 'number', multipleOf: 42 } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'number', multipleOf: 42 }) +}) + +test('should merge two schemas with multipleOf keywords', () => { + const schema1 = { type: 'number', multipleOf: 2 } + const schema2 = { type: 'number', multipleOf: 3 } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'number', multipleOf: 6 }) +}) + +test('should merge multiple schemas with float multipleOf keywords', () => { + const schema1 = { type: 'number', multipleOf: 0.2 } + const schema2 = { type: 'number', multipleOf: 2 } + const schema3 = { type: 'number', multipleOf: 2 } + const schema4 = { type: 'number', multipleOf: 0.5 } + const schema5 = { type: 'number', multipleOf: 1.5 } + + const mergedSchema = mergeSchemas( + [schema1, schema2, schema3, schema4, schema5], + { defaultResolver } + ) + assert.deepStrictEqual(mergedSchema, { type: 'number', multipleOf: 6 }) +}) diff --git a/test/not.test.js b/test/not.test.js new file mode 100644 index 0000000..e73131d --- /dev/null +++ b/test/not.test.js @@ -0,0 +1,29 @@ +'use strict' + +const assert = require('node:assert/strict') +const { test } = require('node:test') +const { mergeSchemas } = require('../index') +const { defaultResolver } = require('./utils') + +test('should merge two "not" keyword schemas', () => { + const schema1 = { + type: 'array', + not: { + type: 'string' + } + } + const schema2 = { + type: 'array', + not: { + type: 'string', minLength: 1 + } + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + type: 'array', + not: { + type: 'string', minLength: 1 + } + }) +}) diff --git a/test/nullable.test.js b/test/nullable.test.js new file mode 100644 index 0000000..ef5fcfd --- /dev/null +++ b/test/nullable.test.js @@ -0,0 +1,30 @@ +'use strict' + +const assert = require('node:assert/strict') +const { test } = require('node:test') +const { mergeSchemas } = require('../index') +const { defaultResolver } = require('./utils') + +test('should merge empty schema and nullable = true keyword', () => { + const schema1 = { type: 'string' } + const schema2 = { type: 'string', nullable: true } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'string', nullable: true }) +}) + +test('should merge empty schema and nullable = false keyword', () => { + const schema1 = { type: 'string' } + const schema2 = { type: 'string', nullable: false } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'string', nullable: false }) +}) + +test('should merge schemas with nullable true and false values', () => { + const schema1 = { type: 'string', nullable: false } + const schema2 = { type: 'string', nullable: true } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'string', nullable: false }) +}) diff --git a/test/one-of.test.js b/test/one-of.test.js new file mode 100644 index 0000000..94eab45 --- /dev/null +++ b/test/one-of.test.js @@ -0,0 +1,144 @@ +'use strict' + +const assert = require('node:assert/strict') +const { test } = require('node:test') +const { mergeSchemas, MergeError } = require('../index') +const { defaultResolver } = require('./utils') + +test('should merge empty schema and oneOf keyword', () => { + const schema1 = {} + const schema2 = { + oneOf: [ + { type: 'string', const: 'foo' } + ] + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + oneOf: [ + { type: 'string', const: 'foo' } + ] + }) +}) + +test('should merge two schemas with oneOfs schemas', () => { + const schema1 = { + oneOf: [ + { type: 'string', enum: ['foo1', 'foo2', 'foo3'] }, + { type: 'string', enum: ['foo3', 'foo4', 'foo5'] } + ] + } + const schema2 = { + oneOf: [ + { type: 'string', enum: ['foo2', 'foo3', 'foo4'] }, + { type: 'string', enum: ['foo3', 'foo6', 'foo7'] } + ] + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + oneOf: [ + { type: 'string', enum: ['foo2', 'foo3'] }, + { type: 'string', enum: ['foo3'] }, + { type: 'string', enum: ['foo3', 'foo4'] }, + { type: 'string', enum: ['foo3'] } + ] + }) +}) + +test('should merge three schemas with oneOfs schemas', () => { + const schema1 = { + oneOf: [ + { type: 'string', enum: ['foo1', 'foo2', 'foo3', 'foo4'] }, + { type: 'string', enum: ['foo3', 'foo4', 'foo5', 'foo7'] } + ] + } + const schema2 = { + oneOf: [ + { type: 'string', enum: ['foo2', 'foo3', 'foo4', 'foo5'] }, + { type: 'string', enum: ['foo3', 'foo6', 'foo7', 'foo8'] } + ] + } + + const schema3 = { + oneOf: [ + { type: 'string', enum: ['foo1', 'foo3', 'foo5', 'foo7'] }, + { type: 'string', enum: ['foo2', 'foo4', 'foo6', 'foo8'] } + ] + } + + const mergedSchema = mergeSchemas( + [schema1, schema2, schema3], + { defaultResolver } + ) + assert.deepStrictEqual(mergedSchema, { + oneOf: [ + { type: 'string', enum: ['foo3'] }, + { type: 'string', enum: ['foo2', 'foo4'] }, + { type: 'string', enum: ['foo3'] }, + { type: 'string', enum: ['foo3', 'foo5'] }, + { type: 'string', enum: ['foo4'] }, + { type: 'string', enum: ['foo3', 'foo7'] } + ] + }) +}) + +test('should throw a non MergeError error during oneOf merge', () => { + const schema1 = { + oneOf: [ + { type: 'string', customKeyword: 42 }, + { type: 'string', customKeyword: 43 } + ] + } + const schema2 = { + oneOf: [ + { type: 'string', customKeyword: 44 }, + { type: 'string', customKeyword: 45 } + ] + } + + assert.throws(() => { + mergeSchemas( + [schema1, schema2], + { + resolvers: { + customKeyword: () => { + throw new Error('Custom error') + } + }, + defaultResolver + } + ) + }, { + name: 'Error', + message: 'Custom error' + }) +}) + +test('should not throw a MergeError error during oneOf merge', () => { + const schema1 = { + oneOf: [ + { type: 'string', customKeyword: 42 }, + { type: 'string', customKeyword: 43 } + ] + } + const schema2 = { + oneOf: [ + { type: 'string', customKeyword: 44 }, + { type: 'string', customKeyword: 45 } + ] + } + + const mergedSchema = mergeSchemas( + [schema1, schema2], + { + resolvers: { + customKeyword: (keyword, values) => { + throw new MergeError(keyword, values) + } + }, + defaultResolver + } + ) + assert.deepStrictEqual(mergedSchema, { oneOf: [] }) +}) diff --git a/test/properties.test.js b/test/properties.test.js new file mode 100644 index 0000000..6e26492 --- /dev/null +++ b/test/properties.test.js @@ -0,0 +1,312 @@ +'use strict' + +const assert = require('node:assert/strict') +const { test } = require('node:test') +const { mergeSchemas } = require('../index') +const { defaultResolver } = require('./utils') + +test('should merge empty schema and properties keyword', () => { + const schema1 = { type: 'object' } + const schema2 = { + type: 'object', + properties: { + foo: { type: 'string' } + } + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + type: 'object', + properties: { + foo: { type: 'string' } + } + }) +}) + +test('should merge two equal property schemas', () => { + const schema1 = { + type: 'object', + properties: { + foo: { type: 'string' } + } + } + + const schema2 = { + type: 'object', + properties: { + foo: { type: 'string' } + } + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + type: 'object', + properties: { + foo: { type: 'string' } + } + }) +}) + +test('should merge two different sets of property schemas', () => { + const schema1 = { + type: 'object', + properties: { + foo: { type: 'string' }, + bar: { type: 'number' } + } + } + + const schema2 = { + type: 'object', + properties: { + foo: { type: 'string' }, + baz: { type: 'boolean' } + } + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + type: 'object', + properties: { + foo: { type: 'string' }, + bar: { type: 'number' }, + baz: { type: 'boolean' } + } + }) +}) + +test('should merge property with different schemas', () => { + const schema1 = { + type: 'object', + properties: { + foo: { + type: ['string', 'number'], + enum: ['42', 2, 3] + } + } + } + + const schema2 = { + type: 'object', + properties: { + foo: { + type: ['number', 'null'], + enum: [1, 2, 3, null] + } + } + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + type: 'object', + properties: { + foo: { type: 'number', enum: [2, 3] } + } + }) +}) + +test('should merge properties if one schema has additionalProperties = false', () => { + const schema1 = { + type: 'object', + properties: { + foo: { type: 'string' }, + bar: { type: 'number' } + }, + additionalProperties: false + } + + const schema2 = { + type: 'object', + properties: { + foo: { type: 'string', enum: ['42'] }, + baz: { type: 'string' } + } + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + type: 'object', + properties: { + foo: { type: 'string', enum: ['42'] }, + bar: { type: 'number' }, + baz: false + }, + additionalProperties: false + }) +}) + +test('should merge properties if both schemas have additionalProperties = false', () => { + const schema1 = { + type: 'object', + properties: { + foo: { type: 'string' }, + bar: { type: 'number' } + }, + additionalProperties: false + } + + const schema2 = { + type: 'object', + properties: { + foo: { type: 'string', enum: ['42'] }, + baz: { type: 'string' } + }, + additionalProperties: false + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + type: 'object', + properties: { + foo: { type: 'string', enum: ['42'] }, + bar: false, + baz: false + }, + additionalProperties: false + }) +}) + +test('should merge properties if one schema has additionalProperties schema', () => { + const schema1 = { + type: 'object', + properties: { + foo: { type: 'string' }, + bar: { type: 'number' } + }, + additionalProperties: { type: 'string', enum: ['43'] } + } + + const schema2 = { + type: 'object', + properties: { + foo: { type: 'string', enum: ['42'] }, + baz: { type: 'string' } + } + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + type: 'object', + properties: { + foo: { type: 'string', enum: ['42'] }, + bar: { type: 'number' }, + baz: { type: 'string', enum: ['43'] } + }, + additionalProperties: { type: 'string', enum: ['43'] } + }) +}) + +test('should merge properties if both schemas have additionalProperties schemas', () => { + const schema1 = { + type: 'object', + properties: { + foo: { type: 'string' }, + bar: { type: 'number' } + }, + additionalProperties: { + type: ['string', 'number', 'null'], + enum: ['45', '43', 41, null] + } + } + + const schema2 = { + type: 'object', + properties: { + foo: { type: 'string', enum: ['42'] }, + baz: { type: 'string' } + }, + additionalProperties: { + type: ['string', 'boolean', 'number'], + enum: ['44', '43', true, 41] + } + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + type: 'object', + properties: { + foo: { type: 'string', enum: ['42'] }, + bar: { type: 'number', enum: ['44', '43', true, 41] }, + baz: { type: 'string', enum: ['45', '43', 41, null] } + }, + additionalProperties: { type: ['string', 'number'], enum: ['43', 41] } + }) +}) + +test('should merge properties if one schema has patternProperties schema', () => { + const schema1 = { + type: 'object', + properties: { + foo: { type: 'string' }, + bar: { type: 'number' } + }, + patternProperties: { + '^baz$': { type: 'string', enum: ['43'] } + } + } + + const schema2 = { + type: 'object', + properties: { + foo: { type: 'string', enum: ['42'] }, + baz: { type: 'string' }, + qux: { type: 'string' } + } + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + type: 'object', + properties: { + foo: { type: 'string', enum: ['42'] }, + bar: { type: 'number' }, + baz: { type: 'string', enum: ['43'] }, + qux: { type: 'string' } + }, + patternProperties: { + '^baz$': { type: 'string', enum: ['43'] } + } + }) +}) + +test('should merge properties if both schemas have patternProperties schemas', () => { + const schema1 = { + type: 'object', + properties: { + foo: { type: 'string' }, + bar: { type: 'number' }, + bak: { type: 'number' } + }, + patternProperties: { + '^baz$': { type: 'string', enum: ['43'] } + } + } + + const schema2 = { + type: 'object', + properties: { + foo: { type: 'string', enum: ['42'] }, + baz: { type: 'string' }, + qux: { type: 'string' } + }, + patternProperties: { + '^bar$': { type: 'number', minimum: 2 } + } + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + type: 'object', + properties: { + foo: { type: 'string', enum: ['42'] }, + bar: { type: 'number', minimum: 2 }, + bak: { type: 'number' }, + baz: { type: 'string', enum: ['43'] }, + qux: { type: 'string' } + }, + patternProperties: { + '^bar$': { type: 'number', minimum: 2 }, + '^baz$': { type: 'string', enum: ['43'] } + } + }) +}) diff --git a/test/property-names.test.js b/test/property-names.test.js new file mode 100644 index 0000000..9f27572 --- /dev/null +++ b/test/property-names.test.js @@ -0,0 +1,49 @@ +'use strict' + +const assert = require('node:assert/strict') +const { test } = require('node:test') +const { mergeSchemas } = require('../index') +const { defaultResolver } = require('./utils') + +test('should merge empty schema and propertyNames keyword', () => { + const schema1 = {} + const schema2 = { + type: 'object', + propertyNames: { + pattern: '^[a-zA-Z]+$', + minLength: 42 + } + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + type: 'object', + propertyNames: { + pattern: '^[a-zA-Z]+$', + minLength: 42 + } + }) +}) + +test('should merge two propertyNames keyword schemas', () => { + const schema1 = { + type: 'object', + propertyNames: { + minLength: 42 + } + } + const schema2 = { + type: 'object', + propertyNames: { + minLength: 43 + } + } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { + type: 'object', + propertyNames: { + minLength: 43 + } + }) +}) diff --git a/test/required.test.js b/test/required.test.js new file mode 100644 index 0000000..3afa392 --- /dev/null +++ b/test/required.test.js @@ -0,0 +1,30 @@ +'use strict' + +const assert = require('node:assert/strict') +const { test } = require('node:test') +const { mergeSchemas } = require('../index') +const { defaultResolver } = require('./utils') + +test('should merge empty schema and required keyword', () => { + const schema1 = { type: 'object' } + const schema2 = { type: 'object', required: ['foo'] } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'object', required: ['foo'] }) +}) + +test('should merge two equal required keywords', () => { + const schema1 = { type: 'object', required: ['foo'] } + const schema2 = { type: 'object', required: ['foo'] } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'object', required: ['foo'] }) +}) + +test('should merge two different required keywords', () => { + const schema1 = { type: 'object', required: ['foo', 'bar'] } + const schema2 = { type: 'object', required: ['foo', 'baz'] } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'object', required: ['foo', 'bar', 'baz'] }) +}) diff --git a/test/type.test.js b/test/type.test.js new file mode 100644 index 0000000..69c0861 --- /dev/null +++ b/test/type.test.js @@ -0,0 +1,52 @@ +'use strict' + +const assert = require('node:assert/strict') +const { test } = require('node:test') +const { mergeSchemas } = require('../index') +const { defaultResolver } = require('./utils') + +test('should merge equal type values', () => { + const schema1 = { type: 'string' } + const schema2 = { type: 'string' } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'string' }) +}) + +test('should merge array type values', () => { + const schema1 = { type: ['string', 'number'] } + const schema2 = { type: ['null', 'string'] } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'string' }) +}) + +test('should merge array type values', () => { + const schema1 = { type: ['string', 'number'] } + const schema2 = { type: 'string' } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'string' }) +}) + +test('should merge array type values', () => { + const schema1 = { type: ['number', 'string', 'boolean'] } + const schema2 = { type: ['string', 'number', 'null'] } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: ['number', 'string'] }) +}) + +test('should throw an error if can not merge type values', () => { + const schema1 = { type: 'string' } + const schema2 = { type: 'number' } + + assert.throws(() => { + mergeSchemas([schema1, schema2], { defaultResolver }) + }, { + name: 'JsonSchemaMergeError', + code: 'JSON_SCHEMA_MERGE_ERROR', + message: 'Failed to merge "type" keyword schemas.', + schemas: [['string'], ['number']] + }) +}) diff --git a/test/unique-items.test.js b/test/unique-items.test.js new file mode 100644 index 0000000..61a55e8 --- /dev/null +++ b/test/unique-items.test.js @@ -0,0 +1,38 @@ +'use strict' + +const assert = require('node:assert/strict') +const { test } = require('node:test') +const { mergeSchemas } = require('../index') +const { defaultResolver } = require('./utils') + +test('should merge empty schema and uniqueItems keyword', () => { + const schema1 = { type: 'array' } + const schema2 = { type: 'array', uniqueItems: true } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'array', uniqueItems: true }) +}) + +test('should merge two equal uniqueItems keyword schemas = true', () => { + const schema1 = { type: 'array', uniqueItems: true } + const schema2 = { type: 'array', uniqueItems: true } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'array', uniqueItems: true }) +}) + +test('should merge two equal uniqueItems keyword schemas = false', () => { + const schema1 = { type: 'array', uniqueItems: false } + const schema2 = { type: 'array', uniqueItems: false } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'array', uniqueItems: false }) +}) + +test('should merge two equal uniqueItems keyword schemas', () => { + const schema1 = { type: 'array', uniqueItems: false } + const schema2 = { type: 'array', uniqueItems: true } + + const mergedSchema = mergeSchemas([schema1, schema2], { defaultResolver }) + assert.deepStrictEqual(mergedSchema, { type: 'array', uniqueItems: true }) +}) diff --git a/test/utils.js b/test/utils.js new file mode 100644 index 0000000..956ba24 --- /dev/null +++ b/test/utils.js @@ -0,0 +1,9 @@ +'use strict' + +function defaultResolver () { + throw new Error('Default resolver should not be called.') +} + +module.exports = { + defaultResolver +} diff --git a/types/index.d.ts b/types/index.d.ts new file mode 100644 index 0000000..c2dd06c --- /dev/null +++ b/types/index.d.ts @@ -0,0 +1,62 @@ +export type KeywordResolver = ( + keyword: string, + keywordValues: any[], + mergedSchema: any, + parentSchemas: any[], + options: MergeOptions +) => any + +export type KeywordResolvers = { + $id: KeywordResolver, + type: KeywordResolver, + enum: KeywordResolver, + minLength: KeywordResolver, + maxLength: KeywordResolver, + minimum: KeywordResolver, + maximum: KeywordResolver, + multipleOf: KeywordResolver, + exclusiveMinimum: KeywordResolver, + exclusiveMaximum: KeywordResolver, + minItems: KeywordResolver, + maxItems: KeywordResolver, + maxProperties: KeywordResolver, + minProperties: KeywordResolver, + const: KeywordResolver, + default: KeywordResolver, + format: KeywordResolver, + required: KeywordResolver, + properties: KeywordResolver, + patternProperties: KeywordResolver, + additionalProperties: KeywordResolver, + items: KeywordResolver, + additionalItems: KeywordResolver, + definitions: KeywordResolver, + $defs: KeywordResolver, + nullable: KeywordResolver, + oneOf: KeywordResolver, + anyOf: KeywordResolver, + allOf: KeywordResolver, + not: KeywordResolver, + if: KeywordResolver, + then: KeywordResolver, + else: KeywordResolver, + dependencies: KeywordResolver, + dependentRequired: KeywordResolver, + dependentSchemas: KeywordResolver, + propertyNames: KeywordResolver, + uniqueItems: KeywordResolver, + contains: KeywordResolver +} + +export type MergeOptions = { + defaultResolver?: KeywordResolver, + resolvers?: Partial, + // enum of ["throw", "skip", "first"] + onConflict?: "throw" | "skip" | "first" +} + +export function mergeSchemas(schemas: any[], options?: MergeOptions): any; + +export const keywordsResolvers: KeywordResolvers +export const defaultResolver: KeywordResolver + diff --git a/types/index.test-d.ts b/types/index.test-d.ts new file mode 100644 index 0000000..7d82a14 --- /dev/null +++ b/types/index.test-d.ts @@ -0,0 +1,52 @@ +import { mergeSchemas, MergeOptions } from '..' +import { expectType } from 'tsd' + +{ + const schema1 = { type: 'string', enum: ['foo', 'bar'] } + const schema2 = { type: 'string', enum: ['foo', 'baz'] } + + mergeSchemas([schema1, schema2]) +} + +{ + const schema1 = { type: 'string', enum: ['foo', 'bar'] } + const schema2 = { type: 'string', enum: ['foo', 'baz'] } + + const mergeOptions: MergeOptions = { + resolvers: { + enum: ( + keyword: string, + keywordValues: any[], + mergedSchema: any, + parentSchemas: any[], + options: MergeOptions + ) => { + expectType(keyword) + expectType(keywordValues) + expectType(mergedSchema) + expectType(parentSchemas) + expectType(options) + + return keywordValues + } + }, + defaultResolver: ( + keyword: string, + keywordValues: any[], + mergedSchema: any, + parentSchemas: any[], + options: MergeOptions + ) => { + expectType(keyword) + expectType(keywordValues) + expectType(mergedSchema) + expectType(parentSchemas) + expectType(options) + + return keywordValues + }, + onConflict: 'throw' + } + + mergeSchemas([schema1, schema2], mergeOptions) +}