Skip to content

Commit 12ca910

Browse files
authored
Add support for the @availability annotations
1 parent a531f54 commit 12ca910

File tree

452 files changed

+4021
-1312
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

452 files changed

+4021
-1312
lines changed

.github/validate-pr/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ async function run () {
6060
}
6161
}
6262

63-
const specFiles = files.filter(file => file.includes('specification'))
63+
const specFiles = files.filter(file => file.includes('specification') && !file.includes('compiler/test'))
6464
const table = []
6565

6666
cd(tsValidationPath)

compiler/src/model/build-model.ts

+9
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,15 @@ export function compileEndpoints (): Record<string, model.Endpoint> {
6868
name: api,
6969
description: spec.documentation.description,
7070
docUrl: spec.documentation.url,
71+
// Setting these values by default should be removed
72+
// when we no longer use rest-api-spec stubs as the
73+
// source of truth for stability/visibility.
74+
availability: {
75+
stack: {
76+
stability: spec.stability,
77+
visibility: spec.visibility
78+
}
79+
},
7180
stability: spec.stability,
7281
visibility: spec.visibility,
7382
request: null,

compiler/src/model/metamodel.ts

+14-1
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,7 @@ export enum Stability {
369369
}
370370
export enum Visibility {
371371
public = 'public',
372-
featureFlag = 'feature_flag',
372+
feature_flag = 'feature_flag',
373373
private = 'private'
374374
}
375375

@@ -378,12 +378,25 @@ export class Deprecation {
378378
description: string
379379
}
380380

381+
export class Availabilities {
382+
stack?: Availability
383+
serverless?: Availability
384+
}
385+
386+
export class Availability {
387+
since?: string
388+
featureFlag?: string
389+
stability?: Stability
390+
visibility?: Visibility
391+
}
392+
381393
export class Endpoint {
382394
name: string
383395
description: string
384396
docUrl: string
385397
docId?: string
386398
deprecation?: Deprecation
399+
availability: Availabilities
387400

388401
/**
389402
* If the request value is `null` it means that there is not yet a

compiler/src/model/utils.ts

+102-15
Original file line numberDiff line numberDiff line change
@@ -575,7 +575,7 @@ export function hoistRequestAnnotations (
575575
request: model.Request, jsDocs: JSDoc[], mappings: Record<string, model.Endpoint>, response: model.TypeName | null
576576
): void {
577577
const knownRequestAnnotations = [
578-
'since', 'rest_spec_name', 'stability', 'visibility', 'behavior', 'class_serializer', 'index_privileges', 'cluster_privileges', 'doc_id'
578+
'rest_spec_name', 'behavior', 'class_serializer', 'index_privileges', 'cluster_privileges', 'doc_id', 'availability'
579579
]
580580
// in most of the cases the jsDocs comes in a single block,
581581
// but it can happen that the user defines multiple single line jsDoc.
@@ -602,19 +602,6 @@ export function hoistRequestAnnotations (
602602
setTags(jsDocs, request, tags, knownRequestAnnotations, (tags, tag, value) => {
603603
if (tag.endsWith('_serializer')) {
604604
} else if (tag === 'rest_spec_name') {
605-
} else if (tag === 'visibility') {
606-
if (endpoint.visibility !== null && endpoint.visibility !== undefined) {
607-
assert(jsDocs, endpoint.visibility === value,
608-
`Request ${request.name.name} visibility on annotation ${value} does not match spec: ${endpoint.visibility ?? ''}`)
609-
}
610-
endpoint.visibility = model.Visibility[value]
611-
} else if (tag === 'stability') {
612-
assert(jsDocs, endpoint.stability === value,
613-
`Request ${request.name.name} stability on annotation ${value} does not match spec: ${endpoint.stability ?? ''}`)
614-
endpoint.stability = model.Stability[value]
615-
} else if (tag === 'since') {
616-
assert(jsDocs, semver.valid(value), `Request ${request.name.name}'s @since is not valid semver: ${value}`)
617-
endpoint.since = value
618605
} else if (tag === 'index_privileges') {
619606
const privileges = [
620607
'all', 'auto_configure', 'create', 'create_doc', 'create_index', 'delete', 'delete_index', 'index',
@@ -648,6 +635,33 @@ export function hoistRequestAnnotations (
648635
const docUrl = docIds.find(entry => entry[0] === value.trim())
649636
assert(jsDocs, docUrl != null, `The @doc_id '${value.trim()}' is not present in _doc_ids/table.csv`)
650637
endpoint.docUrl = docUrl[1]
638+
} else if (tag === 'availability') {
639+
// The @availability jsTag is different than most because it allows
640+
// multiple values within the same docstring, hence needing to parse
641+
// the values again in order to preserve multiple values.
642+
const jsDocsMulti = parseJsDocTagsAllowDuplicates(jsDocs)
643+
const availabilities = parseAvailabilityTags(jsDocs, jsDocsMulti.availability)
644+
645+
// Apply the availabilities to the Endpoint.
646+
for (const [availabilityName, availabilityValue] of Object.entries(availabilities)) {
647+
endpoint.availability[availabilityName] = availabilityValue
648+
649+
// Backfilling deprecated fields on an endpoint.
650+
if (availabilityName === 'stack') {
651+
if (availabilityValue.since !== undefined) {
652+
endpoint.since = availabilityValue.since
653+
}
654+
if (availabilityValue.stability !== undefined) {
655+
endpoint.stability = availabilityValue.stability
656+
}
657+
if (availabilityValue.visibility !== undefined) {
658+
endpoint.visibility = availabilityValue.visibility
659+
}
660+
if (availabilityValue.featureFlag !== undefined) {
661+
endpoint.featureFlag = availabilityValue.featureFlag
662+
}
663+
}
664+
}
651665
} else {
652666
assert(jsDocs, false, `Unhandled tag: '${tag}' with value: '${value}' on request ${request.name.name}`)
653667
}
@@ -899,7 +913,7 @@ export function getNameSpace (node: Node): string {
899913

900914
function cleanPath (path: string): string {
901915
path = dirname(path)
902-
.replace(/.*[/\\]specification[/\\]?/, '')
916+
.replace(/.*[/\\]specification[^/\\]*[/\\]?/, '')
903917
.replace(/[/\\]/g, '.')
904918
if (path === '') path = '_builtins'
905919
return path
@@ -966,6 +980,25 @@ export function parseJsDocTags (jsDoc: JSDoc[]): Record<string, string> {
966980
return mapped
967981
}
968982

983+
/**
984+
* Given a JSDoc definition, return a mapping from a tag name to all its values.
985+
* This function is similar to the above parseJsDocTags() function except
986+
* it allows for multiple annotations with the same name.
987+
*/
988+
export function parseJsDocTagsAllowDuplicates (jsDoc: JSDoc[]): Record<string, string[]> {
989+
const mapped = {}
990+
jsDoc.forEach((elem: JSDoc) => {
991+
elem.getTags().forEach((tag) => {
992+
const tagName = tag.getTagName()
993+
if (mapped[tagName] === undefined) {
994+
mapped[tagName] = []
995+
}
996+
mapped[tagName].push(tag.getComment() ?? '')
997+
})
998+
})
999+
return mapped
1000+
}
1001+
9691002
/**
9701003
* Given a JSDoc definition, it returns the Variants is present.
9711004
* It also validates the variants syntax.
@@ -1023,6 +1056,60 @@ export function parseVariantNameTag (jsDoc: JSDoc[]): string | undefined {
10231056
return name.replace(/'/g, '')
10241057
}
10251058

1059+
/**
1060+
* Parses the '@availability' JS tags into known values.
1061+
*/
1062+
export function parseAvailabilityTags (node: Node | Node[], values: string[]): model.Availability {
1063+
return values.reduce((result, value, index, array) => {
1064+
// Ensure that there is actually a name for this availability definition.
1065+
assert(node, value.split(' ').length >= 1, 'The @availability tag must include a name (either stack or serverless)')
1066+
const [availabilityName, ...values] = value.split(' ')
1067+
1068+
// Since we're using reduce() we need to check that there's no duplicates.
1069+
assert(node, !(availabilityName in result), `Duplicate @availability tag: '${availabilityName}'`)
1070+
1071+
// Enforce only known availability names.
1072+
assert(node, availabilityName === 'stack' || availabilityName === 'serverless', 'The @availablility <name> value must either be stack or serverless')
1073+
1074+
// Now we can parse all the key-values and load them into variables
1075+
// for easier access below.
1076+
const validKeys = ['stability', 'visibility', 'since', 'feature_flag']
1077+
const parsedKeyValues = parseKeyValues(node, values, ...validKeys)
1078+
const visibility = parsedKeyValues.visibility
1079+
const stability = parsedKeyValues.stability
1080+
const since = parsedKeyValues.since
1081+
const featureFlag = parsedKeyValues.feature_flag
1082+
1083+
// Remove the 'feature_flag' name used in the annotations
1084+
// in favor of 'featureFlag' as used in the metamodel.
1085+
delete parsedKeyValues.feature_flag
1086+
1087+
// Lastly we go through all the fields and validate them.
1088+
if (visibility !== undefined) {
1089+
parsedKeyValues.visibility = model.Visibility[visibility]
1090+
assert(node, parsedKeyValues.visibility !== undefined, `visibility is not valid: ${visibility}`)
1091+
if (visibility === model.Visibility.feature_flag) {
1092+
assert(node, featureFlag !== undefined, '\'feature_flag\' must be defined if visibility is \'feature_flag\'')
1093+
}
1094+
}
1095+
if (stability !== undefined) {
1096+
parsedKeyValues.stability = model.Stability[stability]
1097+
assert(node, parsedKeyValues.stability !== undefined, `stability is not valid: ${stability}`)
1098+
}
1099+
if (since !== undefined) {
1100+
assert(node, semver.valid(since), `'since' is not valid semver: ${since}`)
1101+
}
1102+
if (featureFlag !== undefined) {
1103+
assert(node, visibility === 'feature_flag', '\'visibility\' must be \'feature_flag\' if a feature flag is defined')
1104+
parsedKeyValues.featureFlag = featureFlag
1105+
}
1106+
1107+
// Add the computed set of fields to the result.
1108+
result[availabilityName] = parsedKeyValues
1109+
return result
1110+
}, {})
1111+
}
1112+
10261113
/**
10271114
* Parses a list of comma-separated values as an array. Values can optionally be enclosed with single
10281115
* or double quotes.

compiler/test/body-codegen-name/specification/_global/index/request.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@
1919

2020
/**
2121
* @rest_spec_name index
22-
* @since 0.0.0
23-
* @stability stable
22+
* @availability stack since=0.0.0 stability=stable
2423
*/
2524
export interface Request {
2625
body: Foo

compiler/test/duplicate-body-codegen-name/specification/_global/index/request.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@
1919

2020
/**
2121
* @rest_spec_name index
22-
* @since 0.0.0
23-
* @stability stable
22+
* @availability stack since=0.0.0 stability=stable
2423
*/
2524
export interface Request {
2625
path_parts: {

compiler/test/no-body/specification/_global/info/request.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@
1919

2020
/**
2121
* @rest_spec_name info
22-
* @since 0.0.0
23-
* @stability stable
22+
* @availability stack since=0.0.0 stability=stable
2423
*/
2524
export interface Request {
2625
body: {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
/**
21+
* @rest_spec_name index
22+
* @availability stack stability=stable
23+
* @availability stack stability=stable
24+
*/
25+
export interface Request<TDocument> {
26+
path_parts: {
27+
id?: string
28+
index: string
29+
}
30+
query_parameters: {
31+
if_primary_term?: number
32+
if_seq_no?: number
33+
op_type?: string
34+
pipeline?: string
35+
refresh?: string
36+
routing?: string
37+
timeout?: string
38+
version?: number
39+
version_type?: string
40+
wait_for_active_shards?: string
41+
require_alias?: boolean
42+
}
43+
/** @codegen_name document */
44+
body?: TDocument
45+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"extends": "../../../../specification/tsconfig.json",
3+
"typeRoots": ["./**/*.ts"],
4+
"include": ["./**/*.ts"]
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
/**
21+
* @rest_spec_name index
22+
* @availability stack stability=stable visibility=feature_flag
23+
*/
24+
export interface Request<TDocument> {
25+
path_parts: {
26+
id?: string
27+
index: string
28+
}
29+
query_parameters: {
30+
if_primary_term?: number
31+
if_seq_no?: number
32+
op_type?: string
33+
pipeline?: string
34+
refresh?: string
35+
routing?: string
36+
timeout?: string
37+
version?: number
38+
version_type?: string
39+
wait_for_active_shards?: string
40+
require_alias?: boolean
41+
}
42+
/** @codegen_name document */
43+
body?: TDocument
44+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"extends": "../../../../specification/tsconfig.json",
3+
"typeRoots": ["./**/*.ts"],
4+
"include": ["./**/*.ts"]
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
/**
21+
* @rest_spec_name index
22+
* @availability stack stability=unknown
23+
*/
24+
export interface Request<TDocument> {
25+
path_parts: {
26+
id?: string
27+
index: string
28+
}
29+
query_parameters: {
30+
if_primary_term?: number
31+
if_seq_no?: number
32+
op_type?: string
33+
pipeline?: string
34+
refresh?: string
35+
routing?: string
36+
timeout?: string
37+
version?: number
38+
version_type?: string
39+
wait_for_active_shards?: string
40+
require_alias?: boolean
41+
}
42+
/** @codegen_name document */
43+
body?: TDocument
44+
}

0 commit comments

Comments
 (0)