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 | ;
|
11 |
|
12 | const BaseGenerator = require( "./BaseGenerator" );
|
13 | const 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 | */
|
21 | class 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
|
396 | module.exports = SchemaGenerator;
|