Skip to content

Add support for non exhaustive variants #1662

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions compiler/src/model/metamodel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,19 +177,28 @@ export abstract class BaseType {

export type Variants = ExternalTag | InternalTag | Container

export class ExternalTag {
export class VariantBase {
/**
* Is this variant type open to extensions? Default to false. Used for variants that can
* be extended with plugins. If true, target clients should allow for additional variants
* with a variant tag outside the ones defined in the spec and arbitrary data as the value.
*/
nonExhaustive?: boolean
}

export class ExternalTag extends VariantBase {
kind: 'external_tag'
}

export class InternalTag {
export class InternalTag extends VariantBase {
kind: 'internal_tag'
/* Name of the property that holds the variant tag */
tag: string
/* Default value for the variant tag if it's missing */
defaultTag?: string
}

export class Container {
export class Container extends VariantBase {
kind: 'container'
}

Expand Down
26 changes: 20 additions & 6 deletions compiler/src/model/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,7 @@ export function modelEnumDeclaration (declaration: EnumDeclaration): model.Enum
}

const tags = parseJsDocTags(declaration.getJsDocs())
if (typeof tags.open_enum === 'string') {
if (typeof tags.non_exhaustive === 'string') {
type.isOpen = true
}

Expand Down Expand Up @@ -647,7 +647,7 @@ export function hoistTypeAnnotations (type: model.TypeDefinition, jsDocs: JSDoc[
// We want to enforce a single jsDoc block.
assert(jsDocs, jsDocs.length < 2, 'Use a single multiline jsDoc block instead of multiple single line blocks')

const validTags = ['class_serializer', 'doc_url', 'doc_id', 'behavior', 'variants', 'variant', 'shortcut_property', 'codegen_names']
const validTags = ['class_serializer', 'doc_url', 'doc_id', 'behavior', 'variants', 'variant', 'shortcut_property', 'codegen_names', 'non_exhaustive']
const tags = parseJsDocTags(jsDocs)
if (jsDocs.length === 1) {
const description = jsDocs[0].getDescription()
Expand All @@ -665,6 +665,8 @@ export function hoistTypeAnnotations (type: model.TypeDefinition, jsDocs: JSDoc[
}
} else if (tag === 'variants') {
} else if (tag === 'variant') {
} else if (tag === 'non_exhaustive') {
assert(jsDocs, typeof tags.variants === 'string', '@non_exhaustive only applies to enums and @variants')
} else if (tag === 'doc_url') {
assert(jsDocs, isValidUrl(value), '@doc_url is not a valid url')
type.docUrl = value
Expand Down Expand Up @@ -954,13 +956,21 @@ export function parseVariantsTag (jsDoc: JSDoc[]): model.Variants | undefined {
return undefined
}

const nonExhaustive = (typeof tags.non_exhaustive === 'string') ? true : undefined

const [type, ...values] = tags.variants.split(' ')
if (type === 'external') {
return { kind: 'external_tag' }
return {
kind: 'external_tag',
nonExhaustive: nonExhaustive
}
}

if (type === 'container') {
return { kind: 'container' }
return {
kind: 'container',
nonExhaustive: nonExhaustive
}
}

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

return {
kind: 'internal_tag',
nonExhaustive: nonExhaustive,
tag: pairs.tag,
defaultTag: pairs.default
}
Expand Down Expand Up @@ -1002,13 +1013,16 @@ export function parseCommaSeparated (value: string): string[] {

/**
* Parses an array of "key=value" pairs and validate key names. Values can optionally be enclosed with single
* or double quotes.
* or double quotes. If there is only a key with no value (no '=') the value is set to 'true'
*/
export function parseKeyValues (node: Node | Node[], pairs: string[], ...validKeys: string[]): Record<string, string> {
const result = {}
pairs.forEach(item => {
const kv = item.split('=')
assert(node, kv.length === 2, 'Malformed key/value list')
assert(node, kv.length <= 2, 'Malformed key/value list')
if (kv.length === 1) {
kv.push('true')
}
assert(node, validKeys.includes(kv[0]), `Unknown key '${kv[0]}'`)
result[kv[0]] = kv[1].replace(/["']/g, '')
})
Expand Down
11 changes: 8 additions & 3 deletions docs/modeling-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,11 @@ enum Orientation {
}
```

Some enumerations can accept arbitrary values other than the one defined. The `@open_enum` jsdoc tac can be used to describe this behavior.
By default, an enum is to be considered closed.
Some enumerations can accept arbitrary values other than the ones defined. The `@non_exhaustive` jsdoc tag can be used to describe this behavior.
By default, an enum is to be considered exhaustive.

```ts
/** @open_enum */
/** @non_exhaustive */
export enum ScriptLanguage {
painless,
expression,
Expand Down Expand Up @@ -234,6 +234,11 @@ class Response {

Variants is a special syntax that can be used by language generators to understand
which type they will need to build based on the variant configuration.

If the list of variants is not exhaustive (e.g. for types where new variants can be added by
Elasticsearch plugins), you can add the `@non_exhaustive` js doc tag to indicate that additional
variants can exist and should be accepted.

There are three type of variants:

#### Internal
Expand Down
Loading