Skip to content

Commit 8c40d20

Browse files
committed
Merge remote-tracking branch 'origin/main' into keyvault-folder-structure-refactor
2 parents f9478fc + d8f039e commit 8c40d20

File tree

183 files changed

+39771
-518
lines changed

Some content is hidden

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

183 files changed

+39771
-518
lines changed

.github/shared/eslint.config.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ import globals from "globals";
66
import tseslint from "typescript-eslint";
77

88
/** @type {import('eslint').Linter.Config[]} */
9-
export default defineConfig(eslint.configs.recommended, tseslint.configs.recommended, {
10-
languageOptions: { globals: globals.node },
9+
export default defineConfig(eslint.configs.recommended, tseslint.configs.recommendedTypeChecked, {
10+
languageOptions: {
11+
// we only run in node, not browser
12+
globals: globals.node,
13+
// required to use tseslint.configs.recommendedTypeChecked
14+
parserOptions: { projectService: true },
15+
},
1116
});

.github/shared/src/readme.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { readFile } from "fs/promises";
22
import yaml from "js-yaml";
33
import { marked } from "marked";
44
import { dirname, normalize, relative, resolve } from "path";
5+
import { inspect } from "util";
56
import * as z from "zod";
67
import { mapAsync } from "./array.js";
78
import { SpecModelError } from "./spec-model-error.js";
@@ -174,7 +175,7 @@ export class Readme {
174175
} /* v8 ignore start: defensive rethrow */ else {
175176
throw error;
176177
}
177-
/* v8 ignore end */
178+
/* v8 ignore stop */
178179
}
179180

180181
if (!parsedObj["input-file"]) {
@@ -274,6 +275,6 @@ export class Readme {
274275
* @returns {string}
275276
*/
276277
toString() {
277-
return `Readme(${this.#path}, {logger: ${this.#logger}})`;
278+
return `Readme(${this.#path}, {logger: ${inspect(this.#logger)}})`;
278279
}
279280
}

.github/shared/src/spec-model.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { readdir } from "fs/promises";
22
import { resolve } from "path";
3+
import { inspect } from "util";
34
import { flatMapAsync, mapAsync } from "./array.js";
45
import { readme } from "./changed-files.js";
56
import { Readme } from "./readme.js";
@@ -247,7 +248,7 @@ export class SpecModel {
247248
* @returns {string}
248249
*/
249250
toString() {
250-
return `SpecModel(${this.#folder}, {logger: ${this.#logger}}})`;
251+
return `SpecModel(${this.#folder}, {logger: ${inspect(this.#logger)}}})`;
251252
}
252253
}
253254

.github/shared/src/swagger.js

Lines changed: 156 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import $RefParser, { ResolverError } from "@apidevtools/json-schema-ref-parser";
1+
import $RefParser from "@apidevtools/json-schema-ref-parser";
22
import { readFile } from "fs/promises";
33
import { dirname, relative, resolve } from "path";
4+
import { inspect } from "util";
5+
import { z } from "zod";
46
import { mapAsync } from "./array.js";
5-
import { example } from "./changed-files.js";
6-
import { includesSegment } from "./path.js";
7+
import { example, preview } from "./changed-files.js";
78
import { SpecModelError } from "./spec-model-error.js";
89
import { embedError } from "./spec-model.js";
910

@@ -26,6 +27,42 @@ import { embedError } from "./spec-model.js";
2627
* @property {Object[]} [refs]
2728
*/
2829

30+
const pathSchema = z.record(z.string(), z.object({ operationId: z.string().optional() }));
31+
/**
32+
* @typedef {import("zod").infer<typeof pathSchema>} PathObject
33+
*/
34+
35+
const pathsSchema = z.record(z.string(), pathSchema);
36+
/**
37+
* @typedef {import("zod").infer<typeof pathsSchema>} PathsObject
38+
*/
39+
40+
const swaggerSchema = z.object({
41+
paths: pathsSchema.optional(),
42+
"x-ms-paths": pathsSchema.optional(),
43+
});
44+
/**
45+
* @typedef {import("zod").infer<typeof swaggerSchema>} SwaggerObject
46+
*
47+
* @example
48+
* const swagger = {
49+
* "paths": {
50+
* "/foo": {
51+
* "get": {
52+
* "operationId": "Foo_Get"
53+
* },
54+
* "put": {
55+
* "operationId": "Foo_CreateOrUpdate"
56+
* }
57+
* },
58+
* "/bar": { ... }
59+
* },
60+
* "x-ms-paths": {
61+
* "/baz": { ... }
62+
* }
63+
* };
64+
*/
65+
2966
/**
3067
* @type {import('@apidevtools/json-schema-ref-parser').ResolverOptions}
3168
*/
@@ -44,74 +81,99 @@ const excludeExamples = {
4481
};
4582

4683
export class Swagger {
84+
/**
85+
* Content of swagger file, either loaded from `#path` or passed in via `options`.
86+
*
87+
* Reset to `undefined` after `#data` is loaded to save memory.
88+
*
89+
* @type {string | undefined}
90+
*/
91+
#content;
92+
93+
// operations: Map of the operations in this swagger, using `operationId` as key
94+
/** @type {{operations: Map<string, Operation>, refs: Map<string, Swagger>} | undefined} */
95+
#data;
96+
4797
/** @type {import('./logger.js').ILogger | undefined} */
4898
#logger;
4999

50100
/** @type {string} absolute path */
51101
#path;
52102

53-
/** @type {Map<string, Swagger> | undefined} */
54-
#refs;
55-
56103
/** @type {Tag | undefined} Tag that contains this Swagger */
57104
#tag;
58105

59-
/** @type {Map<string, Operation> | undefined} map of the operations in this swagger with key as 'operation_id*/
60-
#operations;
61-
62106
/**
63107
* @param {string} path
64108
* @param {Object} [options]
109+
* @param {string} [options.content] If specified, is used instead of reading path from disk
65110
* @param {import('./logger.js').ILogger} [options.logger]
66111
* @param {Tag} [options.tag]
67112
*/
68113
constructor(path, options = {}) {
69-
const { logger, tag } = options;
114+
const { content, logger, tag } = options;
70115

71116
const rootDir = dirname(tag?.readme?.path ?? "");
72117
this.#path = resolve(rootDir, path);
118+
119+
this.#content = content;
73120
this.#logger = logger;
74121
this.#tag = tag;
75122
}
76123

77-
/**
78-
* @returns {Promise<Map<string, Swagger>>}
79-
*/
80-
async getRefs() {
81-
const allRefs = await this.#getRefs();
124+
async #getData() {
125+
if (!this.#data) {
126+
const path = this.#path;
82127

83-
// filter out any paths that are examples
84-
const filtered = new Map([...allRefs].filter(([path]) => !example(path)));
128+
const content =
129+
this.#content ??
130+
(await this.#wrapError(
131+
async () => await readFile(path, { encoding: "utf8" }),
132+
"Failed to read file for swagger",
133+
));
85134

86-
return filtered;
87-
}
135+
/** @type {Map<string, Operation>} */
136+
const operations = new Map();
88137

89-
async #getRefs() {
90-
if (!this.#refs) {
91-
let schema;
92-
try {
93-
schema = await $RefParser.resolve(this.#path, {
94-
resolve: { file: excludeExamples, http: false },
95-
});
96-
} catch (error) {
97-
if (error instanceof ResolverError) {
98-
throw new SpecModelError(`Failed to resolve file for swagger: ${this.#path}`, {
99-
cause: error,
100-
source: error.source,
101-
tag: this.#tag?.name,
102-
readme: this.#tag?.readme?.path,
103-
});
138+
const swaggerJson = await this.#wrapError(
139+
() => /** @type {unknown} */ (JSON.parse(content)),
140+
"Failed to parse JSON for swagger",
141+
);
142+
143+
/** @type {SwaggerObject} */
144+
const swagger = await this.#wrapError(
145+
() => swaggerSchema.parse(swaggerJson),
146+
"Failed to parse schema for swagger",
147+
);
148+
149+
// Process regular paths
150+
if (swagger.paths) {
151+
for (const [path, pathObject] of Object.entries(swagger.paths)) {
152+
this.#addOperations(operations, path, pathObject);
104153
}
154+
}
105155

106-
throw error;
156+
// Process x-ms-paths (Azure extension)
157+
if (swagger["x-ms-paths"]) {
158+
for (const [path, pathObject] of Object.entries(swagger["x-ms-paths"])) {
159+
this.#addOperations(operations, path, pathObject);
160+
}
107161
}
108162

163+
const schema = await this.#wrapError(
164+
async () =>
165+
await $RefParser.resolve(this.#path, swaggerJson, {
166+
resolve: { file: excludeExamples, http: false },
167+
}),
168+
"Failed to resolve file for swagger",
169+
);
170+
109171
const refPaths = schema
110172
.paths("file")
111173
// Exclude ourself
112174
.filter((p) => resolve(p) !== resolve(this.#path));
113175

114-
this.#refs = new Map(
176+
const refs = new Map(
115177
refPaths.map((p) => {
116178
const swagger = new Swagger(p, {
117179
logger: this.#logger,
@@ -120,13 +182,34 @@ export class Swagger {
120182
return [swagger.path, swagger];
121183
}),
122184
);
185+
186+
this.#data = { operations, refs };
187+
188+
// Clear #content to save memory, since it's no longer needed after #data is loaded
189+
this.#content = undefined;
123190
}
124191

125-
return this.#refs;
192+
return this.#data;
193+
}
194+
195+
/**
196+
* @returns {Promise<Map<string, Swagger>>} Map of swaggers referenced from this swagger, using `path` as key
197+
*/
198+
async getRefs() {
199+
const allRefs = await this.#getRefs();
200+
201+
// filter out any paths that are examples
202+
const filtered = new Map([...allRefs].filter(([path]) => !example(path)));
203+
204+
return filtered;
205+
}
206+
207+
async #getRefs() {
208+
return (await this.#getData()).refs;
126209
}
127210

128211
/**
129-
* @returns {Promise<Map<string, Swagger>>}
212+
* @returns {Promise<Map<string, Swagger>>} Map of examples referenced from this swagger, using `path` as key
130213
*/
131214
async getExamples() {
132215
const allRefs = await this.#getRefs();
@@ -138,40 +221,22 @@ export class Swagger {
138221
}
139222

140223
/**
141-
* @returns {Promise<Map<string, Operation>>}
224+
* @returns {Promise<Map<string, Operation>>} Map of the operations in this swagger, using `operationId` as key
142225
*/
143226
async getOperations() {
144-
if (!this.#operations) {
145-
this.#operations = new Map();
146-
const content = await readFile(this.#path, "utf8");
147-
const swagger = JSON.parse(content);
148-
// Process regular paths
149-
if (swagger.paths) {
150-
for (const [path, pathItem] of Object.entries(swagger.paths)) {
151-
this.addOperations(this.#operations, path, pathItem);
152-
}
153-
}
154-
155-
// Process x-ms-paths (Azure extension)
156-
if (swagger["x-ms-paths"]) {
157-
for (const [path, pathItem] of Object.entries(swagger["x-ms-paths"])) {
158-
this.addOperations(this.#operations, path, pathItem);
159-
}
160-
}
161-
}
162-
return this.#operations;
227+
return (await this.#getData()).operations;
163228
}
164229

165230
/**
166231
*
167232
* @param {Map<string, Operation>} operations
168233
* @param {string} path
169-
* @param {any} pathItem
234+
* @param {PathObject} pathObject
170235
* @returns {void}
171236
*/
172-
addOperations(operations, path, pathItem) {
173-
for (const [method, operation] of Object.entries(pathItem)) {
174-
if (typeof operation === "object" && operation.operationId && method !== "parameters") {
237+
#addOperations(operations, path, pathObject) {
238+
for (const [method, operation] of Object.entries(pathObject)) {
239+
if (operation.operationId !== undefined && method !== "parameters") {
175240
const operationObj = {
176241
id: operation.operationId,
177242
httpMethod: method.toUpperCase(),
@@ -200,7 +265,7 @@ export class Swagger {
200265
* @returns {string} version kind (stable or preview)
201266
*/
202267
get versionKind() {
203-
return includesSegment(this.#path, "preview")
268+
return preview(this.#path)
204269
? API_VERSION_LIFECYCLE_STAGES.PREVIEW
205270
: API_VERSION_LIFECYCLE_STAGES.STABLE;
206271
}
@@ -232,7 +297,34 @@ export class Swagger {
232297
}
233298

234299
toString() {
235-
return `Swagger(${this.#path}, {logger: ${this.#logger}})`;
300+
return `Swagger(${this.#path}, {logger: ${inspect(this.#logger)}})`;
301+
}
302+
303+
/**
304+
* Returns value of `func()`, wrapping any `Error` in `SpecModelError`
305+
*
306+
* @template T
307+
* @param {() => T | Promise<T>} func
308+
* @param {string} message
309+
* @returns {Promise<T>}
310+
* @throws {SpecModelError}
311+
*/
312+
async #wrapError(func, message) {
313+
try {
314+
return await func();
315+
} catch (error) {
316+
if (error instanceof Error) {
317+
throw new SpecModelError(`${message}: ${this.#path}`, {
318+
cause: error,
319+
source: this.#path,
320+
tag: this.#tag?.name,
321+
readme: this.#tag?.readme?.path,
322+
});
323+
} /* v8 ignore start: defensive rethrow */ else {
324+
throw error;
325+
}
326+
/* v8 ignore stop */
327+
}
236328
}
237329
}
238330

.github/shared/src/tag.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { inspect } from "util";
12
import { mapAsync } from "./array.js";
23
import { embedError } from "./spec-model.js";
34
import { Swagger } from "./swagger.js";
@@ -92,6 +93,6 @@ export class Tag {
9293
}
9394

9495
toString() {
95-
return `Tag(${this.#name}, {logger: ${this.#logger}})`;
96+
return `Tag(${this.#name}, {logger: ${inspect(this.#logger)}})`;
9697
}
9798
}

0 commit comments

Comments
 (0)