diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fbfd909..74cf263b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/). ### Fixed - Inline enums are now available during runtime as well +- Inline enums can now be used as action parameter types as well. These enums will not have a runtime representation, but will only assert type safety! ## Version 0.14.0 - 2023-12-13 ### Added diff --git a/lib/components/enum.js b/lib/components/enum.js index bcf08c15..afd27d46 100644 --- a/lib/components/enum.js +++ b/lib/components/enum.js @@ -29,17 +29,33 @@ function printEnum(buffer, name, kvs, options = {}) { buffer.add('// enum') buffer.add(`${opts.export ? 'export ' : ''}const ${name} = {`) buffer.indent() - const vals = new Set() for (const [k, v] of kvs) { buffer.add(`${k}: ${v},`) - vals.add(v?.val ?? v) // in case of wrapped vals we need to unwrap here for the type } buffer.outdent() buffer.add('} as const;') - buffer.add(`${opts.export ? 'export ' : ''}type ${name} = ${[...vals].join(' | ')}`) + buffer.add(`${opts.export ? 'export ' : ''}type ${name} = ${stringifyEnumType(kvs)}`) buffer.add('') } +/** + * Stringifies a list of enum key-value pairs into the righthand side of a TS type. + * @param {[string, string][]} kvs list of key-value pairs + * @returns {string} a stringified type + * @example + * ```js + * ['A', 'B', 'A'] // -> '"A" | "B"' + * ``` + */ +const stringifyEnumType = kvs => [...uniqueValues(kvs)].join(' | ') + +/** + * Extracts all unique values from a list of enum key-value pairs. + * If the value is an object, then the `.val` property is used. + * @param {[string, any | {val: any}][]} kvs + */ +const uniqueValues = kvs => new Set(kvs.map(([,v]) => v?.val ?? v)) // in case of wrapped vals we need to unwrap here for the type + // in case of strings, wrap in quotes and fallback to key to make sure values are attached for every key const enumVal = (key, value, enumType) => enumType === 'cds.String' ? JSON.stringify(`${value ?? key}`) : value @@ -113,5 +129,6 @@ module.exports = { csnToEnumPairs, propertyToInlineEnumName, isInlineEnumType, - stringifyEnumImplementation + stringifyEnumImplementation, + stringifyEnumType } \ No newline at end of file diff --git a/lib/visitor.js b/lib/visitor.js index 61fe2397..44a0af92 100644 --- a/lib/visitor.js +++ b/lib/visitor.js @@ -9,7 +9,7 @@ const { FlatInlineDeclarationResolver, StructuredInlineDeclarationResolver } = r const { Resolver } = require('./components/resolver') const { Logger } = require('./logging') const { docify } = require('./components/wrappers') -const { csnToEnumPairs, propertyToInlineEnumName, isInlineEnumType } = require('./components/enum') +const { csnToEnumPairs, propertyToInlineEnumName, isInlineEnumType, stringifyEnumType } = require('./components/enum') /** @typedef {import('./file').File} File */ /** @typedef {{ entity: String }} Context */ @@ -313,11 +313,17 @@ class Visitor { .filter(([, type]) => type?.type !== '$self' && !(type.items?.type === '$self')) .map(([name, type]) => [ name, - this.resolver.visitor.inlineDeclarationResolver.getPropertyDatatype(this.resolver.resolveAndRequire(type, file)), + this.#stringifyFunctionParamType(type, file) ]) : [] } + #stringifyFunctionParamType(type, file) { + return type.enum + ? stringifyEnumType(csnToEnumPairs(type)) + : this.inlineDeclarationResolver.getPropertyDatatype(this.resolver.resolveAndRequire(type, file)) + } + #printFunction(name, func) { // FIXME: mostly duplicate of printAction -> reuse this.logger.debug(`Printing function ${name}:\n${JSON.stringify(func, null, 2)}`) diff --git a/test/ast.js b/test/ast.js index 2e75015e..69904e18 100644 --- a/test/ast.js +++ b/test/ast.js @@ -444,6 +444,7 @@ const check = { isUnionType: (node, of = []) => checkKeyword(node, 'uniontype') && of.reduce((acc, predicate) => acc && node.subtypes.some(st => predicate(st)), true), isNullable: (node, of = []) => check.isUnionType(node, of.concat([check.isNull])), + isLiteral: (node, literal = undefined) => checkKeyword(node, 'literaltype') && (literal === undefined || node.literal === literal), } diff --git a/test/unit/enum.test.js b/test/unit/enum.test.js index 4e14e11b..f43b655b 100644 --- a/test/unit/enum.test.js +++ b/test/unit/enum.test.js @@ -3,12 +3,35 @@ const fs = require('fs').promises const path = require('path') const cds2ts = require('../../lib/compile') -const { ASTWrapper, check, JSASTWrapper } = require('../ast') +const { ASTWrapper, check, JSASTWrapper, checkFunction } = require('../ast') const { locations } = require('../util') const dir = locations.testOutput('enums_test') // FIXME: missing: inline enums (entity Foo { bar: String enum { ... }}) +describe('Enum Action Parameters', () => { + let astw + + beforeEach(async () => await fs.unlink(dir).catch(() => {})) + beforeAll(async () => { + const paths = await cds2ts + .compileFromFile(locations.unit.files('enums/actions.cds'), { outputDirectory: dir, inlineDeclarations: 'structured' }) + astw = new ASTWrapper(path.join(paths[1], 'index.ts')) + }) + + test('Coalescing Assignment Present', () => { + const actions = astw.getAspectProperty('_FoobarAspect', 'actions') + checkFunction(actions.type.members.find(fn => fn.name === 'f'), { + parameterCheck: ({members: [fst]}) => fst.name === 'p' + && check.isUnionType(fst.type, [ + t => check.isLiteral(t, 'A'), + t => check.isLiteral(t, 'b'), + ]) + }) + }) +}) + + describe('Nested Enums', () => { let astw @@ -26,8 +49,6 @@ describe('Nested Enums', () => { const { left } = enm.expression // not checking the entire object chain here... expect(left.property.name).toBe('someEnumProperty') - - console.log(42) }) }) diff --git a/test/unit/files/enums/actions.cds b/test/unit/files/enums/actions.cds new file mode 100644 index 00000000..2dc2ae9f --- /dev/null +++ b/test/unit/files/enums/actions.cds @@ -0,0 +1,6 @@ +entity Foobar {} actions { + action f(p: String enum { + A; + B = 'b' + }) +}; \ No newline at end of file