Skip to content

Commit 495f763

Browse files
feat: simplify patternProperties (#153)
Co-authored-by: Maciej Urbańczyk <urbanczyk.maciej.95@gmail.com>
1 parent 2f1c8f5 commit 495f763

33 files changed

+673
-36
lines changed

src/models/CommonModel.ts

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ export class CommonModel extends CommonSchema<CommonModel> {
3535
}
3636
return newCommonModel;
3737
}
38+
/**
39+
* Merge two common model properties together
40+
* @param mergeTo
41+
* @param mergeFrom
42+
* @param originalSchema
43+
*/
3844
private static mergeProperties(mergeTo: CommonModel, mergeFrom: CommonModel, originalSchema: Schema) {
3945
const mergeToProperties = mergeTo.properties;
4046
const mergeFromProperties = mergeFrom.properties;
@@ -52,6 +58,46 @@ export class CommonModel extends CommonSchema<CommonModel> {
5258
}
5359
}
5460
}
61+
/**
62+
* Merge two common model additional properties together
63+
* @param mergeTo
64+
* @param mergeFrom
65+
* @param originalSchema
66+
*/
67+
private static mergeAdditionalProperties(mergeTo: CommonModel, mergeFrom: CommonModel, originalSchema: Schema) {
68+
const mergeToAdditionalProperties = mergeTo.additionalProperties;
69+
const mergeFromAdditionalProperties = mergeFrom.additionalProperties;
70+
if (mergeFromAdditionalProperties !== undefined) {
71+
if (mergeToAdditionalProperties === undefined) {
72+
mergeTo.additionalProperties = mergeFromAdditionalProperties;
73+
} else {
74+
mergeTo.additionalProperties = CommonModel.mergeCommonModels(mergeToAdditionalProperties, mergeFromAdditionalProperties, originalSchema);
75+
}
76+
}
77+
}
78+
/**
79+
* Merge two common model pattern properties together
80+
* @param mergeTo
81+
* @param mergeFrom
82+
* @param originalSchema
83+
*/
84+
private static mergePatternProperties(mergeTo: CommonModel, mergeFrom: CommonModel, originalSchema: Schema) {
85+
const mergeToPatternProperties = mergeTo.patternProperties;
86+
const mergeFromPatternProperties = mergeFrom.patternProperties;
87+
if (mergeFromPatternProperties !== undefined) {
88+
if (mergeToPatternProperties === undefined) {
89+
mergeTo.patternProperties = mergeFromPatternProperties;
90+
} else {
91+
for (const [pattern, patternModel] of Object.entries(mergeFromPatternProperties)) {
92+
if (mergeToPatternProperties[`${pattern}`] !== undefined) {
93+
mergeToPatternProperties[`${pattern}`] = CommonModel.mergeCommonModels(mergeToPatternProperties[`${pattern}`], patternModel, originalSchema);
94+
} else {
95+
mergeToPatternProperties[`${pattern}`] = patternModel;
96+
}
97+
}
98+
}
99+
}
100+
}
55101

56102
/**
57103
* Merge items together so only one CommonModel remains.
@@ -128,6 +174,8 @@ export class CommonModel extends CommonSchema<CommonModel> {
128174
static mergeCommonModels(mergeTo: CommonModel | undefined, mergeFrom: CommonModel, originalSchema: Schema): CommonModel {
129175
if (mergeTo === undefined) return mergeFrom;
130176

177+
CommonModel.mergeAdditionalProperties(mergeTo, mergeFrom, originalSchema);
178+
CommonModel.mergePatternProperties(mergeTo, mergeFrom, originalSchema);
131179
CommonModel.mergeProperties(mergeTo, mergeFrom, originalSchema);
132180
CommonModel.mergeItems(mergeTo, mergeFrom, originalSchema);
133181
CommonModel.mergeTypes(mergeTo, mergeFrom);
@@ -162,8 +210,7 @@ export class CommonModel extends CommonSchema<CommonModel> {
162210
getFromSchema<K extends keyof Schema>(key: K) {
163211
let schema = this.originalSchema || {};
164212
if (typeof schema === 'boolean') schema = {};
165-
// eslint-disable-next-line security/detect-object-injection
166-
return schema[key];
213+
return schema[`${key}`];
167214
}
168215

169216
/**

src/models/CommonSchema.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ export class CommonSchema<T> {
77
enum?: any[];
88
items?: T | T[];
99
properties?: { [key: string]: T; };
10-
additionalProperties?: boolean | T;
10+
additionalProperties?: T;
11+
patternProperties?: { [key: string]: T; };
1112
$ref?: string;
1213
required?: string[];
1314

@@ -36,9 +37,17 @@ export class CommonSchema<T> {
3637
schema.properties = properties;
3738
}
3839
if (typeof schema.additionalProperties === 'object' &&
39-
schema.additionalProperties !== null) {
40+
schema.additionalProperties !== undefined) {
4041
schema.additionalProperties = transformationSchemaCallback(schema.additionalProperties, seenSchemas);
4142
}
43+
if (typeof schema.patternProperties === 'object' &&
44+
schema.patternProperties !== undefined) {
45+
const patternProperties : {[key: string]: T | boolean} = {};
46+
Object.entries(schema.patternProperties).forEach(([pattern, patternSchema]) => {
47+
patternProperties[`${pattern}`] = transformationSchemaCallback(patternSchema, seenSchemas);
48+
});
49+
schema.patternProperties = patternProperties;
50+
}
4251
return schema;
4352
}
4453
}

src/models/Schema.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ export class Schema extends CommonSchema<Schema | boolean> {
3030
const?: any;
3131
dependencies?: { [key: string]: Schema | boolean | string[]; };
3232
propertyNames?: Schema | boolean;
33-
patternProperties?: { [key: string]: Schema | boolean; };
3433
if?: Schema | boolean;
3534
then?: Schema | boolean;
3635
else?: Schema | boolean;
@@ -101,14 +100,6 @@ export class Schema extends CommonSchema<Schema | boolean> {
101100
if (schema.propertyNames !== undefined) {
102101
schema.propertyNames = Schema.toSchema(schema.propertyNames, seenSchemas);
103102
}
104-
105-
if (schema.patternProperties !== undefined) {
106-
const patternProperties: { [key: string]: Schema | boolean } = {};
107-
Object.entries(schema.patternProperties).forEach(([propertyName, property]) => {
108-
patternProperties[`${propertyName}`] = Schema.toSchema(property, seenSchemas);
109-
});
110-
schema.patternProperties = patternProperties;
111-
}
112103

113104
if (schema.if !== undefined) {
114105
schema.if = Schema.toSchema(schema.if, seenSchemas);

src/simplification/Simplifier.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import simplifyRequired from './SimplifyRequired';
77
import simplifyTypes from './SimplifyTypes';
88
import { SimplificationOptions } from '../models/SimplificationOptions';
99
import simplifyAdditionalProperties from './SimplifyAdditionalProperties';
10+
import simplifyPatternProperties from './SimplifyPatternProperties';
1011
import { isModelObject } from './Utils';
1112
import simplifyName from './SimplifyName';
1213

@@ -86,6 +87,11 @@ export class Simplifier {
8687
model.properties = simplifiedProperties;
8788
}
8889

90+
const simplifiedPatternProperties = simplifyPatternProperties(schema, this);
91+
if (simplifiedPatternProperties !== undefined) {
92+
model.patternProperties = simplifiedPatternProperties;
93+
}
94+
8995
const simplifiedAdditionalProperties = simplifyAdditionalProperties(schema, this, model);
9096
if (simplifiedAdditionalProperties !== undefined) {
9197
model.additionalProperties = simplifiedAdditionalProperties;
@@ -142,7 +148,13 @@ export class Simplifier {
142148
}
143149
if (model.additionalProperties) {
144150
const existingAdditionalProperties = model.additionalProperties;
145-
model.additionalProperties = this.splitModels(existingAdditionalProperties as CommonModel);
151+
model.additionalProperties = this.splitModels(existingAdditionalProperties);
152+
}
153+
if (model.patternProperties) {
154+
const existingPatternProperties = model.patternProperties;
155+
for (const [pattern, patternModel] of Object.entries(existingPatternProperties)) {
156+
model.patternProperties[`${pattern}`] = this.splitModels(patternModel);
157+
}
146158
}
147159
}
148160
}

src/simplification/SimplifyAdditionalProperties.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { isModelObject } from './Utils';
55
type Output = CommonModel | undefined;
66

77
/**
8-
* Find out which common models we should extend
8+
* Simplifier function for finding the simplified version of additional properties
99
*
1010
* @param schema to find extends of
1111
*/
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { CommonModel } from '../models';
2+
import { Schema } from '../models/Schema';
3+
import { Simplifier } from './Simplifier';
4+
type Output = Record<string, CommonModel> | undefined;
5+
6+
/**
7+
* Find out which common models we should extend
8+
*
9+
* @param schema to find extends of
10+
*/
11+
export default function simplifyPatternProperties(schema: Schema | boolean, simplifier : Simplifier, seenSchemas: Map<any, Output> = new Map()) : Output {
12+
if (typeof schema !== 'boolean') {
13+
if (seenSchemas.has(schema)) return seenSchemas.get(schema);
14+
const output: Output = {};
15+
seenSchemas.set(schema, output);
16+
17+
for (const [pattern, patternSchema] of Object.entries(schema.patternProperties || {})) {
18+
const newModels = simplifier.simplify(patternSchema);
19+
if (newModels.length > 0) {
20+
addToPatterns(pattern, newModels[0], schema, output);
21+
}
22+
}
23+
24+
//If we encounter combination schemas ensure we recursively find the properties
25+
if (simplifier.options.allowInheritance !== true) {
26+
//Only merge allOf schemas if we don't allow inheritance
27+
combineSchemas(schema.allOf, output, simplifier, seenSchemas, schema);
28+
}
29+
combineSchemas(schema.oneOf, output, simplifier, seenSchemas, schema);
30+
combineSchemas(schema.anyOf, output, simplifier, seenSchemas, schema);
31+
32+
//If we encounter combination schemas ensure we recursively find the properties
33+
combineSchemas(schema.then, output, simplifier, seenSchemas, schema);
34+
combineSchemas(schema.else, output, simplifier, seenSchemas, schema);
35+
36+
return Object.keys(output).length ? output : undefined;
37+
}
38+
return undefined;
39+
}
40+
/**
41+
* Adds a pattern to the model
42+
*
43+
* @param pattern for the model
44+
* @param patternModel the corresponding model for the pattern
45+
* @param schema for the model
46+
* @param currentModel the current output
47+
*/
48+
function addToPatterns(pattern: string, patternModel: CommonModel, schema: Schema, currentModel: Output) {
49+
if (currentModel === undefined) return;
50+
//If the pattern already exist, merge the two
51+
if (currentModel[`${pattern}`] !== undefined) {
52+
currentModel[`${pattern}`] = CommonModel.mergeCommonModels(currentModel[`${pattern}`], patternModel, schema);
53+
} else {
54+
currentModel[`${pattern}`] = patternModel;
55+
}
56+
}
57+
/**
58+
* Go through schema(s) and combine the simplified properties together.
59+
*
60+
* @param schema to go through
61+
* @param currentModel the current output
62+
* @param simplifier to use
63+
* @param seenSchemas which we already have outputs for
64+
* @param rootSchema we are combining schemas for
65+
*/
66+
function combineSchemas(schema: (Schema | boolean) | (Schema | boolean)[] | undefined, currentModel: Output, simplifier : Simplifier, seenSchemas: Map<any, Output>, rootSchema : Schema) {
67+
if (typeof schema !== 'object') return;
68+
if (Array.isArray(schema)) {
69+
schema.forEach((combinationSchema) => {
70+
combineSchemas(combinationSchema, currentModel, simplifier, seenSchemas, rootSchema);
71+
});
72+
} else {
73+
const patternProperties = simplifyPatternProperties(schema, simplifier, seenSchemas);
74+
if (patternProperties !== undefined) {
75+
for (const [pattern, patternSchema] of Object.entries(patternProperties)) {
76+
addToPatterns(pattern, patternSchema, rootSchema, currentModel);
77+
}
78+
}
79+
}
80+
}

src/simplification/SimplifyProperties.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export default function simplifyProperties(schema: Schema | boolean, simplifier
3636
combineSchemas(schema.then, output, simplifier, seenSchemas, schema);
3737
combineSchemas(schema.else, output, simplifier, seenSchemas, schema);
3838

39-
return !Object.keys(output).length ? undefined : output;
39+
return Object.keys(output).length ? output : undefined;
4040
}
4141
return undefined;
4242
}
@@ -46,7 +46,7 @@ export default function simplifyProperties(schema: Schema | boolean, simplifier
4646
*
4747
* @param propName name of the property
4848
* @param propModel the corresponding model
49-
* @param schema the schema for the model
49+
* @param schema for the model
5050
* @param currentModel the current output
5151
*/
5252
function addToProperty(propName: string, propModel: CommonModel, schema: Schema, currentModel: Output) {
@@ -64,9 +64,9 @@ function addToProperty(propName: string, propModel: CommonModel, schema: Schema,
6464
*
6565
* @param schema to go through
6666
* @param currentModel the current output
67-
* @param simplifier the simplifier to use
68-
* @param seenSchemas schemas which we already have outputs for
69-
* @param rootSchema the root schema we are combining schemas for
67+
* @param simplifier to use
68+
* @param seenSchemas which we already have outputs for
69+
* @param rootSchema we are combining schemas for
7070
*/
7171
function combineSchemas(schema: (Schema | boolean) | (Schema | boolean)[] | undefined, currentModel: Output, simplifier : Simplifier, seenSchemas: Map<any, Output>, rootSchema : Schema) {
7272
if (typeof schema !== 'object') return;

test/models/CommonModel.spec.ts

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -97,13 +97,6 @@ describe('CommonModel', function() {
9797
const d = CommonModel.toCommonModel(doc);
9898
expect(d.additionalProperties).toEqual(undefined);
9999
});
100-
101-
test('should return undefined when null', function() {
102-
const doc: any = { additionalProperties: null };
103-
const d = CommonModel.toCommonModel(doc);
104-
expect(d.additionalProperties).not.toBeUndefined();
105-
expect(d.additionalProperties).toEqual(doc.additionalProperties);
106-
});
107100
});
108101

109102
describe('$ref', function() {
@@ -423,8 +416,69 @@ describe('CommonModel', function() {
423416
expect(doc1.properties).toBeUndefined();
424417
});
425418
});
426-
});
419+
describe('additionalProperties', function() {
420+
test('should be merged when only right side is defined', function() {
421+
const doc: Schema = { };
422+
let doc1 = CommonModel.toCommonModel(doc);
423+
let doc2 = CommonModel.toCommonModel(doc);
424+
doc2.additionalProperties = CommonModel.toCommonModel({type: "string"});
425+
doc1 = CommonModel.mergeCommonModels(doc1, doc2, doc);
426+
expect(doc1.additionalProperties).toEqual(doc2.additionalProperties);
427+
});
428+
test('should be merged together when both sides are defined', function() {
429+
const doc: Schema = { };
430+
let doc1 = CommonModel.toCommonModel(doc);
431+
let doc2 = CommonModel.toCommonModel(doc);
432+
doc2.additionalProperties = CommonModel.toCommonModel({type: "string"});
433+
doc1.additionalProperties = CommonModel.toCommonModel({type: "number"});
434+
doc1 = CommonModel.mergeCommonModels(doc1, doc2, doc);
435+
expect(doc1.additionalProperties).toEqual({type: ["number", "string"], originalSchema: {}});
436+
});
437+
test('should not change if nothing is defined', function() {
438+
const doc: Schema = { };
439+
let doc1 = CommonModel.toCommonModel(doc);
440+
let doc2 = CommonModel.toCommonModel(doc);
441+
doc1 = CommonModel.mergeCommonModels(doc1, doc2, doc);
442+
expect(doc1.patternProperties).toBeUndefined();
443+
});
444+
});
427445

446+
describe('patternProperties', function() {
447+
test('should be merged when only right side is defined', function() {
448+
const doc: Schema = { };
449+
let doc1 = CommonModel.toCommonModel(doc);
450+
let doc2 = CommonModel.toCommonModel(doc);
451+
doc2.patternProperties = {"pattern1": CommonModel.toCommonModel({type: "string"})};
452+
doc1 = CommonModel.mergeCommonModels(doc1, doc2, doc);
453+
expect(doc1.patternProperties).toEqual(doc2.patternProperties);
454+
});
455+
test('should be merged when both sides are defined', function() {
456+
const doc: Schema = { };
457+
let doc1 = CommonModel.toCommonModel(doc);
458+
let doc2 = CommonModel.toCommonModel(doc);
459+
doc2.patternProperties = {"pattern1": CommonModel.toCommonModel({type: "string"})};
460+
doc1.patternProperties = {"pattern2": CommonModel.toCommonModel({type: "number"})};
461+
doc1 = CommonModel.mergeCommonModels(doc1, doc2, doc);
462+
expect(doc1.patternProperties).toEqual({"pattern1": {type: "string"}, "pattern2": {type: "number"}});
463+
});
464+
test('should be merged together when both sides are defined', function() {
465+
const doc: Schema = { };
466+
let doc1 = CommonModel.toCommonModel(doc);
467+
let doc2 = CommonModel.toCommonModel(doc);
468+
doc2.patternProperties = {"pattern1": CommonModel.toCommonModel({type: "string"})};
469+
doc1.patternProperties = {"pattern1": CommonModel.toCommonModel({type: "number"})};
470+
doc1 = CommonModel.mergeCommonModels(doc1, doc2, doc);
471+
expect(doc1.patternProperties).toEqual({"pattern1": {type: ["number", "string"], originalSchema: {}}});
472+
});
473+
test('should not change if nothing is defined', function() {
474+
const doc: Schema = { };
475+
let doc1 = CommonModel.toCommonModel(doc);
476+
let doc2 = CommonModel.toCommonModel(doc);
477+
doc1 = CommonModel.mergeCommonModels(doc1, doc2, doc);
478+
expect(doc1.patternProperties).toBeUndefined();
479+
});
480+
});
481+
});
428482
describe('helpers', function() {
429483
describe('getFromSchema', function() {
430484
test('should work', function() {

test/models/Schema.spec.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -313,13 +313,6 @@ describe('Schema', function() {
313313
expect(typeof d).toEqual("object");
314314
expect(d.additionalProperties).toEqual(undefined);
315315
});
316-
317-
test('should return undefined when null', function() {
318-
const doc: any = { additionalProperties: null };
319-
const d = Schema.toSchema(doc) as Schema;
320-
expect(typeof d).toEqual("object");
321-
expect(d.additionalProperties).toEqual(null);
322-
});
323316
});
324317

325318
describe('additionalItems', function() {

0 commit comments

Comments
 (0)