Skip to content

Commit

Permalink
Merge pull request #280 from willosborne/metadata-generation
Browse files Browse the repository at this point in the history
Add metadata generation support and remove 'additional properties' code
  • Loading branch information
willosborne authored Jun 13, 2024
2 parents 7703c31 + 601d43a commit b2f0fef
Show file tree
Hide file tree
Showing 6 changed files with 224 additions and 114 deletions.
2 changes: 1 addition & 1 deletion cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@finos/calm-cli",
"version": "0.1.3",
"version": "0.1.4",
"description": "A set of tools for interacting with the Common Architecture Language Model (CALM)",
"main": "dist/index.js",
"files": [
Expand Down
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
80 changes: 0 additions & 80 deletions cli/src/commands/generate/generate.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

import { exportedForTesting } from './generate';
import { runGenerate } from './generate';
import { tmpdir } from 'node:os';
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
import path from 'node:path';
import { SchemaDirectory } from './schema-directory';

jest.mock('../helper', () => {
return {
Expand All @@ -24,82 +20,6 @@ jest.mock('../../consts', () => ({
get CALM_META_SCHEMA_DIRECTORY() { return '../calm/draft/2024-04/meta'; }
}));

let mockSchemaDir;

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

const {
instantiateAdditionalTopLevelProperties
} = exportedForTesting;


describe('instantiateAdditionalTopLevelProperties', () => {
it('instantiate an additional top level array property', () => {
const pattern = {
properties: {
'extra-property': {
properties: {
values: {
type: 'array'
}
}
}
}
};

expect(instantiateAdditionalTopLevelProperties(pattern, mockSchemaDir))
.toEqual({
'extra-property': {
values: [ '{{ VALUES }}' ]
}
});
});

it('instantiate an additional top level const property', () => {
const pattern = {
properties: {
'extra': {
properties: {
'extra-property': {
const: 'value here'
}
}
}
}
};

expect(instantiateAdditionalTopLevelProperties(pattern, mockSchemaDir))
.toEqual({
'extra': {
'extra-property': 'value here'
}
});
});

it('instantiate an additional top level string property', () => {
const pattern = {
properties: {
'extra': {
properties: {
'extra-property': {
'type': 'string'
}
}
}
}
};

expect(instantiateAdditionalTopLevelProperties(pattern, mockSchemaDir))
.toEqual({
extra: {
'extra-property': '{{ EXTRA_PROPERTY }}'
}
});
});
});


describe('runGenerate', () => {
let tempDirectoryPath;
Expand Down
Loading

0 comments on commit b2f0fef

Please sign in to comment.