Skip to content

Commit dc89a47

Browse files
heiskrCopilot
andauthored
Remove no-explicit-any from REST sync pipeline (#61687)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 01d4615 commit dc89a47

7 files changed

Lines changed: 218 additions & 118 deletions

File tree

eslint.config.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -249,11 +249,6 @@ export default [
249249
'src/rest/components/get-rest-code-samples.ts',
250250
'src/rest/pages/category.tsx',
251251
'src/rest/pages/subcategory.tsx',
252-
'src/rest/scripts/utils/create-rest-examples.ts',
253-
'src/rest/scripts/utils/get-operations.ts',
254-
'src/rest/scripts/utils/inject-models-schema.ts',
255-
'src/rest/scripts/utils/operation.ts',
256-
'src/rest/scripts/utils/sync.ts',
257252
'src/rest/scripts/utils/update-markdown.ts',
258253
'src/rest/tests/get-rest-code-samples-2.ts',
259254
'src/rest/tests/get-rest-code-samples.ts',

src/rest/scripts/utils/create-rest-examples.ts

Lines changed: 69 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,44 @@
1+
import type { OpenApiMediaType } from './openapi-types'
2+
13
// In the case that there are more than one example requests, and
24
// no content responses, a request with an example key that matches the
35
// status code of a response will be matched.
46
const DEFAULT_EXAMPLE_DESCRIPTION = 'Example'
57
const DEFAULT_EXAMPLE_KEY = 'default'
68
const DEFAULT_ACCEPT_HEADER = 'application/vnd.github.v3+json'
79

8-
// OpenAPI operation structure is dynamic and complex
9-
type Operation = any
10+
// These functions only read the request body, parameters, and responses of an
11+
// operation, so they accept a narrower shape than the full OpenApiOperation.
12+
// Content maps are typed as `unknown` values (cast to OpenApiMediaType at the
13+
// point of use) so the partial operation fixtures in tests remain assignable.
14+
interface CodeSampleParameter {
15+
in?: string
16+
name: string
17+
examples?: Record<string, { value?: unknown }>
18+
[key: string]: unknown
19+
}
20+
21+
interface CodeSampleResponse {
22+
description?: string
23+
content?: unknown
24+
[key: string]: unknown
25+
}
26+
27+
interface CodeSampleOperation {
28+
requestBody?: { content: Record<string, unknown>; [key: string]: unknown }
29+
parameters?: CodeSampleParameter[]
30+
responses?: Record<string, CodeSampleResponse>
31+
[key: string]: unknown
32+
}
1033

1134
interface RequestExample {
1235
key: string
1336
request: {
1437
contentType?: string
1538
description: string
1639
acceptHeader: string
17-
bodyParameters?: any
18-
parameters?: Record<string, any>
40+
bodyParameters?: unknown
41+
parameters?: Record<string, unknown>
1942
}
2043
}
2144

@@ -25,33 +48,35 @@ interface ResponseExample {
2548
statusCode: string
2649
contentType?: string
2750
description: string
28-
example?: any
29-
schema?: any
51+
example?: unknown
52+
schema?: unknown
3053
}
3154
}
3255

33-
interface MergedExample {
56+
export interface MergedExample {
3457
request: {
3558
contentType?: string
3659
description: string
3760
acceptHeader: string
38-
bodyParameters?: any
39-
parameters?: Record<string, any>
61+
bodyParameters?: unknown
62+
parameters?: Record<string, unknown>
4063
}
4164
response?: {
4265
statusCode: string
4366
contentType?: string
4467
description: string
45-
example?: any
46-
schema?: any
68+
example?: unknown
69+
schema?: unknown
4770
}
4871
}
4972

5073
// Retrieves request and response examples and attempts to
5174
// merge them to create matching request/response examples
5275
// The key used in the media type `examples` property is
5376
// used to match requests to responses.
54-
export default async function getCodeSamples(operation: Operation): Promise<MergedExample[]> {
77+
export default async function getCodeSamples(
78+
operation: CodeSampleOperation,
79+
): Promise<MergedExample[]> {
5580
const responseExamples = getResponseExamples(operation)
5681
const requestExamples = getRequestExamples(operation)
5782

@@ -66,7 +91,7 @@ export default async function getCodeSamples(operation: Operation): Promise<Merg
6691
}
6792

6893
return mergedExamples.map((example, i) => {
69-
delete (example as any).key
94+
delete (example as { key?: string }).key
7095
return {
7196
...example,
7297
request: {
@@ -81,7 +106,7 @@ export default async function getCodeSamples(operation: Operation): Promise<Merg
81106
}
82107

83108
// Strip the key field — it's only needed during merging, not at runtime
84-
for (const example of mergedExamples) delete (example as any).key
109+
for (const example of mergedExamples) delete (example as { key?: string }).key
85110
return mergedExamples
86111
}
87112

@@ -171,7 +196,7 @@ export function mergeExamples(
171196
}
172197
}
173198
*/
174-
export function getRequestExamples(operation: Operation): RequestExample[] {
199+
export function getRequestExamples(operation: CodeSampleOperation): RequestExample[] {
175200
const requestExamples: RequestExample[] = []
176201
const parameterExamples = getParameterExamples(operation)
177202

@@ -207,20 +232,21 @@ export function getRequestExamples(operation: Operation): RequestExample[] {
207232
// Requests can have multiple content types each with their own set of
208233
// examples.
209234
for (const contentType of Object.keys(operation.requestBody.content)) {
210-
let examples: Record<string, any> = {}
235+
const mediaType = operation.requestBody.content[contentType] as OpenApiMediaType
236+
let examples: Record<string, { summary?: string; value?: unknown }> = {}
211237
// This is a fallback to allow using the `example` property in
212238
// the schema. If we start to enforce using examples vs. example using
213239
// a linter, we can remove the check for `example`.
214240
// For now, we'll use the key default, which is a common default
215241
// example name in the OpenAPI schema.
216-
if (operation.requestBody.content[contentType].example) {
242+
if (mediaType.example) {
217243
examples = {
218244
default: {
219-
value: operation.requestBody.content[contentType].example,
245+
value: mediaType.example,
220246
},
221247
}
222-
} else if (operation.requestBody.content[contentType].examples) {
223-
examples = operation.requestBody.content[contentType].examples
248+
} else if (mediaType.examples) {
249+
examples = mediaType.examples
224250
} else {
225251
// Example for this content type doesn't exist so we'll try and create one
226252
requestExamples.push({
@@ -268,12 +294,12 @@ export function getRequestExamples(operation: Operation): RequestExample[] {
268294
by the runtime rendering code, but they account for ~131 MB of the total
269295
schema.json size across all versions.
270296
*/
271-
function stripSchemaExamples(schema: any): any {
297+
function stripSchemaExamples(schema: unknown): unknown {
272298
if (!schema || typeof schema !== 'object') return schema
273299
if (Array.isArray(schema)) return schema.map(stripSchemaExamples)
274300

275-
const result: any = {}
276-
for (const [key, value] of Object.entries(schema)) {
301+
const result: Record<string, unknown> = {}
302+
for (const [key, value] of Object.entries(schema as Record<string, unknown>)) {
277303
if (key === 'example' || key === 'examples') continue
278304
result[key] = stripSchemaExamples(value)
279305
}
@@ -297,14 +323,16 @@ function stripSchemaExamples(schema: any): any {
297323
}
298324
}
299325
*/
300-
export function getResponseExamples(operation: Operation): ResponseExample[] {
326+
export function getResponseExamples(operation: CodeSampleOperation): ResponseExample[] {
301327
const responseExamples: ResponseExample[] = []
302-
for (const statusCode of Object.keys(operation.responses)) {
328+
const responses = operation.responses as Record<string, CodeSampleResponse>
329+
for (const statusCode of Object.keys(responses)) {
303330
// We don't want to create examples for error codes
304331
// Error codes are displayed in the status table in the docs
305332
if (parseInt(statusCode, 10) >= 400) continue
306333

307-
const content = operation.responses[statusCode].content
334+
const response = responses[statusCode]
335+
const content = response.content as Record<string, unknown> | undefined
308336

309337
// A response doesn't always have content (ex:, status 304)
310338
// In this case we create a generic example for the status code
@@ -314,7 +342,7 @@ export function getResponseExamples(operation: Operation): ResponseExample[] {
314342
key: statusCode,
315343
response: {
316344
statusCode,
317-
description: operation.responses[statusCode].description,
345+
description: response.description ?? '',
318346
},
319347
}
320348
responseExamples.push(example)
@@ -324,20 +352,21 @@ export function getResponseExamples(operation: Operation): ResponseExample[] {
324352
// Responses can have multiple content types each with their own set of
325353
// examples.
326354
for (const contentType of Object.keys(content)) {
327-
let examples: Record<string, any> = {}
355+
const mediaType = content[contentType] as OpenApiMediaType
356+
let examples: Record<string, { summary?: string; value?: unknown }> = {}
328357
// This is a fallback to allow using the `example` property in
329358
// the schema. If we start to enforce using examples vs. example using
330359
// a linter, we can remove the check for `example`.
331360
// We key by statusCode so that operations with multiple success
332361
// responses (e.g. 200 + 201) get unique keys instead of colliding.
333-
if (operation.responses[statusCode].content[contentType].example) {
362+
if (mediaType.example) {
334363
examples = {
335364
[statusCode]: {
336-
value: operation.responses[statusCode].content[contentType].example,
365+
value: mediaType.example,
337366
},
338367
}
339-
} else if (operation.responses[statusCode].content[contentType].examples) {
340-
examples = operation.responses[statusCode].content[contentType].examples
368+
} else if (mediaType.examples) {
369+
examples = mediaType.examples
341370
} else if (parseInt(statusCode, 10) < 300) {
342371
// Sometimes there are missing examples for say a 200 response and
343372
// the operation also has a 304 no content status. If we don't add
@@ -349,7 +378,7 @@ export function getResponseExamples(operation: Operation): ResponseExample[] {
349378
key: statusCode,
350379
response: {
351380
statusCode,
352-
description: operation.responses[statusCode].description,
381+
description: response.description ?? '',
353382
},
354383
}
355384
responseExamples.push(example)
@@ -370,15 +399,13 @@ export function getResponseExamples(operation: Operation): ResponseExample[] {
370399
response: {
371400
statusCode,
372401
contentType,
373-
description: examples[key].summary || operation.responses[statusCode].description,
402+
description: examples[key].summary || response.description || '',
374403
example: examples[key].value,
375404
// Note: Including the schema significantly increases JSON file size (~4x),
376405
// but it's necessary to support the schema/example toggle in the UI.
377406
// Users can switch between viewing the example response and the full schema.
378407
// example/examples annotation fields are stripped as they are not rendered.
379-
schema: stripSchemaExamples(
380-
operation.responses[statusCode].content[contentType].schema,
381-
),
408+
schema: stripSchemaExamples(mediaType.schema),
382409
},
383410
}
384411
responseExamples.push(example)
@@ -399,12 +426,14 @@ export function getResponseExamples(operation: Operation): ResponseExample[] {
399426
}
400427
}
401428
*/
402-
export function getParameterExamples(operation: Operation): Record<string, Record<string, any>> {
429+
export function getParameterExamples(
430+
operation: CodeSampleOperation,
431+
): Record<string, Record<string, unknown>> {
403432
if (!operation.parameters) {
404433
return {}
405434
}
406-
const parameters = operation.parameters.filter((param: any) => param.in === 'path')
407-
const parameterExamples: Record<string, Record<string, any>> = {}
435+
const parameters = operation.parameters.filter((param) => param.in === 'path')
436+
const parameterExamples: Record<string, Record<string, unknown>> = {}
408437
for (const parameter of parameters) {
409438
const examples = parameter.examples
410439
// If there are no examples, create an example from the uppercase parameter

src/rest/scripts/utils/get-operations.ts

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,19 @@
11
import Operation from '@/rest/scripts/utils/operation'
2+
import type { OpenApiSchema } from './openapi-types'
23

34
interface ProgAccessData {
4-
[key: string]: any
5+
[key: string]: unknown
56
}
67

7-
export interface SchemaInput {
8-
paths?: {
9-
[requestPath: string]: {
10-
[verb: string]: any
11-
}
12-
}
13-
servers?: any[]
14-
[key: string]: any
15-
}
8+
export type SchemaInput = OpenApiSchema
169

1710
// The module accepts a JSON schema object as input
1811
// and returns an array of its operation objects with their
1912
// HTTP verb and requestPath attached as properties
2013
export async function processOperations(
21-
operations: any[],
14+
operations: Operation[],
2215
progAccessData: ProgAccessData,
23-
): Promise<any[]> {
16+
): Promise<Operation[]> {
2417
await Promise.all(
2518
operations.map(async (operation) => {
2619
await operation.process(progAccessData)
@@ -29,7 +22,7 @@ export async function processOperations(
2922
return operations
3023
}
3124

32-
export async function createOperations(schema: SchemaInput): Promise<any[]> {
25+
export async function createOperations(schema: SchemaInput): Promise<Operation[]> {
3326
if (!schema.paths) {
3427
return []
3528
}

src/rest/scripts/utils/inject-models-schema.ts

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,7 @@ import dereferenceJsonSchema from 'dereference-json-schema'
33
import { existsSync } from 'fs'
44
import { readFile, readdir } from 'fs/promises'
55

6-
// OpenAPI 3.0 schema interface with the properties we need to access
7-
// The dereference-json-schema library returns a DereferencedJSONSchema type
8-
// but the actual object contains OpenAPI-specific properties that aren't in that type
9-
interface OpenAPISchema {
10-
openapi?: string
11-
info?: any
12-
servers?: any[]
13-
paths?: Record<string, Record<string, any>>
14-
[key: string]: any
15-
}
6+
import type { OpenApiSchema, OpenApiServer } from './openapi-types'
167

178
export const MODELS_GATEWAY_ROOT = 'models-gateway'
189
const MODELS_GATEWAY_PATH = 'docs/api'
@@ -21,7 +12,10 @@ const MODELS_GATEWAY_PATH = 'docs/api'
2112
// We "inject" the descriptions from that repo into the core GitHub API descriptions so that
2213
// from the perspective of our app code,
2314
// models descriptions are part of the same REST API schema and don't need additional processing
24-
export async function injectModelsSchema(schema: any, schemaName: string): Promise<any> {
15+
export async function injectModelsSchema(
16+
schema: OpenApiSchema,
17+
schemaName: string,
18+
): Promise<OpenApiSchema> {
2519
if (!schemaName.includes('fpt')) {
2620
return schema
2721
}
@@ -44,15 +38,15 @@ export async function injectModelsSchema(schema: any, schemaName: string): Promi
4438
const yamlContent = await readFile(endpointPath, 'utf8')
4539
const loadedYaml = yaml.load(yamlContent) as {
4640
openapi: string
47-
info: any
48-
servers: any[]
49-
paths: { [x: string]: any }
41+
info?: unknown
42+
servers?: OpenApiServer[]
43+
paths?: Record<string, Record<string, unknown>>
5044
}
5145
const deferencedYaml = dereferenceJsonSchema.dereferenceSync(loadedYaml)
5246

5347
// Copy over top-level OpenAPI fields
54-
// Cast to OpenAPISchema because dereference-json-schema doesn't include OpenAPI-specific properties in its type
55-
const openApiYaml = deferencedYaml as OpenAPISchema
48+
// Cast to OpenApiSchema because dereference-json-schema doesn't include OpenAPI-specific properties in its type
49+
const openApiYaml = deferencedYaml as unknown as OpenApiSchema
5650
schema.openapi = schema.openapi || openApiYaml.openapi
5751
schema.info = schema.info || openApiYaml.info
5852
schema.servers = schema.servers || openApiYaml.servers
@@ -85,6 +79,7 @@ export async function injectModelsSchema(schema: any, schemaName: string): Promi
8579
}
8680

8781
// Add the enhanced operation to the schema
82+
if (!schema.paths) schema.paths = {}
8883
schema.paths[path] = schema.paths[path] || {}
8984
schema.paths[path][operation] = enhancedOperation
9085

0 commit comments

Comments
 (0)