Skip to content

Commit

Permalink
Explicitly name the "untyped" variant and prevent declaration of new …
Browse files Browse the repository at this point in the history
…untagged variants
  • Loading branch information
flobernd committed Jun 28, 2024
1 parent 9ae4eb0 commit 734db8d
Show file tree
Hide file tree
Showing 9 changed files with 563 additions and 521 deletions.
1 change: 1 addition & 0 deletions compiler/src/model/metamodel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ export class Container extends VariantBase {

export class Untagged extends VariantBase {
kind: 'untagged'
untyped_variant: Inherits
}

/**
Expand Down
34 changes: 22 additions & 12 deletions compiler/src/model/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1110,24 +1110,34 @@ export function parseVariantsTag (jsDoc: JSDoc[]): model.Variants | undefined {
}
}

if (type === 'internal') {
const pairs = parseKeyValues(jsDoc, values, 'tag', 'default')
assert(jsDoc, typeof pairs.tag === 'string', 'Internal variant requires a tag definition')
return {
kind: 'internal_tag',
nonExhaustive: nonExhaustive,
tag: pairs.tag,
defaultTag: pairs.default
}
}

if (type === 'untagged') {
const pairs = parseKeyValues(jsDoc, values, 'untyped')
assert(jsDoc, typeof pairs.untyped === 'string', 'Untagged variant requires an untyped definition')
const fqn = pairs.untyped.split('.')
return {
kind: 'untagged',
nonExhaustive: nonExhaustive
nonExhaustive: nonExhaustive,
untyped_variant: {
type: {
namespace: fqn.slice(0, fqn.length - 1).join('.'),
name: fqn[fqn.length - 1]
}
}
}
}

assert(jsDoc, type === 'internal', `Bad variant type: ${type}`)

const pairs = parseKeyValues(jsDoc, values, 'tag', 'default')
assert(jsDoc, typeof pairs.tag === 'string', 'Internal variant requires a tag definition')

return {
kind: 'internal_tag',
nonExhaustive: nonExhaustive,
tag: pairs.tag,
defaultTag: pairs.default
}
assert(jsDoc, false, `Bad variant type: ${type}`)
}

/**
Expand Down
25 changes: 22 additions & 3 deletions compiler/src/steps/validate-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -559,14 +559,14 @@ export default async function validateModel (apiModel: model.Model, restSpec: Ma
if (typeDef.type.kind !== 'union_of') {
modelError('The "variants" tag only applies to unions')
} else {
validateTaggedUnion(typeDef.type, typeDef.variants)
validateTaggedUnion(typeDef.name, typeDef.type, typeDef.variants)
}
} else {
validateValueOf(typeDef.type, openGenerics)
}
}

function validateTaggedUnion (valueOf: model.UnionOf, variants: model.InternalTag | model.ExternalTag | model.Untagged): void {
function validateTaggedUnion (parentName: TypeName, valueOf: model.UnionOf, variants: model.InternalTag | model.ExternalTag | model.Untagged): void {
if (variants.kind === 'external_tag') {
// All items must have a 'variant' attribute
const items = flattenUnionMembers(valueOf)
Expand Down Expand Up @@ -611,9 +611,20 @@ export default async function validateModel (apiModel: model.Model, restSpec: Ma

validateValueOf(valueOf, new Set())
} else if (variants.kind === 'untagged') {
const items = flattenUnionMembers(valueOf)
if (fqn(parentName) !== '_types.query_dsl:DecayFunction' &&
fqn(parentName) !== '_types.query_dsl:DistanceFeatureQuery' &&
fqn(parentName) !== '_types.query_dsl:RangeQuery') {
throw new Error(`Please contact the devtools team before adding new untagged variant ${fqn(parentName)}`)
}

const untypedVariant = getTypeDef(variants.untyped_variant.type)
if (untypedVariant == null) {
modelError(`Type ${fqn(variants.untyped_variant.type)} not found`)
}

const items = flattenUnionMembers(valueOf)
const baseTypes = new Set<string>()
let foundUntyped = false

for (const item of items) {
if (item.kind !== 'instance_of') {
Expand All @@ -629,6 +640,10 @@ export default async function validateModel (apiModel: model.Model, restSpec: Ma
continue
}

if (untypedVariant != null && fqn(item.type) === fqn(untypedVariant.name)) {
foundUntyped = true
}

if (type.inherits == null) {
modelError(`Type ${fqn(item.type)} must derive from a base type to be used in an untagged union`)
continue
Expand Down Expand Up @@ -657,6 +672,10 @@ export default async function validateModel (apiModel: model.Model, restSpec: Ma
if (baseTypes.size !== 1) {
modelError('All items of an untagged union must derive from the same common base type')
}

if (!foundUntyped) {
modelError('The untyped variant of an untagged variant must be contained in the union items')
}
}
}

Expand Down
7 changes: 4 additions & 3 deletions docs/modeling-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -455,16 +455,17 @@ For example:
```ts
export class MyTypeBase<T1, T2, ...> { ... }

export class MyTypeUntyped extends MyTypeBase<UserDefinedValue> {}
export class MyTypeSpecialized1 extends MyTypeBase<int> {}
export class MyTypeSpecialized2 extends MyTypeBase<string> {}
export class MyTypeSpecialized3 extends MyTypeBase<bool> {}

/**
* @codegen_names mytype1, mytypet2, mytype3
* @variant untagged
* @codegen_names untyped, mytype1, mytypet2, mytype3
* @variant untagged untyped=_types.MyTypeUntyped
*/
// Note: deserialization depends on value types
export type MyType = MyTypeSpecialized1 | MyTypeSpecialized2 | MyTypeSpecialized3
export type MyType = MyTypeUntyped | MyTypeSpecialized1 | MyTypeSpecialized2 | MyTypeSpecialized3
```
### Shortcut properties
Expand Down
Loading

0 comments on commit 734db8d

Please sign in to comment.