Skip to content

Commit

Permalink
HCK-9578: union types mapping (#41)
Browse files Browse the repository at this point in the history
* add union types mapping

* add unit tests
  • Loading branch information
VitaliiBedletskyi authored Jan 22, 2025
1 parent 5796fd5 commit 1fc941f
Show file tree
Hide file tree
Showing 7 changed files with 264 additions and 6 deletions.
4 changes: 2 additions & 2 deletions forward_engineering/helpers/referenceHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
* Get the definition name from the reference path
*
* @param {Object} param0
* @param {string} param0.referencePath - The reference path, separated by '/', where the definition name is the last element.
* @param {string} [param0.referencePath] - The reference path, separated by '/', where the definition name is the last element.
* @returns {string} - The definition name.
*/
function getDefinitionNameFromReferencePath({ referencePath }) {
function getDefinitionNameFromReferencePath({ referencePath = '' }) {
return referencePath.split('/').pop();
}

Expand Down
4 changes: 3 additions & 1 deletion forward_engineering/mappers/typeDefinitions.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const { formatFEStatement } = require('../helpers/feStatementFormatHelper');
const { getCustomScalars } = require('./customScalars');
const { getEnums } = require('./enums');
const { getUnions } = require('./unions');

/**
* Gets the type definition statements from model definitions.
Expand All @@ -14,8 +15,9 @@ function getTypeDefinitionStatements({ modelDefinitions }) {
customScalars: getModelDefinitionsBySubtype({ modelDefinitions, subtype: 'scalar' }),
});
const enums = getEnums({ enumsDefinitions: getModelDefinitionsBySubtype({ modelDefinitions, subtype: 'enum' }) });
const unions = getUnions({ unions: getModelDefinitionsBySubtype({ modelDefinitions, subtype: 'union' }) });

const typeDefinitions = [...customScalars, ...enums];
const typeDefinitions = [...customScalars, ...enums, ...unions];
const formattedTypeDefinitions = typeDefinitions
.map(typeDefinition => formatFEStatement({ feStatement: typeDefinition }))
.join('\n\n');
Expand Down
59 changes: 59 additions & 0 deletions forward_engineering/mappers/unions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* @import { UnionSchema, FEStatement, Union } from "../../types/types"
*/

const { getDefinitionNameFromReferencePath } = require('../helpers/referenceHelper');
const { joinInlineStatements } = require('../helpers/feStatementJoinHelper');
const { getDirectivesUsageStatement } = require('./directives');

/**
* Map the union member types to a string.
*
* @param {Object} args - The arguments
* @param {UnionMemberType[]} unionMemberTypes - The union member types with all properties
* @return {string}
*/
const getUnionMemberTypes = ({ unionMemberTypes }) => {
return unionMemberTypes
.map(unionMemberType => {
if (unionMemberType.$ref) {
return getDefinitionNameFromReferencePath({ referencePath: unionMemberType.$ref });
}
})
.filter(Boolean) // Filter out empty subschemas when a user missed to add union member types
.join(' | ');
};

/**
* Maps a union to an FEStatement.
*
* @param {Object} args - The arguments
* @param {string} args.name - The name of the union.
* @param {Union} args.union - The union object with all properties
* @returns {FEStatement}
*/
const mapUnion = ({ name, union }) => {
const unionMemberTypes = getUnionMemberTypes({ unionMemberTypes: union.oneOf });
const unionDirectives = getDirectivesUsageStatement({ directives: union.typeDirectives });
return {
statement: joinInlineStatements({ statements: ['union', name, unionDirectives, '=', unionMemberTypes] }),
description: union.description,
isActivated: union.isActivated,
};
};
/**
* Maps the union types to an array of FEStatement.
*
* @param {Object} args - The arguments
* @param {UnionSchema} args.unions - The union types schema.
* @return {FEStatement[]}
*/
const getUnions = ({ unions }) => {
return Object.entries(unions).map(([name, union]) => mapUnion({ name, union }));
};

module.exports = {
getUnionMemberTypes,
mapUnion,
getUnions,
};
30 changes: 30 additions & 0 deletions forward_engineering/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,33 @@ export type Argument = {
};

export type IdToNameMap = Record<string, string>;

type UnionMemberType = {
$ref: string;
GUID: string;
displayName: string;
isActivated: boolean;
}

type OneOfMeta = {
choice: string;
index: number;
isActivated: boolean;
}

export type Union = {
type: 'union';
GUID: string;
description?: string;
comments?: string;
typeDirectives?: DirectivePropertyData[];
additionalProperties: boolean;
ignore_z_value: boolean;
isActivated: boolean;
oneOf: UnionMemberType[];
oneOf_meta: OneOfMeta;
schemaType: string;
snippet: 'union';
}

export type UnionSchema = Record<string, Union>
28 changes: 28 additions & 0 deletions test/forward_engineering/helpers/referenceHelper.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const { describe, it } = require('node:test');
const { strictEqual } = require('node:assert');
const { getDefinitionNameFromReferencePath } = require('../../../forward_engineering/helpers/referenceHelper');

describe('getDefinitionNameFromReferencePath', () => {
it('should return the definition name from a reference path', () => {
const referencePath = '#/model/definitions/Objects/User';
const result = getDefinitionNameFromReferencePath({ referencePath });
strictEqual(result, 'User');
});

it('should return the last element from a simple reference path', () => {
const referencePath = '#/User';
const result = getDefinitionNameFromReferencePath({ referencePath });
strictEqual(result, 'User');
});

it('should return an empty string if the reference path is undefined', () => {
const result = getDefinitionNameFromReferencePath({ referencePath: undefined });
strictEqual(result, '');
});

it('should return the correct name when reference path has multiple slashes', () => {
const referencePath = '#/model/definitions/Objects/Account/User';
const result = getDefinitionNameFromReferencePath({ referencePath });
strictEqual(result, 'User');
});
});
23 changes: 20 additions & 3 deletions test/forward_engineering/mappers/arguments.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,12 @@ mock.module('../../../forward_engineering/helpers/feStatementFormatHelper', {
// This require should be after the mocks to ensure that the mocks are applied before the module is required
const { getArguments, getArgumentType, mapArgument } = require('../../../forward_engineering/mappers/arguments');

describe.skip('getArgumentType', () => {
describe('getArgumentType', () => {
afterEach(() => {
mock.restore();
getDirectivesUsageStatementMock.mock.resetCalls();
getArgumentDefaultValueMock.mock.resetCalls();
joinInlineStatementsMock.mock.resetCalls();
formatFEStatementMock.mock.resetCalls();
});

it('should return the argument type if type is definition ID', () => {
Expand Down Expand Up @@ -103,7 +106,14 @@ describe.skip('getArgumentType', () => {
});
});

describe.skip('mapArgument', () => {
describe('mapArgument', () => {
afterEach(() => {
getDirectivesUsageStatementMock.mock.resetCalls();
getArgumentDefaultValueMock.mock.resetCalls();
joinInlineStatementsMock.mock.resetCalls();
formatFEStatementMock.mock.resetCalls();
});

it('should map an argument to a string with all configured properties', () => {
const argument = {
name: 'arg1',
Expand All @@ -127,6 +137,13 @@ describe.skip('mapArgument', () => {
});

describe('getArguments', () => {
afterEach(() => {
getDirectivesUsageStatementMock.mock.resetCalls();
getArgumentDefaultValueMock.mock.resetCalls();
joinInlineStatementsMock.mock.resetCalls();
formatFEStatementMock.mock.resetCalls();
});

it('should return arguments as a single line if no descriptions are present', () => {
const arguments = [
{ name: 'arg1', type: 'String' },
Expand Down
122 changes: 122 additions & 0 deletions test/forward_engineering/mappers/unions.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
const { describe, it, mock, afterEach } = require('node:test');
const { strictEqual, deepStrictEqual } = require('node:assert');

const getDefinitionNameFromReferencePathMock = mock.fn(() => '');
const joinInlineStatementsMock = mock.fn(() => '');
const getDirectivesUsageStatementMock = mock.fn(() => '');

mock.module('../../../forward_engineering/helpers/referenceHelper', {
namedExports: {
getDefinitionNameFromReferencePath: getDefinitionNameFromReferencePathMock,
},
});

mock.module('../../../forward_engineering/helpers/feStatementJoinHelper', {
namedExports: {
joinInlineStatements: joinInlineStatementsMock,
},
});

mock.module('../../../forward_engineering/mappers/directives', {
namedExports: {
getDirectivesUsageStatement: getDirectivesUsageStatementMock,
},
});

// This require should be after the mocks to ensure that the mocks are applied before the module is required
const { getUnionMemberTypes, mapUnion, getUnions } = require('../../../forward_engineering/mappers/unions');

describe('getUnionMemberTypes', () => {
afterEach(() => {
getDefinitionNameFromReferencePathMock.mock.resetCalls();
joinInlineStatementsMock.mock.resetCalls();
getDirectivesUsageStatementMock.mock.resetCalls();
});

it('should return union member types as a string', () => {
const unionMemberTypes = [{ $ref: '#/definitions/User' }, { $ref: '#/definitions/Account' }];

getDefinitionNameFromReferencePathMock.mock.mockImplementationOnce(() => 'User', 0);
getDefinitionNameFromReferencePathMock.mock.mockImplementationOnce(() => 'Account', 1);

const result = getUnionMemberTypes({ unionMemberTypes });

strictEqual(getDefinitionNameFromReferencePathMock.mock.calls.length, 2);
strictEqual(result, 'User | Account');
});

it('should filter out empty union member types', () => {
const unionMemberTypes = [{ $ref: '#/definitions/User' }, {}];

getDefinitionNameFromReferencePathMock.mock.mockImplementationOnce(() => 'User', 0);

const result = getUnionMemberTypes({ unionMemberTypes });

strictEqual(getDefinitionNameFromReferencePathMock.mock.calls.length, 1);
strictEqual(result, 'User');
});
});

describe('mapUnion', () => {
afterEach(() => {
getDefinitionNameFromReferencePathMock.mock.resetCalls();
joinInlineStatementsMock.mock.resetCalls();
getDirectivesUsageStatementMock.mock.resetCalls();
});

it('should map a union to an FEStatement', () => {
const union = {
oneOf: [{ $ref: '#/definitions/User' }],
typeDirectives: [{ directiveFormat: 'Raw', rawDirective: '@directive' }],
description: 'A union type',
isActivated: true,
};

getDefinitionNameFromReferencePathMock.mock.mockImplementationOnce(() => 'User');
getDirectivesUsageStatementMock.mock.mockImplementationOnce(() => '@directive');
joinInlineStatementsMock.mock.mockImplementationOnce(() => 'union UserUnion @directive = User');

const result = mapUnion({ name: 'UserUnion', union });

strictEqual(getDefinitionNameFromReferencePathMock.mock.calls.length, 1);
strictEqual(getDirectivesUsageStatementMock.mock.calls.length, 1);
strictEqual(joinInlineStatementsMock.mock.calls.length, 1);
deepStrictEqual(result, {
statement: 'union UserUnion @directive = User',
description: 'A union type',
isActivated: true,
});
});
});

describe('getUnions', () => {
afterEach(() => {
getDefinitionNameFromReferencePathMock.mock.resetCalls();
joinInlineStatementsMock.mock.resetCalls();
getDirectivesUsageStatementMock.mock.resetCalls();
});

it('should map union types to an array of FEStatement', () => {
const unions = {
UserUnion: {
oneOf: [{ $ref: '#/definitions/User' }],
typeDirectives: [{ directiveFormat: 'Raw', rawDirective: '@directive' }],
description: 'A union type',
isActivated: true,
},
};

getDefinitionNameFromReferencePathMock.mock.mockImplementationOnce(() => 'User');
getDirectivesUsageStatementMock.mock.mockImplementationOnce(() => '@directive');
joinInlineStatementsMock.mock.mockImplementationOnce(() => 'union UserUnion @directive = User');

const result = getUnions({ unions });
deepStrictEqual(result, [
{
statement: 'union UserUnion @directive = User',
description: 'A union type',
isActivated: true,
},
]);
});
});

0 comments on commit 1fc941f

Please sign in to comment.