json-schema-diff
Advanced tools
Comparing version
@@ -0,1 +1,19 @@ | ||
<a name="0.12.0"></a> | ||
# [0.12.0](https://bitbucket.org/atlassian/json-schema-diff/compare/0.11.0...0.12.0) (2019-04-09) | ||
### Bug Fixes | ||
* generate a complete difference when schemas with both additionalProperties and properties are changed ([56b0149](https://bitbucket.org/atlassian/json-schema-diff/commits/56b0149)) | ||
### Features | ||
* add support for the allOf keyword ([37513a5](https://bitbucket.org/atlassian/json-schema-diff/commits/37513a5)) | ||
* add support for the maxItems keyword ([770b511](https://bitbucket.org/atlassian/json-schema-diff/commits/770b511)) | ||
* add support for the maxProperties keyword ([dfe643b](https://bitbucket.org/atlassian/json-schema-diff/commits/dfe643b)) | ||
* add support for the minProperties keyword ([a5f428f](https://bitbucket.org/atlassian/json-schema-diff/commits/a5f428f)) | ||
<a name="0.11.0"></a> | ||
@@ -2,0 +20,0 @@ # [0.11.0](https://bitbucket.org/atlassian/json-schema-diff/compare/0.10.0...0.11.0) (2019-03-01) |
@@ -12,8 +12,7 @@ "use strict"; | ||
const RefParser = require("json-schema-ref-parser"); | ||
const util_1 = require("util"); | ||
exports.dereferenceSchema = (schema) => __awaiter(this, void 0, void 0, function* () { | ||
const refParser = new RefParser(); | ||
return util_1.isBoolean(schema) | ||
return typeof schema === 'boolean' | ||
? schema | ||
: yield refParser.dereference(schema, { dereference: { circular: false } }); | ||
}); |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const util_1 = require("util"); | ||
const create_json_set_1 = require("./set-factories/create-json-set"); | ||
const set_1 = require("./set/set"); | ||
const keyword_defaults_1 = require("./set/keyword-defaults"); | ||
const parseSchemaProperties = (schemaProperties = {}) => { | ||
@@ -16,3 +15,3 @@ const objectSetProperties = {}; | ||
if (!type) { | ||
return set_1.allSchemaTypes; | ||
return keyword_defaults_1.defaultTypes; | ||
} | ||
@@ -24,22 +23,27 @@ if (typeof type === 'string') { | ||
}; | ||
const parseRequiredKeyword = (schema) => schema.required || []; | ||
const generateDefaultMinPropertiesKeyword = () => 0; | ||
const generateDefaultMaxItemsKeyword = () => Infinity; | ||
const parseMinItemsKeyword = (schema) => schema.minItems || 0; | ||
const parseCoreSchemaMetaSchema = (schema) => create_json_set_1.createSomeJsonSet({ | ||
const parseRequiredKeyword = (required) => required || keyword_defaults_1.defaultRequired; | ||
const parseNumericKeyword = (keywordValue, defaultValue) => typeof keywordValue === 'number' ? keywordValue : defaultValue; | ||
const parseTypeKeywords = (schema) => create_json_set_1.createSomeJsonSet({ | ||
additionalProperties: parseSchemaOrUndefinedAsJsonSet(schema.additionalProperties), | ||
items: parseSchemaOrUndefinedAsJsonSet(schema.items), | ||
maxItems: generateDefaultMaxItemsKeyword(), | ||
minItems: parseMinItemsKeyword(schema), | ||
minProperties: generateDefaultMinPropertiesKeyword(), | ||
maxItems: parseNumericKeyword(schema.maxItems, keyword_defaults_1.defaultMaxItems), | ||
maxProperties: parseNumericKeyword(schema.maxProperties, keyword_defaults_1.defaultMaxProperties), | ||
minItems: parseNumericKeyword(schema.minItems, keyword_defaults_1.defaultMinItems), | ||
minProperties: parseNumericKeyword(schema.minProperties, keyword_defaults_1.defaultMinProperties), | ||
properties: parseSchemaProperties(schema.properties), | ||
required: parseRequiredKeyword(schema), | ||
required: parseRequiredKeyword(schema.required), | ||
type: parseType(schema.type) | ||
}); | ||
const parseBooleanLogicKeywords = (schema) => (schema.allOf || []).map(parseSchemaOrUndefinedAsJsonSet); | ||
const parseCoreSchemaMetaSchema = (schema) => { | ||
const typeKeywordsSet = parseTypeKeywords(schema); | ||
const booleanLogicKeywordSets = parseBooleanLogicKeywords(schema); | ||
return booleanLogicKeywordSets.reduce((accumulator, set) => accumulator.intersect(set), typeKeywordsSet); | ||
}; | ||
const parseBooleanSchema = (schema) => { | ||
const allowsAllJsonValues = util_1.isUndefined(schema) ? true : schema; | ||
const allowsAllJsonValues = schema === undefined ? true : schema; | ||
return allowsAllJsonValues ? create_json_set_1.createAllJsonSet() : create_json_set_1.createEmptyJsonSet(); | ||
}; | ||
const parseSchemaOrUndefinedAsJsonSet = (schema) => { | ||
return (util_1.isBoolean(schema) || util_1.isUndefined(schema)) | ||
return (typeof schema === 'boolean' || schema === undefined) | ||
? parseBooleanSchema(schema) | ||
@@ -46,0 +50,0 @@ : parseCoreSchemaMetaSchema(schema); |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const keyword_defaults_1 = require("../set/keyword-defaults"); | ||
const set_of_subsets_1 = require("../set/set-of-subsets"); | ||
const array_subset_1 = require("../set/subset/array-subset"); | ||
const is_type_supported_1 = require("./is-type-supported"); | ||
const supportsAllArrays = (arraySetParsedKeywords) => arraySetParsedKeywords.items.type === 'all' && arraySetParsedKeywords.minItems === 0; | ||
const supportsAllArrays = (arraySetParsedKeywords) => arraySetParsedKeywords.items.type === 'all' | ||
&& arraySetParsedKeywords.minItems === keyword_defaults_1.defaultMinItems | ||
&& arraySetParsedKeywords.maxItems === keyword_defaults_1.defaultMaxItems; | ||
const createArraySubset = (arraySetParsedKeywords) => { | ||
@@ -8,0 +11,0 @@ if (!is_type_supported_1.isTypeSupported(arraySetParsedKeywords.type, 'array')) { |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const _ = require("lodash"); | ||
const keyword_defaults_1 = require("../set/keyword-defaults"); | ||
const set_of_subsets_1 = require("../set/set-of-subsets"); | ||
@@ -7,9 +9,10 @@ const object_subset_1 = require("../set/subset/object-subset"); | ||
const supportsAllObjects = (objectSetParsedKeywords) => { | ||
// TODO: This should look at the minProperties keyword, but we need minProperties support to do that | ||
const everyPropertyIsAll = Object | ||
.keys(objectSetParsedKeywords.properties) | ||
// tslint:disable:cyclomatic-complexity | ||
const everyPropertyIsAll = Object.keys(objectSetParsedKeywords.properties) | ||
.every((propertyName) => objectSetParsedKeywords.properties[propertyName].type === 'all'); | ||
return everyPropertyIsAll | ||
&& objectSetParsedKeywords.required.length === 0 | ||
&& objectSetParsedKeywords.additionalProperties.type === 'all'; | ||
&& _.isEqual(objectSetParsedKeywords.required, keyword_defaults_1.defaultRequired) | ||
&& objectSetParsedKeywords.additionalProperties.type === 'all' | ||
&& objectSetParsedKeywords.minProperties === keyword_defaults_1.defaultMinProperties | ||
&& objectSetParsedKeywords.maxProperties === keyword_defaults_1.defaultMaxProperties; | ||
}; | ||
@@ -23,8 +26,3 @@ const createObjectSubset = (objectSetParsedKeywords) => { | ||
} | ||
return object_subset_1.createObjectSubsetFromConfig({ | ||
additionalProperties: objectSetParsedKeywords.additionalProperties, | ||
minProperties: objectSetParsedKeywords.minProperties, | ||
properties: objectSetParsedKeywords.properties, | ||
required: objectSetParsedKeywords.required | ||
}); | ||
return object_subset_1.createObjectSubsetFromConfig(objectSetParsedKeywords); | ||
}; | ||
@@ -31,0 +29,0 @@ exports.createObjectSet = (objectSetParsedKeywords) => { |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const _ = require("lodash"); | ||
const keyword_defaults_1 = require("../keyword-defaults"); | ||
const omitDefaultAdditionalProperties = (schema) => { | ||
@@ -10,13 +11,16 @@ return schema.additionalProperties === true | ||
const omitEmptyProperties = (schema) => { | ||
return schema.properties && Object.keys(schema.properties).length > 0 | ||
? schema | ||
: _.omit(schema, ['properties']); | ||
return _.isEqual(schema.properties, keyword_defaults_1.defaultProperties) ? _.omit(schema, ['properties']) : schema; | ||
}; | ||
const omitEmptyRequired = (schema) => { | ||
return schema.required && schema.required.length > 0 | ||
? schema | ||
: _.omit(schema, ['required']); | ||
return _.isEqual(schema.required, keyword_defaults_1.defaultRequired) | ||
? _.omit(schema, ['required']) | ||
: schema; | ||
}; | ||
const omitDefaultMaxProperties = (schema) => { | ||
return schema.maxProperties === keyword_defaults_1.defaultMaxProperties | ||
? _.omit(schema, ['maxProperties']) | ||
: schema; | ||
}; | ||
const omitDefaultMinProperties = (schema) => { | ||
return schema.minProperties === 0 | ||
return schema.minProperties === keyword_defaults_1.defaultMinProperties | ||
? _.omit(schema, ['minProperties']) | ||
@@ -31,3 +35,3 @@ : schema; | ||
const omitDefaultMinItems = (schema) => { | ||
return schema.minItems === 0 | ||
return schema.minItems === keyword_defaults_1.defaultMinItems | ||
? _.omit(schema, ['minItems']) | ||
@@ -37,3 +41,3 @@ : schema; | ||
const omitDefaultMaxItems = (schema) => { | ||
return schema.maxItems === Infinity | ||
return schema.maxItems === keyword_defaults_1.defaultMaxItems | ||
? _.omit(schema, ['maxItems']) | ||
@@ -47,2 +51,2 @@ : schema; | ||
}; | ||
exports.omitDefaults = (originalSchema) => omitDefaultsFromAnyOfSchema(omitDefaultAdditionalProperties(omitEmptyProperties(omitEmptyRequired(omitDefaultMinProperties(omitDefaultMinItems(omitDefaultMaxItems(omitDefaultItems(originalSchema)))))))); | ||
exports.omitDefaults = (originalSchema) => omitDefaultsFromAnyOfSchema(omitDefaultAdditionalProperties(omitEmptyProperties(omitEmptyRequired(omitDefaultMaxProperties(omitDefaultMinProperties(omitDefaultMinItems(omitDefaultMaxItems(omitDefaultItems(originalSchema))))))))); |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.allSchemaTypes = ['array', 'boolean', 'integer', 'null', 'number', 'object', 'string']; | ||
exports.isCoreRepresentationJsonSchema = (schema) => !!schema; |
"use strict"; | ||
// tslint:disable:max-classes-per-file | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const json_set_1 = require("../json-set"); | ||
const array_has_contradictions_1 = require("./array-subset/array-has-contradictions"); | ||
const array_subset_config_has_contradictions_1 = require("./array-subset/array-subset-config-has-contradictions"); | ||
const complement_array_subset_config_1 = require("./array-subset/complement-array-subset-config"); | ||
const intersect_array_subset_config_1 = require("./array-subset/intersect-array-subset-config"); | ||
const simplify_array_subset_config_1 = require("./array-subset/simplify-array-subset-config"); | ||
const subset_1 = require("./subset"); | ||
@@ -14,6 +16,3 @@ class SomeArraySubset { | ||
complement() { | ||
// TODO: This should complement the maxItems keyword, however cannot be tested without maxItems support | ||
const complementedItems = this.complementItems(); | ||
const complementedMinItems = this.complementMinItems(); | ||
return [complementedItems, complementedMinItems]; | ||
return complement_array_subset_config_1.complementArraySubsetConfig(this.config).map(exports.createArraySubsetFromConfig); | ||
} | ||
@@ -24,7 +23,3 @@ intersect(other) { | ||
intersectWithSome(other) { | ||
return exports.createArraySubsetFromConfig({ | ||
items: this.config.items.intersect(other.config.items), | ||
maxItems: this.config.maxItems, | ||
minItems: this.intersectMinItems(other) | ||
}); | ||
return exports.createArraySubsetFromConfig(intersect_array_subset_config_1.intersectArraySubsetConfig(this.config, other.config)); | ||
} | ||
@@ -39,28 +34,10 @@ toJsonSchema() { | ||
} | ||
complementItems() { | ||
return exports.createArraySubsetFromConfig({ | ||
items: this.config.items.complement(), | ||
maxItems: Infinity, | ||
minItems: 1 | ||
}); | ||
} | ||
complementMinItems() { | ||
return exports.createArraySubsetFromConfig({ | ||
items: json_set_1.allJsonSet, | ||
maxItems: this.config.minItems - 1, | ||
minItems: 0 | ||
}); | ||
} | ||
intersectMinItems(other) { | ||
return Math.max(this.config.minItems, other.config.minItems); | ||
} | ||
} | ||
const simplifyArraySubsetConfig = (config) => config.maxItems === 0 ? Object.assign({}, config, { items: json_set_1.allJsonSet }) : config; | ||
exports.allArraySubset = new subset_1.AllSubset('array'); | ||
exports.emptyArraySubset = new subset_1.EmptySubset('array'); | ||
exports.createArraySubsetFromConfig = (config) => { | ||
const simplifiedConfig = simplifyArraySubsetConfig(config); | ||
return array_has_contradictions_1.arrayHasContradictions(simplifiedConfig) | ||
const simplifiedConfig = simplify_array_subset_config_1.simplifyArraySubsetConfig(config); | ||
return array_subset_config_has_contradictions_1.arraySubsetConfigHasContradictions(simplifiedConfig) | ||
? exports.emptyArraySubset | ||
: new SomeArraySubset(simplifiedConfig); | ||
}; |
"use strict"; | ||
// tslint:disable:max-classes-per-file | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const json_set_1 = require("../json-set"); | ||
const object_has_contradictions_1 = require("./object-subset/object-has-contradictions"); | ||
const unique_1 = require("./object-subset/unique"); | ||
const complement_object_subset_config_1 = require("./object-subset/complement-object-subset-config"); | ||
const intersect_object_subset_config_1 = require("./object-subset/intersect-object-subset-config"); | ||
const object_subset_config_1 = require("./object-subset/object-subset-config"); | ||
const object_subset_config_has_contradictions_1 = require("./object-subset/object-subset-config-has-contradictions"); | ||
const simplify_object_subset_config_1 = require("./object-subset/simplify-object-subset-config"); | ||
const subset_1 = require("./subset"); | ||
@@ -18,6 +19,3 @@ class SomeObjectSubset { | ||
complement() { | ||
const complementedProperties = this.complementProperties(); | ||
const complementedAdditionalProperties = this.complementAdditionalProperties(); | ||
const complementedRequiredProperties = this.complementRequiredProperties(); | ||
return [...complementedAdditionalProperties, ...complementedProperties, ...complementedRequiredProperties]; | ||
return complement_object_subset_config_1.complementObjectSubsetConfig(this.config).map(exports.createObjectSubsetFromConfig); | ||
} | ||
@@ -28,8 +26,3 @@ intersect(other) { | ||
intersectWithSome(other) { | ||
return exports.createObjectSubsetFromConfig({ | ||
additionalProperties: this.config.additionalProperties.intersect(other.config.additionalProperties), | ||
minProperties: this.intersectMinProperties(other), | ||
properties: this.intersectProperties(other), | ||
required: this.intersectRequired(other) | ||
}); | ||
return exports.createObjectSubsetFromConfig(intersect_object_subset_config_1.intersectObjectSubsetConfig(this.config, other.config)); | ||
} | ||
@@ -41,2 +34,3 @@ toJsonSchema() { | ||
additionalProperties, | ||
maxProperties: this.config.maxProperties, | ||
minProperties: this.config.minProperties, | ||
@@ -48,81 +42,8 @@ properties, | ||
} | ||
getPropertySet(propertyName) { | ||
return this.config.properties[propertyName] || this.config.additionalProperties; | ||
} | ||
getPropertyNames() { | ||
return Object.keys(this.config.properties); | ||
} | ||
toJsonSchemaMap() { | ||
return this.getPropertyNames().reduce((acc, propertyName) => { | ||
acc[propertyName] = this.getPropertySet(propertyName).toJsonSchema(); | ||
return object_subset_config_1.getPropertyNames(this.config).reduce((acc, propertyName) => { | ||
acc[propertyName] = object_subset_config_1.getPropertySet(this.config, propertyName).toJsonSchema(); | ||
return acc; | ||
}, {}); | ||
} | ||
complementProperties() { | ||
return this.getPropertyNames().map((propertyName) => { | ||
const complementedPropertySchema = this.getPropertySet(propertyName).complement(); | ||
return exports.createObjectSubsetFromConfig({ | ||
additionalProperties: json_set_1.allJsonSet, | ||
// TODO: Untestable today, need: | ||
// not: {properties: {name: {type: 'string'}, minProperties: 1, type: 'object'} -> true | ||
minProperties: 0, | ||
properties: { [propertyName]: complementedPropertySchema }, | ||
required: [propertyName] | ||
}); | ||
}); | ||
} | ||
complementRequiredProperties() { | ||
return this.config.required.map((requiredPropertyName) => exports.createObjectSubsetFromConfig({ | ||
additionalProperties: json_set_1.allJsonSet, | ||
minProperties: 0, | ||
properties: { | ||
[requiredPropertyName]: json_set_1.emptyJsonSet | ||
}, | ||
required: [] | ||
})); | ||
} | ||
complementAdditionalProperties() { | ||
const defaultComplementedAdditionalProperties = [this.complementAdditionalPropertiesWithEmpty()]; | ||
return this.getPropertyNames().length > 0 | ||
? defaultComplementedAdditionalProperties.concat(this.complementAdditionalPropertiesWithRequired()) | ||
: defaultComplementedAdditionalProperties; | ||
} | ||
complementAdditionalPropertiesWithEmpty() { | ||
const propertyNames = this.getPropertyNames(); | ||
const emptyProperties = propertyNames | ||
.reduce((acc, propertyName) => { | ||
acc[propertyName] = json_set_1.emptyJsonSet; | ||
return acc; | ||
}, {}); | ||
return exports.createObjectSubsetFromConfig({ | ||
additionalProperties: this.config.additionalProperties.complement(), | ||
minProperties: 1, | ||
properties: emptyProperties, | ||
required: [] | ||
}); | ||
} | ||
complementAdditionalPropertiesWithRequired() { | ||
return exports.createObjectSubsetFromConfig({ | ||
additionalProperties: this.config.additionalProperties.complement(), | ||
minProperties: 1 + Object.keys(this.config.properties).length, | ||
// TODO: All the tests went green when I made this an empty object, whats up with that? | ||
properties: this.config.properties, | ||
required: Object.keys(this.config.properties) | ||
}); | ||
} | ||
intersectMinProperties(other) { | ||
// TODO: can't be asserted without minProperties support: | ||
// {minProperties: 1, type: 'object'} -> {minProperties: 2, type: 'object'} | ||
return Math.max(this.config.minProperties, other.config.minProperties); | ||
} | ||
intersectProperties(other) { | ||
const allPropertyNames = unique_1.unique(this.getPropertyNames(), other.getPropertyNames()); | ||
return allPropertyNames.reduce((accumulator, propertyName) => { | ||
accumulator[propertyName] = this.getPropertySet(propertyName).intersect(other.getPropertySet(propertyName)); | ||
return accumulator; | ||
}, {}); | ||
} | ||
intersectRequired(other) { | ||
return unique_1.unique(this.config.required, other.config.required); | ||
} | ||
} | ||
@@ -132,5 +53,6 @@ exports.allObjectSubset = new subset_1.AllSubset('object'); | ||
exports.createObjectSubsetFromConfig = (config) => { | ||
return object_has_contradictions_1.objectHasContradictions(config) | ||
const simplifiedConfig = simplify_object_subset_config_1.simplifyObjectSubsetConfig(config); | ||
return object_subset_config_has_contradictions_1.objectSubsetConfigHasContradictions(simplifiedConfig) | ||
? exports.emptyObjectSubset | ||
: new SomeObjectSubset(config); | ||
: new SomeObjectSubset(simplifiedConfig); | ||
}; |
@@ -39,3 +39,3 @@ # Json Schema Diff Supported Keywords | ||
| additionalItems | no | | ||
| maxItems | no | | ||
| maxItems | yes | | ||
| minItems | yes | | ||
@@ -50,4 +50,4 @@ | uniqueItems | no | | ||
|---|---| | ||
| maxProperties | no | | ||
| minProperties | no | | ||
| maxProperties | yes | | ||
| minProperties | yes | | ||
| required | yes | | ||
@@ -60,2 +60,8 @@ | properties | yes | | ||
### Additional Properties and Properties | ||
The `additionalProperties` and `properties` keywords interact with each other in a way that can create multiple sets of | ||
differences. In order to make json-schema-diff execute in a reasonable amount of time and consume a reasonable amount of | ||
memory a compromise was made to return a limited number of these differences when the number of sets grows too large. | ||
This means that there are some circumstances where the added or removed schema does not completely represent every | ||
single value that has been added or removed. | ||
@@ -75,3 +81,3 @@ ## Keywords for Applying Subschemas Conditionally | ||
|---|---| | ||
| allOf | no | | ||
| allOf | yes | | ||
| anyOf | no | | ||
@@ -78,0 +84,0 @@ | oneOf | no | |
import RefParser = require('json-schema-ref-parser'); | ||
import {JsonSchema} from 'json-schema-spec-types'; | ||
import {isBoolean} from 'util'; | ||
export const dereferenceSchema = async (schema: JsonSchema): Promise<JsonSchema> => { | ||
const refParser = new RefParser(); | ||
return isBoolean(schema) | ||
return typeof schema === 'boolean' | ||
? schema | ||
: await refParser.dereference(schema as object, {dereference: {circular: false}}) as JsonSchema; | ||
}; |
import {CoreSchemaMetaSchema, JsonSchema, JsonSchemaMap, SimpleTypes} from 'json-schema-spec-types'; | ||
import {isBoolean, isUndefined} from 'util'; | ||
import {createAllJsonSet, createEmptyJsonSet, createSomeJsonSet} from './set-factories/create-json-set'; | ||
import {allSchemaTypes, ParsedPropertiesKeyword, Set} from './set/set'; | ||
import { | ||
defaultMaxItems, | ||
defaultMaxProperties, | ||
defaultMinItems, | ||
defaultMinProperties, | ||
defaultRequired, | ||
defaultTypes | ||
} from './set/keyword-defaults'; | ||
import {ParsedPropertiesKeyword, Set} from './set/set'; | ||
@@ -18,3 +25,3 @@ const parseSchemaProperties = (schemaProperties: JsonSchemaMap = {}): ParsedPropertiesKeyword => { | ||
if (!type) { | ||
return allSchemaTypes; | ||
return defaultTypes; | ||
} | ||
@@ -29,24 +36,31 @@ | ||
const parseRequiredKeyword = (schema: CoreSchemaMetaSchema): string[] => schema.required || []; | ||
const parseRequiredKeyword = (required: string[] | undefined): string[] => required || defaultRequired; | ||
const generateDefaultMinPropertiesKeyword = (): number => 0; | ||
const parseNumericKeyword = (keywordValue: number | undefined, defaultValue: number): number => | ||
typeof keywordValue === 'number' ? keywordValue : defaultValue; | ||
const generateDefaultMaxItemsKeyword = (): number => Infinity; | ||
const parseMinItemsKeyword = (schema: CoreSchemaMetaSchema): number => schema.minItems || 0; | ||
const parseCoreSchemaMetaSchema = (schema: CoreSchemaMetaSchema): Set<'json'> => | ||
const parseTypeKeywords = (schema: CoreSchemaMetaSchema): Set<'json'> => | ||
createSomeJsonSet({ | ||
additionalProperties: parseSchemaOrUndefinedAsJsonSet(schema.additionalProperties), | ||
items: parseSchemaOrUndefinedAsJsonSet(schema.items), | ||
maxItems: generateDefaultMaxItemsKeyword(), | ||
minItems: parseMinItemsKeyword(schema), | ||
minProperties: generateDefaultMinPropertiesKeyword(), | ||
maxItems: parseNumericKeyword(schema.maxItems, defaultMaxItems), | ||
maxProperties: parseNumericKeyword(schema.maxProperties, defaultMaxProperties), | ||
minItems: parseNumericKeyword(schema.minItems, defaultMinItems), | ||
minProperties: parseNumericKeyword(schema.minProperties, defaultMinProperties), | ||
properties: parseSchemaProperties(schema.properties), | ||
required: parseRequiredKeyword(schema), | ||
required: parseRequiredKeyword(schema.required), | ||
type: parseType(schema.type) | ||
}); | ||
const parseBooleanLogicKeywords = (schema: CoreSchemaMetaSchema): Array<Set<'json'>> => | ||
(schema.allOf || []).map(parseSchemaOrUndefinedAsJsonSet); | ||
const parseCoreSchemaMetaSchema = (schema: CoreSchemaMetaSchema): Set<'json'> => { | ||
const typeKeywordsSet = parseTypeKeywords(schema); | ||
const booleanLogicKeywordSets = parseBooleanLogicKeywords(schema); | ||
return booleanLogicKeywordSets.reduce((accumulator, set) => accumulator.intersect(set), typeKeywordsSet); | ||
}; | ||
const parseBooleanSchema = (schema: boolean | undefined): Set<'json'> => { | ||
const allowsAllJsonValues = isUndefined(schema) ? true : schema; | ||
const allowsAllJsonValues = schema === undefined ? true : schema; | ||
return allowsAllJsonValues ? createAllJsonSet() : createEmptyJsonSet(); | ||
@@ -56,3 +70,3 @@ }; | ||
const parseSchemaOrUndefinedAsJsonSet = (schema: JsonSchema | undefined): Set<'json'> => { | ||
return (isBoolean(schema) || isUndefined(schema)) | ||
return (typeof schema === 'boolean' || schema === undefined) | ||
? parseBooleanSchema(schema) | ||
@@ -59,0 +73,0 @@ : parseCoreSchemaMetaSchema(schema); |
import {SimpleTypes} from 'json-schema-spec-types'; | ||
import {defaultMaxItems, defaultMinItems} from '../set/keyword-defaults'; | ||
import {Set, Subset} from '../set/set'; | ||
@@ -15,3 +16,5 @@ import {SetOfSubsets} from '../set/set-of-subsets'; | ||
const supportsAllArrays = (arraySetParsedKeywords: ArraySetParsedKeywords): boolean => | ||
arraySetParsedKeywords.items.type === 'all' && arraySetParsedKeywords.minItems === 0; | ||
arraySetParsedKeywords.items.type === 'all' | ||
&& arraySetParsedKeywords.minItems === defaultMinItems | ||
&& arraySetParsedKeywords.maxItems === defaultMaxItems; | ||
@@ -18,0 +21,0 @@ const createArraySubset = (arraySetParsedKeywords: ArraySetParsedKeywords): Subset<'array'> => { |
@@ -12,2 +12,3 @@ import {SimpleTypes} from 'json-schema-spec-types'; | ||
maxItems: number; | ||
maxProperties: number; | ||
minItems: number; | ||
@@ -14,0 +15,0 @@ minProperties: number; |
import {SimpleTypes} from 'json-schema-spec-types'; | ||
import * as _ from 'lodash'; | ||
import {defaultMaxProperties, defaultMinProperties, defaultRequired} from '../set/keyword-defaults'; | ||
import {ParsedPropertiesKeyword, Set, Subset} from '../set/set'; | ||
@@ -9,17 +11,19 @@ import {SetOfSubsets} from '../set/set-of-subsets'; | ||
additionalProperties: Set<'json'>; | ||
type: SimpleTypes[]; | ||
maxProperties: number; | ||
minProperties: number; | ||
properties: ParsedPropertiesKeyword; | ||
required: string[]; | ||
minProperties: number; | ||
type: SimpleTypes[]; | ||
} | ||
const supportsAllObjects = (objectSetParsedKeywords: ObjectSetParsedKeywords): boolean => { | ||
// TODO: This should look at the minProperties keyword, but we need minProperties support to do that | ||
const everyPropertyIsAll = Object | ||
.keys(objectSetParsedKeywords.properties) | ||
// tslint:disable:cyclomatic-complexity | ||
const everyPropertyIsAll = Object.keys(objectSetParsedKeywords.properties) | ||
.every((propertyName) => objectSetParsedKeywords.properties[propertyName].type === 'all'); | ||
return everyPropertyIsAll | ||
&& objectSetParsedKeywords.required.length === 0 | ||
&& objectSetParsedKeywords.additionalProperties.type === 'all'; | ||
&& _.isEqual(objectSetParsedKeywords.required, defaultRequired) | ||
&& objectSetParsedKeywords.additionalProperties.type === 'all' | ||
&& objectSetParsedKeywords.minProperties === defaultMinProperties | ||
&& objectSetParsedKeywords.maxProperties === defaultMaxProperties; | ||
}; | ||
@@ -36,8 +40,3 @@ | ||
return createObjectSubsetFromConfig({ | ||
additionalProperties: objectSetParsedKeywords.additionalProperties, | ||
minProperties: objectSetParsedKeywords.minProperties, | ||
properties: objectSetParsedKeywords.properties, | ||
required: objectSetParsedKeywords.required | ||
}); | ||
return createObjectSubsetFromConfig(objectSetParsedKeywords); | ||
}; | ||
@@ -44,0 +43,0 @@ |
import * as _ from 'lodash'; | ||
import { | ||
defaultMaxItems, | ||
defaultMaxProperties, | ||
defaultMinItems, | ||
defaultMinProperties, | ||
defaultProperties, | ||
defaultRequired | ||
} from '../keyword-defaults'; | ||
import {CoreRepresentationJsonSchema, RepresentationJsonSchema} from '../set'; | ||
@@ -11,15 +19,19 @@ | ||
const omitEmptyProperties = (schema: CoreRepresentationJsonSchema): CoreRepresentationJsonSchema => { | ||
return schema.properties && Object.keys(schema.properties).length > 0 | ||
? schema | ||
: _.omit(schema, ['properties']); | ||
return _.isEqual(schema.properties, defaultProperties) ? _.omit(schema, ['properties']) : schema; | ||
}; | ||
const omitEmptyRequired = (schema: CoreRepresentationJsonSchema): CoreRepresentationJsonSchema => { | ||
return schema.required && schema.required.length > 0 | ||
? schema | ||
: _.omit(schema, ['required']); | ||
return _.isEqual(schema.required, defaultRequired) | ||
? _.omit(schema, ['required']) | ||
: schema; | ||
}; | ||
const omitDefaultMaxProperties = (schema: CoreRepresentationJsonSchema): CoreRepresentationJsonSchema => { | ||
return schema.maxProperties === defaultMaxProperties | ||
? _.omit(schema, ['maxProperties']) | ||
: schema; | ||
}; | ||
const omitDefaultMinProperties = (schema: CoreRepresentationJsonSchema): CoreRepresentationJsonSchema => { | ||
return schema.minProperties === 0 | ||
return schema.minProperties === defaultMinProperties | ||
? _.omit(schema, ['minProperties']) | ||
@@ -36,3 +48,3 @@ : schema; | ||
const omitDefaultMinItems = (schema: CoreRepresentationJsonSchema): CoreRepresentationJsonSchema => { | ||
return schema.minItems === 0 | ||
return schema.minItems === defaultMinItems | ||
? _.omit(schema, ['minItems']) | ||
@@ -43,3 +55,3 @@ : schema; | ||
const omitDefaultMaxItems = (schema: CoreRepresentationJsonSchema): CoreRepresentationJsonSchema => { | ||
return schema.maxItems === Infinity | ||
return schema.maxItems === defaultMaxItems | ||
? _.omit(schema, ['maxItems']) | ||
@@ -64,5 +76,6 @@ : schema; | ||
omitEmptyRequired( | ||
omitDefaultMinProperties( | ||
omitDefaultMinItems( | ||
omitDefaultMaxItems( | ||
omitDefaultItems(originalSchema)))))))); | ||
omitDefaultMaxProperties( | ||
omitDefaultMinProperties( | ||
omitDefaultMinItems( | ||
omitDefaultMaxItems( | ||
omitDefaultItems(originalSchema))))))))); |
@@ -37,5 +37,3 @@ import {CoreSchemaMetaSchema, SimpleTypes} from 'json-schema-spec-types'; | ||
export const allSchemaTypes: SimpleTypes[] = ['array', 'boolean', 'integer', 'null', 'number', 'object', 'string']; | ||
export const isCoreRepresentationJsonSchema = | ||
(schema: RepresentationJsonSchema): schema is CoreRepresentationJsonSchema => !!schema; |
// tslint:disable:max-classes-per-file | ||
import {allJsonSet} from '../json-set'; | ||
import {RepresentationJsonSchema, Set, Subset} from '../set'; | ||
import {arrayHasContradictions} from './array-subset/array-has-contradictions'; | ||
import {RepresentationJsonSchema, Subset} from '../set'; | ||
import {ArraySubsetConfig} from './array-subset/array-subset-config'; | ||
import {arraySubsetConfigHasContradictions} from './array-subset/array-subset-config-has-contradictions'; | ||
import {complementArraySubsetConfig} from './array-subset/complement-array-subset-config'; | ||
import {intersectArraySubsetConfig} from './array-subset/intersect-array-subset-config'; | ||
import {simplifyArraySubsetConfig} from './array-subset/simplify-array-subset-config'; | ||
import {AllSubset, EmptySubset} from './subset'; | ||
export interface SomeArraySubsetConfig { | ||
items: Set<'json'>; | ||
maxItems: number; | ||
minItems: number; | ||
} | ||
class SomeArraySubset implements Subset<'array'> { | ||
@@ -18,10 +15,7 @@ public readonly setType = 'array'; | ||
public constructor(private readonly config: SomeArraySubsetConfig) { | ||
public constructor(private readonly config: ArraySubsetConfig) { | ||
} | ||
public complement(): Array<Subset<'array'>> { | ||
// TODO: This should complement the maxItems keyword, however cannot be tested without maxItems support | ||
const complementedItems = this.complementItems(); | ||
const complementedMinItems = this.complementMinItems(); | ||
return [complementedItems, complementedMinItems]; | ||
return complementArraySubsetConfig(this.config).map(createArraySubsetFromConfig); | ||
} | ||
@@ -34,7 +28,3 @@ | ||
public intersectWithSome(other: SomeArraySubset): Subset<'array'> { | ||
return createArraySubsetFromConfig({ | ||
items: this.config.items.intersect(other.config.items), | ||
maxItems: this.config.maxItems, | ||
minItems: this.intersectMinItems(other) | ||
}); | ||
return createArraySubsetFromConfig(intersectArraySubsetConfig(this.config, other.config)); | ||
} | ||
@@ -50,36 +40,13 @@ | ||
} | ||
private complementItems(): Subset<'array'> { | ||
return createArraySubsetFromConfig({ | ||
items: this.config.items.complement(), | ||
maxItems: Infinity, | ||
minItems: 1 | ||
}); | ||
} | ||
private complementMinItems(): Subset<'array'> { | ||
return createArraySubsetFromConfig({ | ||
items: allJsonSet, | ||
maxItems: this.config.minItems - 1, | ||
minItems: 0 | ||
}); | ||
} | ||
private intersectMinItems(other: SomeArraySubset): number { | ||
return Math.max(this.config.minItems, other.config.minItems); | ||
} | ||
} | ||
const simplifyArraySubsetConfig = (config: SomeArraySubsetConfig): SomeArraySubsetConfig => | ||
config.maxItems === 0 ? {...config, items: allJsonSet} : config; | ||
export const allArraySubset = new AllSubset('array'); | ||
export const emptyArraySubset = new EmptySubset('array'); | ||
export const createArraySubsetFromConfig = (config: SomeArraySubsetConfig): Subset<'array'> => { | ||
export const createArraySubsetFromConfig = (config: ArraySubsetConfig): Subset<'array'> => { | ||
const simplifiedConfig = simplifyArraySubsetConfig(config); | ||
return arrayHasContradictions(simplifiedConfig) | ||
return arraySubsetConfigHasContradictions(simplifiedConfig) | ||
? emptyArraySubset | ||
: new SomeArraySubset(simplifiedConfig); | ||
}; |
@@ -1,16 +0,9 @@ | ||
// tslint:disable:max-classes-per-file | ||
import {allJsonSet, emptyJsonSet} from '../json-set'; | ||
import {ParsedPropertiesKeyword, RepresentationJsonSchema, SchemaProperties, Set, Subset} from '../set'; | ||
import {objectHasContradictions} from './object-subset/object-has-contradictions'; | ||
import {unique} from './object-subset/unique'; | ||
import {RepresentationJsonSchema, SchemaProperties, Subset} from '../set'; | ||
import {complementObjectSubsetConfig} from './object-subset/complement-object-subset-config'; | ||
import {intersectObjectSubsetConfig} from './object-subset/intersect-object-subset-config'; | ||
import {getPropertyNames, getPropertySet, ObjectSubsetConfig} from './object-subset/object-subset-config'; | ||
import {objectSubsetConfigHasContradictions} from './object-subset/object-subset-config-has-contradictions'; | ||
import {simplifyObjectSubsetConfig} from './object-subset/simplify-object-subset-config'; | ||
import {AllSubset, EmptySubset} from './subset'; | ||
export interface SomeObjectSubsetConfig { | ||
properties: ParsedPropertiesKeyword; | ||
additionalProperties: Set<'json'>; | ||
minProperties: number; | ||
required: string[]; | ||
} | ||
class SomeObjectSubset implements Subset<'object'> { | ||
@@ -20,3 +13,3 @@ public readonly setType = 'object'; | ||
public constructor(private readonly config: SomeObjectSubsetConfig) { | ||
public constructor(private readonly config: ObjectSubsetConfig) { | ||
} | ||
@@ -29,6 +22,3 @@ | ||
public complement(): Array<Subset<'object'>> { | ||
const complementedProperties = this.complementProperties(); | ||
const complementedAdditionalProperties = this.complementAdditionalProperties(); | ||
const complementedRequiredProperties = this.complementRequiredProperties(); | ||
return [...complementedAdditionalProperties, ...complementedProperties, ...complementedRequiredProperties]; | ||
return complementObjectSubsetConfig(this.config).map(createObjectSubsetFromConfig); | ||
} | ||
@@ -41,8 +31,3 @@ | ||
public intersectWithSome(other: SomeObjectSubset): Subset<'object'> { | ||
return createObjectSubsetFromConfig({ | ||
additionalProperties: this.config.additionalProperties.intersect(other.config.additionalProperties), | ||
minProperties: this.intersectMinProperties(other), | ||
properties: this.intersectProperties(other), | ||
required: this.intersectRequired(other) | ||
}); | ||
return createObjectSubsetFromConfig(intersectObjectSubsetConfig(this.config, other.config)); | ||
} | ||
@@ -55,2 +40,3 @@ | ||
additionalProperties, | ||
maxProperties: this.config.maxProperties, | ||
minProperties: this.config.minProperties, | ||
@@ -63,99 +49,8 @@ properties, | ||
private getPropertySet(propertyName: string): Set<'json'> { | ||
return this.config.properties[propertyName] || this.config.additionalProperties; | ||
} | ||
private getPropertyNames(): string[] { | ||
return Object.keys(this.config.properties); | ||
} | ||
private toJsonSchemaMap(): SchemaProperties { | ||
return this.getPropertyNames().reduce<SchemaProperties>((acc, propertyName) => { | ||
acc[propertyName] = this.getPropertySet(propertyName).toJsonSchema(); | ||
return getPropertyNames(this.config).reduce<SchemaProperties>((acc, propertyName) => { | ||
acc[propertyName] = getPropertySet(this.config, propertyName).toJsonSchema(); | ||
return acc; | ||
}, {}); | ||
} | ||
private complementProperties(): Array<Subset<'object'>> { | ||
return this.getPropertyNames().map((propertyName) => { | ||
const complementedPropertySchema = this.getPropertySet(propertyName).complement(); | ||
return createObjectSubsetFromConfig({ | ||
additionalProperties: allJsonSet, | ||
// TODO: Untestable today, need: | ||
// not: {properties: {name: {type: 'string'}, minProperties: 1, type: 'object'} -> true | ||
minProperties: 0, | ||
properties: {[propertyName]: complementedPropertySchema}, | ||
required: [propertyName] | ||
} | ||
); | ||
}); | ||
} | ||
private complementRequiredProperties(): Array<Subset<'object'>> { | ||
return this.config.required.map((requiredPropertyName) => | ||
createObjectSubsetFromConfig({ | ||
additionalProperties: allJsonSet, | ||
minProperties: 0, | ||
properties: { | ||
[requiredPropertyName]: emptyJsonSet | ||
}, | ||
required: [] | ||
})); | ||
} | ||
private complementAdditionalProperties(): Array<Subset<'object'>> { | ||
const defaultComplementedAdditionalProperties = [this.complementAdditionalPropertiesWithEmpty()]; | ||
return this.getPropertyNames().length > 0 | ||
? defaultComplementedAdditionalProperties.concat(this.complementAdditionalPropertiesWithRequired()) | ||
: defaultComplementedAdditionalProperties; | ||
} | ||
private complementAdditionalPropertiesWithEmpty(): Subset<'object'> { | ||
const propertyNames = this.getPropertyNames(); | ||
const emptyProperties = propertyNames | ||
.reduce<ParsedPropertiesKeyword>((acc, propertyName) => { | ||
acc[propertyName] = emptyJsonSet; | ||
return acc; | ||
}, {}); | ||
return createObjectSubsetFromConfig({ | ||
additionalProperties: this.config.additionalProperties.complement(), | ||
minProperties: 1, | ||
properties: emptyProperties, | ||
required: [] | ||
}); | ||
} | ||
private complementAdditionalPropertiesWithRequired(): Subset<'object'> { | ||
return createObjectSubsetFromConfig({ | ||
additionalProperties: this.config.additionalProperties.complement(), | ||
minProperties: 1 + Object.keys(this.config.properties).length, | ||
// TODO: All the tests went green when I made this an empty object, whats up with that? | ||
properties: this.config.properties, | ||
required: Object.keys(this.config.properties) | ||
}); | ||
} | ||
private intersectMinProperties(other: SomeObjectSubset): number { | ||
// TODO: can't be asserted without minProperties support: | ||
// {minProperties: 1, type: 'object'} -> {minProperties: 2, type: 'object'} | ||
return Math.max(this.config.minProperties, other.config.minProperties); | ||
} | ||
private intersectProperties(other: SomeObjectSubset): ParsedPropertiesKeyword { | ||
const allPropertyNames = unique(this.getPropertyNames(), other.getPropertyNames()); | ||
return allPropertyNames.reduce<ParsedPropertiesKeyword>((accumulator, propertyName) => { | ||
accumulator[propertyName] = this.getPropertySet(propertyName).intersect(other.getPropertySet(propertyName)); | ||
return accumulator; | ||
}, {}); | ||
} | ||
private intersectRequired(other: SomeObjectSubset): string[] { | ||
return unique(this.config.required, other.config.required); | ||
} | ||
} | ||
@@ -165,6 +60,8 @@ | ||
export const emptyObjectSubset = new EmptySubset('object'); | ||
export const createObjectSubsetFromConfig = (config: SomeObjectSubsetConfig): Subset<'object'> => { | ||
return objectHasContradictions(config) | ||
export const createObjectSubsetFromConfig = (config: ObjectSubsetConfig): Subset<'object'> => { | ||
const simplifiedConfig = simplifyObjectSubsetConfig(config); | ||
return objectSubsetConfigHasContradictions(simplifiedConfig) | ||
? emptyObjectSubset | ||
: new SomeObjectSubset(config); | ||
: new SomeObjectSubset(simplifiedConfig); | ||
}; |
{ | ||
"name": "json-schema-diff", | ||
"version": "0.11.0", | ||
"version": "0.12.0", | ||
"description": "A language agnostic CLI tool and nodejs api to identify differences between two json schema files.", | ||
@@ -38,6 +38,7 @@ "bin": { | ||
"devDependencies": { | ||
"@types/jasmine": "^2.8.6", | ||
"@types/json-schema": "^6.0.1", | ||
"@types/jasmine": "^3.3.9", | ||
"@types/js-combinatorics": "^0.5.31", | ||
"@types/json-schema": "^7.0.3", | ||
"@types/lodash": "^4.14.108", | ||
"@types/node": "^9.6.5", | ||
"@types/node": "^11.9.6", | ||
"@types/verror": "^1.10.3", | ||
@@ -57,4 +58,4 @@ "conventional-changelog-lint": "^2.1.1", | ||
"run-sequence": "^2.2.1", | ||
"tslint": "5.9.1", | ||
"typescript": "2.8.3" | ||
"tslint": "5.13.1", | ||
"typescript": "3.3.3333" | ||
}, | ||
@@ -65,3 +66,4 @@ "types": "lib/api-types.d.ts", | ||
"commander": "^2.15.1", | ||
"json-schema-ref-parser": "^5.0.3", | ||
"js-combinatorics": "^0.5.4", | ||
"json-schema-ref-parser": "^6.1.0", | ||
"json-schema-spec-types": "^0.1.2", | ||
@@ -68,0 +70,0 @@ "lodash": "^4.17.10", |
@@ -5,4 +5,4 @@ # Json Schema Diff | ||
## Requirements | ||
- nodejs 6.x or higher (tested using 6.x, 8.x and 9.x) | ||
- npm 2.x or higher (tested using 2.x, 3.x and 5x) | ||
- nodejs 6.x or higher (tested using 6.x, 8.x, 10.x and 11.x) | ||
- npm 3.x or higher (tested using 3.x and 6.x) | ||
@@ -9,0 +9,0 @@ ## Installation |
@@ -178,10 +178,9 @@ import {JsonSchema, SimpleTypes} from 'json-schema-spec-types'; | ||
it('should return a remove difference representing the empty array value', async () => { | ||
it('should find a removed difference with correct minItems-maxItems constraint range', async () => { | ||
const sourceSchema: JsonSchema = { | ||
items: {type: 'string'}, | ||
minItems: 2, | ||
type: 'array' | ||
}; | ||
const destinationSchema: JsonSchema = { | ||
items: {type: 'string'}, | ||
minItems: 1, | ||
minItems: 4, | ||
type: 'array' | ||
@@ -193,3 +192,4 @@ }; | ||
const arraysWithNoItems: JsonSchema = { | ||
maxItems: 0, | ||
maxItems: 3, | ||
minItems: 2, | ||
type: ['array'] | ||
@@ -201,9 +201,8 @@ }; | ||
it('should find a removed difference with correct minItems-maxItems constraint range', async () => { | ||
it('should represent an array with no items in a consistent way', async () => { | ||
const sourceSchema: JsonSchema = { | ||
minItems: 2, | ||
type: 'array' | ||
}; | ||
const destinationSchema: JsonSchema = { | ||
minItems: 4, | ||
minItems: 1, | ||
type: 'array' | ||
@@ -214,12 +213,70 @@ }; | ||
const arraysWithNoItems: JsonSchema = { | ||
maxItems: 3, | ||
minItems: 2, | ||
const anArrayWithNoItems: JsonSchema = { | ||
items: false, | ||
type: ['array'] | ||
}; | ||
expect(diffResult.addedJsonSchema).toEqual(false); | ||
expect(diffResult.removedJsonSchema).toEqual(arraysWithNoItems); | ||
expect(diffResult.removedJsonSchema).toEqual(anArrayWithNoItems); | ||
}); | ||
}); | ||
describe('maxItems', () => { | ||
it('should find a remove difference when an array with maxItems constraint is removed', async () => { | ||
const sourceSchema: JsonSchema = { | ||
maxItems: 2, | ||
type: 'array' | ||
}; | ||
const destinationSchema: JsonSchema = false; | ||
const diffResult = await invokeDiff(sourceSchema, destinationSchema); | ||
const allArraysWithAtMostTwoItems: JsonSchema = { | ||
maxItems: 2, | ||
type: ['array'] | ||
}; | ||
expect(diffResult.addedJsonSchema).toEqual(false); | ||
expect(diffResult.removedJsonSchema).toEqual(allArraysWithAtMostTwoItems); | ||
}); | ||
it('should find a removed difference when an array is constrained with maxItems', async () => { | ||
const sourceSchema: JsonSchema = { | ||
type: 'array' | ||
}; | ||
const destinationSchema: JsonSchema = { | ||
maxItems: 2, | ||
type: 'array' | ||
}; | ||
const diffResult = await invokeDiff(sourceSchema, destinationSchema); | ||
const allArraysWithAtLeastThreeItems: JsonSchema = { | ||
minItems: 3, | ||
type: ['array'] | ||
}; | ||
expect(diffResult.addedJsonSchema).toEqual(false); | ||
expect(diffResult.removedJsonSchema).toEqual(allArraysWithAtLeastThreeItems); | ||
}); | ||
it('should find an added difference when an maxItems is increased', async () => { | ||
const sourceSchema: JsonSchema = { | ||
maxItems: 10, | ||
type: 'array' | ||
}; | ||
const destinationSchema: JsonSchema = { | ||
maxItems: 15, | ||
type: 'array' | ||
}; | ||
const diffResult = await invokeDiff(sourceSchema, destinationSchema); | ||
const allArraysWithAtLeastElevenAndAtMostFifteenItems: JsonSchema = { | ||
maxItems: 15, | ||
minItems: 11, | ||
type: ['array'] | ||
}; | ||
expect(diffResult.addedJsonSchema).toEqual(allArraysWithAtLeastElevenAndAtMostFifteenItems); | ||
expect(diffResult.removedJsonSchema).toEqual(false); | ||
}); | ||
}); | ||
describe('items + minItems', () => { | ||
@@ -278,10 +335,110 @@ it('should find a remove difference if items schema is constrained but minItems remains the same', async () => { | ||
const allArraysWithNoItems: JsonSchema = { | ||
const anArrayWithNoItems: JsonSchema = { | ||
items: false, | ||
type: ['array'] | ||
}; | ||
expect(diffResult.addedJsonSchema).toEqual(allArraysWithNoItems); | ||
expect(diffResult.addedJsonSchema).toEqual(anArrayWithNoItems); | ||
expect(diffResult.removedJsonSchema).toEqual(false); | ||
}); | ||
}); | ||
describe('items + maxItems', () => { | ||
it('should not find a difference when comparing different ways of an array with no items', async () => { | ||
const sourceSchema: JsonSchema = { | ||
maxItems: 0, | ||
type: 'array' | ||
}; | ||
const destinationSchema: JsonSchema = { | ||
items: false, | ||
type: 'array' | ||
}; | ||
const diffResult = await invokeDiff(sourceSchema, destinationSchema); | ||
expect(diffResult.addedJsonSchema).toEqual(false); | ||
expect(diffResult.removedJsonSchema).toEqual(false); | ||
}); | ||
}); | ||
describe('minItems + maxItems', () => { | ||
it('should find two sets of added differences when an item range is expanded', async () => { | ||
const sourceSchema: JsonSchema = { | ||
maxItems: 10, | ||
minItems: 5, | ||
type: 'array' | ||
}; | ||
const destinationSchema: JsonSchema = { | ||
maxItems: 15, | ||
minItems: 1, | ||
type: 'array' | ||
}; | ||
const diffResult = await invokeDiff(sourceSchema, destinationSchema); | ||
const allArraysOfLengthOneToFourOrElevenToFifteen: JsonSchema = { | ||
anyOf: [ | ||
{ | ||
maxItems: 4, | ||
minItems: 1, | ||
type: ['array'] | ||
}, | ||
{ | ||
maxItems: 15, | ||
minItems: 11, | ||
type: ['array'] | ||
} | ||
] | ||
}; | ||
expect(diffResult.addedJsonSchema).toEqual(allArraysOfLengthOneToFourOrElevenToFifteen); | ||
expect(diffResult.removedJsonSchema).toEqual(false); | ||
}); | ||
}); | ||
describe('items + minItems + maxItems', () => { | ||
it('should find three sets of added differences when an item range and schema is expanded', async () => { | ||
const sourceSchema: JsonSchema = { | ||
items: { | ||
type: ['array', 'boolean', 'integer', 'null', 'number', 'object'] | ||
}, | ||
maxItems: 10, | ||
minItems: 5, | ||
type: 'array' | ||
}; | ||
const destinationSchema: JsonSchema = { | ||
maxItems: 15, | ||
minItems: 1, | ||
type: 'array' | ||
}; | ||
const diffResult = await invokeDiff(sourceSchema, destinationSchema); | ||
const allArraysWithLengthOneToFour: JsonSchema = { | ||
maxItems: 4, | ||
minItems: 1, | ||
type: ['array'] | ||
}; | ||
const allArraysWithLengthElevenToFifteen: JsonSchema = { | ||
maxItems: 15, | ||
minItems: 11, | ||
type: ['array'] | ||
}; | ||
const allArraysOfTypeStringWithLengthFiveToTen: JsonSchema = { | ||
items: { | ||
type: ['string'] | ||
}, | ||
maxItems: 15, | ||
minItems: 1, | ||
type: ['array'] | ||
}; | ||
const allArraysMatchingConstraints: JsonSchema = { | ||
anyOf: [ | ||
allArraysOfTypeStringWithLengthFiveToTen, | ||
allArraysWithLengthOneToFour, | ||
allArraysWithLengthElevenToFifteen | ||
] | ||
}; | ||
expect(diffResult.addedJsonSchema).toEqual(allArraysMatchingConstraints); | ||
expect(diffResult.removedJsonSchema).toEqual(false); | ||
}); | ||
}); | ||
}); |
@@ -1,323 +0,365 @@ | ||
import {JsonSchema, SimpleTypes} from 'json-schema-spec-types'; | ||
import {CoreSchemaMetaSchema, JsonSchema, SimpleTypes} from 'json-schema-spec-types'; | ||
import {invokeDiff} from '../support/invoke-diff'; | ||
describe('diff-schemas type object', () => { | ||
it('should find a difference between schemas with boolean additional properties', async () => { | ||
const sourceSchema: JsonSchema = { | ||
additionalProperties: true, | ||
type: 'object' | ||
}; | ||
const destinationSchema: JsonSchema = { | ||
additionalProperties: false, | ||
type: 'object' | ||
}; | ||
describe('additionalProperties', () => { | ||
it('should find a difference between schemas with boolean additional properties', async () => { | ||
const sourceSchema: JsonSchema = { | ||
additionalProperties: true, | ||
type: 'object' | ||
}; | ||
const destinationSchema: JsonSchema = { | ||
additionalProperties: false, | ||
type: 'object' | ||
}; | ||
const diffResult = await invokeDiff(sourceSchema, destinationSchema); | ||
const diffResult = await invokeDiff(sourceSchema, destinationSchema); | ||
const allObjectsWithAtLeastOneProperty: JsonSchema = { | ||
minProperties: 1, | ||
type: ['object'] | ||
}; | ||
expect(diffResult.addedJsonSchema).toEqual(false); | ||
expect(diffResult.removedJsonSchema).toEqual(allObjectsWithAtLeastOneProperty); | ||
const allObjectsWithAtLeastOneProperty: JsonSchema = { | ||
minProperties: 1, | ||
type: ['object'] | ||
}; | ||
expect(diffResult.addedJsonSchema).toEqual(false); | ||
expect(diffResult.removedJsonSchema).toEqual(allObjectsWithAtLeastOneProperty); | ||
}); | ||
}); | ||
it('should infer that all objects are supported when no constraints are applied by properties', async () => { | ||
const sourceSchema: JsonSchema = { | ||
properties: { | ||
name: true | ||
}, | ||
type: 'object' | ||
}; | ||
const destinationSchema: JsonSchema = { | ||
additionalProperties: false, | ||
type: 'object' | ||
}; | ||
describe('properties', () => { | ||
it('should find removed differences when a property constraint is added', async () => { | ||
const allTypesButNull: SimpleTypes[] = ['object', 'string', 'array', 'integer', 'number', 'boolean']; | ||
const sourceSchema: JsonSchema = { | ||
type: 'object' | ||
}; | ||
const destinationSchema: JsonSchema = { | ||
properties: { | ||
name: { | ||
type: allTypesButNull | ||
} | ||
}, | ||
type: 'object' | ||
}; | ||
const diffResult = await invokeDiff(sourceSchema, destinationSchema); | ||
const diffResult = await invokeDiff(sourceSchema, destinationSchema); | ||
const allObjectsWithAtLeastOneProperty: JsonSchema = { | ||
minProperties: 1, | ||
type: ['object'] | ||
}; | ||
expect(diffResult.addedJsonSchema).toEqual(false); | ||
expect(diffResult.removedJsonSchema).toEqual(allObjectsWithAtLeastOneProperty); | ||
}); | ||
const allObjectsWithRequiredNullName: JsonSchema = { | ||
properties: { | ||
name: {type: ['null']} | ||
}, | ||
required: ['name'], | ||
type: ['object'] | ||
}; | ||
expect(diffResult.addedJsonSchema).toEqual(false); | ||
expect(diffResult.removedJsonSchema).toEqual(allObjectsWithRequiredNullName); | ||
}); | ||
it('should not infer that all objects are supported when constraints are applied by some properties', async () => { | ||
const sourceSchema: JsonSchema = { | ||
properties: { | ||
first: true, | ||
last: false | ||
}, | ||
type: 'object' | ||
}; | ||
const destinationSchema: JsonSchema = { | ||
additionalProperties: true, | ||
type: 'object' | ||
}; | ||
it('should find an add and a remove differences when a property constraint is changed', async () => { | ||
const sourceSchema: JsonSchema = { | ||
properties: { | ||
name: {type: 'array'} | ||
}, | ||
type: 'object' | ||
}; | ||
const destinationSchema: JsonSchema = { | ||
properties: { | ||
name: {type: 'string'} | ||
}, | ||
type: 'object' | ||
}; | ||
const diffResult = await invokeDiff(sourceSchema, destinationSchema); | ||
const diffResult = await invokeDiff(sourceSchema, destinationSchema); | ||
const allObjectsWithRequiredLastProperty: JsonSchema = { | ||
properties: { | ||
last: true | ||
}, | ||
required: ['last'], | ||
type: ['object'] | ||
}; | ||
expect(diffResult.addedJsonSchema).toEqual(allObjectsWithRequiredLastProperty); | ||
expect(diffResult.removedJsonSchema).toEqual(false); | ||
}); | ||
const allObjectsWithRequiredStringName: JsonSchema = { | ||
properties: { | ||
name: {type: ['string']} | ||
}, | ||
required: ['name'], | ||
type: ['object'] | ||
}; | ||
const allObjectsWithRequiredArrayName: JsonSchema = { | ||
properties: { | ||
name: {type: ['array']} | ||
}, | ||
required: ['name'], | ||
type: ['object'] | ||
}; | ||
expect(diffResult.addedJsonSchema).toEqual(allObjectsWithRequiredStringName); | ||
expect(diffResult.removedJsonSchema).toEqual(allObjectsWithRequiredArrayName); | ||
}); | ||
it('should infer empty object set when required contradicts properties', async () => { | ||
const sourceSchema: JsonSchema = { | ||
additionalProperties: false, | ||
properties: { | ||
name: false | ||
}, | ||
required: ['name'], | ||
type: 'object' | ||
}; | ||
const destinationSchema: JsonSchema = { | ||
additionalProperties: true, | ||
type: 'object' | ||
}; | ||
it('should not infer that all objects are supported when some properties are constrained ', async () => { | ||
const sourceSchema: JsonSchema = { | ||
properties: { | ||
first: true, | ||
last: false | ||
}, | ||
type: 'object' | ||
}; | ||
const destinationSchema: JsonSchema = { | ||
type: 'object' | ||
}; | ||
const diffResult = await invokeDiff(sourceSchema, destinationSchema); | ||
const diffResult = await invokeDiff(sourceSchema, destinationSchema); | ||
const allObjects: JsonSchema = {type: ['object']}; | ||
expect(diffResult.addedJsonSchema).toEqual(allObjects); | ||
expect(diffResult.removedJsonSchema).toEqual(false); | ||
const allObjectsWithRequiredLastProperty: JsonSchema = { | ||
properties: { | ||
last: true | ||
}, | ||
required: ['last'], | ||
type: ['object'] | ||
}; | ||
expect(diffResult.addedJsonSchema).toEqual(allObjectsWithRequiredLastProperty); | ||
expect(diffResult.removedJsonSchema).toEqual(false); | ||
}); | ||
}); | ||
it('should find an add and a remove differences when changing properties type', async () => { | ||
const sourceSchema: JsonSchema = { | ||
properties: { | ||
name: {type: 'array'} | ||
}, | ||
type: 'object' | ||
}; | ||
const destinationSchema: JsonSchema = { | ||
properties: { | ||
name: {type: 'string'} | ||
}, | ||
type: 'object' | ||
}; | ||
describe('required', () => { | ||
it('should find a difference between schemas with required property completely removed', async () => { | ||
const sourceSchema: JsonSchema = { | ||
required: ['first'], | ||
type: 'object' | ||
}; | ||
const destinationSchema: JsonSchema = {type: 'object'}; | ||
const diffResult = await invokeDiff(sourceSchema, destinationSchema); | ||
const diffResult = await invokeDiff(sourceSchema, destinationSchema); | ||
const allObjectsWithRequiredStringName: JsonSchema = { | ||
properties: { | ||
name: {type: ['string']} | ||
}, | ||
required: ['name'], | ||
type: ['object'] | ||
}; | ||
const allObjectsWithRequiredArrayName: JsonSchema = { | ||
properties: { | ||
name: {type: ['array']} | ||
}, | ||
required: ['name'], | ||
type: ['object'] | ||
}; | ||
expect(diffResult.addedJsonSchema).toEqual(allObjectsWithRequiredStringName); | ||
expect(diffResult.removedJsonSchema).toEqual(allObjectsWithRequiredArrayName); | ||
}); | ||
const allObjectsWithNoFirst: JsonSchema = { | ||
properties: { | ||
first: false | ||
}, | ||
type: ['object'] | ||
}; | ||
expect(diffResult.addedJsonSchema).toEqual(allObjectsWithNoFirst); | ||
expect(diffResult.removedJsonSchema).toEqual(false); | ||
}); | ||
it('should find no differences when a property matching additionalProperties is removed', async () => { | ||
const sourceSchema: JsonSchema = { | ||
additionalProperties: {type: 'string'}, | ||
properties: { | ||
name: {type: 'string'} | ||
}, | ||
type: 'object' | ||
}; | ||
const destinationSchema: JsonSchema = { | ||
additionalProperties: {type: 'string'}, | ||
type: 'object' | ||
}; | ||
it('should find a difference between schemas with required property removed', async () => { | ||
const sourceSchema: JsonSchema = { | ||
required: ['name', 'first'], | ||
type: 'object' | ||
}; | ||
const destinationSchema: JsonSchema = { | ||
required: ['name'], | ||
type: 'object' | ||
}; | ||
const diffResult = await invokeDiff(sourceSchema, destinationSchema); | ||
const diffResult = await invokeDiff(sourceSchema, destinationSchema); | ||
expect(diffResult.addedJsonSchema).toEqual(false); | ||
expect(diffResult.removedJsonSchema).toEqual(false); | ||
const allObjectsWithRequiredNameAndNoFirst: JsonSchema = { | ||
properties: {first: false}, | ||
required: ['name'], | ||
type: ['object'] | ||
}; | ||
expect(diffResult.addedJsonSchema).toEqual(allObjectsWithRequiredNameAndNoFirst); | ||
expect(diffResult.removedJsonSchema).toEqual(false); | ||
}); | ||
}); | ||
it('should find differences between schemas with properties and additionalProperties', async () => { | ||
const allTypesButNull: SimpleTypes[] = ['object', 'string', 'array', 'integer', 'number', 'boolean']; | ||
const sourceSchema: JsonSchema = { | ||
additionalProperties: true, | ||
type: 'object' | ||
}; | ||
const destinationSchema: JsonSchema = { | ||
additionalProperties: true, | ||
properties: { | ||
name: { | ||
type: allTypesButNull | ||
} | ||
}, | ||
type: 'object' | ||
}; | ||
describe('minProperties', () => { | ||
it('should find a removed difference when a minProperties constraint is added', async () => { | ||
const sourceSchema: JsonSchema = { | ||
type: 'object' | ||
}; | ||
const destinationSchema: JsonSchema = { | ||
minProperties: 10, | ||
type: 'object' | ||
}; | ||
const diffResult = await invokeDiff(sourceSchema, destinationSchema); | ||
const diffResult = await invokeDiff(sourceSchema, destinationSchema); | ||
const allObjectsWithRequiredNullName: JsonSchema = { | ||
properties: { | ||
name: {type: ['null']} | ||
}, | ||
required: ['name'], | ||
type: ['object'] | ||
}; | ||
expect(diffResult.addedJsonSchema).toEqual(false); | ||
expect(diffResult.removedJsonSchema).toEqual(allObjectsWithRequiredNullName); | ||
}); | ||
const allObjectsWithZeroToNineProperties: JsonSchema = { | ||
maxProperties: 9, | ||
type: ['object'] | ||
}; | ||
expect(diffResult.addedJsonSchema).toEqual(false); | ||
expect(diffResult.removedJsonSchema).toEqual(allObjectsWithZeroToNineProperties); | ||
}); | ||
it('should find differences between schemas with properties and non boolean additionalProperties', async () => { | ||
const sourceSchema: JsonSchema = { | ||
additionalProperties: {type: ['string', 'number']}, | ||
type: 'object' | ||
}; | ||
const destinationSchema: JsonSchema = { | ||
additionalProperties: {type: ['string', 'number']}, | ||
properties: { | ||
name: { | ||
type: 'string' | ||
} | ||
}, | ||
type: 'object' | ||
}; | ||
it('should find an removed difference when a minProperties constraint made more strict', async () => { | ||
const sourceSchema: JsonSchema = { | ||
minProperties: 5, | ||
type: 'object' | ||
}; | ||
const destinationSchema: JsonSchema = { | ||
minProperties: 10, | ||
type: 'object' | ||
}; | ||
const diffResult = await invokeDiff(sourceSchema, destinationSchema); | ||
const diffResult = await invokeDiff(sourceSchema, destinationSchema); | ||
const allObjectsWithRequiredNumberNameAndStringOrNumberAdditionalProperties: JsonSchema = { | ||
additionalProperties: {type: ['number', 'string']}, | ||
properties: { | ||
name: {type: ['number']} | ||
}, | ||
required: ['name'], | ||
type: ['object'] | ||
}; | ||
expect(diffResult.addedJsonSchema).toEqual(false); | ||
expect(diffResult.removedJsonSchema) | ||
.toEqual(allObjectsWithRequiredNumberNameAndStringOrNumberAdditionalProperties); | ||
const allObjectsWithFiveToNineProperties: JsonSchema = { | ||
maxProperties: 9, | ||
minProperties: 5, | ||
type: ['object'] | ||
}; | ||
expect(diffResult.addedJsonSchema).toEqual(false); | ||
expect(diffResult.removedJsonSchema).toEqual(allObjectsWithFiveToNineProperties); | ||
}); | ||
it('should find an object with no properties removed when minProperties 1 constraint is added', async () => { | ||
const sourceSchema: JsonSchema = { | ||
type: 'object' | ||
}; | ||
const destinationSchema: JsonSchema = { | ||
minProperties: 1, | ||
type: 'object' | ||
}; | ||
const diffResult = await invokeDiff(sourceSchema, destinationSchema); | ||
const anObjectWithNoProperties: JsonSchema = { | ||
additionalProperties: false, | ||
type: ['object'] | ||
}; | ||
expect(diffResult.addedJsonSchema).toEqual(false); | ||
expect(diffResult.removedJsonSchema).toEqual(anObjectWithNoProperties); | ||
}); | ||
}); | ||
it('should find differences in schemas with boolean additionalProperties and optional properties', async () => { | ||
const sourceSchema: JsonSchema = { | ||
additionalProperties: true, | ||
properties: { | ||
name: {type: 'string'} | ||
}, | ||
type: ['object'] | ||
}; | ||
const destinationSchema: JsonSchema = { | ||
additionalProperties: false, | ||
properties: { | ||
name: {type: 'string'} | ||
}, | ||
type: ['object'] | ||
}; | ||
describe('maxProperties', () => { | ||
it('should find a removed difference when a maxProperties constraint is added', async () => { | ||
const sourceSchema: JsonSchema = { | ||
type: 'object' | ||
}; | ||
const destinationSchema: JsonSchema = { | ||
maxProperties: 10, | ||
type: 'object' | ||
}; | ||
const diffResult = await invokeDiff(sourceSchema, destinationSchema); | ||
const diffResult = await invokeDiff(sourceSchema, destinationSchema); | ||
const allObjectsWithNoNameAndAtLeastOneProperty: JsonSchema = { | ||
minProperties: 1, | ||
properties: { | ||
name: false | ||
}, | ||
type: ['object'] | ||
}; | ||
const allObjectsWithRequiredStringNameAndAtLeastAnotherProperty: JsonSchema = { | ||
minProperties: 2, | ||
properties: { | ||
name: { | ||
type: ['string'] | ||
} | ||
}, | ||
required: ['name'], | ||
type: ['object'] | ||
}; | ||
expect(diffResult.addedJsonSchema).toEqual(false); | ||
expect(diffResult.removedJsonSchema).toEqual({ | ||
anyOf: [ | ||
allObjectsWithNoNameAndAtLeastOneProperty, | ||
allObjectsWithRequiredStringNameAndAtLeastAnotherProperty | ||
] | ||
const allObjectsWithAtLeastElevenProperties: JsonSchema = { | ||
minProperties: 11, | ||
type: ['object'] | ||
}; | ||
expect(diffResult.addedJsonSchema).toEqual(false); | ||
expect(diffResult.removedJsonSchema).toEqual(allObjectsWithAtLeastElevenProperties); | ||
}); | ||
it('should find an removed difference when a maxProperties constraint is made more strict', async () => { | ||
const sourceSchema: JsonSchema = { | ||
maxProperties: 10, | ||
type: 'object' | ||
}; | ||
const destinationSchema: JsonSchema = { | ||
maxProperties: 5, | ||
type: 'object' | ||
}; | ||
const diffResult = await invokeDiff(sourceSchema, destinationSchema); | ||
const allObjectsWithSixToTenProperties: JsonSchema = { | ||
maxProperties: 10, | ||
minProperties: 6, | ||
type: ['object'] | ||
}; | ||
expect(diffResult.addedJsonSchema).toEqual(false); | ||
expect(diffResult.removedJsonSchema).toEqual(allObjectsWithSixToTenProperties); | ||
}); | ||
it('should consider a maxProperties constraint of 0 to be an object with no properties', async () => { | ||
const sourceSchema: JsonSchema = { | ||
maxProperties: 0, | ||
type: 'object' | ||
}; | ||
const destinationSchema: JsonSchema = false; | ||
const diffResult = await invokeDiff(sourceSchema, destinationSchema); | ||
const anObjectWithNoProperties: JsonSchema = { | ||
additionalProperties: false, | ||
type: ['object'] | ||
}; | ||
expect(diffResult.addedJsonSchema).toEqual(false); | ||
expect(diffResult.removedJsonSchema).toEqual(anObjectWithNoProperties); | ||
}); | ||
}); | ||
it('should add numbers into the anyOf when the object differences result in an anyOf', async () => { | ||
const sourceSchema: JsonSchema = { | ||
additionalProperties: true, | ||
properties: { | ||
name: {type: 'string'} | ||
}, | ||
type: ['object', 'number'] | ||
}; | ||
const destinationSchema: JsonSchema = { | ||
additionalProperties: false, | ||
properties: { | ||
name: {type: 'string'} | ||
}, | ||
type: ['object'] | ||
}; | ||
describe('additionalProperties + maxProperties', () => { | ||
it('should find no differences between different representations of an object with no properties', async () => { | ||
const sourceSchema: JsonSchema = { | ||
additionalProperties: false, | ||
type: 'object' | ||
}; | ||
const destinationSchema: JsonSchema = { | ||
maxProperties: 0, | ||
type: 'object' | ||
}; | ||
const diffResult = await invokeDiff(sourceSchema, destinationSchema); | ||
const diffResult = await invokeDiff(sourceSchema, destinationSchema); | ||
const allObjectsWithNoNameAndAtLeastOneProperty: JsonSchema = { | ||
minProperties: 1, | ||
properties: { | ||
name: false | ||
}, | ||
type: ['object'] | ||
}; | ||
const allObjectsWithRequiredStringNameAndAtLeastAnotherProperty: JsonSchema = { | ||
minProperties: 2, | ||
properties: { | ||
name: { | ||
type: ['string'] | ||
} | ||
}, | ||
required: ['name'], | ||
type: ['object'] | ||
}; | ||
const allNumbers: JsonSchema = {type: ['number']}; | ||
expect(diffResult.addedJsonSchema).toEqual(false); | ||
expect(diffResult.removedJsonSchema).toEqual({ | ||
anyOf: [ | ||
allObjectsWithNoNameAndAtLeastOneProperty, | ||
allObjectsWithRequiredStringNameAndAtLeastAnotherProperty, | ||
allNumbers | ||
] | ||
expect(diffResult.addedJsonSchema).toEqual(false); | ||
expect(diffResult.removedJsonSchema).toEqual(false); | ||
}); | ||
}); | ||
describe('required properties', () => { | ||
it('should find a difference between schemas with required property completely removed', async () => { | ||
describe('additionalProperties + minProperties', () => { | ||
it('should infer that no objects are supported when additional and min properties contradict', async () => { | ||
const sourceSchema: JsonSchema = { | ||
required: ['first'], | ||
additionalProperties: false, | ||
minProperties: 1, | ||
type: 'object' | ||
}; | ||
const destinationSchema: JsonSchema = {type: 'object'}; | ||
const destinationSchema: JsonSchema = false; | ||
const diffResult = await invokeDiff(sourceSchema, destinationSchema); | ||
const allObjectsWithNoFirst: JsonSchema = { | ||
expect(diffResult.addedJsonSchema).toEqual(false); | ||
expect(diffResult.removedJsonSchema).toEqual(false); | ||
}); | ||
}); | ||
describe('additionalProperties + properties', () => { | ||
it('should infer that all objects are supported when no constraints are applied by properties', async () => { | ||
const sourceSchema: JsonSchema = { | ||
properties: { | ||
first: false | ||
name: true | ||
}, | ||
type: 'object' | ||
}; | ||
const destinationSchema: JsonSchema = { | ||
additionalProperties: false, | ||
type: 'object' | ||
}; | ||
const diffResult = await invokeDiff(sourceSchema, destinationSchema); | ||
const allObjectsWithAtLeastOneProperty: JsonSchema = { | ||
minProperties: 1, | ||
type: ['object'] | ||
}; | ||
expect(diffResult.addedJsonSchema).toEqual(allObjectsWithNoFirst); | ||
expect(diffResult.addedJsonSchema).toEqual(false); | ||
expect(diffResult.removedJsonSchema).toEqual(allObjectsWithAtLeastOneProperty); | ||
}); | ||
it('should find no differences when a property matching additionalProperties is removed', async () => { | ||
const sourceSchema: JsonSchema = { | ||
additionalProperties: {type: 'string'}, | ||
properties: { | ||
name: {type: 'string'} | ||
}, | ||
type: 'object' | ||
}; | ||
const destinationSchema: JsonSchema = { | ||
additionalProperties: {type: 'string'}, | ||
type: 'object' | ||
}; | ||
const diffResult = await invokeDiff(sourceSchema, destinationSchema); | ||
expect(diffResult.addedJsonSchema).toEqual(false); | ||
expect(diffResult.removedJsonSchema).toEqual(false); | ||
}); | ||
it('should find a difference between schemas with required property removed', async () => { | ||
it('should find differences between schemas with properties and non boolean additionalProperties', async () => { | ||
const sourceSchema: JsonSchema = { | ||
required: ['name', 'first'], | ||
additionalProperties: {type: ['string', 'number']}, | ||
type: 'object' | ||
}; | ||
const destinationSchema: JsonSchema = { | ||
required: ['name'], | ||
additionalProperties: {type: ['string', 'number']}, | ||
properties: { | ||
name: { | ||
type: 'string' | ||
} | ||
}, | ||
type: 'object' | ||
@@ -328,11 +370,415 @@ }; | ||
const allObjectsWithRequiredNameAndNoFirst: JsonSchema = { | ||
properties: { first: false }, | ||
const allObjectsWithRequiredNumberNameAndStringOrNumberAdditionalProperties: JsonSchema = { | ||
additionalProperties: {type: ['number', 'string']}, | ||
properties: { | ||
name: {type: ['number']} | ||
}, | ||
required: ['name'], | ||
type: ['object'] | ||
}; | ||
expect(diffResult.addedJsonSchema).toEqual(allObjectsWithRequiredNameAndNoFirst); | ||
expect(diffResult.addedJsonSchema).toEqual(false); | ||
expect(diffResult.removedJsonSchema) | ||
.toEqual(allObjectsWithRequiredNumberNameAndStringOrNumberAdditionalProperties); | ||
}); | ||
it('should find differences in schemas with boolean additionalProperties and optional properties', async () => { | ||
const sourceSchema: JsonSchema = { | ||
properties: { | ||
name: {type: 'string'} | ||
}, | ||
type: ['object'] | ||
}; | ||
const destinationSchema: JsonSchema = { | ||
additionalProperties: false, | ||
properties: { | ||
name: {type: 'string'} | ||
}, | ||
type: ['object'] | ||
}; | ||
const diffResult = await invokeDiff(sourceSchema, destinationSchema); | ||
const allObjectsWithNoNameAndAtLeastOneProperty: JsonSchema = { | ||
minProperties: 1, | ||
properties: { | ||
name: false | ||
}, | ||
type: ['object'] | ||
}; | ||
const allObjectsWithRequiredStringNameAndAtLeastAnotherProperty: JsonSchema = { | ||
minProperties: 2, | ||
properties: { | ||
name: { | ||
type: ['string'] | ||
} | ||
}, | ||
required: ['name'], | ||
type: ['object'] | ||
}; | ||
expect(diffResult.addedJsonSchema).toEqual(false); | ||
expect(diffResult.removedJsonSchema).toEqual({ | ||
anyOf: [ | ||
allObjectsWithNoNameAndAtLeastOneProperty, | ||
allObjectsWithRequiredStringNameAndAtLeastAnotherProperty | ||
] | ||
}); | ||
}); | ||
it('should add numbers into the anyOf when the object differences result in an anyOf', async () => { | ||
const sourceSchema: JsonSchema = { | ||
properties: { | ||
name: {type: 'string'} | ||
}, | ||
type: ['object', 'number'] | ||
}; | ||
const destinationSchema: JsonSchema = { | ||
additionalProperties: false, | ||
properties: { | ||
name: {type: 'string'} | ||
}, | ||
type: ['object'] | ||
}; | ||
const diffResult = await invokeDiff(sourceSchema, destinationSchema); | ||
const allObjectsWithNoNameAndAtLeastOneProperty: JsonSchema = { | ||
minProperties: 1, | ||
properties: { | ||
name: false | ||
}, | ||
type: ['object'] | ||
}; | ||
const allObjectsWithRequiredStringNameAndAtLeastAnotherProperty: JsonSchema = { | ||
minProperties: 2, | ||
properties: { | ||
name: { | ||
type: ['string'] | ||
} | ||
}, | ||
required: ['name'], | ||
type: ['object'] | ||
}; | ||
const allNumbers: JsonSchema = {type: ['number']}; | ||
expect(diffResult.addedJsonSchema).toEqual(false); | ||
expect(diffResult.removedJsonSchema).toEqual({ | ||
anyOf: [ | ||
allObjectsWithNoNameAndAtLeastOneProperty, | ||
allObjectsWithRequiredStringNameAndAtLeastAnotherProperty, | ||
allNumbers | ||
] | ||
}); | ||
}); | ||
it('should find 4 sets of differences when additional properties is changed with 2 properties', async () => { | ||
const sourceSchema: JsonSchema = { | ||
properties: { | ||
first: {type: 'string'}, | ||
last: {type: 'string'} | ||
}, | ||
type: 'object' | ||
}; | ||
const destinationSchema: JsonSchema = { | ||
additionalProperties: false, | ||
properties: { | ||
first: {type: 'string'}, | ||
last: {type: 'string'} | ||
}, | ||
type: 'object' | ||
}; | ||
const diffResult = await invokeDiff(sourceSchema, destinationSchema); | ||
const anyObjectWithNoFirstAndNoLastAndAtLeastOneProperty: JsonSchema = { | ||
minProperties: 1, | ||
properties: { | ||
first: false, | ||
last: false | ||
}, | ||
type: ['object'] | ||
}; | ||
const anyObjectWithNoFirstAndRequiredStringLastAndAtLeastTwoProperties: JsonSchema = { | ||
minProperties: 2, | ||
properties: { | ||
first: false, | ||
last: {type: ['string']} | ||
}, | ||
required: ['last'], | ||
type: ['object'] | ||
}; | ||
const anyObjectWithRequiredStringFirstAndNoLastAndAtLeastTwoProperties: JsonSchema = { | ||
minProperties: 2, | ||
properties: { | ||
first: {type: ['string']}, | ||
last: false | ||
}, | ||
required: ['first'], | ||
type: ['object'] | ||
}; | ||
const anyObjectWithRequiredStringFirstAndRequiredStringLastAndAtLeastThreeProperties: JsonSchema = { | ||
minProperties: 3, | ||
properties: { | ||
first: {type: ['string']}, | ||
last: {type: ['string']} | ||
}, | ||
required: ['first', 'last'], | ||
type: ['object'] | ||
}; | ||
expect(diffResult.addedJsonSchema).toEqual(false); | ||
expect(diffResult.removedJsonSchema).toEqual({ | ||
anyOf: [ | ||
anyObjectWithNoFirstAndNoLastAndAtLeastOneProperty, | ||
anyObjectWithRequiredStringFirstAndNoLastAndAtLeastTwoProperties, | ||
anyObjectWithNoFirstAndRequiredStringLastAndAtLeastTwoProperties, | ||
anyObjectWithRequiredStringFirstAndRequiredStringLastAndAtLeastThreeProperties | ||
] | ||
}); | ||
}); | ||
it('should find 8 sets of differences when additional properties is changed with 3 properties', async () => { | ||
const sourceSchema: JsonSchema = { | ||
properties: { | ||
p1: {type: 'string'}, | ||
p2: {type: 'string'}, | ||
p3: {type: 'string'} | ||
}, | ||
type: ['object'] | ||
}; | ||
const destinationSchema: JsonSchema = { | ||
additionalProperties: false, | ||
properties: { | ||
p1: {type: 'string'}, | ||
p2: {type: 'string'}, | ||
p3: {type: 'string'} | ||
}, | ||
type: ['object'] | ||
}; | ||
const diffResult = await invokeDiff(sourceSchema, destinationSchema); | ||
const anyObjectWithNoP123AndAtLeastOneProperty: JsonSchema = { | ||
minProperties: 1, | ||
properties: { | ||
p1: false, | ||
p2: false, | ||
p3: false | ||
}, | ||
type: ['object'] | ||
}; | ||
const anyObjectWithRequiredStringP1AndNoP23AndAtLeastTwoProperties: JsonSchema = { | ||
minProperties: 2, | ||
properties: { | ||
p1: {type: ['string']}, | ||
p2: false, | ||
p3: false | ||
}, | ||
required: ['p1'], | ||
type: ['object'] | ||
}; | ||
const anyObjectWithNoP13AndRequiredStringP2AndAtLeastTwoProperties: JsonSchema = { | ||
minProperties: 2, | ||
properties: { | ||
p1: false, | ||
p2: {type: ['string']}, | ||
p3: false | ||
}, | ||
required: ['p2'], | ||
type: ['object'] | ||
}; | ||
const anyObjectWithNoP12AndRequiredStringP3AndAtLeastTwoProperties: JsonSchema = { | ||
minProperties: 2, | ||
properties: { | ||
p1: false, | ||
p2: false, | ||
p3: {type: ['string']} | ||
}, | ||
required: ['p3'], | ||
type: ['object'] | ||
}; | ||
const anyObjectWithRequiredStringP12AndNoP3AndAtLeastThreeProperties: JsonSchema = { | ||
minProperties: 3, | ||
properties: { | ||
p1: {type: ['string']}, | ||
p2: {type: ['string']}, | ||
p3: false | ||
}, | ||
required: ['p1', 'p2'], | ||
type: ['object'] | ||
}; | ||
const anyObjectWithRequiredStringP13AndNoP2AtLeastThreeProperties: JsonSchema = { | ||
minProperties: 3, | ||
properties: { | ||
p1: {type: ['string']}, | ||
p2: false, | ||
p3: {type: ['string']} | ||
}, | ||
required: ['p1', 'p3'], | ||
type: ['object'] | ||
}; | ||
const anyObjectWithNoP1AndRequiredStringP23AndAtLeastThreeProperties: JsonSchema = { | ||
minProperties: 3, | ||
properties: { | ||
p1: false, | ||
p2: {type: ['string']}, | ||
p3: {type: ['string']} | ||
}, | ||
required: ['p2', 'p3'], | ||
type: ['object'] | ||
}; | ||
const anyObjectWithRequiredStringP123AndAtLeastFourProperties: JsonSchema = { | ||
minProperties: 4, | ||
properties: { | ||
p1: {type: ['string']}, | ||
p2: {type: ['string']}, | ||
p3: {type: ['string']} | ||
}, | ||
required: ['p1', 'p2', 'p3'], | ||
type: ['object'] | ||
}; | ||
expect(diffResult.addedJsonSchema).toEqual(false); | ||
expect(diffResult.removedJsonSchema).toEqual({ | ||
anyOf: [ | ||
anyObjectWithNoP123AndAtLeastOneProperty, | ||
anyObjectWithRequiredStringP1AndNoP23AndAtLeastTwoProperties, | ||
anyObjectWithNoP13AndRequiredStringP2AndAtLeastTwoProperties, | ||
anyObjectWithRequiredStringP12AndNoP3AndAtLeastThreeProperties, | ||
anyObjectWithNoP12AndRequiredStringP3AndAtLeastTwoProperties, | ||
anyObjectWithRequiredStringP13AndNoP2AtLeastThreeProperties, | ||
anyObjectWithNoP1AndRequiredStringP23AndAtLeastThreeProperties, | ||
anyObjectWithRequiredStringP123AndAtLeastFourProperties | ||
] | ||
}); | ||
}); | ||
it('should find 32 sets of differences when additional properties is changed with 5 properties', async () => { | ||
const sourceSchema: JsonSchema = { | ||
properties: { | ||
p1: {type: 'string'}, | ||
p2: {type: 'string'}, | ||
p3: {type: 'string'}, | ||
p4: {type: 'string'}, | ||
p5: {type: 'string'} | ||
}, | ||
type: ['object'] | ||
}; | ||
const destinationSchema: JsonSchema = { | ||
additionalProperties: false, | ||
properties: { | ||
p1: {type: 'string'}, | ||
p2: {type: 'string'}, | ||
p3: {type: 'string'}, | ||
p4: {type: 'string'}, | ||
p5: {type: 'string'} | ||
}, | ||
type: ['object'] | ||
}; | ||
const diffResult = await invokeDiff(sourceSchema, destinationSchema); | ||
const removedJsonSchema = diffResult.removedJsonSchema as CoreSchemaMetaSchema; | ||
const removedJsonSchemaAnyOf = removedJsonSchema.anyOf as JsonSchema[]; | ||
expect(diffResult.addedJsonSchema).toEqual(false); | ||
expect(removedJsonSchemaAnyOf.length).toBe(32); | ||
}); | ||
it('should limit the number of sets of differences found to 32 to avoid performance problems', async () => { | ||
const sourceSchema: JsonSchema = { | ||
properties: { | ||
p1: {type: 'string'}, | ||
p2: {type: 'string'}, | ||
p3: {type: 'string'}, | ||
p4: {type: 'string'}, | ||
p5: {type: 'string'}, | ||
p6: {type: 'string'} | ||
}, | ||
type: ['object'] | ||
}; | ||
const destinationSchema: JsonSchema = { | ||
additionalProperties: false, | ||
properties: { | ||
p1: {type: 'string'}, | ||
p2: {type: 'string'}, | ||
p3: {type: 'string'}, | ||
p4: {type: 'string'}, | ||
p5: {type: 'string'}, | ||
p6: {type: 'string'} | ||
}, | ||
type: ['object'] | ||
}; | ||
const diffResult = await invokeDiff(sourceSchema, destinationSchema); | ||
const removedJsonSchema = diffResult.removedJsonSchema as CoreSchemaMetaSchema; | ||
const removedJsonSchemaAnyOf = removedJsonSchema.anyOf as JsonSchema[]; | ||
expect(diffResult.addedJsonSchema).toEqual(false); | ||
expect(removedJsonSchemaAnyOf.length).toBe(32); | ||
}); | ||
}); | ||
describe('maxProperties + required', () => { | ||
it('should infer that no objects are supported when required and maxProperties contradict', async () => { | ||
const sourceSchema: JsonSchema = { | ||
maxProperties: 1, | ||
required: ['first', 'last'], | ||
type: 'object' | ||
}; | ||
const destinationSchema: JsonSchema = false; | ||
const diffResult = await invokeDiff(sourceSchema, destinationSchema); | ||
expect(diffResult.addedJsonSchema).toEqual(false); | ||
expect(diffResult.removedJsonSchema).toEqual(false); | ||
}); | ||
}); | ||
describe('maxProperties + minProperties', () => { | ||
it('should consider contradictions to accept no objects', async () => { | ||
const sourceSchema: JsonSchema = { | ||
maxProperties: 0, | ||
minProperties: 1, | ||
type: 'object' | ||
}; | ||
const destinationSchema: JsonSchema = { | ||
type: 'object' | ||
}; | ||
const diffResult = await invokeDiff(sourceSchema, destinationSchema); | ||
const allObjects: JsonSchema = {type: ['object']}; | ||
expect(diffResult.addedJsonSchema).toEqual(allObjects); | ||
expect(diffResult.removedJsonSchema).toEqual(false); | ||
}); | ||
}); | ||
describe('properties + required', () => { | ||
it('should not find differences between two different contradictions', async () => { | ||
const sourceSchema: JsonSchema = { | ||
properties: { | ||
name: false | ||
}, | ||
required: ['name'], | ||
type: 'object' | ||
}; | ||
const destinationSchema: JsonSchema = { | ||
properties: { | ||
address: { | ||
properties: { | ||
street: false | ||
}, | ||
required: ['street'], | ||
type: 'object' | ||
} | ||
}, | ||
required: ['address'], | ||
type: 'object' | ||
}; | ||
const diffResult = await invokeDiff(sourceSchema, destinationSchema); | ||
expect(diffResult.addedJsonSchema).toEqual(false); | ||
expect(diffResult.removedJsonSchema).toEqual(false); | ||
}); | ||
it('should find differences when required property with changed schema is added', async () => { | ||
@@ -379,2 +825,24 @@ const sourceSchema: JsonSchema = { | ||
}); | ||
describe('additionalProperties + properties + required', () => { | ||
it('should infer no objects are accepted when required contradicts properties', async () => { | ||
const sourceSchema: JsonSchema = { | ||
additionalProperties: false, | ||
properties: { | ||
name: false | ||
}, | ||
required: ['name'], | ||
type: 'object' | ||
}; | ||
const destinationSchema: JsonSchema = { | ||
type: 'object' | ||
}; | ||
const diffResult = await invokeDiff(sourceSchema, destinationSchema); | ||
const allObjects: JsonSchema = {type: ['object']}; | ||
expect(diffResult.addedJsonSchema).toEqual(allObjects); | ||
expect(diffResult.removedJsonSchema).toEqual(false); | ||
}); | ||
}); | ||
}); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
214930
25.42%110
20.88%4471
24.68%7
16.67%21
5%+ Added
+ Added
+ Added
- Removed
- Removed
- Removed