Repository: asyncapi/modelina (ts)

Issue:
Schema dependencies should be iterated
#### Reason/Context
Currently we do not iterate schema dependencies. We should iterate them and combine properties/items/types etc together.

```json
{
  "type": "object",
  "properties": {
    "name": { "type": "string" },
    "credit_card": { "type": "number" }
  },
  "required": ["name"],
  "dependencies": {
    "credit_card": {
      "properties": {
        "billing_address": { "type": "string" }
      },
      "required": ["billing_address"]
    }
  }
}
```

should merge it together to give the model:

```
{
  "type": "object",
  "properties": {
    "name": { "type": "string" },
    "credit_card": { "type": "number" },
    "billing_address": { "type": "string" }
  },
  "required": ["name"]
}
```
Reason why `billing_address` is not added to the list of required properties, is that it is a runtime requirement that we cannot do anything about when rendering the model.

see more here: http://json-schema.org/understanding-json-schema/reference/object.html#schema-dependencies


Pull request:
feat: interpret dependencies
**Description**
This PR makes sure schema dependencies are iterated and merged together.
 
**Related issue(s)**
fixes #79 

Patch:
diff --git a/docs/interpretation_of_JSON_Schema_draft_7.md b/docs/interpretation_of_JSON_Schema_draft_7.md
index 218a6e57..bc4a2c5b 100644
--- a/docs/interpretation_of_JSON_Schema_draft_7.md
+++ b/docs/interpretation_of_JSON_Schema_draft_7.md
@@ -18,6 +18,7 @@ The order of interpretation:
 - `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.
 - `properties` are interpreted as is, where duplicate `properties` for the model are [merged](#Merging-models). Usage of `properties` infers `object` model type.
 - [allOf](#allOf-sub-schemas)
+- `dependencies` only apply to schema dependencies, since property dependencies adds nothing to the underlying model. Any schema dependencies are interpreted and then [merged](#Merging-models) together with the current interpreted model.
 - `enum` is interpreted as is, where each `enum`. Usage of `enum` infers the enumerator value type to the model, but only if the schema does not have `type` specified.
 - `const` interpretation overwrite already interpreted `enum`. Usage of `const` infers the constant value type to the model, but only if the schema does not have `type` specified.
 - [oneOf/anyOf/then/else](#Processing-sub-schemas)
diff --git a/src/interpreter/InterpretDependencies.ts b/src/interpreter/InterpretDependencies.ts
new file mode 100644
index 00000000..413d5b16
--- /dev/null
+++ b/src/interpreter/InterpretDependencies.ts
@@ -0,0 +1,19 @@
+import { CommonModel } from '../models/CommonModel';
+import { Schema } from '../models/Schema';
+import { Interpreter, InterpreterOptions } from './Interpreter';
+
+/**
+ * Interpreter function for JSON Schema draft 7 dependencies keyword.
+ * 
+ * @param schema 
+ * @param model
+ */
+export default function interpretDependencies(schema: Schema, model: CommonModel, interpreter: Interpreter, interpreterOptions: InterpreterOptions = Interpreter.defaultInterpreterOptions): void {
+  if (schema.dependencies === undefined) {return;}
+  for (const dependency of Object.values(schema.dependencies)) {
+    // Only handle schema dependency and skip property dependencies
+    if (!Array.isArray(dependency)) {
+      interpreter.interpretAndCombineSchema(dependency, model, schema, interpreterOptions);
+    }
+  }
+}
diff --git a/src/interpreter/Interpreter.ts b/src/interpreter/Interpreter.ts
index 8720d5fe..caf8a308 100644
--- a/src/interpreter/Interpreter.ts
+++ b/src/interpreter/Interpreter.ts
@@ -8,6 +8,7 @@ import interpretAdditionalProperties from './InterpretAdditionalProperties';
 import interpretItems from './InterpretItems';
 import interpretPatternProperties from './InterpretPatternProperties';
 import interpretNot from './InterpretNot';
+import interpretDependencies from './InterpretDependencies';
 
 export type InterpreterOptions = {
   allowInheritance?: boolean
@@ -73,6 +74,7 @@ export class Interpreter {
       interpretItems(schema, model, this, interpreterOptions);
       interpretProperties(schema, model, this, interpreterOptions);
       interpretAllOf(schema, model, this, interpreterOptions);
+      interpretDependencies(schema, model, this, interpreterOptions);
       interpretConst(schema, model);
       interpretEnum(schema, model);
 


Test patch:
diff --git a/test/interpreter/unit/Intepreter.spec.ts b/test/interpreter/unit/Intepreter.spec.ts
index 95c4ba25..50d1f7b5 100644
--- a/test/interpreter/unit/Intepreter.spec.ts
+++ b/test/interpreter/unit/Intepreter.spec.ts
@@ -7,6 +7,7 @@ import interpretAllOf from '../../../src/interpreter/InterpretAllOf';
 import interpretItems from '../../../src/interpreter/InterpretItems';
 import interpretAdditionalProperties from '../../../src/interpreter/InterpretAdditionalProperties';
 import interpretNot from '../../../src/interpreter/InterpretNot';
+import interpretDependencies from '../../../src/interpreter/InterpretDependencies';
 import { CommonModel, Schema } from '../../../src/models';
 
 jest.mock('../../../src/interpreter/Utils');
@@ -17,6 +18,7 @@ jest.mock('../../../src/interpreter/InterpretAllOf');
 jest.mock('../../../src/interpreter/InterpretItems');
 jest.mock('../../../src/interpreter/InterpretAdditionalProperties');
 jest.mock('../../../src/interpreter/InterpretNot');
+jest.mock('../../../src/interpreter/InterpretDependencies');
 CommonModel.mergeCommonModels = jest.fn();
 /**
  * Some of these test are purely theoretical and have little if any merit 
@@ -157,7 +159,12 @@ describe('Interpreter', () => {
     interpreter.interpret(schema);
     expect(interpretNot).toHaveBeenNthCalledWith(1, schema, expect.anything(), interpreter, Interpreter.defaultInterpreterOptions);
   });
-
+  test('should always try to interpret dependencies', () => {
+    const schema = {};
+    const interpreter = new Interpreter();
+    interpreter.interpret(schema);
+    expect(interpretDependencies).toHaveBeenNthCalledWith(1, schema, expect.anything(), expect.anything(), Interpreter.defaultInterpreterOptions);
+  });
   test('should support primitive roots', () => {
     const schema = { type: 'string' };
     const interpreter = new Interpreter();
diff --git a/test/interpreter/unit/InterpretDependencies.spec.ts b/test/interpreter/unit/InterpretDependencies.spec.ts
new file mode 100644
index 00000000..2cce98e5
--- /dev/null
+++ b/test/interpreter/unit/InterpretDependencies.spec.ts
@@ -0,0 +1,49 @@
+/* eslint-disable no-undef */
+import { CommonModel } from '../../../src/models/CommonModel';
+import { Interpreter } from '../../../src/interpreter/Interpreter';
+import interpretDependencies from '../../../src/interpreter/InterpretDependencies';
+jest.mock('../../../src/interpreter/Interpreter');
+jest.mock('../../../src/models/CommonModel');
+jest.mock('../../../src/interpreter/Utils');
+/**
+ * Some of these test are purely theoretical and have little if any merit 
+ * on a JSON Schema which actually makes sense but are used to test the principles.
+ */
+describe('Interpretation of dependencies', () => {
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+  afterAll(() => {
+    jest.restoreAllMocks();
+  });
+
+  test('should not do anything if schema does not contain dependencies', () => {
+    const model = new CommonModel();
+    const interpreter = new Interpreter(); 
+    
+    interpretDependencies({}, model, interpreter);
+
+    expect(interpreter.interpretAndCombineSchema).not.toHaveBeenCalled();
+    expect(JSON.stringify(model)).toEqual(JSON.stringify(new CommonModel()));
+  });
+  test('should not do anything with property dependencies', () => {
+    const model = new CommonModel();
+    const interpreter = new Interpreter(); 
+    const schema = { dependencies: {dep1: ['dep2']} };
+    
+    interpretDependencies(schema, model, interpreter);
+
+    expect(interpreter.interpretAndCombineSchema).not.toHaveBeenCalled();
+    expect(JSON.stringify(model)).toEqual(JSON.stringify(new CommonModel()));
+  });
+
+  test('should interpret and combine schema dependencies', () => {
+    const model = new CommonModel();
+    const schema = { dependencies: {dep1: {}} };
+    const interpreter = new Interpreter();
+
+    interpretDependencies(schema, model, interpreter);
+
+    expect(interpreter.interpretAndCombineSchema).toHaveBeenNthCalledWith(1, schema.dependencies.dep1, model, schema, Interpreter.defaultInterpreterOptions);
+  });
+});