UNPKG

67.4 kBJavaScriptView Raw
1// @ts-check
2'use strict';
3
4const fs = require('fs');
5const url = require('url');
6const pathlib = require('path');
7
8const maybe = require('call-me-maybe');
9const fetch = require('node-fetch-h2');
10const yaml = require('yaml');
11
12const jptr = require('reftools/lib/jptr.js');
13const resolveInternal = jptr.jptr;
14const isRef = require('reftools/lib/isref.js').isRef;
15const clone = require('reftools/lib/clone.js').clone;
16const cclone = require('reftools/lib/clone.js').circularClone;
17const recurse = require('reftools/lib/recurse.js').recurse;
18const resolver = require('oas-resolver');
19const sw = require('oas-schema-walker');
20const common = require('oas-kit-common');
21
22const statusCodes = require('./lib/statusCodes.js').statusCodes;
23
24const ourVersion = require('./package.json').version;
25
26// TODO handle specification-extensions with plugins?
27
28const targetVersion = '3.0.0';
29let componentNames; // initialised in main
30
31class S2OError extends Error {
32 constructor(message) {
33 super(message);
34 this.name = 'S2OError';
35 }
36}
37
38function 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
49function 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
58function 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; // means was just null => nullable
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 // do not else this
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 // TODO if we have a nested properties (object inside an object) and the
137 // *parent* type is not set, force it to object
138 // TODO if default is set but type is not set, force type to typeof default
139
140 if (schema.xml && typeof schema.xml.namespace === 'string') {
141 if (!schema.xml.namespace) delete schema.xml.namespace;
142 }
143}
144
145function 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
179function fixUpSchema(schema,options) {
180 sw.walkSchema(schema,{},{},function(schema,parent,state){
181 fixUpSubSchemaExtensions(schema,parent);
182 fixUpSubSchema(schema,parent,options);
183 });
184}
185
186function 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
196function fixupRefs(obj, key, state) {
197 let options = state.payload.options;
198 if (isRef(obj,key)) {
199 if (obj[key].startsWith('#/components/')) {
200 // no-op
201 }
202 else if (obj[key] === '#/consumes') {
203 // people are *so* creative
204 delete obj[key];
205 state.parent[state.pkey] = clone(options.openapi.consumes);
206 }
207 else if (obj[key] === '#/produces') {
208 // and by creative, I mean devious
209 delete obj[key];
210 state.parent[state.pkey] = clone(options.openapi.produces);
211 }
212 else if (obj[key].startsWith('#/definitions/')) {
213 //only the first part of a schema component name must be sanitised
214 let keys = obj[key].replace('#/definitions/', '').split('/');
215 const ref = jptr.jpunescape(keys[0]);
216
217 let newKey = componentNames.schemas[decodeURIComponent(ref)]; // lookup, resolves a $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 // for extensions like Apigee's x-templates
228 obj[key] = '#/components/parameters/' + common.sanitise(obj[key].replace('#/parameters/', ''));
229 }
230 else if (obj[key].startsWith('#/responses/')) {
231 // for extensions like Apigee's x-templates
232 obj[key] = '#/components/responses/' + common.sanitise(obj[key].replace('#/responses/', ''));
233 }
234 else if (obj[key].startsWith('#')) {
235 // fixes up direct $refs or those created by resolvers
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 // we use a heuristic to determine what kind of thing is being referenced
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 // non-body/form parameters have not moved in the overall structure (like responses)
257 // but extracting the requestBodies can cause the *number* of parameters to change
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 // do this last - rework cases where $ref object has sibling properties
296 if (Object.keys(obj).length > 1) {
297 const tmpRef = obj[key];
298 const inSchema = state.path.indexOf('/schema') >= 0; // not perfect, but in the absence of a reasonably-sized and complete OAS 2.0 parser...
299 if (options.refSiblings === 'preserve') {
300 // no-op
301 }
302 else if (inSchema && (options.refSiblings === 'allOf')) {
303 delete obj.$ref;
304 state.parent[state.pkey] = { allOf: [ { $ref: tmpRef }, obj ]};
305 }
306 else { // remove, or not 'preserve' and not in a schema
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])]; // lookup, resolves a $ref
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* This has to happen as a separate pass because multiple $refs may point
327* through elements of the same path
328*/
329function dedupeRefs(openapi, options) {
330 for (let ref in options.refmap) {
331 jptr.jptr(openapi,ref,{ $ref: options.refmap[ref] });
332 }
333}
334
335function 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
347function 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
377function keepParameters(value) {
378 return (value && !value["x-s2o-delete"]);
379}
380
381function 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); // not lossless
399 }
400 else if (header.collectionFormat === 'pipes') {
401 throwOrWarn('collectionFormat:pipes is no longer supported for headers', header, options); // not lossless
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); // not lossless
408 header["x-collectionFormat"] = 'tsv';
409 }
410 else { // 'csv'
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
439function 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
449function 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 = {}; // just in case there are no parameters
459 return newOp;
460}
461
462/**
463 * @returns op, as it may have changed
464 */
465function 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 // if we still have a ref here, it must be an internal one
483 fixParamRef(param, options);
484 let ptr = decodeURIComponent(param.$ref.replace('#/components/parameters/', ''));
485 let rbody = false;
486 let target = openapi.components.parameters[ptr]; // resolves a $ref, must have been sanitised already
487
488 if (((!target) || (target["x-s2o-delete"])) && param.$ref.startsWith('#/')) {
489 // if it's gone, chances are it's a requestBody component now unless spec was broken
490 param["x-s2o-delete"] = true;
491 rbody = true;
492 }
493
494 // shared formData parameters from swagger or path level could be used in any combination.
495 // we dereference all op.requestBody's then hash them and pull out common ones later
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; // preserve reference
505 }
506 }
507 }
508
509 if (param && (param.name || param.in)) { // if it's a real parameter OR we've dereferenced it
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 // $ref anywhere sensibility
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 // $ref anywhere sensibility
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); // not lossless
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); // not lossless
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); // not lossless
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]; // not lossless
605 }
606 // items in 2.0 was a subset of the JSON-Schema items
607 // object, it gets fixed up below
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') { // might be in:path, not allowed in OAS3
623 param.allowReserved = true;
624 delete param["x-ms-skip-url-encoding"];
625 }
626 }
627 }
628
629 if (param && param.in === 'formData') {
630 // convert to requestBody component
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; // new are anyOf, oneOf, not
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 // Copy any extensions on the form param to the target schema property.
677 copyExtensions(param, target);
678 }
679 }
680 else if (param && param.type === 'file') {
681 // convert to requestBody
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 // Set the "request body name" extension on the operation if requested.
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'); // TODO verify default
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 // Copy any extensions from the original parameter to the new requestBody
718 copyExtensions(param, result);
719 }
720
721 if (Object.keys(result).length > 0) {
722 param["x-s2o-delete"] = true;
723 // work out where to attach the requestBody
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); // make sure we have one
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 // tidy up
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
788function 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
796function 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 //response.$ref = '#/components/schemas/'+common.sanitise(response.$ref.replace('#/definitions/',''));
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('*/*'); // TODO verify default
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 // examples for content-types not listed in produces
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
887function processPaths(container, containerName, options, requestBodyCache, openapi) {
888 for (let p in container) {
889 let path = container[p];
890 // path.$ref is external only
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 //don't need to remove requestBody for non-supported ops as they "SHALL be ignored"
944
945 // responses
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); // index here is the path string
1066 }
1067 if (!options.debug && Array.isArray(path.parameters)) {
1068 path.parameters = path.parameters.filter(keepParameters);
1069 }
1070 }
1071 }
1072}
1073
1074function 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 // @ts-ignore
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 // fix all $refs to their new locations (and potentially new names)
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) { // converted ones
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 = {}; // for now as we've dereffed them
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 // create a shared requestBody
1197 let suffix = '';
1198 if (!entry.name) {
1199 entry.name = 'requestBody';
1200 // @ts-ignore
1201 suffix = counter++;
1202 }
1203 while (rbNamesGenerated.indexOf(entry.name + suffix) >= 0) {
1204 // @ts-ignore - this can happen if descriptions are not exactly the same (e.g. bitbucket)
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
1247function 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) { // TODO extend to :parameters (not port)?
1252 if (!server.variables) {
1253 server.variables = {};
1254 }
1255 server.variables[group1] = { default: 'unknown' };
1256 });
1257 return server;
1258}
1259
1260function 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
1326function 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
1337function 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
1356function 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; // avoids stack explosions
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) // is a no-op if options.resolve is not set
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; // we should have resolved or rejected by now
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; // semver
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 // we want the new and existing properties to appear in a sensible order. Not guaranteed
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]; // this saves *so* much grief later
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 // TODO APIMatic extensions (x-server-configuration) ?
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; // all true
1460 delete param.type; // all strings
1461 delete param.in; // all 'host'
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 // The server URL already includes a protocol scheme
1477 openapi.servers.push(server);
1478 } else {
1479 // Define this server once for each given protocol scheme
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) // is a no-op if options.resolve is not set
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
1538function 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
1568function 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
1594function 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
1610function 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
1624module.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};