1 |
|
2 | 'use strict';
|
3 |
|
4 | const fs = require('fs');
|
5 | const url = require('url');
|
6 | const pathlib = require('path');
|
7 |
|
8 | const maybe = require('call-me-maybe');
|
9 | const fetch = require('node-fetch-h2');
|
10 | const yaml = require('yaml');
|
11 |
|
12 | const jptr = require('reftools/lib/jptr.js');
|
13 | const resolveInternal = jptr.jptr;
|
14 | const isRef = require('reftools/lib/isref.js').isRef;
|
15 | const clone = require('reftools/lib/clone.js').clone;
|
16 | const cclone = require('reftools/lib/clone.js').circularClone;
|
17 | const recurse = require('reftools/lib/recurse.js').recurse;
|
18 | const resolver = require('oas-resolver');
|
19 | const sw = require('oas-schema-walker');
|
20 | const common = require('oas-kit-common');
|
21 |
|
22 | const statusCodes = require('./lib/statusCodes.js').statusCodes;
|
23 |
|
24 | const ourVersion = require('./package.json').version;
|
25 |
|
26 |
|
27 |
|
28 | const targetVersion = '3.0.0';
|
29 | let componentNames;
|
30 |
|
31 | class S2OError extends Error {
|
32 | constructor(message) {
|
33 | super(message);
|
34 | this.name = 'S2OError';
|
35 | }
|
36 | }
|
37 |
|
38 | function throwError(message, options) {
|
39 | let err = new S2OError(message);
|
40 | err.options = options;
|
41 | if (options.promise) {
|
42 | options.promise.reject(err);
|
43 | }
|
44 | else {
|
45 | throw err;
|
46 | }
|
47 | }
|
48 |
|
49 | function throwOrWarn(message, container, options) {
|
50 | if (options.warnOnly) {
|
51 | container[options.warnProperty||'x-s2o-warning'] = message;
|
52 | }
|
53 | else {
|
54 | throwError(message, options);
|
55 | }
|
56 | }
|
57 |
|
58 | function fixUpSubSchema(schema,parent,options) {
|
59 | if (schema.discriminator && typeof schema.discriminator === 'string') {
|
60 | schema.discriminator = { propertyName: schema.discriminator };
|
61 | }
|
62 | if (schema.items && Array.isArray(schema.items)) {
|
63 | if (schema.items.length === 0) {
|
64 | schema.items = {};
|
65 | }
|
66 | else if (schema.items.length === 1) {
|
67 | schema.items = schema.items[0];
|
68 | }
|
69 | else schema.items = { anyOf: schema.items };
|
70 | }
|
71 |
|
72 | if (schema.type && Array.isArray(schema.type)) {
|
73 | if (options.patch) {
|
74 | if (schema.type.length === 0) {
|
75 | delete schema.type;
|
76 | }
|
77 | else {
|
78 | if (!schema.oneOf) schema.oneOf = [];
|
79 | for (let type of schema.type) {
|
80 | let newSchema = {};
|
81 | if (type === 'null') {
|
82 | schema.nullable = true;
|
83 | }
|
84 | else {
|
85 | newSchema.type = type;
|
86 | for (let prop of common.arrayProperties) {
|
87 | if (typeof schema.prop !== 'undefined') {
|
88 | newSchema[prop] = schema[prop];
|
89 | delete schema[prop];
|
90 | }
|
91 | }
|
92 | }
|
93 | if (newSchema.type) {
|
94 | schema.oneOf.push(newSchema);
|
95 | }
|
96 | }
|
97 | delete schema.type;
|
98 | if (schema.oneOf.length === 0) {
|
99 | delete schema.oneOf;
|
100 | }
|
101 | else if (schema.oneOf.length < 2) {
|
102 | schema.type = schema.oneOf[0].type;
|
103 | if (Object.keys(schema.oneOf[0]).length > 1) {
|
104 | throwOrWarn('Lost properties from oneOf',schema,options);
|
105 | }
|
106 | delete schema.oneOf;
|
107 | }
|
108 | }
|
109 |
|
110 | if (schema.type && Array.isArray(schema.type) && schema.type.length === 1) {
|
111 | schema.type = schema.type[0];
|
112 | }
|
113 | }
|
114 | else {
|
115 | throwError('(Patchable) schema type must not be an array', options);
|
116 | }
|
117 | }
|
118 |
|
119 | if (schema.type && schema.type === 'null') {
|
120 | delete schema.type;
|
121 | schema.nullable = true;
|
122 | }
|
123 | if ((schema.type === 'array') && (!schema.items)) {
|
124 | schema.items = {};
|
125 | }
|
126 | if (typeof schema.required === 'boolean') {
|
127 | if (schema.required && schema.name) {
|
128 | if (typeof parent.required === 'undefined') {
|
129 | parent.required = [];
|
130 | }
|
131 | if (Array.isArray(parent.required)) parent.required.push(schema.name);
|
132 | }
|
133 | delete schema.required;
|
134 | }
|
135 |
|
136 |
|
137 |
|
138 |
|
139 |
|
140 | if (schema.xml && typeof schema.xml.namespace === 'string') {
|
141 | if (!schema.xml.namespace) delete schema.xml.namespace;
|
142 | }
|
143 | }
|
144 |
|
145 | function fixUpSubSchemaExtensions(schema,parent) {
|
146 | if (schema["x-required"] && Array.isArray(schema["x-required"])) {
|
147 | if (!schema.required) schema.required = [];
|
148 | schema.required = schema.required.concat(schema["x-required"]);
|
149 | delete schema["x-required"];
|
150 | }
|
151 | if (schema["x-anyOf"]) {
|
152 | schema.anyOf = schema["x-anyOf"];
|
153 | delete schema["x-anyOf"];
|
154 | }
|
155 | if (schema["x-oneOf"]) {
|
156 | schema.oneOf = schema["x-oneOf"];
|
157 | delete schema["x-oneOf"];
|
158 | }
|
159 | if (schema["x-not"]) {
|
160 | schema.not = schema["x-not"];
|
161 | delete schema["x-not"];
|
162 | }
|
163 | if (typeof schema["x-nullable"] === 'boolean') {
|
164 | schema.nullable = schema["x-nullable"];
|
165 | delete schema["x-nullable"];
|
166 | }
|
167 | if ((typeof schema["x-discriminator"] === 'object') && (typeof schema["x-discriminator"].propertyName === 'string')) {
|
168 | schema.discriminator = schema["x-discriminator"];
|
169 | delete schema["x-discriminator"];
|
170 | for (let entry in schema.discriminator.mapping) {
|
171 | let schemaOrRef = schema.discriminator.mapping[entry];
|
172 | if (schemaOrRef.startsWith('#/definitions/')) {
|
173 | schema.discriminator.mapping[entry] = schemaOrRef.replace('#/definitions/','#/components/schemas/');
|
174 | }
|
175 | }
|
176 | }
|
177 | }
|
178 |
|
179 | function fixUpSchema(schema,options) {
|
180 | sw.walkSchema(schema,{},{},function(schema,parent,state){
|
181 | fixUpSubSchemaExtensions(schema,parent);
|
182 | fixUpSubSchema(schema,parent,options);
|
183 | });
|
184 | }
|
185 |
|
186 | function getMiroComponentName(ref) {
|
187 | if (ref.indexOf('#')>=0) {
|
188 | ref = ref.split('#')[1].split('/').pop();
|
189 | }
|
190 | else {
|
191 | ref = ref.split('/').pop().split('.')[0];
|
192 | }
|
193 | return encodeURIComponent(common.sanitise(ref));
|
194 | }
|
195 |
|
196 | function fixupRefs(obj, key, state) {
|
197 | let options = state.payload.options;
|
198 | if (isRef(obj,key)) {
|
199 | if (obj[key].startsWith('#/components/')) {
|
200 |
|
201 | }
|
202 | else if (obj[key] === '#/consumes') {
|
203 |
|
204 | delete obj[key];
|
205 | state.parent[state.pkey] = clone(options.openapi.consumes);
|
206 | }
|
207 | else if (obj[key] === '#/produces') {
|
208 |
|
209 | delete obj[key];
|
210 | state.parent[state.pkey] = clone(options.openapi.produces);
|
211 | }
|
212 | else if (obj[key].startsWith('#/definitions/')) {
|
213 |
|
214 | let keys = obj[key].replace('#/definitions/', '').split('/');
|
215 | const ref = jptr.jpunescape(keys[0]);
|
216 |
|
217 | let newKey = componentNames.schemas[decodeURIComponent(ref)];
|
218 | if (newKey) {
|
219 | keys[0] = newKey;
|
220 | }
|
221 | else {
|
222 | throwOrWarn('Could not resolve reference '+obj[key],obj,options);
|
223 | }
|
224 | obj[key] = '#/components/schemas/' + keys.join('/');
|
225 | }
|
226 | else if (obj[key].startsWith('#/parameters/')) {
|
227 |
|
228 | obj[key] = '#/components/parameters/' + common.sanitise(obj[key].replace('#/parameters/', ''));
|
229 | }
|
230 | else if (obj[key].startsWith('#/responses/')) {
|
231 |
|
232 | obj[key] = '#/components/responses/' + common.sanitise(obj[key].replace('#/responses/', ''));
|
233 | }
|
234 | else if (obj[key].startsWith('#')) {
|
235 |
|
236 | let target = clone(jptr.jptr(options.openapi,obj[key]));
|
237 | if (target === false) throwOrWarn('direct $ref not found '+obj[key],obj,options)
|
238 | else if (options.refmap[obj[key]]) {
|
239 | obj[key] = options.refmap[obj[key]];
|
240 | }
|
241 | else {
|
242 |
|
243 | let oldRef = obj[key];
|
244 | oldRef = oldRef.replace('/properties/headers/','');
|
245 | oldRef = oldRef.replace('/properties/responses/','');
|
246 | oldRef = oldRef.replace('/properties/parameters/','');
|
247 | oldRef = oldRef.replace('/properties/schemas/','');
|
248 | let type = 'schemas';
|
249 | let schemaIndex = oldRef.lastIndexOf('/schema');
|
250 | type = (oldRef.indexOf('/headers/')>schemaIndex) ? 'headers' :
|
251 | ((oldRef.indexOf('/responses/')>schemaIndex) ? 'responses' :
|
252 | ((oldRef.indexOf('/example')>schemaIndex) ? 'examples' :
|
253 | ((oldRef.indexOf('/x-')>schemaIndex) ? 'extensions' :
|
254 | ((oldRef.indexOf('/parameters/')>schemaIndex) ? 'parameters' : 'schemas'))));
|
255 |
|
256 |
|
257 |
|
258 |
|
259 | if (type === 'schemas') {
|
260 | fixUpSchema(target,options);
|
261 | }
|
262 |
|
263 | if ((type !== 'responses') && (type !== 'extensions')) {
|
264 | let prefix = type.substr(0,type.length-1);
|
265 | if ((prefix === 'parameter') && target.name && (target.name === common.sanitise(target.name))) {
|
266 | prefix = encodeURIComponent(target.name);
|
267 | }
|
268 |
|
269 | let suffix = 1;
|
270 | if (obj['x-miro']) {
|
271 | prefix = getMiroComponentName(obj['x-miro']);
|
272 | suffix = '';
|
273 | }
|
274 |
|
275 | while (jptr.jptr(options.openapi,'#/components/'+type+'/'+prefix+suffix)) {
|
276 | suffix = (suffix === '' ? 2 : ++suffix);
|
277 | }
|
278 |
|
279 | let newRef = '#/components/'+type+'/'+prefix+suffix;
|
280 | let refSuffix = '';
|
281 |
|
282 | if (type === 'examples') {
|
283 | target = { value: target };
|
284 | refSuffix = '/value';
|
285 | }
|
286 |
|
287 | jptr.jptr(options.openapi,newRef,target);
|
288 | options.refmap[obj[key]] = newRef+refSuffix;
|
289 | obj[key] = newRef+refSuffix;
|
290 | }
|
291 | }
|
292 | }
|
293 |
|
294 | delete obj['x-miro'];
|
295 |
|
296 | if (Object.keys(obj).length > 1) {
|
297 | const tmpRef = obj[key];
|
298 | const inSchema = state.path.indexOf('/schema') >= 0;
|
299 | if (options.refSiblings === 'preserve') {
|
300 |
|
301 | }
|
302 | else if (inSchema && (options.refSiblings === 'allOf')) {
|
303 | delete obj.$ref;
|
304 | state.parent[state.pkey] = { allOf: [ { $ref: tmpRef }, obj ]};
|
305 | }
|
306 | else {
|
307 | state.parent[state.pkey] = { $ref: tmpRef };
|
308 | }
|
309 | }
|
310 |
|
311 | }
|
312 | if ((key === 'x-ms-odata') && (typeof obj[key] === 'string') && (obj[key].startsWith('#/'))) {
|
313 | let keys = obj[key].replace('#/definitions/', '').replace('#/components/schemas/','').split('/');
|
314 | let newKey = componentNames.schemas[decodeURIComponent(keys[0])];
|
315 | if (newKey) {
|
316 | keys[0] = newKey;
|
317 | }
|
318 | else {
|
319 | throwOrWarn('Could not resolve reference '+obj[key],obj,options);
|
320 | }
|
321 | obj[key] = '#/components/schemas/' + keys.join('/');
|
322 | }
|
323 | }
|
324 |
|
325 |
|
326 |
|
327 |
|
328 |
|
329 | function dedupeRefs(openapi, options) {
|
330 | for (let ref in options.refmap) {
|
331 | jptr.jptr(openapi,ref,{ $ref: options.refmap[ref] });
|
332 | }
|
333 | }
|
334 |
|
335 | function processSecurity(securityObject) {
|
336 | for (let s in securityObject) {
|
337 | for (let k in securityObject[s]) {
|
338 | let sname = common.sanitise(k);
|
339 | if (k != sname) {
|
340 | securityObject[s][sname] = securityObject[s][k];
|
341 | delete securityObject[s][k];
|
342 | }
|
343 | }
|
344 | }
|
345 | }
|
346 |
|
347 | function processSecurityScheme(scheme, options) {
|
348 | if (scheme.type === 'basic') {
|
349 | scheme.type = 'http';
|
350 | scheme.scheme = 'basic';
|
351 | }
|
352 | if (scheme.type === 'oauth2') {
|
353 | let flow = {};
|
354 | let flowName = scheme.flow;
|
355 | if (scheme.flow === 'application') flowName = 'clientCredentials';
|
356 | if (scheme.flow === 'accessCode') flowName = 'authorizationCode';
|
357 | if (typeof scheme.authorizationUrl !== 'undefined') flow.authorizationUrl = scheme.authorizationUrl.split('?')[0].trim() || '/';
|
358 | if (typeof scheme.tokenUrl === 'string') flow.tokenUrl = scheme.tokenUrl.split('?')[0].trim() || '/';
|
359 | flow.scopes = scheme.scopes || {};
|
360 | scheme.flows = {};
|
361 | scheme.flows[flowName] = flow;
|
362 | delete scheme.flow;
|
363 | delete scheme.authorizationUrl;
|
364 | delete scheme.tokenUrl;
|
365 | delete scheme.scopes;
|
366 | if (typeof scheme.name !== 'undefined') {
|
367 | if (options.patch) {
|
368 | delete scheme.name;
|
369 | }
|
370 | else {
|
371 | throwError('(Patchable) oauth2 securitySchemes should not have name property', options);
|
372 | }
|
373 | }
|
374 | }
|
375 | }
|
376 |
|
377 | function keepParameters(value) {
|
378 | return (value && !value["x-s2o-delete"]);
|
379 | }
|
380 |
|
381 | function processHeader(header, options) {
|
382 | if (header.$ref) {
|
383 | header.$ref = header.$ref.replace('#/responses/', '#/components/responses/');
|
384 | }
|
385 | else {
|
386 | if (header.type && !header.schema) {
|
387 | header.schema = {};
|
388 | }
|
389 | if (header.type) header.schema.type = header.type;
|
390 | if (header.items && header.items.type !== 'array') {
|
391 | if (header.items.collectionFormat !== header.collectionFormat) {
|
392 | throwOrWarn('Nested collectionFormats are not supported', header, options);
|
393 | }
|
394 | delete header.items.collectionFormat;
|
395 | }
|
396 | if (header.type === 'array') {
|
397 | if (header.collectionFormat === 'ssv') {
|
398 | throwOrWarn('collectionFormat:ssv is no longer supported for headers', header, options);
|
399 | }
|
400 | else if (header.collectionFormat === 'pipes') {
|
401 | throwOrWarn('collectionFormat:pipes is no longer supported for headers', header, options);
|
402 | }
|
403 | else if (header.collectionFormat === 'multi') {
|
404 | header.explode = true;
|
405 | }
|
406 | else if (header.collectionFormat === 'tsv') {
|
407 | throwOrWarn('collectionFormat:tsv is no longer supported', header, options);
|
408 | header["x-collectionFormat"] = 'tsv';
|
409 | }
|
410 | else {
|
411 | header.style = 'simple';
|
412 | }
|
413 | delete header.collectionFormat;
|
414 | }
|
415 | else if (header.collectionFormat) {
|
416 | if (options.patch) {
|
417 | delete header.collectionFormat;
|
418 | }
|
419 | else {
|
420 | throwError('(Patchable) collectionFormat is only applicable to header.type array', options);
|
421 | }
|
422 | }
|
423 | delete header.type;
|
424 | for (let prop of common.parameterTypeProperties) {
|
425 | if (typeof header[prop] !== 'undefined') {
|
426 | header.schema[prop] = header[prop];
|
427 | delete header[prop];
|
428 | }
|
429 | }
|
430 | for (let prop of common.arrayProperties) {
|
431 | if (typeof header[prop] !== 'undefined') {
|
432 | header.schema[prop] = header[prop];
|
433 | delete header[prop];
|
434 | }
|
435 | }
|
436 | }
|
437 | }
|
438 |
|
439 | function fixParamRef(param, options) {
|
440 | if (param.$ref.indexOf('#/parameters/') >= 0) {
|
441 | let refComponents = param.$ref.split('#/parameters/');
|
442 | param.$ref = refComponents[0] + '#/components/parameters/' + common.sanitise(refComponents[1]);
|
443 | }
|
444 | if (param.$ref.indexOf('#/definitions/') >= 0) {
|
445 | throwOrWarn('Definition used as parameter', param, options);
|
446 | }
|
447 | }
|
448 |
|
449 | function attachRequestBody(op,options) {
|
450 | let newOp = {};
|
451 | for (let key of Object.keys(op)) {
|
452 | newOp[key] = op[key];
|
453 | if (key === 'parameters') {
|
454 | newOp.requestBody = {};
|
455 | if (options.rbname) newOp[options.rbname] = '';
|
456 | }
|
457 | }
|
458 | newOp.requestBody = {};
|
459 | return newOp;
|
460 | }
|
461 |
|
462 |
|
463 |
|
464 |
|
465 | function processParameter(param, op, path, method, index, openapi, options) {
|
466 | let result = {};
|
467 | let singularRequestBody = true;
|
468 | let originalType;
|
469 |
|
470 | if (op && op.consumes && (typeof op.consumes === 'string')) {
|
471 | if (options.patch) {
|
472 | op.consumes = [op.consumes];
|
473 | }
|
474 | else {
|
475 | return throwError('(Patchable) operation.consumes must be an array', options);
|
476 | }
|
477 | }
|
478 | if (!Array.isArray(openapi.consumes)) delete openapi.consumes;
|
479 | let consumes = ((op ? op.consumes : null) || (openapi.consumes || [])).filter(common.uniqueOnly);
|
480 |
|
481 | if (param && param.$ref && (typeof param.$ref === 'string')) {
|
482 |
|
483 | fixParamRef(param, options);
|
484 | let ptr = decodeURIComponent(param.$ref.replace('#/components/parameters/', ''));
|
485 | let rbody = false;
|
486 | let target = openapi.components.parameters[ptr];
|
487 |
|
488 | if (((!target) || (target["x-s2o-delete"])) && param.$ref.startsWith('#/')) {
|
489 |
|
490 | param["x-s2o-delete"] = true;
|
491 | rbody = true;
|
492 | }
|
493 |
|
494 |
|
495 |
|
496 |
|
497 | if (rbody) {
|
498 | let ref = param.$ref;
|
499 | let newParam = resolveInternal(openapi, param.$ref);
|
500 | if (!newParam && ref.startsWith('#/')) {
|
501 | throwOrWarn('Could not resolve reference ' + ref, param, options);
|
502 | }
|
503 | else {
|
504 | if (newParam) param = newParam;
|
505 | }
|
506 | }
|
507 | }
|
508 |
|
509 | if (param && (param.name || param.in)) {
|
510 |
|
511 | if (typeof param['x-deprecated'] === 'boolean') {
|
512 | param.deprecated = param['x-deprecated'];
|
513 | delete param['x-deprecated'];
|
514 | }
|
515 |
|
516 | if (typeof param['x-example'] !== 'undefined') {
|
517 | param.example = param['x-example'];
|
518 | delete param['x-example'];
|
519 | }
|
520 |
|
521 | if ((param.in != 'body') && (!param.type)) {
|
522 | if (options.patch) {
|
523 | param.type = 'string';
|
524 | }
|
525 | else {
|
526 | throwError('(Patchable) parameter.type is mandatory for non-body parameters', options);
|
527 | }
|
528 | }
|
529 | if (param.type && typeof param.type === 'object' && param.type.$ref) {
|
530 |
|
531 | param.type = resolveInternal(openapi, param.type.$ref);
|
532 | }
|
533 | if (param.type === 'file') {
|
534 | param['x-s2o-originalType'] = param.type;
|
535 | originalType = param.type;
|
536 | }
|
537 | if (param.description && typeof param.description === 'object' && param.description.$ref) {
|
538 |
|
539 | param.description = resolveInternal(openapi, param.description.$ref);
|
540 | }
|
541 | if (param.description === null) delete param.description;
|
542 |
|
543 | let oldCollectionFormat = param.collectionFormat;
|
544 | if ((param.type === 'array') && !oldCollectionFormat) {
|
545 | oldCollectionFormat = 'csv';
|
546 | }
|
547 | if (oldCollectionFormat) {
|
548 | if (param.type != 'array') {
|
549 | if (options.patch) {
|
550 | delete param.collectionFormat;
|
551 | }
|
552 | else {
|
553 | throwError('(Patchable) collectionFormat is only applicable to param.type array', options);
|
554 | }
|
555 | }
|
556 | if ((oldCollectionFormat === 'csv') && ((param.in === 'query') || (param.in === 'cookie'))) {
|
557 | param.style = 'form';
|
558 | param.explode = false;
|
559 | }
|
560 | if ((oldCollectionFormat === 'csv') && ((param.in === 'path') || (param.in === 'header'))) {
|
561 | param.style = 'simple';
|
562 | }
|
563 | if (oldCollectionFormat === 'ssv') {
|
564 | if (param.in === 'query') {
|
565 | param.style = 'spaceDelimited';
|
566 | }
|
567 | else {
|
568 | throwOrWarn('collectionFormat:ssv is no longer supported except for in:query parameters', param, options);
|
569 | }
|
570 | }
|
571 | if (oldCollectionFormat === 'pipes') {
|
572 | if (param.in === 'query') {
|
573 | param.style = 'pipeDelimited';
|
574 | }
|
575 | else {
|
576 | throwOrWarn('collectionFormat:pipes is no longer supported except for in:query parameters', param, options);
|
577 | }
|
578 | }
|
579 | if (oldCollectionFormat === 'multi') {
|
580 | param.explode = true;
|
581 | }
|
582 | if (oldCollectionFormat === 'tsv') {
|
583 | throwOrWarn('collectionFormat:tsv is no longer supported', param, options);
|
584 | param["x-collectionFormat"] = 'tsv';
|
585 | }
|
586 | delete param.collectionFormat;
|
587 | }
|
588 |
|
589 | if (param.type && (param.type != 'object') && (param.type != 'body') && (param.in != 'formData')) {
|
590 | if (param.items && param.schema) {
|
591 | throwOrWarn('parameter has array,items and schema', param, options);
|
592 | }
|
593 | else {
|
594 | if ((!param.schema) || (typeof param.schema !== 'object')) param.schema = {};
|
595 | param.schema.type = param.type;
|
596 | if (param.items) {
|
597 | param.schema.items = param.items;
|
598 | delete param.items;
|
599 | recurse(param.schema.items, null, function (obj, key, state) {
|
600 | if ((key === 'collectionFormat') && (typeof obj[key] === 'string')) {
|
601 | if (oldCollectionFormat && obj[key] !== oldCollectionFormat) {
|
602 | throwOrWarn('Nested collectionFormats are not supported', param, options);
|
603 | }
|
604 | delete obj[key];
|
605 | }
|
606 |
|
607 |
|
608 | });
|
609 | }
|
610 | for (let prop of common.parameterTypeProperties) {
|
611 | if (typeof param[prop] !== 'undefined') param.schema[prop] = param[prop];
|
612 | delete param[prop];
|
613 | }
|
614 | }
|
615 | }
|
616 |
|
617 | if (param.schema) {
|
618 | fixUpSchema(param.schema,options);
|
619 | }
|
620 |
|
621 | if (param["x-ms-skip-url-encoding"]) {
|
622 | if (param.in === 'query') {
|
623 | param.allowReserved = true;
|
624 | delete param["x-ms-skip-url-encoding"];
|
625 | }
|
626 | }
|
627 | }
|
628 |
|
629 | if (param && param.in === 'formData') {
|
630 |
|
631 | singularRequestBody = false;
|
632 | result.content = {};
|
633 | let contentType = 'application/x-www-form-urlencoded';
|
634 | if ((consumes.length) && (consumes.indexOf('multipart/form-data') >= 0)) {
|
635 | contentType = 'multipart/form-data';
|
636 | }
|
637 |
|
638 | result.content[contentType] = {};
|
639 | if (param.schema) {
|
640 | result.content[contentType].schema = param.schema;
|
641 | if (param.schema.$ref) {
|
642 | result['x-s2o-name'] = decodeURIComponent(param.schema.$ref.replace('#/components/schemas/', ''));
|
643 | }
|
644 | }
|
645 | else {
|
646 | result.content[contentType].schema = {};
|
647 | result.content[contentType].schema.type = 'object';
|
648 | result.content[contentType].schema.properties = {};
|
649 | result.content[contentType].schema.properties[param.name] = {};
|
650 | let schema = result.content[contentType].schema;
|
651 | let target = result.content[contentType].schema.properties[param.name];
|
652 | if (param.description) target.description = param.description;
|
653 | if (param.example) target.example = param.example;
|
654 | if (param.type) target.type = param.type;
|
655 |
|
656 | for (let prop of common.parameterTypeProperties) {
|
657 | if (typeof param[prop] !== 'undefined') target[prop] = param[prop];
|
658 | }
|
659 | if (param.required === true) {
|
660 | if (!schema.required) schema.required = [];
|
661 | schema.required.push(param.name);
|
662 | result.required = true;
|
663 | }
|
664 | if (typeof param.default !== 'undefined') target.default = param.default;
|
665 | if (target.properties) target.properties = param.properties;
|
666 | if (param.allOf) target.allOf = param.allOf;
|
667 | if ((param.type === 'array') && (param.items)) {
|
668 | target.items = param.items;
|
669 | if (target.items.collectionFormat) delete target.items.collectionFormat;
|
670 | }
|
671 | if ((originalType === 'file') || (param['x-s2o-originalType'] === 'file')) {
|
672 | target.type = 'string';
|
673 | target.format = 'binary';
|
674 | }
|
675 |
|
676 |
|
677 | copyExtensions(param, target);
|
678 | }
|
679 | }
|
680 | else if (param && param.type === 'file') {
|
681 |
|
682 | if (param.required) result.required = param.required;
|
683 | result.content = {};
|
684 | result.content["application/octet-stream"] = {};
|
685 | result.content["application/octet-stream"].schema = {};
|
686 | result.content["application/octet-stream"].schema.type = 'string';
|
687 | result.content["application/octet-stream"].schema.format = 'binary';
|
688 | copyExtensions(param, result);
|
689 | }
|
690 | if (param && param.in === 'body') {
|
691 | result.content = {};
|
692 | if (param.name) result['x-s2o-name'] = (op && op.operationId ? common.sanitiseAll(op.operationId) : '') + ('_' + param.name).toCamelCase();
|
693 | if (param.description) result.description = param.description;
|
694 | if (param.required) result.required = param.required;
|
695 |
|
696 |
|
697 | if (op && options.rbname && param.name) {
|
698 | op[options.rbname] = param.name;
|
699 | }
|
700 | if (param.schema && param.schema.$ref) {
|
701 | result['x-s2o-name'] = decodeURIComponent(param.schema.$ref.replace('#/components/schemas/', ''));
|
702 | }
|
703 | else if (param.schema && (param.schema.type === 'array') && param.schema.items && param.schema.items.$ref) {
|
704 | result['x-s2o-name'] = decodeURIComponent(param.schema.items.$ref.replace('#/components/schemas/', '')) + 'Array';
|
705 | }
|
706 |
|
707 | if (!consumes.length) {
|
708 | consumes.push('application/json');
|
709 | }
|
710 |
|
711 | for (let mimetype of consumes) {
|
712 | result.content[mimetype] = {};
|
713 | result.content[mimetype].schema = clone(param.schema || {});
|
714 | fixUpSchema(result.content[mimetype].schema,options);
|
715 | }
|
716 |
|
717 |
|
718 | copyExtensions(param, result);
|
719 | }
|
720 |
|
721 | if (Object.keys(result).length > 0) {
|
722 | param["x-s2o-delete"] = true;
|
723 |
|
724 | if (op) {
|
725 | if (op.requestBody && singularRequestBody) {
|
726 | op.requestBody["x-s2o-overloaded"] = true;
|
727 | let opId = op.operationId || index;
|
728 |
|
729 | throwOrWarn('Operation ' + opId + ' has multiple requestBodies', op, options);
|
730 | }
|
731 | else {
|
732 | if (!op.requestBody) {
|
733 | op = path[method] = attachRequestBody(op,options);
|
734 | }
|
735 | if ((op.requestBody.content && op.requestBody.content["multipart/form-data"])
|
736 | && (op.requestBody.content["multipart/form-data"].schema) && (op.requestBody.content["multipart/form-data"].schema.properties) && (result.content["multipart/form-data"]) && (result.content["multipart/form-data"].schema) && (result.content["multipart/form-data"].schema.properties)) {
|
737 | op.requestBody.content["multipart/form-data"].schema.properties =
|
738 | Object.assign(op.requestBody.content["multipart/form-data"].schema.properties, result.content["multipart/form-data"].schema.properties);
|
739 | op.requestBody.content["multipart/form-data"].schema.required = (op.requestBody.content["multipart/form-data"].schema.required || []).concat(result.content["multipart/form-data"].schema.required||[]);
|
740 | if (!op.requestBody.content["multipart/form-data"].schema.required.length) {
|
741 | delete op.requestBody.content["multipart/form-data"].schema.required;
|
742 | }
|
743 | }
|
744 | else if ((op.requestBody.content && op.requestBody.content["application/x-www-form-urlencoded"] && op.requestBody.content["application/x-www-form-urlencoded"].schema && op.requestBody.content["application/x-www-form-urlencoded"].schema.properties)
|
745 | && result.content["application/x-www-form-urlencoded"] && result.content["application/x-www-form-urlencoded"].schema && result.content["application/x-www-form-urlencoded"].schema.properties) {
|
746 | op.requestBody.content["application/x-www-form-urlencoded"].schema.properties =
|
747 | Object.assign(op.requestBody.content["application/x-www-form-urlencoded"].schema.properties, result.content["application/x-www-form-urlencoded"].schema.properties);
|
748 | op.requestBody.content["application/x-www-form-urlencoded"].schema.required = (op.requestBody.content["application/x-www-form-urlencoded"].schema.required || []).concat(result.content["application/x-www-form-urlencoded"].schema.required||[]);
|
749 | if (!op.requestBody.content["application/x-www-form-urlencoded"].schema.required.length) {
|
750 | delete op.requestBody.content["application/x-www-form-urlencoded"].schema.required;
|
751 | }
|
752 | }
|
753 | else {
|
754 | op.requestBody = Object.assign(op.requestBody, result);
|
755 | if (!op.requestBody['x-s2o-name']) {
|
756 | if (op.requestBody.schema && op.requestBody.schema.$ref) {
|
757 | op.requestBody['x-s2o-name'] = decodeURIComponent(op.requestBody.schema.$ref.replace('#/components/schemas/', '')).split('/').join('');
|
758 | }
|
759 | else if (op.operationId) {
|
760 | op.requestBody['x-s2o-name'] = common.sanitiseAll(op.operationId);
|
761 | }
|
762 | }
|
763 | }
|
764 | }
|
765 | }
|
766 | }
|
767 |
|
768 |
|
769 | if (param && !param['x-s2o-delete']) {
|
770 | delete param.type;
|
771 | for (let prop of common.parameterTypeProperties) {
|
772 | delete param[prop];
|
773 | }
|
774 |
|
775 | if ((param.in === 'path') && ((typeof param.required === 'undefined') || (param.required !== true))) {
|
776 | if (options.patch) {
|
777 | param.required = true;
|
778 | }
|
779 | else {
|
780 | throwError('(Patchable) path parameters must be required:true ['+param.name+' in '+index+']', options);
|
781 | }
|
782 | }
|
783 | }
|
784 |
|
785 | return op;
|
786 | }
|
787 |
|
788 | function copyExtensions(src, tgt) {
|
789 | for (let prop in src) {
|
790 | if (prop.startsWith('x-') && !prop.startsWith('x-s2o')) {
|
791 | tgt[prop] = src[prop];
|
792 | }
|
793 | }
|
794 | }
|
795 |
|
796 | function processResponse(response, name, op, openapi, options) {
|
797 | if (!response) return false;
|
798 | if (response.$ref && (typeof response.$ref === 'string')) {
|
799 | if (response.$ref.indexOf('#/definitions/') >= 0) {
|
800 |
|
801 | throwOrWarn('definition used as response: ' + response.$ref, response, options);
|
802 | }
|
803 | else {
|
804 | if (response.$ref.startsWith('#/responses/')) {
|
805 | response.$ref = '#/components/responses/' + common.sanitise(decodeURIComponent(response.$ref.replace('#/responses/', '')));
|
806 | }
|
807 | }
|
808 | }
|
809 | else {
|
810 | if ((typeof response.description === 'undefined') || (response.description === null)
|
811 | || ((response.description === '') && options.patch)) {
|
812 | if (options.patch) {
|
813 | if ((typeof response === 'object') && (!Array.isArray(response))) {
|
814 | response.description = (statusCodes[response] || '');
|
815 | }
|
816 | }
|
817 | else {
|
818 | throwError('(Patchable) response.description is mandatory', options);
|
819 | }
|
820 | }
|
821 | if (typeof response.schema !== 'undefined') {
|
822 |
|
823 | fixUpSchema(response.schema,options);
|
824 |
|
825 | if (response.schema.$ref && (typeof response.schema.$ref === 'string') && response.schema.$ref.startsWith('#/responses/')) {
|
826 | response.schema.$ref = '#/components/responses/' + common.sanitise(decodeURIComponent(response.schema.$ref.replace('#/responses/', '')));
|
827 | }
|
828 |
|
829 | if (op && op.produces && (typeof op.produces === 'string')) {
|
830 | if (options.patch) {
|
831 | op.produces = [op.produces];
|
832 | }
|
833 | else {
|
834 | return throwError('(Patchable) operation.produces must be an array', options);
|
835 | }
|
836 | }
|
837 | if (openapi.produces && !Array.isArray(openapi.produces)) delete openapi.produces;
|
838 |
|
839 | let produces = ((op ? op.produces : null) || (openapi.produces || [])).filter(common.uniqueOnly);
|
840 | if (!produces.length) produces.push('*/*');
|
841 |
|
842 | response.content = {};
|
843 | for (let mimetype of produces) {
|
844 | response.content[mimetype] = {};
|
845 | response.content[mimetype].schema = clone(response.schema);
|
846 | if (response.examples && response.examples[mimetype]) {
|
847 | let example = {};
|
848 | example.value = response.examples[mimetype];
|
849 | response.content[mimetype].examples = {};
|
850 | response.content[mimetype].examples.response = example;
|
851 | delete response.examples[mimetype];
|
852 | }
|
853 | if (response.content[mimetype].schema.type === 'file') {
|
854 | response.content[mimetype].schema = { type: 'string', format: 'binary' };
|
855 | }
|
856 | }
|
857 | delete response.schema;
|
858 | }
|
859 |
|
860 | for (let mimetype in response.examples) {
|
861 | if (!response.content) response.content = {};
|
862 | if (!response.content[mimetype]) response.content[mimetype] = {};
|
863 | response.content[mimetype].examples = {};
|
864 | response.content[mimetype].examples.response = {};
|
865 | response.content[mimetype].examples.response.value = response.examples[mimetype];
|
866 | }
|
867 | delete response.examples;
|
868 |
|
869 | if (response.headers) {
|
870 | for (let h in response.headers) {
|
871 | if (h.toLowerCase() === 'status code') {
|
872 | if (options.patch) {
|
873 | delete response.headers[h];
|
874 | }
|
875 | else {
|
876 | throwError('(Patchable) "Status Code" is not a valid header', options);
|
877 | }
|
878 | }
|
879 | else {
|
880 | processHeader(response.headers[h], options);
|
881 | }
|
882 | }
|
883 | }
|
884 | }
|
885 | }
|
886 |
|
887 | function processPaths(container, containerName, options, requestBodyCache, openapi) {
|
888 | for (let p in container) {
|
889 | let path = container[p];
|
890 |
|
891 | if (path && (path['x-trace']) && (typeof path['x-trace'] === 'object')) {
|
892 | path.trace = path['x-trace'];
|
893 | delete path['x-trace'];
|
894 | }
|
895 | if (path && (path['x-summary']) && (typeof path['x-summary'] === 'string')) {
|
896 | path.summary = path['x-summary'];
|
897 | delete path['x-summary'];
|
898 | }
|
899 | if (path && (path['x-description']) && (typeof path['x-description'] === 'string')) {
|
900 | path.description = path['x-description'];
|
901 | delete path['x-description'];
|
902 | }
|
903 | if (path && (path['x-servers']) && (Array.isArray(path['x-servers']))) {
|
904 | path.servers = path['x-servers'];
|
905 | delete path['x-servers'];
|
906 | }
|
907 | for (let method in path) {
|
908 | if ((common.httpMethods.indexOf(method) >= 0) || (method === 'x-amazon-apigateway-any-method')) {
|
909 | let op = path[method];
|
910 |
|
911 | if (op && op.parameters && Array.isArray(op.parameters)) {
|
912 | if (path.parameters) {
|
913 | for (let param of path.parameters) {
|
914 | if (typeof param.$ref === 'string') {
|
915 | fixParamRef(param, options);
|
916 | param = resolveInternal(openapi, param.$ref);
|
917 | }
|
918 | let match = op.parameters.find(function (e, i, a) {
|
919 | return ((e.name === param.name) && (e.in === param.in));
|
920 | });
|
921 |
|
922 | if (!match && ((param.in === 'formData') || (param.in === 'body') || (param.type === 'file'))) {
|
923 | op = processParameter(param, op, path, method, p, openapi, options);
|
924 | if (options.rbname && op[options.rbname] === '') {
|
925 | delete op[options.rbname];
|
926 | }
|
927 | }
|
928 | }
|
929 | }
|
930 | for (let param of op.parameters) {
|
931 | op = processParameter(param, op, path, method, method + ':' + p, openapi, options);
|
932 | }
|
933 | if (options.rbname && op[options.rbname] === '') {
|
934 | delete op[options.rbname];
|
935 | }
|
936 | if (!options.debug) {
|
937 | if (op.parameters) op.parameters = op.parameters.filter(keepParameters);
|
938 | }
|
939 | }
|
940 |
|
941 | if (op && op.security) processSecurity(op.security);
|
942 |
|
943 |
|
944 |
|
945 |
|
946 | if (typeof op === 'object') {
|
947 | if (!op.responses) {
|
948 | let defaultResp = {};
|
949 | defaultResp.description = 'Default response';
|
950 | op.responses = { default: defaultResp };
|
951 | }
|
952 | for (let r in op.responses) {
|
953 | let response = op.responses[r];
|
954 | processResponse(response, r, op, openapi, options);
|
955 | }
|
956 | }
|
957 |
|
958 | if (op && (op['x-servers']) && (Array.isArray(op['x-servers']))) {
|
959 | op.servers = op['x-servers'];
|
960 | delete op['x-servers'];
|
961 | } else if (op && op.schemes && op.schemes.length) {
|
962 | for (let scheme of op.schemes) {
|
963 | if ((!openapi.schemes) || (openapi.schemes.indexOf(scheme) < 0)) {
|
964 | if (!op.servers) {
|
965 | op.servers = [];
|
966 | }
|
967 | if (Array.isArray(openapi.servers)) {
|
968 | for (let server of openapi.servers) {
|
969 | let newServer = clone(server);
|
970 | let serverUrl = url.parse(newServer.url);
|
971 | serverUrl.protocol = scheme;
|
972 | newServer.url = serverUrl.format();
|
973 | op.servers.push(newServer);
|
974 | }
|
975 | }
|
976 | }
|
977 | }
|
978 | }
|
979 |
|
980 | if (options.debug) {
|
981 | op["x-s2o-consumes"] = op.consumes || [];
|
982 | op["x-s2o-produces"] = op.produces || [];
|
983 | }
|
984 | if (op) {
|
985 | delete op.consumes;
|
986 | delete op.produces;
|
987 | delete op.schemes;
|
988 |
|
989 | if (op["x-ms-examples"]) {
|
990 | for (let e in op["x-ms-examples"]) {
|
991 | let example = op["x-ms-examples"][e];
|
992 | let se = common.sanitiseAll(e);
|
993 | if (example.parameters) {
|
994 | for (let p in example.parameters) {
|
995 | let value = example.parameters[p];
|
996 | for (let param of (op.parameters||[]).concat(path.parameters||[])) {
|
997 | if (param.$ref) {
|
998 | param = jptr.jptr(openapi,param.$ref);
|
999 | }
|
1000 | if ((param.name === p) && (!param.example)) {
|
1001 | if (!param.examples) {
|
1002 | param.examples = {};
|
1003 | }
|
1004 | param.examples[e] = {value: value};
|
1005 | }
|
1006 | }
|
1007 | }
|
1008 | }
|
1009 | if (example.responses) {
|
1010 | for (let r in example.responses) {
|
1011 | if (example.responses[r].headers) {
|
1012 | for (let h in example.responses[r].headers) {
|
1013 | let value = example.responses[r].headers[h];
|
1014 | for (let rh in op.responses[r].headers) {
|
1015 | if (rh === h) {
|
1016 | let header = op.responses[r].headers[rh];
|
1017 | header.example = value;
|
1018 | }
|
1019 | }
|
1020 | }
|
1021 | }
|
1022 | if (example.responses[r].body) {
|
1023 | openapi.components.examples[se] = { value: clone(example.responses[r].body) };
|
1024 | if (op.responses[r] && op.responses[r].content) {
|
1025 | for (let ct in op.responses[r].content) {
|
1026 | let contentType = op.responses[r].content[ct];
|
1027 | if (!contentType.examples) {
|
1028 | contentType.examples = {};
|
1029 | }
|
1030 | contentType.examples[e] = { $ref: '#/components/examples/'+se };
|
1031 | }
|
1032 | }
|
1033 | }
|
1034 |
|
1035 | }
|
1036 | }
|
1037 | }
|
1038 | delete op["x-ms-examples"];
|
1039 | }
|
1040 |
|
1041 | if (op.parameters && op.parameters.length === 0) delete op.parameters;
|
1042 | if (op.requestBody) {
|
1043 | let effectiveOperationId = op.operationId ? common.sanitiseAll(op.operationId) : common.sanitiseAll(method + p).toCamelCase();
|
1044 | let rbName = common.sanitise(op.requestBody['x-s2o-name'] || effectiveOperationId || '');
|
1045 | delete op.requestBody['x-s2o-name'];
|
1046 | let rbStr = JSON.stringify(op.requestBody);
|
1047 | let rbHash = common.hash(rbStr);
|
1048 | if (!requestBodyCache[rbHash]) {
|
1049 | let entry = {};
|
1050 | entry.name = rbName;
|
1051 | entry.body = op.requestBody;
|
1052 | entry.refs = [];
|
1053 | requestBodyCache[rbHash] = entry;
|
1054 | }
|
1055 | let ptr = '#/'+containerName+'/'+encodeURIComponent(jptr.jpescape(p))+'/'+method+'/requestBody';
|
1056 | requestBodyCache[rbHash].refs.push(ptr);
|
1057 | }
|
1058 | }
|
1059 |
|
1060 | }
|
1061 | }
|
1062 | if (path && path.parameters) {
|
1063 | for (let p2 in path.parameters) {
|
1064 | let param = path.parameters[p2];
|
1065 | processParameter(param, null, path, null, p, openapi, options);
|
1066 | }
|
1067 | if (!options.debug && Array.isArray(path.parameters)) {
|
1068 | path.parameters = path.parameters.filter(keepParameters);
|
1069 | }
|
1070 | }
|
1071 | }
|
1072 | }
|
1073 |
|
1074 | function main(openapi, options) {
|
1075 |
|
1076 | let requestBodyCache = {};
|
1077 | componentNames = { schemas: {} };
|
1078 |
|
1079 | if (openapi.security) processSecurity(openapi.security);
|
1080 |
|
1081 | for (let s in openapi.components.securitySchemes) {
|
1082 | let sname = common.sanitise(s);
|
1083 | if (s != sname) {
|
1084 | if (openapi.components.securitySchemes[sname]) {
|
1085 | throwError('Duplicate sanitised securityScheme name ' + sname, options);
|
1086 | }
|
1087 | openapi.components.securitySchemes[sname] = openapi.components.securitySchemes[s];
|
1088 | delete openapi.components.securitySchemes[s];
|
1089 | }
|
1090 | processSecurityScheme(openapi.components.securitySchemes[sname], options);
|
1091 | }
|
1092 |
|
1093 | for (let s in openapi.components.schemas) {
|
1094 | let sname = common.sanitiseAll(s);
|
1095 | let suffix = '';
|
1096 | if (s != sname) {
|
1097 | while (openapi.components.schemas[sname + suffix]) {
|
1098 |
|
1099 | suffix = (suffix ? ++suffix : 2);
|
1100 | }
|
1101 | openapi.components.schemas[sname + suffix] = openapi.components.schemas[s];
|
1102 | delete openapi.components.schemas[s];
|
1103 | }
|
1104 | componentNames.schemas[s] = sname + suffix;
|
1105 | fixUpSchema(openapi.components.schemas[sname+suffix],options)
|
1106 | }
|
1107 |
|
1108 |
|
1109 | options.refmap = {};
|
1110 | recurse(openapi, { payload: { options: options } }, fixupRefs);
|
1111 | dedupeRefs(openapi,options);
|
1112 |
|
1113 | for (let p in openapi.components.parameters) {
|
1114 | let sname = common.sanitise(p);
|
1115 | if (p != sname) {
|
1116 | if (openapi.components.parameters[sname]) {
|
1117 | throwError('Duplicate sanitised parameter name ' + sname, options);
|
1118 | }
|
1119 | openapi.components.parameters[sname] = openapi.components.parameters[p];
|
1120 | delete openapi.components.parameters[p];
|
1121 | }
|
1122 | let param = openapi.components.parameters[sname];
|
1123 | processParameter(param, null, null, null, sname, openapi, options);
|
1124 | }
|
1125 |
|
1126 | for (let r in openapi.components.responses) {
|
1127 | let sname = common.sanitise(r);
|
1128 | if (r != sname) {
|
1129 | if (openapi.components.responses[sname]) {
|
1130 | throwError('Duplicate sanitised response name ' + sname, options);
|
1131 | }
|
1132 | openapi.components.responses[sname] = openapi.components.responses[r];
|
1133 | delete openapi.components.responses[r];
|
1134 | }
|
1135 | let response = openapi.components.responses[sname];
|
1136 | processResponse(response, sname, null, openapi, options);
|
1137 | if (response.headers) {
|
1138 | for (let h in response.headers) {
|
1139 | if (h.toLowerCase() === 'status code') {
|
1140 | if (options.patch) {
|
1141 | delete response.headers[h];
|
1142 | }
|
1143 | else {
|
1144 | throwError('(Patchable) "Status Code" is not a valid header', options);
|
1145 | }
|
1146 | }
|
1147 | else {
|
1148 | processHeader(response.headers[h], options);
|
1149 | }
|
1150 | }
|
1151 | }
|
1152 | }
|
1153 |
|
1154 | for (let r in openapi.components.requestBodies) {
|
1155 | let rb = openapi.components.requestBodies[r];
|
1156 | let rbStr = JSON.stringify(rb);
|
1157 | let rbHash = common.hash(rbStr);
|
1158 | let entry = {};
|
1159 | entry.name = r;
|
1160 | entry.body = rb;
|
1161 | entry.refs = [];
|
1162 | requestBodyCache[rbHash] = entry;
|
1163 | }
|
1164 |
|
1165 | processPaths(openapi.paths, 'paths', options, requestBodyCache, openapi);
|
1166 | if (openapi["x-ms-paths"]) {
|
1167 | processPaths(openapi["x-ms-paths"], 'x-ms-paths', options, requestBodyCache, openapi);
|
1168 | }
|
1169 |
|
1170 | if (!options.debug) {
|
1171 | for (let p in openapi.components.parameters) {
|
1172 | let param = openapi.components.parameters[p];
|
1173 | if (param["x-s2o-delete"]) {
|
1174 | delete openapi.components.parameters[p];
|
1175 | }
|
1176 | }
|
1177 | }
|
1178 |
|
1179 | if (options.debug) {
|
1180 | openapi["x-s2o-consumes"] = openapi.consumes || [];
|
1181 | openapi["x-s2o-produces"] = openapi.produces || [];
|
1182 | }
|
1183 | delete openapi.consumes;
|
1184 | delete openapi.produces;
|
1185 | delete openapi.schemes;
|
1186 |
|
1187 | let rbNamesGenerated = [];
|
1188 |
|
1189 | openapi.components.requestBodies = {};
|
1190 |
|
1191 | if (!options.resolveInternal) {
|
1192 | let counter = 1;
|
1193 | for (let e in requestBodyCache) {
|
1194 | let entry = requestBodyCache[e];
|
1195 | if (entry.refs.length > 1) {
|
1196 |
|
1197 | let suffix = '';
|
1198 | if (!entry.name) {
|
1199 | entry.name = 'requestBody';
|
1200 |
|
1201 | suffix = counter++;
|
1202 | }
|
1203 | while (rbNamesGenerated.indexOf(entry.name + suffix) >= 0) {
|
1204 |
|
1205 | suffix = (suffix ? ++suffix : 2);
|
1206 | }
|
1207 | entry.name = entry.name + suffix;
|
1208 | rbNamesGenerated.push(entry.name);
|
1209 | openapi.components.requestBodies[entry.name] = clone(entry.body);
|
1210 | for (let r in entry.refs) {
|
1211 | let ref = {};
|
1212 | ref.$ref = '#/components/requestBodies/' + entry.name;
|
1213 | jptr.jptr(openapi,entry.refs[r],ref);
|
1214 | }
|
1215 | }
|
1216 | }
|
1217 | }
|
1218 |
|
1219 | if (openapi.components.responses && Object.keys(openapi.components.responses).length === 0) {
|
1220 | delete openapi.components.responses;
|
1221 | }
|
1222 | if (openapi.components.parameters && Object.keys(openapi.components.parameters).length === 0) {
|
1223 | delete openapi.components.parameters;
|
1224 | }
|
1225 | if (openapi.components.examples && Object.keys(openapi.components.examples).length === 0) {
|
1226 | delete openapi.components.examples;
|
1227 | }
|
1228 | if (openapi.components.requestBodies && Object.keys(openapi.components.requestBodies).length === 0) {
|
1229 | delete openapi.components.requestBodies;
|
1230 | }
|
1231 | if (openapi.components.securitySchemes && Object.keys(openapi.components.securitySchemes).length === 0) {
|
1232 | delete openapi.components.securitySchemes;
|
1233 | }
|
1234 | if (openapi.components.headers && Object.keys(openapi.components.headers).length === 0) {
|
1235 | delete openapi.components.headers;
|
1236 | }
|
1237 | if (openapi.components.schemas && Object.keys(openapi.components.schemas).length === 0) {
|
1238 | delete openapi.components.schemas;
|
1239 | }
|
1240 | if (openapi.components && Object.keys(openapi.components).length === 0) {
|
1241 | delete openapi.components;
|
1242 | }
|
1243 |
|
1244 | return openapi;
|
1245 | }
|
1246 |
|
1247 | function extractServerParameters(server) {
|
1248 | if (!server || !server.url || (typeof server.url !== 'string')) return server;
|
1249 | server.url = server.url.split('{{').join('{');
|
1250 | server.url = server.url.split('}}').join('}');
|
1251 | server.url.replace(/\{(.+?)\}/g, function (match, group1) {
|
1252 | if (!server.variables) {
|
1253 | server.variables = {};
|
1254 | }
|
1255 | server.variables[group1] = { default: 'unknown' };
|
1256 | });
|
1257 | return server;
|
1258 | }
|
1259 |
|
1260 | function fixInfo(openapi, options, reject) {
|
1261 | if ((typeof openapi.info === 'undefined') || (openapi.info === null)) {
|
1262 | if (options.patch) {
|
1263 | openapi.info = { version: '', title: '' };
|
1264 | }
|
1265 | else {
|
1266 | return reject(new S2OError('(Patchable) info object is mandatory'));
|
1267 | }
|
1268 | }
|
1269 | if ((typeof openapi.info !== 'object') || (Array.isArray(openapi.info))) {
|
1270 | return reject(new S2OError('info must be an object'));
|
1271 | }
|
1272 | if ((typeof openapi.info.title === 'undefined') || (openapi.info.title === null)) {
|
1273 | if (options.patch) {
|
1274 | openapi.info.title = '';
|
1275 | }
|
1276 | else {
|
1277 | return reject(new S2OError('(Patchable) info.title cannot be null'));
|
1278 | }
|
1279 | }
|
1280 | if ((typeof openapi.info.version === 'undefined') || (openapi.info.version === null)) {
|
1281 | if (options.patch) {
|
1282 | openapi.info.version = '';
|
1283 | }
|
1284 | else {
|
1285 | return reject(new S2OError('(Patchable) info.version cannot be null'));
|
1286 | }
|
1287 | }
|
1288 | if (typeof openapi.info.version !== 'string') {
|
1289 | if (options.patch) {
|
1290 | openapi.info.version = openapi.info.version.toString();
|
1291 | }
|
1292 | else {
|
1293 | return reject(new S2OError('(Patchable) info.version must be a string'));
|
1294 | }
|
1295 | }
|
1296 | if (typeof openapi.info.logo !== 'undefined') {
|
1297 | if (options.patch) {
|
1298 | openapi.info['x-logo'] = openapi.info.logo;
|
1299 | delete openapi.info.logo;
|
1300 | }
|
1301 | else return reject(new S2OError('(Patchable) info should not have logo property'));
|
1302 | }
|
1303 | if (typeof openapi.info.termsOfService !== 'undefined') {
|
1304 | if (openapi.info.termsOfService === null) {
|
1305 | if (options.patch) {
|
1306 | openapi.info.termsOfService = '';
|
1307 | }
|
1308 | else {
|
1309 | return reject(new S2OError('(Patchable) info.termsOfService cannot be null'));
|
1310 | }
|
1311 | }
|
1312 | if (url.URL && options.whatwg) {
|
1313 | try {
|
1314 | url.URL.parse(openapi.info.termsOfService);
|
1315 | }
|
1316 | catch (ex) {
|
1317 | if (options.patch) {
|
1318 | delete openapi.info.termsOfService;
|
1319 | }
|
1320 | else return reject(new S2OError('(Patchable) info.termsOfService must be a URL'));
|
1321 | }
|
1322 | }
|
1323 | }
|
1324 | }
|
1325 |
|
1326 | function fixPaths(openapi, options, reject) {
|
1327 | if (typeof openapi.paths === 'undefined') {
|
1328 | if (options.patch) {
|
1329 | openapi.paths = {};
|
1330 | }
|
1331 | else {
|
1332 | return reject(new S2OError('(Patchable) paths object is mandatory'));
|
1333 | }
|
1334 | }
|
1335 | }
|
1336 |
|
1337 | function detectObjectReferences(obj, options) {
|
1338 | const seen = new WeakSet();
|
1339 | recurse(obj, {identityDetection:true}, function (obj, key, state) {
|
1340 | if ((typeof obj[key] === 'object') && (obj[key] !== null)) {
|
1341 | if (seen.has(obj[key])) {
|
1342 | if (options.anchors) {
|
1343 | obj[key] = clone(obj[key]);
|
1344 | }
|
1345 | else {
|
1346 | throwError('YAML anchor or merge key at '+state.path, options);
|
1347 | }
|
1348 | }
|
1349 | else {
|
1350 | seen.add(obj[key]);
|
1351 | }
|
1352 | }
|
1353 | });
|
1354 | }
|
1355 |
|
1356 | function convertObj(swagger, options, callback) {
|
1357 | return maybe(callback, new Promise(function (resolve, reject) {
|
1358 | if (!swagger) swagger = {};
|
1359 | options.original = swagger;
|
1360 | if (!options.text) options.text = yaml.stringify(swagger);
|
1361 | options.externals = [];
|
1362 | options.externalRefs = {};
|
1363 | options.rewriteRefs = true;
|
1364 | options.preserveMiro = true;
|
1365 | options.promise = {};
|
1366 | options.promise.resolve = resolve;
|
1367 | options.promise.reject = reject;
|
1368 | if (!options.cache) options.cache = {};
|
1369 | if (options.source) options.cache[options.source] = options.original;
|
1370 |
|
1371 | detectObjectReferences(swagger, options);
|
1372 |
|
1373 | if (swagger.openapi && (typeof swagger.openapi === 'string') && swagger.openapi.startsWith('3.')) {
|
1374 | options.openapi = cclone(swagger);
|
1375 | fixInfo(options.openapi, options, reject);
|
1376 | fixPaths(options.openapi, options, reject);
|
1377 |
|
1378 | resolver.optionalResolve(options)
|
1379 | .then(function(){
|
1380 | if (options.direct) {
|
1381 | return resolve(options.openapi);
|
1382 | }
|
1383 | else {
|
1384 | return resolve(options);
|
1385 | }
|
1386 | })
|
1387 | .catch(function(ex){
|
1388 | console.warn(ex);
|
1389 | reject(ex);
|
1390 | });
|
1391 | return;
|
1392 | }
|
1393 |
|
1394 | if ((!swagger.swagger) || (swagger.swagger != "2.0")) {
|
1395 | return reject(new S2OError('Unsupported swagger/OpenAPI version: ' + (swagger.openapi ? swagger.openapi : swagger.swagger)));
|
1396 | }
|
1397 |
|
1398 | let openapi = options.openapi = {};
|
1399 | openapi.openapi = (typeof options.targetVersion === 'string' && options.targetVersion.startsWith('3.')) ? options.targetVersion : targetVersion;
|
1400 |
|
1401 | if (options.origin) {
|
1402 | if (!openapi["x-origin"]) {
|
1403 | openapi["x-origin"] = [];
|
1404 | }
|
1405 | let origin = {};
|
1406 | origin.url = options.source||options.origin;
|
1407 | origin.format = 'swagger';
|
1408 | origin.version = swagger.swagger;
|
1409 | origin.converter = {};
|
1410 | origin.converter.url = 'https://github.com/mermade/oas-kit';
|
1411 | origin.converter.version = ourVersion;
|
1412 | openapi["x-origin"].push(origin);
|
1413 | }
|
1414 |
|
1415 |
|
1416 | openapi = Object.assign(openapi, cclone(swagger));
|
1417 | delete openapi.swagger;
|
1418 | recurse(openapi, {}, function(obj, key, state){
|
1419 | if ((obj[key] === null) && (!key.startsWith('x-')) && key !== 'default' && (state.path.indexOf('/example') < 0)) delete obj[key];
|
1420 | });
|
1421 |
|
1422 | if (swagger.host) {
|
1423 | for (let s of (Array.isArray(swagger.schemes) ? swagger.schemes : [''])) {
|
1424 | let server = {};
|
1425 | server.url = (s ? s+':' : '') + '//' + swagger.host + (swagger.basePath ? swagger.basePath : '');
|
1426 | extractServerParameters(server);
|
1427 | if (!openapi.servers) openapi.servers = [];
|
1428 | openapi.servers.push(server);
|
1429 | }
|
1430 | }
|
1431 | else if (swagger.basePath) {
|
1432 | let server = {};
|
1433 | server.url = swagger.basePath;
|
1434 | extractServerParameters(server);
|
1435 | if (!openapi.servers) openapi.servers = [];
|
1436 | openapi.servers.push(server);
|
1437 | }
|
1438 | delete openapi.host;
|
1439 | delete openapi.basePath;
|
1440 |
|
1441 | if (openapi['x-servers'] && Array.isArray(openapi['x-servers'])) {
|
1442 | openapi.servers = openapi['x-servers'];
|
1443 | delete openapi['x-servers'];
|
1444 | }
|
1445 |
|
1446 |
|
1447 |
|
1448 | if (swagger['x-ms-parameterized-host']) {
|
1449 | let xMsPHost = swagger['x-ms-parameterized-host'];
|
1450 | let server = {};
|
1451 | server.url = xMsPHost.hostTemplate + (swagger.basePath ? swagger.basePath : '');
|
1452 | server.variables = {};
|
1453 | for (let msp in xMsPHost.parameters) {
|
1454 | let param = xMsPHost.parameters[msp];
|
1455 | if (param.$ref) {
|
1456 | param = clone(resolveInternal(openapi, param.$ref));
|
1457 | }
|
1458 | if (!msp.startsWith('x-')) {
|
1459 | delete param.required;
|
1460 | delete param.type;
|
1461 | delete param.in;
|
1462 | if (typeof param.default === 'undefined') {
|
1463 | if (param.enum) {
|
1464 | param.default = param.enum[0];
|
1465 | }
|
1466 | else {
|
1467 | param.default = '';
|
1468 | }
|
1469 | }
|
1470 | server.variables[param.name] = param;
|
1471 | delete param.name;
|
1472 | }
|
1473 | }
|
1474 | if (!openapi.servers) openapi.servers = [];
|
1475 | if (xMsPHost.useSchemePrefix === false) {
|
1476 |
|
1477 | openapi.servers.push(server);
|
1478 | } else {
|
1479 |
|
1480 | swagger.schemes.forEach((scheme) => {
|
1481 | openapi.servers.push(
|
1482 | Object.assign({}, server, { url: scheme + '://' + server.url })
|
1483 | )
|
1484 | });
|
1485 | }
|
1486 | delete openapi['x-ms-parameterized-host'];
|
1487 | }
|
1488 |
|
1489 | fixInfo(openapi, options, reject);
|
1490 | fixPaths(openapi, options, reject);
|
1491 |
|
1492 | if (typeof openapi.consumes === 'string') {
|
1493 | openapi.consumes = [openapi.consumes];
|
1494 | }
|
1495 | if (typeof openapi.produces === 'string') {
|
1496 | openapi.produces = [openapi.produces];
|
1497 | }
|
1498 |
|
1499 | openapi.components = {};
|
1500 | if (openapi['x-callbacks']) {
|
1501 | openapi.components.callbacks = openapi['x-callbacks'];
|
1502 | delete openapi['x-callbacks'];
|
1503 | }
|
1504 | openapi.components.examples = {};
|
1505 | openapi.components.headers = {};
|
1506 | if (openapi['x-links']) {
|
1507 | openapi.components.links = openapi['x-links'];
|
1508 | delete openapi['x-links'];
|
1509 | }
|
1510 | openapi.components.parameters = openapi.parameters || {};
|
1511 | openapi.components.responses = openapi.responses || {};
|
1512 | openapi.components.requestBodies = {};
|
1513 | openapi.components.securitySchemes = openapi.securityDefinitions || {};
|
1514 | openapi.components.schemas = openapi.definitions || {};
|
1515 | delete openapi.definitions;
|
1516 | delete openapi.responses;
|
1517 | delete openapi.parameters;
|
1518 | delete openapi.securityDefinitions;
|
1519 |
|
1520 | resolver.optionalResolve(options)
|
1521 | .then(function(){
|
1522 | main(options.openapi, options);
|
1523 | if (options.direct) {
|
1524 | resolve(options.openapi);
|
1525 | }
|
1526 | else {
|
1527 | resolve(options);
|
1528 | }
|
1529 | })
|
1530 | .catch(function(ex){
|
1531 | console.warn(ex);
|
1532 | reject(ex);
|
1533 | });
|
1534 |
|
1535 | }));
|
1536 | }
|
1537 |
|
1538 | function convertStr(str, options, callback) {
|
1539 | return maybe(callback, new Promise(function (resolve, reject) {
|
1540 | let obj = null;
|
1541 | let error = null;
|
1542 | try {
|
1543 | obj = JSON.parse(str);
|
1544 | options.text = JSON.stringify(obj,null,2);
|
1545 | }
|
1546 | catch (ex) {
|
1547 | error = ex;
|
1548 | try {
|
1549 | obj = yaml.parse(str, { schema: 'core', prettyErrors: true });
|
1550 | options.sourceYaml = true;
|
1551 | options.text = str;
|
1552 | }
|
1553 | catch (ex) {
|
1554 | error = ex;
|
1555 | }
|
1556 | }
|
1557 | if (obj) {
|
1558 | convertObj(obj, options)
|
1559 | .then(options => resolve(options))
|
1560 | .catch(ex => reject(ex));
|
1561 | }
|
1562 | else {
|
1563 | reject(new S2OError(error ? error.message : 'Could not parse string'));
|
1564 | }
|
1565 | }));
|
1566 | }
|
1567 |
|
1568 | function convertUrl(url, options, callback) {
|
1569 | return maybe(callback, new Promise(function (resolve, reject) {
|
1570 | options.origin = true;
|
1571 | if (!options.source) {
|
1572 | options.source = url;
|
1573 | }
|
1574 | if (options.verbose) {
|
1575 | console.warn('GET ' + url);
|
1576 | }
|
1577 | if (!options.fetch) {
|
1578 | options.fetch = fetch;
|
1579 | }
|
1580 | const fetchOptions = Object.assign({}, options.fetchOptions, {agent:options.agent});
|
1581 | options.fetch(url, fetchOptions).then(function (res) {
|
1582 | if (res.status !== 200) throw new S2OError(`Received status code ${res.status}: ${url}`);
|
1583 | return res.text();
|
1584 | }).then(function (body) {
|
1585 | convertStr(body, options)
|
1586 | .then(options => resolve(options))
|
1587 | .catch(ex => reject(ex));
|
1588 | }).catch(function (err) {
|
1589 | reject(err);
|
1590 | });
|
1591 | }));
|
1592 | }
|
1593 |
|
1594 | function convertFile(filename, options, callback) {
|
1595 | return maybe(callback, new Promise(function (resolve, reject) {
|
1596 | fs.readFile(filename, options.encoding || 'utf8', function (err, s) {
|
1597 | if (err) {
|
1598 | reject(err);
|
1599 | }
|
1600 | else {
|
1601 | options.sourceFile = filename;
|
1602 | convertStr(s, options)
|
1603 | .then(options => resolve(options))
|
1604 | .catch(ex => reject(ex));
|
1605 | }
|
1606 | });
|
1607 | }));
|
1608 | }
|
1609 |
|
1610 | function convertStream(readable, options, callback) {
|
1611 | return maybe(callback, new Promise(function (resolve, reject) {
|
1612 | let data = '';
|
1613 | readable.on('data', function (chunk) {
|
1614 | data += chunk;
|
1615 | })
|
1616 | .on('end', function () {
|
1617 | convertStr(data, options)
|
1618 | .then(options => resolve(options))
|
1619 | .catch(ex => reject(ex));
|
1620 | });
|
1621 | }));
|
1622 | }
|
1623 |
|
1624 | module.exports = {
|
1625 | S2OError: S2OError,
|
1626 | targetVersion: targetVersion,
|
1627 | convert: convertObj,
|
1628 | convertObj: convertObj,
|
1629 | convertUrl: convertUrl,
|
1630 | convertStr: convertStr,
|
1631 | convertFile: convertFile,
|
1632 | convertStream: convertStream
|
1633 | };
|