UNPKG

9.55 kBJavaScriptView Raw
1/**
2 * Defines the SchemaSpecGenerator class.
3 *
4 * @author Kevin Sanders <kevin@c2cschools.com>
5 * @since 5.1.5
6 * @license See LICENSE.md for details about licensing.
7 * @copyright 2017 C2C Schools, LLC
8 */
9
10"use strict";
11
12const BaseGenerator = require( "./BaseGenerator" );
13const REF_PARSER = require( "json-schema-ref-parser" );
14
15/**
16 * This class is used in the automatic generation of schema.
17 *
18 * @memberOf Util
19 * @extends Util.BaseGenerator
20 */
21class SchemaGenerator extends BaseGenerator {
22
23 /**
24 * The configuration that will be passed to the `json-schema-ref-parser`.
25 *
26 * @private
27 * @type {Object}
28 * @readonly
29 */
30 get _refParserConfig() {
31
32 const me = this;
33
34 // Build the config
35 return {
36
37 // Tells the resolver not to automatically parse JSON.
38 // (this might be unnecessary and might be removed)
39 parse: {
40 json: false,
41 },
42
43 // $ref resolution config...
44 resolve: {
45
46 // Add a custom resolve (see `_customRefResolver()`)
47 customResolver: {
48
49 // Ensure our custom parser is given the
50 // highest priority; it will be called FIRST,
51 // for each $ref that matches the `canRead` regex.
52 order: 1,
53
54 // A regular expression that tells the $ref parser
55 // to forward specific $ref values to our custom
56 // resolver function (`_customRefResolver`).
57 canRead: /^(common|endpoints?|service):/i,
58
59 // Pass in a reference to our custom resolver
60 read: me._customRefResolver.bind( me ),
61 },
62 },
63 };
64 }
65
66 /**
67 * A custom $ref resolver for the `json-schema-ref-parser` module.
68 *
69 * @see https://github.com/BigstickCarpet/json-schema-ref-parser/blob/master/docs/plugins/resolvers.md
70 * @private
71 * @param {Object} file - Information about the $ref value.
72 * @param {string} file.url - The actual $ref string value, minus any
73 * property references (e.g. `#/path/etc`)
74 * @returns {Object} The resolved schema part.
75 */
76 _customRefResolver( file ) {
77
78 const me = this;
79
80 // Dependencies
81 const _ = me.$dep( "lodash" );
82
83 let url = file.url;
84 let path = null;
85 let refType = null;
86 let resolverMethod = null;
87
88 // Determine the fundamental $ref type and remove
89 // the type from the path string.
90 // e.g. common:response/Response -> response/Response
91 if ( _.startsWith( url, "service:" ) ) {
92
93 refType = "Service";
94 path = url.substr( 8 );
95
96 } else if ( _.startsWith( url, "common:" ) ) {
97
98 refType = "Common";
99 path = url.substr( 7 );
100
101 } else if ( _.startsWith( url, "endpoint:" ) ) {
102
103 refType = "Endpoint";
104 path = url.substr( 9 );
105
106 } else if ( _.startsWith( url, "endpoints:" ) ) {
107
108 refType = "Endpoint";
109 path = url.substr( 10 );
110 }
111
112 // If the $ref type is valid, we'll defer
113 // to a more specific method
114 if ( refType !== null ) {
115
116 // Find the specialist method for the $ref type...
117 // e.g. refType = "common"
118 // use the `_resolveCommonRef` method...
119 resolverMethod = "_resolve" + refType + "Ref";
120
121 // Call the specialist method
122
123 try {
124
125 let specRes = me[ resolverMethod ]( path );
126
127 return specRes;
128
129 } catch ( err ) {
130
131 console.log( "\n\n\n" );
132 console.log( "An error occurred in the SchemaGenerator's custom $ref resolver.\n" );
133 console.log( err );
134 console.log( "\n\n\n" );
135
136 return {};
137 }
138
139 } else {
140
141 // If the $ref type is not valid, or not recognized,
142 // we'll return an empty object.
143 return {};
144 }
145 }
146
147 /**
148 * A specialist resolver for $ref values that start with "service:".
149 * This method is a helper for, and is called exclusively by, the
150 * `_customRefResolver` method.
151 *
152 * @see _customRefResolver
153 * @private
154 * @param {string} path - The $ref value, with the $ref type removed.
155 * @returns {Object} The resolved schema object.
156 */
157 _resolveServiceRef( path ) {
158
159 /*
160 Valid Reference Examples
161 ------------------------
162 "$ref": "service:Package#/version"
163 "$ref": "service:Package#/author"
164 "$ref": "service:EndpointPaths"
165 */
166
167 const me = this;
168
169 // Dependencies
170 const PATH = me.$dep( "path" );
171
172 if ( path === "package" ) {
173
174 // service:Package refers to values within the services' package.json
175 return require( PATH.join( me.serviceRootPath, "package.json" ) );
176
177 } else if ( path === "endpointpaths" ) {
178
179 // service:EndpointPaths refers to the special object created
180 // by the `_buildEndpointPaths` method.
181
182 return me._buildEndpointPaths();
183 }
184
185 // throw an error for anything else...
186 throw new Error(
187 "Invalid or unrecognized 'Service' $ref path: " +
188 "'" + path + "'"
189 );
190 }
191
192 /**
193 * A specialist resolver for $ref values that start with "common:".
194 * This method is a helper for, and is called exclusively by, the
195 * `_customRefResolver` method.
196 *
197 * $refs of the "common" type will load common schema files from
198 * the `core-microservices` library's `schema/definitions` directory.
199 *
200 * @see _customRefResolver
201 * @private
202 * @param {string} path - The $ref value, with the $ref type removed.
203 * @returns {Object} The resolved schema object.
204 */
205 _resolveCommonRef( path ) {
206
207 /*
208 Valid Reference Examples
209 ------------------------
210 "$ref" : "common:response/ErrorResponse"
211 */
212
213 const me = this;
214
215 // Dependencies
216 const PATH = me.$dep( "path" );
217
218 let absPath;
219 let ret;
220
221 try {
222
223 // Resolve the absolute path to the common schema file
224 absPath = PATH.join( me.commonSchemaDefinitionRoot, path + ".yml" );
225
226 // Load the common schema file
227 ret = me._loadSchema( absPath );
228
229 } catch ( err ) {
230
231 // Throw an error if the file is not found
232 throw new Error(
233 "Invalid or unrecognized 'Common' $ref path: " +
234 "'" + path + "' (File Not Found)"
235 );
236 }
237
238 // Return the common schema
239 return ret;
240 }
241
242 /**
243 * A specialist resolver for $ref values that start with "endpoint:".
244 * This method is a helper for, and is called exclusively by, the
245 * `_customRefResolver` method.
246 *
247 * $refs of the "endpoint" type will load endpoint-specific schema files
248 * from the `schema` sub-directory within the endpoint's root directory.
249 *
250 * @see _customRefResolver
251 * @private
252 * @param {string} path - The $ref value, with the $ref type removed.
253 * @returns {Object} The resolved schema object.
254 */
255 _resolveEndpointRef( path ) {
256
257 /*
258 Valid Reference Examples
259 ------------------------
260 "$ref" : "endpoint:/ReadManyActivities/SuccessResponse"
261 */
262
263 const me = this;
264
265 // Dependencies
266 const PATH = me.$dep( "path" );
267 const _ = me.$dep( "lodash" );
268
269 // Split up the 'path' so that we can resolve
270 // and remove the endpoint name from it.
271 let spl = path.split( "/" );
272
273 // Capture the endpoint name...
274 let endpointName = spl.shift();
275
276 if ( _.isNil( endpointName ) || endpointName === "" ) {
277
278 endpointName = spl.shift();
279 }
280
281 // The remaining path parts constitute a relative
282 // path within the endpoint's `schema` directory
283 let relPath = spl.join( "/" );
284
285 // Find the details of the target endpoint; we need to
286 // find the absolute path of the endpoint's schema.
287 let epDetails = me.getServiceEndpoint( endpointName );
288
289 // Error if the endpoint was not found
290 if ( epDetails === null ) {
291
292 throw new Error(
293 "Invalid or unrecognized 'Endpoint' $ref path: " +
294 "'" + path + "' (No such endpoint)"
295 );
296 }
297
298 // Resolve the absolute path to the endpoint schema file
299 let epSchemaRoot = epDetails.schemaRootPath;
300 let absPath = PATH.join( epSchemaRoot, relPath + ".yml" );
301
302 // Load the file and return it as an object
303 return me._loadSchema( absPath );
304 }
305
306 /**
307 * Builds an object that contains the `paths` data from every endpoint
308 * in the service.
309 *
310 * The schema object returned by this method is available within any
311 * schema parsed by this generator as `{ "$ref": "service:EndpointPaths" }`,
312 * which should always exist, somewhere, within service-level OpenAPI
313 * templates.
314 *
315 * @private
316 * @returns {Object} An object containing the `paths` from every endpoint.
317 */
318 _buildEndpointPaths() {
319
320 const me = this;
321
322 // Dependencies
323 const _ = me.$dep( "lodash" );
324
325 let epd = me.getServiceEndpoints( false );
326 let ret = {};
327
328 _.each( epd, function ( data ) {
329
330 if ( _.isPlainObject( data.pathConfig ) ) {
331
332 _.each( data.pathConfig, function ( pathData, pathName ) {
333
334 ret[ pathName ] = pathData;
335 } );
336 }
337 } );
338
339 return ret;
340 }
341
342 /**
343 * Loads the schema file at the specified path.
344 *
345 * @param {string} path - Schema path
346 * @returns {Object} The loaded schema object.
347 * @private
348 */
349 _loadSchema( path ) {
350
351 const me = this;
352
353 return me.loadYamlFile( path );
354 }
355
356 /**
357 * Generates an OpenAPI specification.
358 *
359 * @public
360 * @param {string|Object} rootSchema - Root schema file/object.
361 * @param {boolean} [dereference=true] - When TRUE, the final specification
362 * will be completely dereferenced, making it as verbose as possible.
363 * @returns {Promise} A promise that is resolved with the full schema
364 * (as an object).
365 */
366 buildSchema( rootSchema, dereference ) {
367
368 const me = this;
369
370 // Dependencies
371 const TIPE = me.$dep( "tipe" );
372
373 let rpMethod;
374
375 // Figure out which method to use on the schema ref parser
376 if ( dereference === false ) {
377
378 rpMethod = "bundle";
379
380 } else {
381
382 rpMethod = "dereference";
383 }
384
385 if ( TIPE( rootSchema ) === "string" ) {
386
387 rootSchema = me._loadSchema( rootSchema );
388 }
389
390 // Defer to the `json-schema-ref-parser` module for processing.
391 return REF_PARSER[ rpMethod ]( rootSchema, me._refParserConfig );
392 }
393}
394
395// Export the class
396module.exports = SchemaGenerator;