UNPKG

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