-
-
Notifications
You must be signed in to change notification settings - Fork 171
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: introduce new interpreter (#184)
- Loading branch information
1 parent
f47276c
commit c2365ad
Showing
10 changed files
with
671 additions
and
66 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
# Interpretation of JSON Schema draft 7 to CommonModel | ||
|
||
The library transforms JSON Schema from data validation rules to data definitions (`CommonModel`(s)). | ||
|
||
The algorithm tries to get to a model whose data can be validated against the JSON schema document. | ||
|
||
We only provide the underlying structure of the schema file for the model formats such as `maxItems`, `uniqueItems`, `multipleOf`, etc, are not transformed. | ||
|
||
## Interpreter | ||
The main functionality is located in the `Transformer` 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 easier to maintain. This main function also ensures to split any created models into separate ones if needed. | ||
|
||
The order of transformation: | ||
- [type](#determining-the-type-for-the-model) | ||
- `required` are determined as is. | ||
- `properties` are determined as is, where duplicate properties for the model are merged. | ||
- [oneOf/anyOf/then/else](#Processing-sub-schemas) | ||
|
||
## Determining the type for the model | ||
To determine the types for the model we use the following interpretation (and in that order): | ||
- `true` schema infers all model types (`object`, `string`, `number`, `array`, `boolean`, `null`, `integer`). | ||
- Usage of `type` infers the initial model type. | ||
- Usage of `properties` infers `object` model type. | ||
|
||
## Processing sub schemas | ||
The following JSON Schema keywords are merged with the already transformed model: | ||
- oneOf | ||
- anyOf | ||
- then | ||
- else |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { CommonModel } from '../models/CommonModel'; | ||
import { Schema } from '../models/Schema'; | ||
import { Interpreter } from './Interpreter'; | ||
|
||
/** | ||
* Interpreter function for interpreting JSON Schema draft 7 properties keyword. | ||
* | ||
* @param schema | ||
* @param model | ||
* @param interpreter | ||
*/ | ||
export default function interpretProperties(schema: Schema | boolean, model: CommonModel, interpreter : Interpreter) { | ||
if (typeof schema === 'boolean' || schema.properties === undefined) return; | ||
model.addTypes('object'); | ||
|
||
for (const [propertyName, propertySchema] of Object.entries(schema.properties)) { | ||
const propertyModels = interpreter.interpret(propertySchema); | ||
if (propertyModels.length > 0) { | ||
model.addProperty(propertyName, propertyModels[0], schema); | ||
} | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
import { CommonModel, Schema } from '../models'; | ||
import { SimplificationOptions } from '../models/SimplificationOptions'; | ||
import { interpretName, isModelObject } from './Utils'; | ||
import interpretProperties from './InterpretProperties'; | ||
import { Logger } from '../utils'; | ||
|
||
export class Interpreter { | ||
static defaultOptions: SimplificationOptions = { | ||
allowInheritance: false | ||
} | ||
|
||
private anonymCounter = 1; | ||
private seenSchemas: Map<Schema | boolean, CommonModel> = new Map(); | ||
private iteratedModels: Record<string, CommonModel> = {}; | ||
|
||
constructor( | ||
readonly options: SimplificationOptions = Interpreter.defaultOptions, | ||
) { | ||
this.options = { ...Interpreter.defaultOptions, ...options }; | ||
} | ||
|
||
/** | ||
* Transforms a schema into instances of CommonModel by processing all JSON Schema draft 7 keywords and infers the model definition. | ||
* | ||
* length == 0 means no model can be generated from the schema | ||
* Index 0 will always be the root schema CommonModel representation. | ||
* Index > 0 will always be the separated models that the interpreter determines are fit to be on their own. | ||
* | ||
* @param schema | ||
* @param splitModels should it split up models | ||
*/ | ||
interpret(schema: Schema | boolean, splitModels = true): CommonModel[] { | ||
const modelsToReturn = Object.values(this.iteratedModels); | ||
if (this.seenSchemas.has(schema)) { | ||
const cachedModel = this.seenSchemas.get(schema); | ||
if (cachedModel !== undefined) { | ||
return [cachedModel, ...modelsToReturn]; | ||
} | ||
} | ||
//If it is a false validation schema return no CommonModel | ||
if (schema === false) { | ||
return []; | ||
} | ||
const model = new CommonModel(); | ||
model.originalSchema = Schema.toSchema(schema); | ||
this.seenSchemas.set(schema, model); | ||
this.interpretSchema(model, schema); | ||
if (splitModels) { | ||
this.ensureModelsAreSplit(model); | ||
if (isModelObject(model)) { | ||
this.iteratedModels[`${model.$id}`] = model; | ||
} | ||
} | ||
return [model, ...modelsToReturn]; | ||
} | ||
|
||
/** | ||
* Function to interpret the JSON schema draft 7 into a CommonModel. | ||
* | ||
* @param model | ||
* @param schema | ||
*/ | ||
private interpretSchema(model: CommonModel, schema: Schema | boolean) { | ||
if (schema === true) { | ||
model.setType(['object', 'string', 'number', 'array', 'boolean', 'null', 'integer']); | ||
} else if (typeof schema === 'object') { | ||
if (schema.type !== undefined) { | ||
model.addTypes(schema.type); | ||
} | ||
|
||
//All schemas of type object MUST have ids | ||
if (model.type !== undefined && model.type.includes('object')) { | ||
model.$id = interpretName(schema) || `anonymSchema${this.anonymCounter++}`; | ||
} else if (schema.$id !== undefined) { | ||
model.$id = interpretName(schema); | ||
} | ||
|
||
model.required = schema.required || model.required; | ||
|
||
interpretProperties(schema, model, this); | ||
|
||
this.combineSchemas(schema.oneOf, model, schema); | ||
this.combineSchemas(schema.anyOf, model, schema); | ||
this.combineSchemas(schema.then, model, schema); | ||
this.combineSchemas(schema.else, model, schema); | ||
} | ||
} | ||
|
||
/** | ||
* Go through schema(s) and combine the interpreted models together. | ||
* | ||
* @param schema to go through | ||
* @param currentModel the current output | ||
*/ | ||
combineSchemas(schema: (Schema | boolean) | (Schema | boolean)[] | undefined, currentModel: CommonModel, rootSchema: Schema) { | ||
if (typeof schema !== 'object') return; | ||
if (Array.isArray(schema)) { | ||
schema.forEach((forEachSchema) => { | ||
this.combineSchemas(forEachSchema, currentModel, rootSchema); | ||
}); | ||
} else { | ||
const models = this.interpret(schema, false); | ||
if (models.length > 0) { | ||
CommonModel.mergeCommonModels(currentModel, models[0], rootSchema); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* This function splits up a model if needed and add the new model to the list of models. | ||
* | ||
* @param model check if it should be split up | ||
* @param models which have already been split up | ||
*/ | ||
private splitModels(model: CommonModel): CommonModel { | ||
if (isModelObject(model)) { | ||
Logger.info(`Splitting model ${model.$id || 'unknown'} since it should be on its own`); | ||
const switchRootModel = new CommonModel(); | ||
switchRootModel.$ref = model.$id; | ||
this.iteratedModels[`${model.$id}`] = model; | ||
return switchRootModel; | ||
} | ||
return model; | ||
} | ||
|
||
/** | ||
* Split up all models which should and use ref instead. | ||
* | ||
* @param model to ensure are split | ||
* @param models which are already split | ||
*/ | ||
ensureModelsAreSplit(model: CommonModel) { | ||
// eslint-disable-next-line sonarjs/no-collapsible-if | ||
if (model.properties) { | ||
const existingProperties = model.properties; | ||
for (const [prop, propSchema] of Object.entries(existingProperties)) { | ||
model.properties[`${prop}`] = this.splitModels(propSchema); | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import { CommonModel } from '../models/CommonModel'; | ||
|
||
/** | ||
* check if CommonModel is a separate model or a simple model. | ||
*/ | ||
export function isModelObject(model: CommonModel) : boolean { | ||
// This check should be done instead, needs a refactor to allow it though: | ||
// this.extend !== undefined || this.properties !== undefined | ||
if (model.type !== undefined) { | ||
// If all possible JSON types are defined, don't split it even if it does contain object. | ||
if (Array.isArray(model.type) && model.type.length === 7) { | ||
return false; | ||
} | ||
return model.type.includes('object'); | ||
} | ||
return false; | ||
} | ||
|
||
/** | ||
* Infers the JSON Schema type from value | ||
* | ||
* @param value to infer type of | ||
*/ | ||
export function inferTypeFromValue(value: any) { | ||
if (Array.isArray(value)) { | ||
return 'array'; | ||
} | ||
if (value === null) { | ||
return 'null'; | ||
} | ||
const typeOfEnum = typeof value; | ||
if (typeOfEnum === 'bigint') { | ||
return 'integer'; | ||
} | ||
return typeOfEnum; | ||
} | ||
|
||
/** | ||
* Find the name for simplified version of schema | ||
* | ||
* @param schema to find the name | ||
*/ | ||
export function interpretName(schema: any | boolean): string | undefined { | ||
if (schema && typeof schema === 'object') { | ||
return schema.title || schema.$id || schema['x-modelgen-inferred-name']; | ||
} | ||
} |
Oops, something went wrong.