Skip to content

Commit 2ba9674

Browse files
[Transform] Filter availability (#2177)
* add filter script for availability tag * add script to package.json for usage through npm * add minimal usage comment * throw error if no filter is specified * rename filter function for clarity * linting * Update compiler/src/transform/filter-by-availability.ts Stricter null checks Co-authored-by: Josh Mock <[email protected]> * improve readability for persistence of known types * add missing reassignments * swap for-loop into lookup on precomputed map for request & response resolution * add target based on provided arguments in default output name * add visibility filter * make visibility a comma separated list to handle public,feature_flag * use stricter assertion for requests and responses * use fqn with a set instead of a map to track what has already been processed --------- Co-authored-by: Josh Mock <[email protected]>
1 parent 20cfefd commit 2ba9674

File tree

2 files changed

+296
-0
lines changed

2 files changed

+296
-0
lines changed

compiler/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"format:fix": "prettier --config .prettierrc.json --write ../specification/",
1111
"generate-schema": "ts-node src/index.ts",
1212
"transform-expand-generics": "ts-node src/transform/expand-generics.ts",
13+
"filter-by-availability": "ts-node src/transform/filter-by-availability.ts",
1314
"dump-routes": "ts-node src/dump/extract-routes.ts",
1415
"compile:specification": "tsc --project ../specification/tsconfig.json --noEmit",
1516
"build": "rm -rf lib && tsc",
Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
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+
import { Availabilities, Endpoint, Model, TypeDefinition, TypeName, ValueOf, Visibility } from '../model/metamodel'
21+
import { readFile, writeFile } from 'fs/promises'
22+
import stringify from 'safe-stable-stringify'
23+
import { argv } from 'zx'
24+
import { join } from 'path'
25+
26+
function filterModel (inputModel: Model, stack: boolean, serverless: boolean, visibility: string[]): Model {
27+
// filter over visibility only exclude if present and not matching, include by default.
28+
function includeVisibility (localVis: Visibility | undefined): boolean {
29+
if (localVis === undefined || visibility === undefined) {
30+
return true
31+
}
32+
return visibility.includes(localVis)
33+
}
34+
35+
// filter used against the provided availability
36+
// used to filter out endpoints and as a filter for items with availability (Enum & Property).
37+
function include (availabilities: Availabilities): boolean {
38+
if ((availabilities.stack !== undefined) && stack) {
39+
return includeVisibility(availabilities.stack.visibility)
40+
}
41+
if ((availabilities.serverless !== undefined) && serverless) {
42+
return includeVisibility(availabilities.serverless.visibility)
43+
}
44+
45+
return false
46+
}
47+
48+
// used to filter out individual items within types.
49+
function filterItem () {
50+
return (item) => {
51+
return (item.availability !== undefined) ? include(item.availability) : true
52+
}
53+
}
54+
55+
// short comparison for two TypeNames
56+
function cmpTypeNames (t1, t2: TypeName): boolean {
57+
return t1.name === t2.name && t1.namespace === t2.namespace
58+
}
59+
60+
// Returns the fully-qualified name of a type name
61+
function fqn (name: TypeName): string {
62+
return `${name.namespace}:${name.name}`
63+
}
64+
65+
// return early if the type already has been added
66+
// fetches the original type from the input model
67+
// save its presence to prevent recursion and doubles
68+
// continues down the type tree for any new types
69+
function addTypeToOutput (typeName: TypeName): void {
70+
if (seen.has(fqn(typeName))) {
71+
return
72+
}
73+
74+
inputModel.types.forEach((typeDef) => {
75+
if (cmpTypeNames(typeName, typeDef.name)) {
76+
// add the TypeDefinition to the output
77+
output.types.push(typeDef)
78+
79+
// store the type infos to prevent doubles & recursive calls
80+
seen.add(fqn(typeName))
81+
82+
// first time seeing this type so we explore the type
83+
exploreTypedef(typeDef)
84+
}
85+
})
86+
}
87+
88+
// handles the basic type field we can find
89+
// user_defined_value and literal_value are omitted.
90+
function addValueOf (item: ValueOf): void {
91+
switch (item.kind) {
92+
case 'instance_of':
93+
addTypeToOutput(item.type)
94+
break
95+
case 'array_of':
96+
addValueOf(item.value)
97+
break
98+
case 'union_of':
99+
item.items.forEach((member) => {
100+
addValueOf(member)
101+
})
102+
break
103+
case 'dictionary_of':
104+
addValueOf(item.key)
105+
addValueOf(item.value)
106+
break
107+
}
108+
}
109+
110+
function exploreTypedef (typeDef: TypeDefinition): void {
111+
// handle generics
112+
// not really useful for everyone, still useful for type_alias
113+
if (typeDef.kind === 'interface' || typeDef.kind === 'request' || typeDef.kind === 'response' || typeDef.kind === 'type_alias') {
114+
typeDef.generics?.forEach((generic) => {
115+
addTypeToOutput(generic)
116+
})
117+
}
118+
119+
// handle behaviors
120+
if (typeDef.kind === 'interface' || typeDef.kind === 'request' || typeDef.kind === 'response') {
121+
typeDef.behaviors?.forEach((behavior) => {
122+
addTypeToOutput(behavior.type)
123+
behavior.generics?.forEach((generic) => {
124+
addValueOf(generic)
125+
})
126+
})
127+
}
128+
129+
// handle inherits & implements
130+
if (typeDef.kind === 'interface' || typeDef.kind === 'request') {
131+
if (typeDef.inherits !== undefined) {
132+
addTypeToOutput(typeDef.inherits.type)
133+
}
134+
typeDef.implements?.forEach((implemented) => {
135+
addTypeToOutput(implemented.type)
136+
})
137+
}
138+
139+
// handle body value and body properties for request and response
140+
if (typeDef.kind === 'request' || typeDef.kind === 'response') {
141+
switch (typeDef.body.kind) {
142+
case 'value':
143+
addValueOf(typeDef.body.value)
144+
break
145+
case 'properties':
146+
typeDef.body.properties.forEach((property) => {
147+
addValueOf(property.type)
148+
})
149+
break
150+
}
151+
}
152+
153+
// left over specific cases
154+
switch (typeDef.kind) {
155+
case 'interface':
156+
typeDef.properties.forEach((property) => {
157+
addValueOf(property.type)
158+
})
159+
break
160+
161+
case 'request':
162+
typeDef.path.forEach((path) => {
163+
addValueOf(path.type)
164+
})
165+
typeDef.query.forEach((query) => {
166+
addValueOf(query.type)
167+
})
168+
break
169+
170+
case 'type_alias':
171+
addValueOf(typeDef.type)
172+
break
173+
}
174+
}
175+
176+
const seen = new Set<string>()
177+
178+
const output: Model = {
179+
_info: inputModel._info,
180+
types: new Array<TypeDefinition>(),
181+
endpoints: new Array<Endpoint>()
182+
}
183+
184+
const typeDefByName = new Map<string, TypeDefinition>()
185+
186+
for (const type of inputModel.types) {
187+
typeDefByName.set(fqn(type.name), type)
188+
}
189+
190+
// we filter to include only the matching endpoints
191+
inputModel.endpoints.forEach((endpoint) => {
192+
if (include(endpoint.availability)) {
193+
// add the current endpoint
194+
output.endpoints.push(endpoint)
195+
196+
if (endpoint.request !== null) {
197+
const requestType = typeDefByName.get(fqn(endpoint.request))
198+
if (requestType !== undefined) output.types.push(requestType)
199+
}
200+
201+
if (endpoint.response !== null) {
202+
const responseType = typeDefByName.get(fqn(endpoint.response))
203+
if (responseType !== undefined) output.types.push(responseType)
204+
}
205+
}
206+
})
207+
208+
// filter type items (properties / enum members)
209+
inputModel.types.forEach((typeDef) => {
210+
switch (typeDef.kind) {
211+
case 'interface':
212+
typeDef.properties = typeDef.properties.filter(filterItem())
213+
break
214+
case 'enum':
215+
typeDef.members = typeDef.members.filter(filterItem())
216+
addTypeToOutput(typeDef.name)
217+
break
218+
case 'request':
219+
output.endpoints.forEach((endpoint) => {
220+
if (endpoint.request?.name === typeDef.name.name && endpoint.request.namespace === typeDef.name.namespace) {
221+
typeDef.path = typeDef.path.filter(filterItem())
222+
typeDef.query = typeDef.query.filter(filterItem())
223+
// filter out body properties
224+
switch (typeDef.body.kind) {
225+
case 'properties':
226+
typeDef.body.properties = typeDef.body.properties.filter(filterItem())
227+
break
228+
}
229+
}
230+
})
231+
break
232+
case 'response':
233+
output.endpoints.forEach((endpoint) => {
234+
if (endpoint.response?.name === typeDef.name.name && endpoint.response.namespace === typeDef.name.namespace) {
235+
// filter out body properties
236+
switch (typeDef.body.kind) {
237+
case 'properties':
238+
typeDef.body.properties = typeDef.body.properties.filter(filterItem())
239+
break
240+
}
241+
}
242+
})
243+
break
244+
case 'type_alias':
245+
addTypeToOutput(typeDef.name)
246+
}
247+
})
248+
249+
// we complete the spec with the missing types for the tree until exhaustion
250+
output.types.forEach((typeDef) => {
251+
exploreTypedef(typeDef)
252+
})
253+
254+
return output
255+
}
256+
257+
async function filterSchema (inPath: string, outPath: string, stack: boolean, serverless: boolean, visibility: string[]): Promise<void> {
258+
if ((!stack && !serverless) || (serverless && stack)) {
259+
throw new Error('Expected one of --stack or --serverless to be specified.')
260+
}
261+
262+
const inputText = await readFile(
263+
inPath,
264+
{ encoding: 'utf8' }
265+
)
266+
267+
const inputModel = JSON.parse(inputText)
268+
const outputModel = filterModel(inputModel, stack, serverless, visibility)
269+
270+
await writeFile(
271+
outPath,
272+
stringify(outputModel, null, 2),
273+
'utf8'
274+
)
275+
}
276+
277+
const stack: boolean = (argv.stack !== undefined)
278+
const serverless: boolean = (argv.serverless !== undefined)
279+
const target: string = (serverless) ? 'serverless' : 'stack'
280+
const visibility: string[] = argv.visibility.split(',')
281+
282+
const inputPath: string = argv.input ?? join(__dirname, '..', '..', '..', 'output', 'schema', 'schema.json')
283+
const outputPath = argv.output ?? join(__dirname, '..', '..', '..', 'output', 'schema', `schema-filtered-${target}.json`)
284+
285+
// Usage:
286+
// npm run filter-by-availability -- [--serverless|--stack]
287+
// Optional args:
288+
// visibility, comma separated list in [public|private|feature_flag]
289+
// input, if not provided default to versioned schema.json
290+
// output, if not provided default to schema-filtered.json
291+
filterSchema(inputPath, outputPath, stack, serverless, visibility)
292+
.catch(reason => {
293+
console.error((reason.message !== null) ? reason.message : reason)
294+
})
295+
.finally(() => console.log('Done, filtered schema is at', outputPath))

0 commit comments

Comments
 (0)