1- import $RefParser , { ResolverError } from "@apidevtools/json-schema-ref-parser" ;
1+ import $RefParser from "@apidevtools/json-schema-ref-parser" ;
22import { readFile } from "fs/promises" ;
33import { dirname , relative , resolve } from "path" ;
4+ import { inspect } from "util" ;
5+ import { z } from "zod" ;
46import { 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" ;
78import { SpecModelError } from "./spec-model-error.js" ;
89import { 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
4683export 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
0 commit comments