Skip to content

Commit

Permalink
Add metadata generation support
Browse files Browse the repository at this point in the history
  • Loading branch information
willosborne committed Jun 11, 2024
1 parent 7703c31 commit 8d98914
Show file tree
Hide file tree
Showing 4 changed files with 224 additions and 7 deletions.
155 changes: 155 additions & 0 deletions cli/src/commands/generate/components/metadata.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { SchemaDirectory } from '../schema-directory';
import { instantiateAllMetadata, instantiateMetadataObject } from './metadata';

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

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

let mockSchemaDir;

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


describe('instantiateMetadataObject', () => {
it('instantiate metadata object with simple properties', () => {
const metadataDef = {
'type': 'object',
'properties': {
'string-prop': {
'type': 'string'
},
'integer-prop': {
'type': 'integer'
},
'const-prop': {
'const': 'constant'
}
}
};
expect(instantiateMetadataObject(metadataDef, mockSchemaDir, false, true))
.toEqual(
{
'string-prop': '{{ STRING_PROP }}',
'integer-prop': -1,
'const-prop': 'constant'
},
);
});

it('instantiate metadata object with nested object properties', () => {
const metadataDef = {
'type': 'object',
'properties': {
'property-name': {
'type': 'object',
'properties': {
'example': {
'type': 'string'
}
}
}
}
};
expect(instantiateMetadataObject(metadataDef, mockSchemaDir, false, true))
.toEqual(
{
'property-name': {
'example': '{{ EXAMPLE }}'
}
},
);
});

it('instantiate metadata object with $ref', () => {
const reference = 'http://calm.com/example-ref';
const metadataDef = {
'$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(instantiateMetadataObject(metadataDef, mockSchemaDir, false, true))
.toEqual(
{
'property-name': {
'example': '{{ EXAMPLE }}'
}
},
);
expect(spy).toHaveBeenCalledWith(reference);
});
});

function getSamplePatternWithMetadata(...metadataDefs): object {
return {
properties: {
metadata: {
type: 'array',
prefixItems: [
...metadataDefs
]
}
}
};
}


describe('instantiateAllMetadata', () => {
it('instantiate simple metadata list with two objects', () => {
const pattern = getSamplePatternWithMetadata({
'type': 'object',
'properties': {
'property-name': {
'type': 'string'
}
}
},
{
'type': 'object',
'properties': {
'property-name-2': {
'type': 'integer'
}
}
}
);
expect(instantiateAllMetadata(pattern, mockSchemaDir, false, true))
.toEqual(
[
{
'property-name': '{{ PROPERTY_NAME }}'
},
{
'property-name-2': -1
}
]
);
});
});
58 changes: 58 additions & 0 deletions cli/src/commands/generate/components/metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { initLogger } from '../../helper.js';
import { SchemaDirectory } from '../schema-directory.js';
import { logRequiredMessage, mergeSchemas } from '../util.js';
import { getPropertyValue } from './property.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 instantiateAllMetadata(pattern: object, schemaDirectory: SchemaDirectory, debug: boolean = false, instantiateAll: boolean = false): object[] {
const logger = initLogger(debug);
const metadataObjects = pattern['properties']?.metadata?.prefixItems;
if (!metadataObjects) {
logger.debug('Warning: pattern has no metadata fields defined, skipping instantiation.');
if (pattern['properties']?.metadata?.items) {
logger.warn('Note: properties.metadata.items is deprecated: please use prefixItems instead.');
}
return [];
}
const outputMetadata = [];

for (const node of metadataObjects) {
outputMetadata.push(instantiateMetadataObject(node, schemaDirectory, debug, instantiateAll));
}
return outputMetadata;
}
2 changes: 1 addition & 1 deletion cli/src/commands/generate/components/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export function instantiateNodes(pattern: any, schemaDirectory: SchemaDirectory,
if (!nodes) {
logger.error('Warning: pattern has no nodes defined.');
if (pattern?.properties?.nodes?.items) {
logger.warn('Note: properties.relationships.items is deprecated: please use prefixItems instead.');
logger.warn('Note: properties.nodes.items is deprecated: please use prefixItems instead.');
}
return [];
}
Expand Down
16 changes: 10 additions & 6 deletions cli/src/commands/generate/generate.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

import * as fs from 'node:fs';
import * as path from 'node:path';
import { mkdirp } from 'mkdirp';
Expand All @@ -11,10 +9,11 @@ import { SchemaDirectory } from './schema-directory.js';
import { instantiateNode, instantiateNodes } from './components/node.js';
import { instantiateRelationships } from './components/relationship.js';
import { CALM_META_SCHEMA_DIRECTORY } from '../../consts.js';
import { instantiateAllMetadata } from './components/metadata.js';

let logger: winston.Logger; // defined later at startup

function loadFile(path: string): any {
function loadFile(path: string): object {
logger.info('Loading pattern from file: ' + path);
const raw = fs.readFileSync(path, 'utf-8');

Expand All @@ -26,12 +25,12 @@ function loadFile(path: string): any {
}


function instantiateAdditionalTopLevelProperties(pattern: any, schemaDirectory: SchemaDirectory): any {
const properties = pattern?.properties;
if (!properties) {
function instantiateAdditionalTopLevelProperties(pattern: object, schemaDirectory: SchemaDirectory): object {
if (!('properties' in pattern)) {
logger.error('Warning: pattern has no properties defined.');
return [];
}
const properties = pattern['properties'];

const extraProperties = {};
for (const [additionalProperty, detail] of Object.entries(properties)) {
Expand Down Expand Up @@ -66,13 +65,18 @@ export async function generate(patternPath: string, debug: boolean, instantiateA
const outputNodes = instantiateNodes(pattern, schemaDirectory, debug, instantiateAll);
const relationshipNodes = instantiateRelationships(pattern, schemaDirectory, debug, instantiateAll);
const additionalProperties = instantiateAdditionalTopLevelProperties(pattern, schemaDirectory);
const metadata = instantiateAllMetadata(pattern, schemaDirectory, debug, instantiateAll);

const final = {
'nodes': outputNodes,
'relationships': relationshipNodes,
...additionalProperties // object spread operator to insert additional props at top level
};

if (metadata) {
final['metadata'] = metadata;
}

return final;
}

Expand Down

0 comments on commit 8d98914

Please sign in to comment.