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

#486 Unify instantiation mechanisms #521

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
166 changes: 166 additions & 0 deletions cli/src/commands/generate/components/instantiate.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

import { SchemaDirectory } from '../schema-directory';
import { instantiateGenericObject } from './instantiate';

jest.mock('../../helper', () => {
return {
initLogger: () => {
return {
info: () => { },
debug: () => { }
};
}
};
});

jest.mock('../schema-directory');

let mockSchemaDir;

beforeEach(() => {
mockSchemaDir = new SchemaDirectory();
});



describe('instantiateGenericObject', () => {
it('instantiate object with simple properties', () => {
const objectDef = {
'type': 'object',
'properties': {
'string-prop': {
'type': 'string'
},
'integer-prop': {
'type': 'integer'
},
'boolean-prop': {
'type': 'boolean'
},
'const-prop': {
'const': 'constant'
}
}
};
expect(instantiateGenericObject(objectDef, mockSchemaDir, 'generic', [], false, true))
.toEqual(
{
'string-prop': '{{ STRING_PROP }}',
'integer-prop': -1,
'boolean-prop': '{{ BOOLEAN_BOOLEAN_PROP }}',
'const-prop': 'constant'
},
);
});

it('instantiate object with nested object properties', () => {
const objectDef = {
'type': 'object',
'properties': {
'property-name': {
'type': 'object',
'properties': {
'example': {
'type': 'string'
}
}
}
}
};
expect(instantiateGenericObject(objectDef, mockSchemaDir, 'generic', [], false, true))
.toEqual(
{
'property-name': {
'example': '{{ EXAMPLE }}'
}
},
);
});

it('instantiate object with $ref', () => {
const reference = 'http://calm.com/example-ref';
const objectDef = {
'$ref': reference
};

const returnedDef = {
'type': 'object',
'properties': {
'property-name': {
'type': 'object',
'properties': {
'example': {
'type': 'string'
}
}
}
}
};

const spy = jest.spyOn(mockSchemaDir, 'getDefinition');
spy.mockReturnValue(returnedDef);


expect(instantiateGenericObject(objectDef, mockSchemaDir, 'generic', [], false, true))
.toEqual(
{
'property-name': {
'example': '{{ EXAMPLE }}'
}
},
);
expect(spy).toHaveBeenCalledWith(reference);
});

it('instantiate object with simple array property to placeholder', () => {
const objectDef = {
'type': 'object',
'properties': {
'property-name': {
'type': 'array',
'items': 'string'
}
}
};
expect(instantiateGenericObject(objectDef, mockSchemaDir, 'generic', [], false, true))
.toEqual(
{
'property-name': [
'{{ PROPERTY_NAME }}'
]
},
);
});

it('instantiate object with complex/prefixItems array property', () => {
const objectDef = {
'type': 'object',
'properties': {
'property-name': {
'type': 'array',
'prefixItems': [
{
'type': 'object',
'properties': {
'property': {
'const': 'value'
}
}
}
]
}
}
};
expect(instantiateGenericObject(objectDef, mockSchemaDir, 'generic', [], false, true))
.toEqual(
{
'property-name': [
{
'property': 'value'
}
]
},
);
});
});
92 changes: 92 additions & 0 deletions cli/src/commands/generate/components/instantiate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { Logger } from 'winston';
import { initLogger } from '../../helper.js';
import { SchemaDirectory } from '../schema-directory.js';
import { appendPath, logRequiredMessage, mergeSchemas, renderPath } from '../util.js';
import { getConstValue, getPropertyValue } from './property.js';

export function instantiateGenericObject(definition: object, schemaDirectory: SchemaDirectory, objectType: string, path: string[], debug: boolean = false, instantiateAll: boolean = false): object {
const logger = initLogger(debug);
let fullDefinition = definition;
if (definition['$ref']) {
const ref = definition['$ref'];
const schemaDef = schemaDirectory.getDefinition(ref);

fullDefinition = mergeSchemas(schemaDef, definition);
}
// TODO rework to properly separate 'verbose' from 'debug' level logging
// logger.debug('Generating ' + objectType + ' object from ' + JSON.stringify(fullDefinition));

if (!('properties' in fullDefinition)) {
return {};
}

const required = fullDefinition['required'];
logRequiredMessage(logger, required, instantiateAll);

const out = {};
for (const [key, detail] of Object.entries(fullDefinition['properties'])) {
const currentPath = appendPath(path, key);
const renderedPath = renderPath(currentPath);

if (!instantiateAll && required && !required.includes(key) && key !== 'interfaces') {
logger.debug(`${renderedPath}: Skipping property ${key} as it is not marked as required.`);
continue;
}
if (detail?.const) {
out[key] = getConstValue(detail);
}
else if (detail?.type === 'object') {
// recursive instantiation
logger.info(`${renderedPath}: Recursively instantiating a ${objectType} object`);
out[key] = instantiateGenericObject(detail, schemaDirectory, objectType, currentPath, instantiateAll, debug);
}
else if (detail?.type === 'array' && isArrayObjectComplex(detail, logger, renderedPath)) {
logger.info(`${renderedPath}: Recursively instantiating an array object.`);

// isArrayObjectComplex ensures this is present
const prefixItems = detail.prefixItems;
out[key] = instantiateArray(prefixItems, schemaDirectory, objectType, currentPath, instantiateAll, debug);
}
else {
out[key] = getPropertyValue(key, detail);
}
}
return out;
}

function isArrayObjectComplex(detail: object, logger: Logger, pathContext: string) {
if (!detail) {
return false;
}

const arrayContentsType = detail['items']?.type;
if (!!arrayContentsType && ['integer', 'number', 'boolean', 'string', 'const'].includes(arrayContentsType)) {
logger.info(`${pathContext}: Skipping recursive instantiation of array as it has a simple type and no prefixItems`);
return false;
}

if (!!detail['prefixItems'] && !!detail['items']) {
logger.warn(`${pathContext}: Both 'items' and 'prefixItems' are defined on this array schema; only prefixItems will be instantiated.`);
}

if (detail['prefixItems']) {
// if we have prefixItems and it's not a simple array, then must be complex.
return true;
}

// fallback if there are neither - let property.ts handle the empty object
return false;
}

export function instantiateArray(prefixItems: object[], schemaDirectory: SchemaDirectory, objectType: string, path: string[], instantiateAll: boolean, debug: boolean) {
const logger = initLogger(debug);
const output = [];

logger.debug(`${path}: Instantiating elements of array as defined in prefixItems`);
for (const [index, element] of prefixItems.entries()) {
const currentPath = appendPath(path, index);
output.push(instantiateGenericObject(element, schemaDirectory, objectType, currentPath, debug, instantiateAll));
}

return output;
}
6 changes: 3 additions & 3 deletions cli/src/commands/generate/components/metadata.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ describe('instantiateMetadataObject', () => {
}
}
};
expect(instantiateMetadataObject(metadataDef, mockSchemaDir, false, true))
expect(instantiateMetadataObject(metadataDef, mockSchemaDir, [], false, true))
.toEqual(
{
'string-prop': '{{ STRING_PROP }}',
Expand All @@ -65,7 +65,7 @@ describe('instantiateMetadataObject', () => {
}
}
};
expect(instantiateMetadataObject(metadataDef, mockSchemaDir, false, true))
expect(instantiateMetadataObject(metadataDef, mockSchemaDir, [], false, true))
.toEqual(
{
'property-name': {
Expand Down Expand Up @@ -99,7 +99,7 @@ describe('instantiateMetadataObject', () => {
spy.mockReturnValue(returnedDef);


expect(instantiateMetadataObject(metadataDef, mockSchemaDir, false, true))
expect(instantiateMetadataObject(metadataDef, mockSchemaDir, [], false, true))
.toEqual(
{
'property-name': {
Expand Down
45 changes: 7 additions & 38 deletions cli/src/commands/generate/components/metadata.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,10 @@
import { initLogger } from '../../helper.js';
import { SchemaDirectory } from '../schema-directory.js';
import { logRequiredMessage, mergeSchemas } from '../util.js';
import { getPropertyValue } from './property.js';
import { appendPath } from '../util.js';
import { instantiateGenericObject } from './instantiate.js';

export function instantiateMetadataObject(definition: object, schemaDirectory: SchemaDirectory, debug: boolean = false, instantiateAll: boolean = false): object {
const logger = initLogger(debug);
let fullDefinition = definition;
if (definition['$ref']) {
const ref = definition['$ref'];
const schemaDef = schemaDirectory.getDefinition(ref);

fullDefinition = mergeSchemas(schemaDef, definition);
}
logger.debug('Generating metadata object from ' + JSON.stringify(fullDefinition));

if (!('properties' in fullDefinition)) {
return {};
}

const required = fullDefinition['required'];
logRequiredMessage(logger, required, instantiateAll);

const out = {};
for (const [key, detail] of Object.entries(fullDefinition['properties'])) {
if (!instantiateAll && required && !required.includes(key)) {
logger.debug('Skipping property ' + key + ' as it is not marked as required.');
continue;
}
if (detail?.type == 'object') {
// recursive instantiation
logger.debug('Recursively instantiating a metadata object');
out[key] = instantiateMetadataObject(detail, schemaDirectory, instantiateAll, debug);
}
else {
out[key] = getPropertyValue(key, detail);
}
}
return out;
export function instantiateMetadataObject(definition: object, schemaDirectory: SchemaDirectory, path: string[], debug: boolean = false, instantiateAll: boolean = false): object {
return instantiateGenericObject(definition, schemaDirectory, 'metadata', path, debug, instantiateAll);
}

export function instantiateAllMetadata(pattern: object, schemaDirectory: SchemaDirectory, debug: boolean = false, instantiateAll: boolean = false): object[] {
Expand All @@ -51,8 +19,9 @@ export function instantiateAllMetadata(pattern: object, schemaDirectory: SchemaD
}
const outputMetadata = [];

for (const node of metadataObjects) {
outputMetadata.push(instantiateMetadataObject(node, schemaDirectory, debug, instantiateAll));
for (const [index, metadataObj] of metadataObjects.entries()) {
const path = appendPath(['metadata'], index);
outputMetadata.push(instantiateMetadataObject(metadataObj, schemaDirectory, path, debug, instantiateAll));
}
return outputMetadata;
}
Loading