Skip to content

Commit

Permalink
Maintain "behavior" meta-data in specification
Browse files Browse the repository at this point in the history
  • Loading branch information
flobernd committed Jun 21, 2024
1 parent ce9d54c commit b875339
Show file tree
Hide file tree
Showing 23 changed files with 453 additions and 257 deletions.
5 changes: 3 additions & 2 deletions compiler/src/model/build-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,8 @@ function compileClassOrInterfaceDeclaration (declaration: ClassDeclaration | Int
properties: new Array<model.Property>()
}

hoistTypeAnnotations(type, declaration.getJsDocs())
const jsDocs = declaration.getJsDocs()
hoistTypeAnnotations(type, jsDocs)

const variant = parseVariantNameTag(declaration.getJsDocs())
if (typeof variant === 'string') {
Expand Down Expand Up @@ -522,7 +523,7 @@ function compileClassOrInterfaceDeclaration (declaration: ClassDeclaration | Int
if (Node.isClassDeclaration(declaration)) {
for (const implement of declaration.getImplements()) {
if (isKnownBehavior(implement)) {
type.behaviors = (type.behaviors ?? []).concat(modelBehaviors(implement))
type.behaviors = (type.behaviors ?? []).concat(modelBehaviors(implement, jsDocs))
} else {
type.implements = (type.implements ?? []).concat(modelImplements(implement))
}
Expand Down
1 change: 1 addition & 0 deletions compiler/src/model/metamodel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ export class Container extends VariantBase {
export class Inherits {
type: TypeName
generics?: ValueOf[]
meta?: string[]
}

/**
Expand Down
28 changes: 23 additions & 5 deletions compiler/src/model/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -414,14 +414,32 @@ export function modelImplements (node: ExpressionWithTypeArguments): model.Inher
* A class could have multiple behaviors from multiple classes,
* which are defined inside the node typeArguments.
*/
export function modelBehaviors (node: ExpressionWithTypeArguments): model.Inherits {
export function modelBehaviors (node: ExpressionWithTypeArguments, jsDocs: JSDoc[]): model.Inherits {
const behaviorName = node.getExpression().getText()
const generics = node.getTypeArguments().map(node => modelType(node))

let meta: string[] | undefined
const tags = parseJsDocTagsAllowDuplicates(jsDocs)
if (tags.behavior_meta !== undefined) {
// Splits a string by comma, but preserves comma in quoted strings
const re = /(?<=")[^"]+?(?="(?:\s*?,|\s*?$))|(?<=(?:^|,)\s*?)(?:[^,"\s][^,"]*[^,"\s])|(?:[^,"\s])(?![^"]*?"(?:\s*?,|\s*?$))(?=\s*?(?:,|$))/g
for (const tag of tags.behavior_meta) {
const id = tag.split(' ')
if (id[0].trim() !== behaviorName) {
continue
}
meta = id.slice(1).join(' ').match(re) as string[]
break
}
}

return {
type: {
name: node.getExpression().getText(),
name: behaviorName,
namespace: getNameSpace(node)
},
...(generics.length > 0 && { generics })
...(generics.length > 0 && { generics }),
meta
}
}

Expand Down Expand Up @@ -574,7 +592,7 @@ function setTags<TType extends model.BaseType | model.Property | model.EnumMembe
)

for (const tag of validTags) {
if (tag === 'behavior') continue
if (tag === 'behavior' || tag === 'behavior_meta') continue
if (tags[tag] !== undefined) {
setter(tags, tag, tags[tag])
}
Expand Down Expand Up @@ -695,7 +713,7 @@ export function hoistTypeAnnotations (type: model.TypeDefinition, jsDocs: JSDoc[
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', 'non_exhaustive', 'es_quirk']
'codegen_names', 'non_exhaustive', 'es_quirk', 'behavior_meta']
const tags = parseJsDocTags(jsDocs)
if (jsDocs.length === 1) {
const description = jsDocs[0].getDescription()
Expand Down
7 changes: 7 additions & 0 deletions compiler/src/steps/validate-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,13 @@ export default async function validateModel (apiModel: model.Model, restSpec: Ma
context.push('Behaviors')
for (const parent of typeDef.behaviors) {
validateTypeRef(parent.type, parent.generics, openGenerics)

if (parent.type.name === 'AdditionalProperty' && (!parent.meta || parent.meta.length < 2)) {
modelError(`AdditionalProperty behavior for type '${fqn(typeDef.name)}' requires a 'behavior_meta' decorator with at least 2 arguments (name of name, name of value, description)`)
}
if (parent.type.name === 'AdditionalProperties' && (!parent.meta || parent.meta.length != 2)) {
modelError(`AdditionalProperties behavior for type '${fqn(typeDef.name)}' requires a 'behavior_meta' decorator with exactly 2 arguments (name, description)`)
}
}
context.pop()
}
Expand Down
6 changes: 6 additions & 0 deletions docs/behaviors.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,18 @@ This puts it into a bin that needs a client specific solution.
We therefore document the requirement to behave like a dictionary for unknown properties with this interface.

```ts
/**
* @behavior_meta AdditionalProperties sub_aggregations
*/
class IpRangeBucket implements AdditionalProperties<AggregateName, Aggregate> {}
```

There are also many places where we expect only one runtime-defined property, such as in field-related queries. To capture that uniqueness constraint, we can use the `AdditionalProperty` (singular) behavior.

```ts
/**
* @behavior_meta AdditionalProperty field, bounding_box
*/
class GeoBoundingBoxQuery extends QueryBase
implements AdditionalProperty<Field, BoundingBox>
```
Expand Down
Loading

0 comments on commit b875339

Please sign in to comment.