Skip to content

Commit 6763f54

Browse files
committed
Add support for non exhaustive variants (#1662)
1 parent 8817477 commit 6763f54

File tree

15 files changed

+324
-306
lines changed

15 files changed

+324
-306
lines changed

compiler/src/model/metamodel.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -177,19 +177,28 @@ export abstract class BaseType {
177177

178178
export type Variants = ExternalTag | InternalTag | Container
179179

180-
export class ExternalTag {
180+
export class VariantBase {
181+
/**
182+
* Is this variant type open to extensions? Default to false. Used for variants that can
183+
* be extended with plugins. If true, target clients should allow for additional variants
184+
* with a variant tag outside the ones defined in the spec and arbitrary data as the value.
185+
*/
186+
nonExhaustive?: boolean
187+
}
188+
189+
export class ExternalTag extends VariantBase {
181190
kind: 'external_tag'
182191
}
183192

184-
export class InternalTag {
193+
export class InternalTag extends VariantBase {
185194
kind: 'internal_tag'
186195
/* Name of the property that holds the variant tag */
187196
tag: string
188197
/* Default value for the variant tag if it's missing */
189198
defaultTag?: string
190199
}
191200

192-
export class Container {
201+
export class Container extends VariantBase {
193202
kind: 'container'
194203
}
195204

@@ -322,6 +331,11 @@ export class EnumMember {
322331
*/
323332
export class Enum extends BaseType {
324333
kind: 'enum'
334+
/**
335+
* If the enum is open, it means that other than the specified values it can accept an arbitrary value.
336+
* If this property is not present, it means that the enum is not open (in other words, is closed).
337+
*/
338+
isOpen?: boolean
325339
members: EnumMember[]
326340
}
327341

compiler/src/model/utils.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,7 @@ export function modelGenerics (node: TypeParameterDeclaration): string {
414414
* which returns an array that only contains the member of the Enum.
415415
*/
416416
export function modelEnumDeclaration (declaration: EnumDeclaration): model.Enum {
417-
return {
417+
const type: model.Enum = {
418418
specLocation: sourceLocation(declaration),
419419
name: {
420420
name: declaration.getName(),
@@ -433,6 +433,13 @@ export function modelEnumDeclaration (declaration: EnumDeclaration): model.Enum
433433
return member
434434
})
435435
}
436+
437+
const tags = parseJsDocTags(declaration.getJsDocs())
438+
if (typeof tags.non_exhaustive === 'string') {
439+
type.isOpen = true
440+
}
441+
442+
return type
436443
}
437444

438445
/**
@@ -632,7 +639,7 @@ export function hoistTypeAnnotations (type: model.TypeDefinition, jsDocs: JSDoc[
632639
// We want to enforce a single jsDoc block.
633640
assert(jsDocs, jsDocs.length < 2, 'Use a single multiline jsDoc block instead of multiple single line blocks')
634641

635-
const validTags = ['class_serializer', 'doc_url', 'doc_id', 'behavior', 'variants', 'variant', 'shortcut_property', 'codegen_names']
642+
const validTags = ['class_serializer', 'doc_url', 'doc_id', 'behavior', 'variants', 'variant', 'shortcut_property', 'codegen_names', 'non_exhaustive']
636643
const tags = parseJsDocTags(jsDocs)
637644
if (jsDocs.length === 1) {
638645
const description = jsDocs[0].getDescription()
@@ -650,6 +657,8 @@ export function hoistTypeAnnotations (type: model.TypeDefinition, jsDocs: JSDoc[
650657
}
651658
} else if (tag === 'variants') {
652659
} else if (tag === 'variant') {
660+
} else if (tag === 'non_exhaustive') {
661+
assert(jsDocs, typeof tags.variants === 'string', '@non_exhaustive only applies to enums and @variants')
653662
} else if (tag === 'doc_url') {
654663
assert(jsDocs, isValidUrl(value), '@doc_url is not a valid url')
655664
type.docUrl = value
@@ -924,13 +933,21 @@ export function parseVariantsTag (jsDoc: JSDoc[]): model.Variants | undefined {
924933
return undefined
925934
}
926935

936+
const nonExhaustive = (typeof tags.non_exhaustive === 'string') ? true : undefined
937+
927938
const [type, ...values] = tags.variants.split(' ')
928939
if (type === 'external') {
929-
return { kind: 'external_tag' }
940+
return {
941+
kind: 'external_tag',
942+
nonExhaustive: nonExhaustive
943+
}
930944
}
931945

932946
if (type === 'container') {
933-
return { kind: 'container' }
947+
return {
948+
kind: 'container',
949+
nonExhaustive: nonExhaustive
950+
}
934951
}
935952

936953
assert(jsDoc, type === 'internal', `Bad variant type: ${type}`)
@@ -940,6 +957,7 @@ export function parseVariantsTag (jsDoc: JSDoc[]): model.Variants | undefined {
940957

941958
return {
942959
kind: 'internal_tag',
960+
nonExhaustive: nonExhaustive,
943961
tag: pairs.tag,
944962
defaultTag: pairs.default
945963
}
@@ -972,13 +990,16 @@ export function parseCommaSeparated (value: string): string[] {
972990

973991
/**
974992
* Parses an array of "key=value" pairs and validate key names. Values can optionally be enclosed with single
975-
* or double quotes.
993+
* or double quotes. If there is only a key with no value (no '=') the value is set to 'true'
976994
*/
977995
export function parseKeyValues (node: Node | Node[], pairs: string[], ...validKeys: string[]): Record<string, string> {
978996
const result = {}
979997
pairs.forEach(item => {
980998
const kv = item.split('=')
981-
assert(node, kv.length === 2, 'Malformed key/value list')
999+
assert(node, kv.length <= 2, 'Malformed key/value list')
1000+
if (kv.length === 1) {
1001+
kv.push('true')
1002+
}
9821003
assert(node, validKeys.includes(kv[0]), `Unknown key '${kv[0]}'`)
9831004
result[kv[0]] = kv[1].replace(/["']/g, '')
9841005
})

docs/modeling-guide.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,19 @@ enum Orientation {
9696
}
9797
```
9898

99+
Some enumerations can accept arbitrary values other than the ones defined. The `@non_exhaustive` jsdoc tag can be used to describe this behavior.
100+
By default, an enum is to be considered exhaustive.
101+
102+
```ts
103+
/** @non_exhaustive */
104+
export enum ScriptLanguage {
105+
painless,
106+
expression,
107+
mustache,
108+
java
109+
}
110+
```
111+
99112
### User defined value
100113

101114
Represents a value that will be defined by the user and has no specific type.
@@ -219,6 +232,11 @@ class Response {
219232

220233
Variants is a special syntax that can be used by language generators to understand
221234
which type they will need to build based on the variant configuration.
235+
236+
If the list of variants is not exhaustive (e.g. for types where new variants can be added by
237+
Elasticsearch plugins), you can add the `@non_exhaustive` js doc tag to indicate that additional
238+
variants can exist and should be accepted.
239+
222240
There are three type of variants:
223241

224242
#### Internal

0 commit comments

Comments
 (0)