Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: pattern properties not being accounted for #1006

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
d13c267
Fixing dependencies
jonaslagoni Sep 27, 2022
c46e70f
fixed renderer
jonaslagoni Sep 27, 2022
8e6150b
fixed blackbox
jonaslagoni Sep 27, 2022
f103f82
add test
jonaslagoni Sep 27, 2022
dd4ddab
Fixed linter
jonaslagoni Sep 27, 2022
cdaf662
fixed dependencies
jonaslagoni Sep 27, 2022
9bf54a7
Fixed unique implementation
jonaslagoni Sep 27, 2022
6f1d6c6
fixed linter
jonaslagoni Sep 27, 2022
bb673d7
Merge branch 'next' into feature/fix_all_blackbox_tests
jonaslagoni Sep 27, 2022
c7e8c1d
Merge remote-tracking branch 'origin/feature/fix_python_generator_wit…
jonaslagoni Sep 27, 2022
80af36d
Merge remote-tracking branch 'origin/feature/fix_self_dependencies' i…
jonaslagoni Sep 27, 2022
c475eee
WIP
jonaslagoni Sep 28, 2022
fff0d08
Merge branch 'next' into feature/fix_all_blackbox_tests
jonaslagoni Oct 3, 2022
e57f7c5
Merge branch 'next' into feature/fix_all_blackbox_tests
jonaslagoni Oct 4, 2022
e31e2bc
Fixed implementation and test
jonaslagoni Oct 5, 2022
c1ccf69
add fix and implementation
jonaslagoni Oct 5, 2022
a7d7649
update snapshot
jonaslagoni Oct 5, 2022
061a31b
wip
jonaslagoni Oct 4, 2022
cca613a
Merge remote-tracking branch 'origin/feature/fix_typescript_array_typ…
jonaslagoni Oct 5, 2022
a9dce75
Merge remote-tracking branch 'origin/feature/fix_javascript_splits_ou…
jonaslagoni Oct 5, 2022
4c7e6c3
add implementation and test
jonaslagoni Oct 5, 2022
aed9512
fixed linter
jonaslagoni Oct 6, 2022
921da11
Merge remote-tracking branch 'origin/feature/fix_java_maps_cannot_hav…
jonaslagoni Oct 6, 2022
89a94e0
wip
jonaslagoni Oct 17, 2022
35239a2
Merge branch 'next' into feature/fix_all_blackbox_tests
jonaslagoni Nov 9, 2022
61aec5f
fixed rest of the problems
jonaslagoni Nov 10, 2022
a3ba013
removed unused test file
jonaslagoni Nov 10, 2022
7c2501f
fixed lint
jonaslagoni Nov 10, 2022
5395956
separated blackbox tests and scripts
jonaslagoni Nov 10, 2022
cf0d339
re-added files
jonaslagoni Nov 10, 2022
40e965c
Merge branch 'next' into feature/fix_all_blackbox_tests
jonaslagoni Nov 10, 2022
2c4de23
updated packagelock
jonaslagoni Nov 10, 2022
7911d28
rename
jonaslagoni Nov 10, 2022
71001e6
fixed linter
jonaslagoni Nov 10, 2022
92bac61
fixed more problems
jonaslagoni Nov 15, 2022
3fe7b78
wip
jonaslagoni Nov 17, 2022
74e7b87
Merge branch 'next' into feature/fix_all_blackbox_tests
jonaslagoni Nov 17, 2022
1fb9f15
update
jonaslagoni Nov 17, 2022
19fa426
renamed parameter
jonaslagoni Nov 17, 2022
8ff1ade
fixed incorrect test scripts was run
jonaslagoni Nov 17, 2022
2114dd2
removed unused file
jonaslagoni Nov 18, 2022
1792c05
added comments
jonaslagoni Nov 23, 2022
24e2651
fixed tests and implementation
jonaslagoni Nov 23, 2022
c6abd12
Merge branch 'next' into feature/fix_pattern_properties_problem
jonaslagoni Nov 23, 2022
af13a3c
removed unneccessary newline
jonaslagoni Nov 23, 2022
30da8fd
Merge branch 'feature/fix_pattern_properties_problem' into feature/fi…
jonaslagoni Nov 23, 2022
c7efd32
Revert "Merge branch 'feature/fix_pattern_properties_problem' into fe…
jonaslagoni Nov 23, 2022
cab7de9
Merge branch 'feature/fix_all_blackbox_tests' into feature/fix_patter…
jonaslagoni Nov 23, 2022
f69c212
fix common model conversion
jonaslagoni Nov 23, 2022
9c2ec21
Merge branch 'next' into feature/fix_pattern_properties_problem
jonaslagoni Nov 29, 2022
903ab1e
add wrong merges
jonaslagoni Nov 29, 2022
b1edfe4
fixed model implementation
jonaslagoni Nov 29, 2022
3d649ea
fixed test
jonaslagoni Nov 29, 2022
de6419c
update snapshots
jonaslagoni Nov 29, 2022
4d28554
update test and implementation
jonaslagoni Nov 29, 2022
397eeeb
update snapshots
jonaslagoni Nov 30, 2022
0c8893a
fixed tests and implementation
jonaslagoni Nov 30, 2022
2989917
fixed lint
jonaslagoni Nov 30, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 125 additions & 1 deletion docs/inputs/JSON_Schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,129 @@ The library transforms JSON Schema from data validation rules to data definition
The algorithm tries to get to a model whose data can be validated against the JSON schema document.

As of now we only provide the underlying structure of the schema file for the model, where constraints/annotations such as `maxItems`, `uniqueItems`, `multipleOf`, etc. are not interpreted.
## Patterns
Beside the regular interpreter we also look for certain patterns that are interpreted slightly differently.

### `oneOf` with `allOf`
If both oneOf and allOf is present, each allOf model is merged into the interpreted oneOf.

For example take this example:
```json
{
"allOf":[
{
"title":"Animal",
"type":"object",
"properties":{
"animalType":{
"title":"Animal Type",
"type":"string"
},
"age":{
"type":"integer",
"min":0
}
}
}
],
"oneOf":[
{
"title":"Cat",
"type":"object",
"properties":{
"animalType":{
"const":"Cat"
},
"huntingSkill":{
"title":"Hunting Skill",
"type":"string",
"enum":[
"clueless",
"lazy"
]
}
}
},
{
"title":"Dog",
"type":"object",
"additionalProperties":false,
"properties":{
"animalType":{
"const":"Dog"
},
"breed":{
"title":"Dog Breed",
"type":"string",
"enum":[
"bulldog",
"bichons frise"
]
}
}
}
]
}
```
Here animal is merged into cat and dog.

### `oneOf` with `properties`
If both oneOf and properties are both present, it's interpreted exactly like [oneOf with allOf](#oneof-with-allof). That means that the following:

```json
{
"title":"Animal",
"type":"object",
"properties":{
"animalType":{
"title":"Animal Type",
"type":"string"
},
"age":{
"type":"integer",
"min":0
}
},
"oneOf":[
{
"title":"Cat",
"type":"object",
"properties":{
"animalType":{
"const":"Cat"
},
"huntingSkill":{
"title":"Hunting Skill",
"type":"string",
"enum":[
"clueless",
"lazy"
]
}
}
},
{
"title":"Dog",
"type":"object",
"additionalProperties":false,
"properties":{
"animalType":{
"const":"Dog"
},
"breed":{
"title":"Dog Breed",
"type":"string",
"enum":[
"bulldog",
"bichons frise"
]
}
}
}
]
}
```
where all the defined behavior on the root object are merged into the two oneOf models cat and dog.

## Interpreter
The main functionality is located in the `Interpreter` class. This class ensures to recursively create (or retrieve from a cache) a `CommonModel` representation of a Schema. We have tried to keep the functionality split out into separate functions to reduce complexity and ensure it is easy to maintain.
Expand All @@ -13,7 +136,7 @@ The order of interpretation:
- `true` boolean schema infers all model types (`object`, `string`, `number`, `array`, `boolean`, `null`, `integer`) schemas.
- `type` infers the initial model type.
- `required` are interpreted as is.
- `patternProperties` are merged together with any additionalProperties, where duplicate additionalProperties are [merged](#Merging-models).
- `patternProperties` are merged together with any additionalProperties, where duplicate pattern models are [merged](#Merging-models).
- `additionalProperties` are interpreted as is, where duplicate additionalProperties for the model are [merged](#Merging-models). If the schema does not define `additionalProperties` it defaults to `true` schema.
- `additionalItems` are interpreted as is, where duplicate additionalItems for the model are [merged](#Merging-models). If the schema does not define `additionalItems` it defaults to `true` schema.
- `items` are interpreted as ether tuples or simple array, where more than 1 item are [merged](#Merging-models). Usage of `items` infers `array` model type.
Expand Down Expand Up @@ -49,6 +172,7 @@ Because of the recursive nature of the interpreter (and the nested nature of JSO

If only one side has a property defined, it is used as is, if both have it defined they are merged based on the following logic (look [here](./input_processing.md#Internal-model-representation) for more information about the CommonModel and its properties):
- `additionalProperties` if both models contain it the two are recursively merged together.
- `patternProperties` if both models contain it each pattern model are recursively merged together.
- `properties` if both models contain the same property the corresponding models are recursively merged together.
- `items` are merged together based on a couple of rules:
- If both models are simple arrays those item models are merged together as is.
Expand Down
62 changes: 52 additions & 10 deletions src/helpers/CommonModelToMetaModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ export function convertToBooleanModel(jsonSchemaModel: CommonModel, name: string
}
return new BooleanModel(name, jsonSchemaModel.originalInput);
}

/**
* Determine whether we have a dictionary or an object. because in some cases inputs might be:
* { "type": "object", "additionalProperties": { "$ref": "#" } } which is to be interpreted as a dictionary not an object model.
Expand All @@ -190,17 +191,57 @@ function isDictionary(jsonSchemaModel: CommonModel): boolean {
}
return true;
}

/**
* Return the original input based on additionalProperties and patternProperties.
*/
function getOriginalInputFromAdditionalAndPatterns(jsonSchemaModel: CommonModel) {
const originalInputs = [];
if (jsonSchemaModel.additionalProperties !== undefined) {
originalInputs.push(jsonSchemaModel.additionalProperties.originalInput);
}

if (jsonSchemaModel.patternProperties !== undefined) {
for (const patternModel of Object.values(jsonSchemaModel.patternProperties)) {
originalInputs.push(patternModel.originalInput);
}
}
return originalInputs;
}

/**
* Function creating the right meta model based on additionalProperties and patternProperties.
*/
function convertAdditionalAndPatterns(jsonSchemaModel: CommonModel, name: string, alreadySeenModels: Map<CommonModel, MetaModel>) {
const modelsAsValue = new Map<string, MetaModel>();
if (jsonSchemaModel.additionalProperties !== undefined) {
const additionalPropertyModel = convertToMetaModel(jsonSchemaModel.additionalProperties, alreadySeenModels);
modelsAsValue.set(additionalPropertyModel.name, additionalPropertyModel);
}

if (jsonSchemaModel.patternProperties !== undefined) {
for (const patternModel of Object.values(jsonSchemaModel.patternProperties)) {
const patternPropertyModel = convertToMetaModel(patternModel);
modelsAsValue.set(patternPropertyModel.name, patternPropertyModel);
}
}
if (modelsAsValue.size === 1) {
return Array.from(modelsAsValue.values())[0];
}
return new UnionModel(name, getOriginalInputFromAdditionalAndPatterns(jsonSchemaModel), Array.from(modelsAsValue.values()));
}

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
export function convertToDictionaryModel(jsonSchemaModel: CommonModel, name: string, alreadySeenModels: Map<CommonModel, MetaModel>): DictionaryModel | undefined {
if (!isDictionary(jsonSchemaModel)) {
return undefined;
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const keyModel = new StringModel(name, jsonSchemaModel.additionalProperties!.originalInput);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const valueModel = convertToMetaModel(jsonSchemaModel.additionalProperties!, alreadySeenModels);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return new DictionaryModel(name, jsonSchemaModel.additionalProperties!.originalInput, keyModel, valueModel, 'normal');
const originalInput = getOriginalInputFromAdditionalAndPatterns(jsonSchemaModel);
const keyModel = new StringModel(name, originalInput);
const valueModel = convertAdditionalAndPatterns(jsonSchemaModel, name, alreadySeenModels);
return new DictionaryModel(name, originalInput, keyModel, valueModel, 'normal');
}

export function convertToObjectModel(jsonSchemaModel: CommonModel, name: string, alreadySeenModels: Map<CommonModel, MetaModel>): ObjectModel | undefined {
if (!jsonSchemaModel.type?.includes('object') ||
isDictionary(jsonSchemaModel)) {
Expand All @@ -219,14 +260,15 @@ export function convertToObjectModel(jsonSchemaModel: CommonModel, name: string,
metaModel.properties[String(propertyName)] = propertyModel;
}

if (jsonSchemaModel.additionalProperties !== undefined) {
if (jsonSchemaModel.additionalProperties !== undefined || jsonSchemaModel.patternProperties !== undefined) {
let propertyName = 'additionalProperties';
while (metaModel.properties[String(propertyName)] !== undefined) {
propertyName = `reserved_${propertyName}`;
}
const keyModel = new StringModel(propertyName, jsonSchemaModel.additionalProperties.originalInput);
const valueModel = convertToMetaModel(jsonSchemaModel.additionalProperties, alreadySeenModels);
const dictionaryModel = new DictionaryModel(propertyName, jsonSchemaModel.additionalProperties.originalInput, keyModel, valueModel, 'unwrap');
const originalInput = getOriginalInputFromAdditionalAndPatterns(jsonSchemaModel);
const keyModel = new StringModel(propertyName, originalInput);
const valueModel = convertAdditionalAndPatterns(jsonSchemaModel, propertyName, alreadySeenModels);
const dictionaryModel = new DictionaryModel(propertyName, originalInput, keyModel, valueModel, 'unwrap');
const propertyModel = new ObjectPropertyModel(propertyName, false, dictionaryModel);
metaModel.properties[String(propertyName)] = propertyModel;
}
Expand Down
4 changes: 2 additions & 2 deletions src/interpreter/InterpretPatternProperties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ import { Interpreter, InterpreterOptions, InterpreterSchemaType } from './Interp
*/
export default function interpretPatternProperties(schema: InterpreterSchemaType, model: CommonModel, interpreter : Interpreter, interpreterOptions: InterpreterOptions = Interpreter.defaultInterpreterOptions): void {
if (typeof schema === 'boolean') {return;}
for (const [,patternSchema] of Object.entries(schema.patternProperties || {})) {
for (const [pattern, patternSchema] of Object.entries(schema.patternProperties || {})) {
const patternModel = interpreter.interpret(patternSchema as any, interpreterOptions);
if (patternModel !== undefined) {
model.addAdditionalProperty(patternModel, schema);
model.addPatternProperty(pattern, patternModel, schema);
}
}
}
84 changes: 48 additions & 36 deletions src/models/CommonModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export class CommonModel {
items?: CommonModel | CommonModel[];
properties?: { [key: string]: CommonModel; };
additionalProperties?: CommonModel;
$ref?: string;
patternProperties?: { [key: string]: CommonModel; };
required?: string[];
additionalItems?: CommonModel;
union?: CommonModel[]
Expand Down Expand Up @@ -274,6 +274,24 @@ export class CommonModel {
}
}

/**
* Adds a patternProperty to the model.
* If the pattern already exist the two models are merged.
*
* @param pattern
* @param patternModel
* @param originalInput corresponding input that got interpreted to this model
*/
addPatternProperty(pattern: string, patternModel: CommonModel, originalInput: any): void {
if (this.patternProperties === undefined) {this.patternProperties = {};}
if (this.patternProperties[`${pattern}`] !== undefined) {
Logger.warn(`While trying to add patternProperty to model, duplicate patterns found. Merging pattern models together for pattern ${pattern}`, patternModel, originalInput, this);
this.patternProperties[String(pattern)] = CommonModel.mergeCommonModels(this.patternProperties[String(pattern)], patternModel, originalInput);
} else {
this.patternProperties[String(pattern)] = patternModel;
}
}

/**
* Adds additionalItems to the model.
* If another model already exist the two are merged.
Expand Down Expand Up @@ -310,40 +328,6 @@ export class CommonModel {
this.extend.push(extendedModel.$id);
}

/**
* Returns an array of unique `$id`s from all the CommonModel's this model depends on.
*/
// eslint-disable-next-line sonarjs/cognitive-complexity
getNearestDependencies(): string[] {
const dependsOn = [];

if (this.$ref !== undefined) {
return [this.$ref];
}

if (this.additionalProperties !== undefined) {
dependsOn.push(...this.additionalProperties.getNearestDependencies());
}
if (this.extend !== undefined) {
dependsOn.push(...this.extend);
}
if (this.items !== undefined) {
const items = Array.isArray(this.items) ? this.items : [this.items];
for (const item of items) {
dependsOn.push(...item.getNearestDependencies());
}
}
if (this.properties !== undefined && Object.keys(this.properties).length) {
for (const property of Object.values(this.properties)) {
dependsOn.push(...property.getNearestDependencies());
}
}
if (this.additionalItems !== undefined) {
dependsOn.push(...this.additionalItems.getNearestDependencies());
}
return [...new Set(dependsOn)];
}

/**
* Merge two common model properties together
*
Expand Down Expand Up @@ -395,6 +379,7 @@ export class CommonModel {
}
}
}

/**
* Merge two common model additionalItems together
*
Expand Down Expand Up @@ -453,6 +438,33 @@ export class CommonModel {
//mergeFrom is not tuple && mergeTo is, do nothing
}

/**
* Merge two common model pattern properties together
*
* @param mergeTo
* @param mergeFrom
* @param originalInput corresponding input that got interpreted to this model
* @param alreadyIteratedModels
*/
private static mergePatternProperties(mergeTo: CommonModel, mergeFrom: CommonModel, originalInput: any, alreadyIteratedModels: Map<CommonModel, CommonModel> = new Map()) {
const mergeToPatternProperties = mergeTo.patternProperties;
const mergeFromPatternProperties = mergeFrom.patternProperties;
if (mergeFromPatternProperties !== undefined) {
if (mergeToPatternProperties === undefined) {
mergeTo.patternProperties = mergeFromPatternProperties;
} else {
for (const [pattern, patternModel] of Object.entries(mergeFromPatternProperties)) {
if (mergeToPatternProperties[String(pattern)] !== undefined) {
Logger.warn(`Found duplicate pattern ${pattern} for model. Model pattern for ${mergeFrom.$id || 'unknown'} merged into ${mergeTo.$id || 'unknown'}`, mergeTo, mergeFrom, originalInput);
mergeToPatternProperties[String(pattern)] = CommonModel.mergeCommonModels(mergeToPatternProperties[String(pattern)], patternModel, originalInput, alreadyIteratedModels);
} else {
mergeToPatternProperties[String(pattern)] = patternModel;
}
}
}
}
}

/**
* Merge types together
*
Expand Down Expand Up @@ -498,6 +510,7 @@ export class CommonModel {
alreadyIteratedModels.set(mergeFrom, mergeTo);

CommonModel.mergeAdditionalProperties(mergeTo, mergeFrom, originalInput, alreadyIteratedModels);
CommonModel.mergePatternProperties(mergeTo, mergeFrom, originalInput, alreadyIteratedModels);
CommonModel.mergeAdditionalItems(mergeTo, mergeFrom, originalInput, alreadyIteratedModels);
CommonModel.mergeProperties(mergeTo, mergeFrom, originalInput, alreadyIteratedModels);
CommonModel.mergeItems(mergeTo, mergeFrom, originalInput, alreadyIteratedModels);
Expand All @@ -510,7 +523,6 @@ export class CommonModel {
mergeTo.required = [... new Set([...(mergeTo.required || []), ...mergeFrom.required])];
}
mergeTo.$id = mergeTo.$id || mergeFrom.$id;
mergeTo.$ref = mergeTo.$ref || mergeFrom.$ref;
mergeTo.extend = mergeTo.extend || mergeFrom.extend;
mergeTo.originalInput = originalInput;
return mergeTo;
Expand Down
Loading