Skip to content

Maintain "behavior" meta-data in specification #2638

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
Jun 28, 2024
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
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 @@ -532,7 +533,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
12 changes: 9 additions & 3 deletions compiler/src/model/metamodel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,12 @@ export class Inherits {
generics?: ValueOf[]
}

export class Behavior {
type: TypeName
generics?: ValueOf[]
meta?: { [p: string]: string }
}

/**
* An interface type
*/
Expand All @@ -231,7 +237,7 @@ export class Interface extends BaseType {
/**
* Behaviors directly implemented by this interface
*/
behaviors?: Inherits[]
behaviors?: Behavior[]

/**
* Behaviors attached to this interface, coming from the interface itself (see `behaviors`)
Expand Down Expand Up @@ -275,7 +281,7 @@ export class Request extends BaseType {
* that don't have a body.
*/
body: Body
behaviors?: Inherits[]
behaviors?: Behavior[]
attachedBehaviors?: string[]
}

Expand All @@ -286,7 +292,7 @@ export class Response extends BaseType {
kind: 'response'
generics?: TypeName[]
body: Body
behaviors?: Inherits[]
behaviors?: Behavior[]
attachedBehaviors?: string[]
exceptions?: ResponseException[]
}
Expand Down
36 changes: 31 additions & 5 deletions compiler/src/model/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -414,14 +414,40 @@ 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.Behavior {
const behaviorName = node.getExpression().getText()
const generics = node.getTypeArguments().map(node => modelType(node))

let meta: Map<string, string> | undefined
const tags = parseJsDocTagsAllowDuplicates(jsDocs)
if (tags.behavior_meta !== undefined) {
// Extracts whitespace/comma-separated key-value-pairs with a "=" delimiter and handles double-quotes
const re = /(?<key>[^=\s,]+)=(?<value>"([^"]*)"|([^\s,]+))/g

for (const tag of tags.behavior_meta) {
const id = tag.split(' ')
if (id[0].trim() !== behaviorName) {
continue
}
const matches = [...id.slice(1).join(' ').matchAll(re)]
meta = new Map<string, string>()
for (const match of matches) {
if (match.groups == null) {
continue
}
meta.set(match.groups.key, match.groups.value.replace(/^"(.+(?="$))"$/, '$1'))
}
break
}
}

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

Expand Down Expand Up @@ -579,7 +605,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 @@ -700,7 +726,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 @@ -643,6 +643,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 == null || parent.meta.key == null || parent.meta.value == null)) {
modelError(`AdditionalProperty behavior for type '${fqn(typeDef.name)}' requires a 'behavior_meta' decorator with at least 2 arguments (key, value)`)
}
if (parent.type.name === 'AdditionalProperties' && (parent.meta == null || parent.meta.fieldname == null || parent.meta.description == null)) {
modelError(`AdditionalProperties behavior for type '${fqn(typeDef.name)}' requires a 'behavior_meta' decorator with exactly 2 arguments (fieldname, 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 fieldname=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 key=field value=bounding_box
*/
class GeoBoundingBoxQuery extends QueryBase
implements AdditionalProperty<Field, BoundingBox>
```
Expand Down
Loading
Loading