From 1fc941f9f10767ea19be14a1c6e7a5bde5d55c3a Mon Sep 17 00:00:00 2001 From: Vitalii Bedletskyi <70570504+VitaliiBedletskyi@users.noreply.github.com> Date: Wed, 22 Jan 2025 17:46:26 +0200 Subject: [PATCH] HCK-9578: union types mapping (#41) * add union types mapping * add unit tests --- .../helpers/referenceHelper.js | 4 +- .../mappers/typeDefinitions.js | 4 +- forward_engineering/mappers/unions.js | 59 +++++++++ forward_engineering/types/types.d.ts | 30 +++++ .../helpers/referenceHelper.spec.js | 28 ++++ .../mappers/arguments.spec.js | 23 +++- .../mappers/unions.spec.js | 122 ++++++++++++++++++ 7 files changed, 264 insertions(+), 6 deletions(-) create mode 100644 forward_engineering/mappers/unions.js create mode 100644 test/forward_engineering/helpers/referenceHelper.spec.js create mode 100644 test/forward_engineering/mappers/unions.spec.js diff --git a/forward_engineering/helpers/referenceHelper.js b/forward_engineering/helpers/referenceHelper.js index f38cf9b..f88aa88 100644 --- a/forward_engineering/helpers/referenceHelper.js +++ b/forward_engineering/helpers/referenceHelper.js @@ -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(); } diff --git a/forward_engineering/mappers/typeDefinitions.js b/forward_engineering/mappers/typeDefinitions.js index 1a2ce42..5cdff9f 100644 --- a/forward_engineering/mappers/typeDefinitions.js +++ b/forward_engineering/mappers/typeDefinitions.js @@ -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. @@ -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'); diff --git a/forward_engineering/mappers/unions.js b/forward_engineering/mappers/unions.js new file mode 100644 index 0000000..509b91e --- /dev/null +++ b/forward_engineering/mappers/unions.js @@ -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, +}; diff --git a/forward_engineering/types/types.d.ts b/forward_engineering/types/types.d.ts index 781294c..53c363e 100644 --- a/forward_engineering/types/types.d.ts +++ b/forward_engineering/types/types.d.ts @@ -27,3 +27,33 @@ export type Argument = { }; export type IdToNameMap = Record; + +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 diff --git a/test/forward_engineering/helpers/referenceHelper.spec.js b/test/forward_engineering/helpers/referenceHelper.spec.js new file mode 100644 index 0000000..745181f --- /dev/null +++ b/test/forward_engineering/helpers/referenceHelper.spec.js @@ -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'); + }); +}); diff --git a/test/forward_engineering/mappers/arguments.spec.js b/test/forward_engineering/mappers/arguments.spec.js index d7abc2e..6ecaae7 100644 --- a/test/forward_engineering/mappers/arguments.spec.js +++ b/test/forward_engineering/mappers/arguments.spec.js @@ -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', () => { @@ -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', @@ -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' }, diff --git a/test/forward_engineering/mappers/unions.spec.js b/test/forward_engineering/mappers/unions.spec.js new file mode 100644 index 0000000..c1a8c6d --- /dev/null +++ b/test/forward_engineering/mappers/unions.spec.js @@ -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, + }, + ]); + }); +});