1 |
|
2 | 'use strict';
|
3 |
|
4 | const fs = require('fs');
|
5 | const url = require('url');
|
6 | const pathlib = require('path');
|
7 |
|
8 | const maybe = require('call-me-maybe');
|
9 | const fetch = require('node-fetch-h2');
|
10 | const yaml = require('yaml');
|
11 |
|
12 | const jptr = require('reftools/lib/jptr.js');
|
13 | const resolveInternal = jptr.jptr;
|
14 | const isRef = require('reftools/lib/isref.js').isRef;
|
15 | const clone = require('reftools/lib/clone.js').clone;
|
16 | const cclone = require('reftools/lib/clone.js').circularClone;
|
17 | const recurse = require('reftools/lib/recurse.js').recurse;
|
18 | const resolver = require('oas-resolver');
|
19 | const sw = require('oas-schema-walker');
|
20 | const common = require('oas-kit-common');
|
21 |
|
22 | const statusCodes = require('./lib/statusCodes.js').statusCodes;
|
23 |
|
24 | const ourVersion = require('./package.json').version;
|
25 |
|
26 |
|
27 |
|
28 | const targetVersion = '3.0.0';
|
29 | let componentNames;
|
30 |
|
31 | class S2OError extends Error {
|
32 | constructor(message) {
|
33 | super(message);
|
34 | this.name = 'S2OError';
|
35 | }
|
36 | }
|
37 |
|
38 | function throwError(message, options) {
|
39 | let err = new S2OError(message);
|
40 | err.options = options;
|
41 | if (options.promise) {
|
42 | options.promise.reject(err);
|
43 | }
|
44 | else {
|
45 | throw err;
|
46 | }
|
47 | }
|
48 |
|
49 | function throwOrWarn(message, container, options) {
|
50 | if (options.warnOnly) {
|
51 | container[options.warnProperty||'x-s2o-warning'] = message;
|
52 | }
|
53 | else {
|
54 | throwError(message, options);
|
55 | }
|
56 | }
|
57 |
|
58 | function fixUpSubSchema(schema,parent,options) {
|
59 | if (schema.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;
|
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 |
|
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 |
|
143 |
|
144 |
|
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 |
|
155 | function 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 |
|
189 | function fixUpSchema(schema,options) {
|
190 | sw.walkSchema(schema,{},{},function(schema,parent,state){
|
191 | fixUpSubSchemaExtensions(schema,parent);
|
192 | fixUpSubSchema(schema,parent,options);
|
193 | });
|
194 | }
|
195 |
|
196 | function 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 |
|
206 | function fixupRefs(obj, key, state) {
|
207 | let options = state.payload.options;
|
208 | if (isRef(obj,key)) {
|
209 | if (obj[key].startsWith('#/components/')) {
|
210 |
|
211 | }
|
212 | else if (obj[key] === '#/consumes') {
|
213 |
|
214 | delete obj[key];
|
215 | state.parent[state.pkey] = clone(options.openapi.consumes);
|
216 | }
|
217 | else if (obj[key] === '#/produces') {
|
218 |
|
219 | delete obj[key];
|
220 | state.parent[state.pkey] = clone(options.openapi.produces);
|
221 | }
|
222 | else if (obj[key].startsWith('#/definitions/')) {
|
223 |
|
224 | let keys = obj[key].replace('#/definitions/', '').split('/');
|
225 | const ref = jptr.jpunescape(keys[0]);
|
226 |
|
227 | let newKey = componentNames.schemas[decodeURIComponent(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 |
|
238 | obj[key] = '#/components/parameters/' + common.sanitise(obj[key].replace('#/parameters/', ''));
|
239 | }
|
240 | else if (obj[key].startsWith('#/responses/')) {
|
241 |
|
242 | obj[key] = '#/components/responses/' + common.sanitise(obj[key].replace('#/responses/', ''));
|
243 | }
|
244 | else if (obj[key].startsWith('#')) {
|
245 |
|
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 |
|
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 |
|
267 |
|
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 |
|
306 | if (Object.keys(obj).length > 1) {
|
307 | const tmpRef = obj[key];
|
308 | const inSchema = state.path.indexOf('/schema') >= 0;
|
309 | if (options.refSiblings === 'preserve') {
|
310 |
|
311 | }
|
312 | else if (inSchema && (options.refSiblings === 'allOf')) {
|
313 | delete obj.$ref;
|
314 | state.parent[state.pkey] = { allOf: [ { $ref: tmpRef }, obj ]};
|
315 | }
|
316 | else {
|
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])];
|
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 |
|
337 |
|
338 |
|
339 | function dedupeRefs(openapi, options) {
|
340 | for (let ref in options.refmap) {
|
341 | jptr.jptr(openapi,ref,{ $ref: options.refmap[ref] });
|
342 | }
|
343 | }
|
344 |
|
345 | function 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 |
|
357 | function 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 |
|
388 | function keepParameters(value) {
|
389 | return (value && !value["x-s2o-delete"]);
|
390 | }
|
391 |
|
392 | function 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);
|
410 | }
|
411 | else if (header.collectionFormat === 'pipes') {
|
412 | throwOrWarn('collectionFormat:pipes is no longer supported for headers', header, options);
|
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);
|
419 | header["x-collectionFormat"] = 'tsv';
|
420 | }
|
421 | else {
|
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 |
|
451 | function 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 |
|
461 | function 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 = {};
|
471 | return newOp;
|
472 | }
|
473 |
|
474 |
|
475 |
|
476 |
|
477 | function 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 |
|
496 | fixParamRef(param, options);
|
497 | let ptr = decodeURIComponent(param.$ref.replace('#/components/parameters/', ''));
|
498 | let rbody = false;
|
499 | let target = openapi.components.parameters[ptr];
|
500 |
|
501 | if (((!target) || (target["x-s2o-delete"])) && param.$ref.startsWith('#/')) {
|
502 |
|
503 | param["x-s2o-delete"] = true;
|
504 | rbody = true;
|
505 | }
|
506 |
|
507 |
|
508 |
|
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;
|
518 | }
|
519 | }
|
520 | }
|
521 |
|
522 | if (param && (param.name || param.in)) {
|
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 |
|
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 |
|
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);
|
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);
|
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);
|
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++;
|
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];
|
621 | }
|
622 |
|
623 |
|
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') {
|
639 | param.allowReserved = true;
|
640 | delete param["x-ms-skip-url-encoding"];
|
641 | }
|
642 | }
|
643 | }
|
644 |
|
645 | if (param && param.in === 'formData') {
|
646 |
|
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;
|
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 |
|
693 | copyExtensions(param, target);
|
694 | }
|
695 | }
|
696 | else if (param && (param.type === 'file')) {
|
697 |
|
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 |
|
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');
|
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 |
|
734 | copyExtensions(param, result);
|
735 | }
|
736 |
|
737 | if (Object.keys(result).length > 0) {
|
738 | param["x-s2o-delete"] = true;
|
739 |
|
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);
|
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 |
|
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 |
|
805 | function 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 |
|
813 | function 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 |
|
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('*/*');
|
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 |
|
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 |
|
907 | function processPaths(container, containerName, options, requestBodyCache, openapi) {
|
908 | for (let p in container) {
|
909 | let path = container[p];
|
910 |
|
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 |
|
964 |
|
965 |
|
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);
|
1086 | }
|
1087 | if (!options.debug && Array.isArray(path.parameters)) {
|
1088 | path.parameters = path.parameters.filter(keepParameters);
|
1089 | }
|
1090 | }
|
1091 | }
|
1092 | }
|
1093 |
|
1094 | function 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 |
|
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 |
|
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) {
|
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 = {};
|
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 |
|
1218 | let suffix = '';
|
1219 | if (!entry.name) {
|
1220 | entry.name = 'requestBody';
|
1221 |
|
1222 | suffix = counter++;
|
1223 | }
|
1224 | while (rbNamesGenerated.indexOf(entry.name + suffix) >= 0) {
|
1225 |
|
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 |
|
1268 | function 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) {
|
1273 | if (!server.variables) {
|
1274 | server.variables = {};
|
1275 | }
|
1276 | server.variables[group1] = { default: 'unknown' };
|
1277 | });
|
1278 | return server;
|
1279 | }
|
1280 |
|
1281 | function 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 |
|
1352 | function 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 |
|
1364 | function 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 |
|
1383 | function 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;
|
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)
|
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;
|
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;
|
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 |
|
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];
|
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(/\/$/, '')
|
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 |
|
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;
|
1490 | delete param.type;
|
1491 | delete param.in;
|
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 |
|
1510 | openapi.servers.push(server);
|
1511 | } else {
|
1512 |
|
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)
|
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 |
|
1571 | function 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 |
|
1601 | function 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 |
|
1627 | function 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 |
|
1643 | function 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 |
|
1657 | module.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 | };
|