Skip to content

Commit

Permalink
Allow inline enums as action parameter types (#132)
Browse files Browse the repository at this point in the history
* Allow inline enums as action parameter types

* Add missing doc

* Fix doc
  • Loading branch information
daogrady authored Dec 20, 2023
1 parent 3605154 commit 609589d
Show file tree
Hide file tree
Showing 6 changed files with 61 additions and 9 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 21 additions & 4 deletions lib/components/enum.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -113,5 +129,6 @@ module.exports = {
csnToEnumPairs,
propertyToInlineEnumName,
isInlineEnumType,
stringifyEnumImplementation
stringifyEnumImplementation,
stringifyEnumType
}
10 changes: 8 additions & 2 deletions lib/visitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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)}`)
Expand Down
1 change: 1 addition & 0 deletions test/ast.js
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}


Expand Down
27 changes: 24 additions & 3 deletions test/unit/enum.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
})
})

Expand Down
6 changes: 6 additions & 0 deletions test/unit/files/enums/actions.cds
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
entity Foobar {} actions {
action f(p: String enum {
A;
B = 'b'
})
};

0 comments on commit 609589d

Please sign in to comment.