Outlook_Addin_LLM/node_modules/swagger2openapi/index.js

1667 lines
67 KiB
JavaScript
Raw Normal View History

// @ts-check
'use strict';
const fs = require('fs');
const url = require('url');
const pathlib = require('path');
const maybe = require('call-me-maybe');
const fetch = require('node-fetch-h2');
const yaml = require('yaml');
const jptr = require('reftools/lib/jptr.js');
const resolveInternal = jptr.jptr;
const isRef = require('reftools/lib/isref.js').isRef;
const clone = require('reftools/lib/clone.js').clone;
const cclone = require('reftools/lib/clone.js').circularClone;
const recurse = require('reftools/lib/recurse.js').recurse;
const resolver = require('oas-resolver');
const sw = require('oas-schema-walker');
const common = require('oas-kit-common');
const statusCodes = require('./lib/statusCodes.js').statusCodes;
const ourVersion = require('./package.json').version;
// TODO handle specification-extensions with plugins?
const targetVersion = '3.0.0';
let componentNames; // initialised in main
class S2OError extends Error {
constructor(message) {
super(message);
this.name = 'S2OError';
}
}
function throwError(message, options) {
let err = new S2OError(message);
err.options = options;
if (options.promise) {
options.promise.reject(err);
}
else {
throw err;
}
}
function throwOrWarn(message, container, options) {
if (options.warnOnly) {
container[options.warnProperty||'x-s2o-warning'] = message;
}
else {
throwError(message, options);
}
}
function fixUpSubSchema(schema,parent,options) {
if (schema.nullable) options.patches++;
if (schema.discriminator && typeof schema.discriminator === 'string') {
schema.discriminator = { propertyName: schema.discriminator };
}
if (schema.items && Array.isArray(schema.items)) {
if (schema.items.length === 0) {
schema.items = {};
}
else if (schema.items.length === 1) {
schema.items = schema.items[0];
}
else schema.items = { anyOf: schema.items };
}
if (schema.type && Array.isArray(schema.type)) {
if (options.patch) {
options.patches++;
if (schema.type.length === 0) {
delete schema.type;
}
else {
if (!schema.oneOf) schema.oneOf = [];
for (let type of schema.type) {
let newSchema = {};
if (type === 'null') {
schema.nullable = true;
}
else {
newSchema.type = type;
for (let prop of common.arrayProperties) {
if (typeof schema.prop !== 'undefined') {
newSchema[prop] = schema[prop];
delete schema[prop];
}
}
}
if (newSchema.type) {
schema.oneOf.push(newSchema);
}
}
delete schema.type;
if (schema.oneOf.length === 0) {
delete schema.oneOf; // means was just null => nullable
}
else if (schema.oneOf.length < 2) {
schema.type = schema.oneOf[0].type;
if (Object.keys(schema.oneOf[0]).length > 1) {
throwOrWarn('Lost properties from oneOf',schema,options);
}
delete schema.oneOf;
}
}
// do not else this
if (schema.type && Array.isArray(schema.type) && schema.type.length === 1) {
schema.type = schema.type[0];
}
}
else {
throwError('(Patchable) schema type must not be an array', options);
}
}
if (schema.type && schema.type === 'null') {
delete schema.type;
schema.nullable = true;
}
if ((schema.type === 'array') && (!schema.items)) {
schema.items = {};
}
if (schema.type === 'file') {
schema.type = 'string';
schema.format = 'binary';
}
if (typeof schema.required === 'boolean') {
if (schema.required && schema.name) {
if (typeof parent.required === 'undefined') {
parent.required = [];
}
if (Array.isArray(parent.required)) parent.required.push(schema.name);
}
delete schema.required;
}
// TODO if we have a nested properties (object inside an object) and the
// *parent* type is not set, force it to object
// TODO if default is set but type is not set, force type to typeof default
if (schema.xml && typeof schema.xml.namespace === 'string') {
if (!schema.xml.namespace) delete schema.xml.namespace;
}
if (typeof schema.allowEmptyValue !== 'undefined') {
options.patches++;
delete schema.allowEmptyValue;
}
}
function fixUpSubSchemaExtensions(schema,parent) {
if (schema["x-required"] && Array.isArray(schema["x-required"])) {
if (!schema.required) schema.required = [];
schema.required = schema.required.concat(schema["x-required"]);
delete schema["x-required"];
}
if (schema["x-anyOf"]) {
schema.anyOf = schema["x-anyOf"];
delete schema["x-anyOf"];
}
if (schema["x-oneOf"]) {
schema.oneOf = schema["x-oneOf"];
delete schema["x-oneOf"];
}
if (schema["x-not"]) {
schema.not = schema["x-not"];
delete schema["x-not"];
}
if (typeof schema["x-nullable"] === 'boolean') {
schema.nullable = schema["x-nullable"];
delete schema["x-nullable"];
}
if ((typeof schema["x-discriminator"] === 'object') && (typeof schema["x-discriminator"].propertyName === 'string')) {
schema.discriminator = schema["x-discriminator"];
delete schema["x-discriminator"];
for (let entry in schema.discriminator.mapping) {
let schemaOrRef = schema.discriminator.mapping[entry];
if (schemaOrRef.startsWith('#/definitions/')) {
schema.discriminator.mapping[entry] = schemaOrRef.replace('#/definitions/','#/components/schemas/');
}
}
}
}
function fixUpSchema(schema,options) {
sw.walkSchema(schema,{},{},function(schema,parent,state){
fixUpSubSchemaExtensions(schema,parent);
fixUpSubSchema(schema,parent,options);
});
}
function getMiroComponentName(ref) {
if (ref.indexOf('#')>=0) {
ref = ref.split('#')[1].split('/').pop();
}
else {
ref = ref.split('/').pop().split('.')[0];
}
return encodeURIComponent(common.sanitise(ref));
}
function fixupRefs(obj, key, state) {
let options = state.payload.options;
if (isRef(obj,key)) {
if (obj[key].startsWith('#/components/')) {
// no-op
}
else if (obj[key] === '#/consumes') {
// people are *so* creative
delete obj[key];
state.parent[state.pkey] = clone(options.openapi.consumes);
}
else if (obj[key] === '#/produces') {
// and by creative, I mean devious
delete obj[key];
state.parent[state.pkey] = clone(options.openapi.produces);
}
else if (obj[key].startsWith('#/definitions/')) {
//only the first part of a schema component name must be sanitised
let keys = obj[key].replace('#/definitions/', '').split('/');
const ref = jptr.jpunescape(keys[0]);
let newKey = componentNames.schemas[decodeURIComponent(ref)]; // lookup, resolves a $ref
if (newKey) {
keys[0] = newKey;
}
else {
throwOrWarn('Could not resolve reference '+obj[key],obj,options);
}
obj[key] = '#/components/schemas/' + keys.join('/');
}
else if (obj[key].startsWith('#/parameters/')) {
// for extensions like Apigee's x-templates
obj[key] = '#/components/parameters/' + common.sanitise(obj[key].replace('#/parameters/', ''));
}
else if (obj[key].startsWith('#/responses/')) {
// for extensions like Apigee's x-templates
obj[key] = '#/components/responses/' + common.sanitise(obj[key].replace('#/responses/', ''));
}
else if (obj[key].startsWith('#')) {
// fixes up direct $refs or those created by resolvers
let target = clone(jptr.jptr(options.openapi,obj[key]));
if (target === false) throwOrWarn('direct $ref not found '+obj[key],obj,options)
else if (options.refmap[obj[key]]) {
obj[key] = options.refmap[obj[key]];
}
else {
// we use a heuristic to determine what kind of thing is being referenced
let oldRef = obj[key];
oldRef = oldRef.replace('/properties/headers/','');
oldRef = oldRef.replace('/properties/responses/','');
oldRef = oldRef.replace('/properties/parameters/','');
oldRef = oldRef.replace('/properties/schemas/','');
let type = 'schemas';
let schemaIndex = oldRef.lastIndexOf('/schema');
type = (oldRef.indexOf('/headers/')>schemaIndex) ? 'headers' :
((oldRef.indexOf('/responses/')>schemaIndex) ? 'responses' :
((oldRef.indexOf('/example')>schemaIndex) ? 'examples' :
((oldRef.indexOf('/x-')>schemaIndex) ? 'extensions' :
((oldRef.indexOf('/parameters/')>schemaIndex) ? 'parameters' : 'schemas'))));
// non-body/form parameters have not moved in the overall structure (like responses)
// but extracting the requestBodies can cause the *number* of parameters to change
if (type === 'schemas') {
fixUpSchema(target,options);
}
if ((type !== 'responses') && (type !== 'extensions')) {
let prefix = type.substr(0,type.length-1);
if ((prefix === 'parameter') && target.name && (target.name === common.sanitise(target.name))) {
prefix = encodeURIComponent(target.name);
}
let suffix = 1;
if (obj['x-miro']) {
prefix = getMiroComponentName(obj['x-miro']);
suffix = '';
}
while (jptr.jptr(options.openapi,'#/components/'+type+'/'+prefix+suffix)) {
suffix = (suffix === '' ? 2 : ++suffix);
}
let newRef = '#/components/'+type+'/'+prefix+suffix;
let refSuffix = '';
if (type === 'examples') {
target = { value: target };
refSuffix = '/value';
}
jptr.jptr(options.openapi,newRef,target);
options.refmap[obj[key]] = newRef+refSuffix;
obj[key] = newRef+refSuffix;
}
}
}
delete obj['x-miro'];
// do this last - rework cases where $ref object has sibling properties
if (Object.keys(obj).length > 1) {
const tmpRef = obj[key];
const inSchema = state.path.indexOf('/schema') >= 0; // not perfect, but in the absence of a reasonably-sized and complete OAS 2.0 parser...
if (options.refSiblings === 'preserve') {
// no-op
}
else if (inSchema && (options.refSiblings === 'allOf')) {
delete obj.$ref;
state.parent[state.pkey] = { allOf: [ { $ref: tmpRef }, obj ]};
}
else { // remove, or not 'preserve' and not in a schema
state.parent[state.pkey] = { $ref: tmpRef };
}
}
}
if ((key === 'x-ms-odata') && (typeof obj[key] === 'string') && (obj[key].startsWith('#/'))) {
let keys = obj[key].replace('#/definitions/', '').replace('#/components/schemas/','').split('/');
let newKey = componentNames.schemas[decodeURIComponent(keys[0])]; // lookup, resolves a $ref
if (newKey) {
keys[0] = newKey;
}
else {
throwOrWarn('Could not resolve reference '+obj[key],obj,options);
}
obj[key] = '#/components/schemas/' + keys.join('/');
}
}
/*
* This has to happen as a separate pass because multiple $refs may point
* through elements of the same path
*/
function dedupeRefs(openapi, options) {
for (let ref in options.refmap) {
jptr.jptr(openapi,ref,{ $ref: options.refmap[ref] });
}
}
function processSecurity(securityObject) {
for (let s in securityObject) {
for (let k in securityObject[s]) {
let sname = common.sanitise(k);
if (k !== sname) {
securityObject[s][sname] = securityObject[s][k];
delete securityObject[s][k];
}
}
}
}
function processSecurityScheme(scheme, options) {
if (scheme.type === 'basic') {
scheme.type = 'http';
scheme.scheme = 'basic';
}
if (scheme.type === 'oauth2') {
let flow = {};
let flowName = scheme.flow;
if (scheme.flow === 'application') flowName = 'clientCredentials';
if (scheme.flow === 'accessCode') flowName = 'authorizationCode';
if (typeof scheme.authorizationUrl !== 'undefined') flow.authorizationUrl = scheme.authorizationUrl.split('?')[0].trim() || '/';
if (typeof scheme.tokenUrl === 'string') flow.tokenUrl = scheme.tokenUrl.split('?')[0].trim() || '/';
flow.scopes = scheme.scopes || {};
scheme.flows = {};
scheme.flows[flowName] = flow;
delete scheme.flow;
delete scheme.authorizationUrl;
delete scheme.tokenUrl;
delete scheme.scopes;
if (typeof scheme.name !== 'undefined') {
if (options.patch) {
options.patches++;
delete scheme.name;
}
else {
throwError('(Patchable) oauth2 securitySchemes should not have name property', options);
}
}
}
}
function keepParameters(value) {
return (value && !value["x-s2o-delete"]);
}
function processHeader(header, options) {
if (header.$ref) {
header.$ref = header.$ref.replace('#/responses/', '#/components/responses/');
}
else {
if (header.type && !header.schema) {
header.schema = {};
}
if (header.type) header.schema.type = header.type;
if (header.items && header.items.type !== 'array') {
if (header.items.collectionFormat !== header.collectionFormat) {
throwOrWarn('Nested collectionFormats are not supported', header, options);
}
delete header.items.collectionFormat;
}
if (header.type === 'array') {
if (header.collectionFormat === 'ssv') {
throwOrWarn('collectionFormat:ssv is no longer supported for headers', header, options); // not lossless
}
else if (header.collectionFormat === 'pipes') {
throwOrWarn('collectionFormat:pipes is no longer supported for headers', header, options); // not lossless
}
else if (header.collectionFormat === 'multi') {
header.explode = true;
}
else if (header.collectionFormat === 'tsv') {
throwOrWarn('collectionFormat:tsv is no longer supported', header, options); // not lossless
header["x-collectionFormat"] = 'tsv';
}
else { // 'csv'
header.style = 'simple';
}
delete header.collectionFormat;
}
else if (header.collectionFormat) {
if (options.patch) {
options.patches++;
delete header.collectionFormat;
}
else {
throwError('(Patchable) collectionFormat is only applicable to header.type array', options);
}
}
delete header.type;
for (let prop of common.parameterTypeProperties) {
if (typeof header[prop] !== 'undefined') {
header.schema[prop] = header[prop];
delete header[prop];
}
}
for (let prop of common.arrayProperties) {
if (typeof header[prop] !== 'undefined') {
header.schema[prop] = header[prop];
delete header[prop];
}
}
}
}
function fixParamRef(param, options) {
if (param.$ref.indexOf('#/parameters/') >= 0) {
let refComponents = param.$ref.split('#/parameters/');
param.$ref = refComponents[0] + '#/components/parameters/' + common.sanitise(refComponents[1]);
}
if (param.$ref.indexOf('#/definitions/') >= 0) {
throwOrWarn('Definition used as parameter', param, options);
}
}
function attachRequestBody(op,options) {
let newOp = {};
for (let key of Object.keys(op)) {
newOp[key] = op[key];
if (key === 'parameters') {
newOp.requestBody = {};
if (options.rbname) newOp[options.rbname] = '';
}
}
newOp.requestBody = {}; // just in case there are no parameters
return newOp;
}
/**
* @returns op, as it may have changed
*/
function processParameter(param, op, path, method, index, openapi, options) {
let result = {};
let singularRequestBody = true;
let originalType;
if (op && op.consumes && (typeof op.consumes === 'string')) {
if (options.patch) {
options.patches++;
op.consumes = [op.consumes];
}
else {
return throwError('(Patchable) operation.consumes must be an array', options);
}
}
if (!Array.isArray(openapi.consumes)) delete openapi.consumes;
let consumes = ((op ? op.consumes : null) || (openapi.consumes || [])).filter(common.uniqueOnly);
if (param && param.$ref && (typeof param.$ref === 'string')) {
// if we still have a ref here, it must be an internal one
fixParamRef(param, options);
let ptr = decodeURIComponent(param.$ref.replace('#/components/parameters/', ''));
let rbody = false;
let target = openapi.components.parameters[ptr]; // resolves a $ref, must have been sanitised already
if (((!target) || (target["x-s2o-delete"])) && param.$ref.startsWith('#/')) {
// if it's gone, chances are it's a requestBody component now unless spec was broken
param["x-s2o-delete"] = true;
rbody = true;
}
// shared formData parameters from swagger or path level could be used in any combination.
// we dereference all op.requestBody's then hash them and pull out common ones later
if (rbody) {
let ref = param.$ref;
let newParam = resolveInternal(openapi, param.$ref);
if (!newParam && ref.startsWith('#/')) {
throwOrWarn('Could not resolve reference ' + ref, param, options);
}
else {
if (newParam) param = newParam; // preserve reference
}
}
}
if (param && (param.name || param.in)) { // if it's a real parameter OR we've dereferenced it
if (typeof param['x-deprecated'] === 'boolean') {
param.deprecated = param['x-deprecated'];
delete param['x-deprecated'];
}
if (typeof param['x-example'] !== 'undefined') {
param.example = param['x-example'];
delete param['x-example'];
}
if ((param.in !== 'body') && (!param.type)) {
if (options.patch) {
options.patches++;
param.type = 'string';
}
else {
throwError('(Patchable) parameter.type is mandatory for non-body parameters', options);
}
}
if (param.type && typeof param.type === 'object' && param.type.$ref) {
// $ref anywhere sensibility
param.type = resolveInternal(openapi, param.type.$ref);
}
if (param.type === 'file') {
param['x-s2o-originalType'] = param.type;
originalType = param.type;
}
if (param.description && typeof param.description === 'object' && param.description.$ref) {
// $ref anywhere sensibility
param.description = resolveInternal(openapi, param.description.$ref);
}
if (param.description === null) delete param.description;
let oldCollectionFormat = param.collectionFormat;
if ((param.type === 'array') && !oldCollectionFormat) {
oldCollectionFormat = 'csv';
}
if (oldCollectionFormat) {
if (param.type !== 'array') {
if (options.patch) {
options.patches++;
delete param.collectionFormat;
}
else {
throwError('(Patchable) collectionFormat is only applicable to param.type array', options);
}
}
if ((oldCollectionFormat === 'csv') && ((param.in === 'query') || (param.in === 'cookie'))) {
param.style = 'form';
param.explode = false;
}
if ((oldCollectionFormat === 'csv') && ((param.in === 'path') || (param.in === 'header'))) {
param.style = 'simple';
}
if (oldCollectionFormat === 'ssv') {
if (param.in === 'query') {
param.style = 'spaceDelimited';
}
else {
throwOrWarn('collectionFormat:ssv is no longer supported except for in:query parameters', param, options); // not lossless
}
}
if (oldCollectionFormat === 'pipes') {
if (param.in === 'query') {
param.style = 'pipeDelimited';
}
else {
throwOrWarn('collectionFormat:pipes is no longer supported except for in:query parameters', param, options); // not lossless
}
}
if (oldCollectionFormat === 'multi') {
param.explode = true;
}
if (oldCollectionFormat === 'tsv') {
throwOrWarn('collectionFormat:tsv is no longer supported', param, options); // not lossless
param["x-collectionFormat"] = 'tsv';
}
delete param.collectionFormat;
}
if (param.type && (param.type !== 'body') && (param.in !== 'formData')) {
if (param.items && param.schema) {
throwOrWarn('parameter has array,items and schema', param, options);
}
else {
if (param.schema) options.patches++; // already present
if ((!param.schema) || (typeof param.schema !== 'object')) param.schema = {};
param.schema.type = param.type;
if (param.items) {
param.schema.items = param.items;
delete param.items;
recurse(param.schema.items, null, function (obj, key, state) {
if ((key === 'collectionFormat') && (typeof obj[key] === 'string')) {
if (oldCollectionFormat && obj[key] !== oldCollectionFormat) {
throwOrWarn('Nested collectionFormats are not supported', param, options);
}
delete obj[key]; // not lossless
}
// items in 2.0 was a subset of the JSON-Schema items
// object, it gets fixed up below
});
}
for (let prop of common.parameterTypeProperties) {
if (typeof param[prop] !== 'undefined') param.schema[prop] = param[prop];
delete param[prop];
}
}
}
if (param.schema) {
fixUpSchema(param.schema,options);
}
if (param["x-ms-skip-url-encoding"]) {
if (param.in === 'query') { // might be in:path, not allowed in OAS3
param.allowReserved = true;
delete param["x-ms-skip-url-encoding"];
}
}
}
if (param && param.in === 'formData') {
// convert to requestBody component
singularRequestBody = false;
result.content = {};
let contentType = 'application/x-www-form-urlencoded';
if ((consumes.length) && (consumes.indexOf('multipart/form-data') >= 0)) {
contentType = 'multipart/form-data';
}
result.content[contentType] = {};
if (param.schema) {
result.content[contentType].schema = param.schema;
if (param.schema.$ref) {
result['x-s2o-name'] = decodeURIComponent(param.schema.$ref.replace('#/components/schemas/', ''));
}
}
else {
result.content[contentType].schema = {};
result.content[contentType].schema.type = 'object';
result.content[contentType].schema.properties = {};
result.content[contentType].schema.properties[param.name] = {};
let schema = result.content[contentType].schema;
let target = result.content[contentType].schema.properties[param.name];
if (param.description) target.description = param.description;
if (param.example) target.example = param.example;
if (param.type) target.type = param.type;
for (let prop of common.parameterTypeProperties) {
if (typeof param[prop] !== 'undefined') target[prop] = param[prop];
}
if (param.required === true) {
if (!schema.required) schema.required = [];
schema.required.push(param.name);
result.required = true;
}
if (typeof param.default !== 'undefined') target.default = param.default;
if (target.properties) target.properties = param.properties;
if (param.allOf) target.allOf = param.allOf; // new are anyOf, oneOf, not
if ((param.type === 'array') && (param.items)) {
target.items = param.items;
if (target.items.collectionFormat) delete target.items.collectionFormat;
}
if ((originalType === 'file') || (param['x-s2o-originalType'] === 'file')) {
target.type = 'string';
target.format = 'binary';
}
// Copy any extensions on the form param to the target schema property.
copyExtensions(param, target);
}
}
else if (param && (param.type === 'file')) {
// convert to requestBody
if (param.required) result.required = param.required;
result.content = {};
result.content["application/octet-stream"] = {};
result.content["application/octet-stream"].schema = {};
result.content["application/octet-stream"].schema.type = 'string';
result.content["application/octet-stream"].schema.format = 'binary';
copyExtensions(param, result);
}
if (param && param.in === 'body') {
result.content = {};
if (param.name) result['x-s2o-name'] = (op && op.operationId ? common.sanitiseAll(op.operationId) : '') + ('_' + param.name).toCamelCase();
if (param.description) result.description = param.description;
if (param.required) result.required = param.required;
// Set the "request body name" extension on the operation if requested.
if (op && options.rbname && param.name) {
op[options.rbname] = param.name;
}
if (param.schema && param.schema.$ref) {
result['x-s2o-name'] = decodeURIComponent(param.schema.$ref.replace('#/components/schemas/', ''));
}
else if (param.schema && (param.schema.type === 'array') && param.schema.items && param.schema.items.$ref) {
result['x-s2o-name'] = decodeURIComponent(param.schema.items.$ref.replace('#/components/schemas/', '')) + 'Array';
}
if (!consumes.length) {
consumes.push('application/json'); // TODO verify default
}
for (let mimetype of consumes) {
result.content[mimetype] = {};
result.content[mimetype].schema = clone(param.schema || {});
fixUpSchema(result.content[mimetype].schema,options);
}
// Copy any extensions from the original parameter to the new requestBody
copyExtensions(param, result);
}
if (Object.keys(result).length > 0) {
param["x-s2o-delete"] = true;
// work out where to attach the requestBody
if (op) {
if (op.requestBody && singularRequestBody) {
op.requestBody["x-s2o-overloaded"] = true;
let opId = op.operationId || index;
throwOrWarn('Operation ' + opId + ' has multiple requestBodies', op, options);
}
else {
if (!op.requestBody) {
op = path[method] = attachRequestBody(op,options); // make sure we have one
}
if ((op.requestBody.content && op.requestBody.content["multipart/form-data"])
&& (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)) {
op.requestBody.content["multipart/form-data"].schema.properties =
Object.assign(op.requestBody.content["multipart/form-data"].schema.properties, result.content["multipart/form-data"].schema.properties);
op.requestBody.content["multipart/form-data"].schema.required = (op.requestBody.content["multipart/form-data"].schema.required || []).concat(result.content["multipart/form-data"].schema.required||[]);
if (!op.requestBody.content["multipart/form-data"].schema.required.length) {
delete op.requestBody.content["multipart/form-data"].schema.required;
}
}
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)
&& result.content["application/x-www-form-urlencoded"] && result.content["application/x-www-form-urlencoded"].schema && result.content["application/x-www-form-urlencoded"].schema.properties) {
op.requestBody.content["application/x-www-form-urlencoded"].schema.properties =
Object.assign(op.requestBody.content["application/x-www-form-urlencoded"].schema.properties, result.content["application/x-www-form-urlencoded"].schema.properties);
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||[]);
if (!op.requestBody.content["application/x-www-form-urlencoded"].schema.required.length) {
delete op.requestBody.content["application/x-www-form-urlencoded"].schema.required;
}
}
else {
op.requestBody = Object.assign(op.requestBody, result);
if (!op.requestBody['x-s2o-name']) {
if (op.requestBody.schema && op.requestBody.schema.$ref) {
op.requestBody['x-s2o-name'] = decodeURIComponent(op.requestBody.schema.$ref.replace('#/components/schemas/', '')).split('/').join('');
}
else if (op.operationId) {
op.requestBody['x-s2o-name'] = common.sanitiseAll(op.operationId);
}
}
}
}
}
}
// tidy up
if (param && !param['x-s2o-delete']) {
delete param.type;
for (let prop of common.parameterTypeProperties) {
delete param[prop];
}
if ((param.in === 'path') && ((typeof param.required === 'undefined') || (param.required !== true))) {
if (options.patch) {
options.patches++;
param.required = true;
}
else {
throwError('(Patchable) path parameters must be required:true ['+param.name+' in '+index+']', options);
}
}
}
return op;
}
function copyExtensions(src, tgt) {
for (let prop in src) {
if (prop.startsWith('x-') && !prop.startsWith('x-s2o')) {
tgt[prop] = src[prop];
}
}
}
function processResponse(response, name, op, openapi, options) {
if (!response) return false;
if (response.$ref && (typeof response.$ref === 'string')) {
if (response.$ref.indexOf('#/definitions/') >= 0) {
//response.$ref = '#/components/schemas/'+common.sanitise(response.$ref.replace('#/definitions/',''));
throwOrWarn('definition used as response: ' + response.$ref, response, options);
}
else {
if (response.$ref.startsWith('#/responses/')) {
response.$ref = '#/components/responses/' + common.sanitise(decodeURIComponent(response.$ref.replace('#/responses/', '')));
}
}
}
else {
if ((typeof response.description === 'undefined') || (response.description === null)
|| ((response.description === '') && options.patch)) {
if (options.patch) {
if ((typeof response === 'object') && (!Array.isArray(response))) {
options.patches++;
response.description = (statusCodes[response] || '');
}
}
else {
throwError('(Patchable) response.description is mandatory', options);
}
}
if (typeof response.schema !== 'undefined') {
fixUpSchema(response.schema,options);
if (response.schema.$ref && (typeof response.schema.$ref === 'string') && response.schema.$ref.startsWith('#/responses/')) {
response.schema.$ref = '#/components/responses/' + common.sanitise(decodeURIComponent(response.schema.$ref.replace('#/responses/', '')));
}
if (op && op.produces && (typeof op.produces === 'string')) {
if (options.patch) {
options.patches++;
op.produces = [op.produces];
}
else {
return throwError('(Patchable) operation.produces must be an array', options);
}
}
if (openapi.produces && !Array.isArray(openapi.produces)) delete openapi.produces;
let produces = ((op ? op.produces : null) || (openapi.produces || [])).filter(common.uniqueOnly);
if (!produces.length) produces.push('*/*'); // TODO verify default
response.content = {};
for (let mimetype of produces) {
response.content[mimetype] = {};
response.content[mimetype].schema = clone(response.schema);
if (response.examples && response.examples[mimetype]) {
let example = {};
example.value = response.examples[mimetype];
response.content[mimetype].examples = {};
response.content[mimetype].examples.response = example;
delete response.examples[mimetype];
}
if (response.content[mimetype].schema.type === 'file') {
response.content[mimetype].schema = { type: 'string', format: 'binary' };
}
}
delete response.schema;
}
// examples for content-types not listed in produces
for (let mimetype in response.examples) {
if (!response.content) response.content = {};
if (!response.content[mimetype]) response.content[mimetype] = {};
response.content[mimetype].examples = {};
response.content[mimetype].examples.response = {};
response.content[mimetype].examples.response.value = response.examples[mimetype];
}
delete response.examples;
if (response.headers) {
for (let h in response.headers) {
if (h.toLowerCase() === 'status code') {
if (options.patch) {
options.patches++;
delete response.headers[h];
}
else {
throwError('(Patchable) "Status Code" is not a valid header', options);
}
}
else {
processHeader(response.headers[h], options);
}
}
}
}
}
function processPaths(container, containerName, options, requestBodyCache, openapi) {
for (let p in container) {
let path = container[p];
// path.$ref is external only
if (path && (path['x-trace']) && (typeof path['x-trace'] === 'object')) {
path.trace = path['x-trace'];
delete path['x-trace'];
}
if (path && (path['x-summary']) && (typeof path['x-summary'] === 'string')) {
path.summary = path['x-summary'];
delete path['x-summary'];
}
if (path && (path['x-description']) && (typeof path['x-description'] === 'string')) {
path.description = path['x-description'];
delete path['x-description'];
}
if (path && (path['x-servers']) && (Array.isArray(path['x-servers']))) {
path.servers = path['x-servers'];
delete path['x-servers'];
}
for (let method in path) {
if ((common.httpMethods.indexOf(method) >= 0) || (method === 'x-amazon-apigateway-any-method')) {
let op = path[method];
if (op && op.parameters && Array.isArray(op.parameters)) {
if (path.parameters) {
for (let param of path.parameters) {
if (typeof param.$ref === 'string') {
fixParamRef(param, options);
param = resolveInternal(openapi, param.$ref);
}
let match = op.parameters.find(function (e, i, a) {
return ((e.name === param.name) && (e.in === param.in));
});
if (!match && ((param.in === 'formData') || (param.in === 'body') || (param.type === 'file'))) {
op = processParameter(param, op, path, method, p, openapi, options);
if (options.rbname && op[options.rbname] === '') {
delete op[options.rbname];
}
}
}
}
for (let param of op.parameters) {
op = processParameter(param, op, path, method, method + ':' + p, openapi, options);
}
if (options.rbname && op[options.rbname] === '') {
delete op[options.rbname];
}
if (!options.debug) {
if (op.parameters) op.parameters = op.parameters.filter(keepParameters);
}
}
if (op && op.security) processSecurity(op.security);
//don't need to remove requestBody for non-supported ops as they "SHALL be ignored"
// responses
if (typeof op === 'object') {
if (!op.responses) {
let defaultResp = {};
defaultResp.description = 'Default response';
op.responses = { default: defaultResp };
}
for (let r in op.responses) {
let response = op.responses[r];
processResponse(response, r, op, openapi, options);
}
}
if (op && (op['x-servers']) && (Array.isArray(op['x-servers']))) {
op.servers = op['x-servers'];
delete op['x-servers'];
} else if (op && op.schemes && op.schemes.length) {
for (let scheme of op.schemes) {
if ((!openapi.schemes) || (openapi.schemes.indexOf(scheme) < 0)) {
if (!op.servers) {
op.servers = [];
}
if (Array.isArray(openapi.servers)) {
for (let server of openapi.servers) {
let newServer = clone(server);
let serverUrl = url.parse(newServer.url);
serverUrl.protocol = scheme;
newServer.url = serverUrl.format();
op.servers.push(newServer);
}
}
}
}
}
if (options.debug) {
op["x-s2o-consumes"] = op.consumes || [];
op["x-s2o-produces"] = op.produces || [];
}
if (op) {
delete op.consumes;
delete op.produces;
delete op.schemes;
if (op["x-ms-examples"]) {
for (let e in op["x-ms-examples"]) {
let example = op["x-ms-examples"][e];
let se = common.sanitiseAll(e);
if (example.parameters) {
for (let p in example.parameters) {
let value = example.parameters[p];
for (let param of (op.parameters||[]).concat(path.parameters||[])) {
if (param.$ref) {
param = jptr.jptr(openapi,param.$ref);
}
if ((param.name === p) && (!param.example)) {
if (!param.examples) {
param.examples = {};
}
param.examples[e] = {value: value};
}
}
}
}
if (example.responses) {
for (let r in example.responses) {
if (example.responses[r].headers) {
for (let h in example.responses[r].headers) {
let value = example.responses[r].headers[h];
for (let rh in op.responses[r].headers) {
if (rh === h) {
let header = op.responses[r].headers[rh];
header.example = value;
}
}
}
}
if (example.responses[r].body) {
openapi.components.examples[se] = { value: clone(example.responses[r].body) };
if (op.responses[r] && op.responses[r].content) {
for (let ct in op.responses[r].content) {
let contentType = op.responses[r].content[ct];
if (!contentType.examples) {
contentType.examples = {};
}
contentType.examples[e] = { $ref: '#/components/examples/'+se };
}
}
}
}
}
}
delete op["x-ms-examples"];
}
if (op.parameters && op.parameters.length === 0) delete op.parameters;
if (op.requestBody) {
let effectiveOperationId = op.operationId ? common.sanitiseAll(op.operationId) : common.sanitiseAll(method + p).toCamelCase();
let rbName = common.sanitise(op.requestBody['x-s2o-name'] || effectiveOperationId || '');
delete op.requestBody['x-s2o-name'];
let rbStr = JSON.stringify(op.requestBody);
let rbHash = common.hash(rbStr);
if (!requestBodyCache[rbHash]) {
let entry = {};
entry.name = rbName;
entry.body = op.requestBody;
entry.refs = [];
requestBodyCache[rbHash] = entry;
}
let ptr = '#/'+containerName+'/'+encodeURIComponent(jptr.jpescape(p))+'/'+method+'/requestBody';
requestBodyCache[rbHash].refs.push(ptr);
}
}
}
}
if (path && path.parameters) {
for (let p2 in path.parameters) {
let param = path.parameters[p2];
processParameter(param, null, path, null, p, openapi, options); // index here is the path string
}
if (!options.debug && Array.isArray(path.parameters)) {
path.parameters = path.parameters.filter(keepParameters);
}
}
}
}
function main(openapi, options) {
let requestBodyCache = {};
componentNames = { schemas: {} };
if (openapi.security) processSecurity(openapi.security);
for (let s in openapi.components.securitySchemes) {
let sname = common.sanitise(s);
if (s !== sname) {
if (openapi.components.securitySchemes[sname]) {
throwError('Duplicate sanitised securityScheme name ' + sname, options);
}
openapi.components.securitySchemes[sname] = openapi.components.securitySchemes[s];
delete openapi.components.securitySchemes[s];
}
processSecurityScheme(openapi.components.securitySchemes[sname], options);
}
for (let s in openapi.components.schemas) {
let sname = common.sanitiseAll(s);
let suffix = '';
if (s !== sname) {
while (openapi.components.schemas[sname + suffix]) {
// @ts-ignore
suffix = (suffix ? ++suffix : 2);
}
openapi.components.schemas[sname + suffix] = openapi.components.schemas[s];
delete openapi.components.schemas[s];
}
componentNames.schemas[s] = sname + suffix;
fixUpSchema(openapi.components.schemas[sname+suffix],options)
}
// fix all $refs to their new locations (and potentially new names)
options.refmap = {};
recurse(openapi, { payload: { options: options } }, fixupRefs);
dedupeRefs(openapi,options);
for (let p in openapi.components.parameters) {
let sname = common.sanitise(p);
if (p !== sname) {
if (openapi.components.parameters[sname]) {
throwError('Duplicate sanitised parameter name ' + sname, options);
}
openapi.components.parameters[sname] = openapi.components.parameters[p];
delete openapi.components.parameters[p];
}
let param = openapi.components.parameters[sname];
processParameter(param, null, null, null, sname, openapi, options);
}
for (let r in openapi.components.responses) {
let sname = common.sanitise(r);
if (r !== sname) {
if (openapi.components.responses[sname]) {
throwError('Duplicate sanitised response name ' + sname, options);
}
openapi.components.responses[sname] = openapi.components.responses[r];
delete openapi.components.responses[r];
}
let response = openapi.components.responses[sname];
processResponse(response, sname, null, openapi, options);
if (response.headers) {
for (let h in response.headers) {
if (h.toLowerCase() === 'status code') {
if (options.patch) {
options.patches++;
delete response.headers[h];
}
else {
throwError('(Patchable) "Status Code" is not a valid header', options);
}
}
else {
processHeader(response.headers[h], options);
}
}
}
}
for (let r in openapi.components.requestBodies) { // converted ones
let rb = openapi.components.requestBodies[r];
let rbStr = JSON.stringify(rb);
let rbHash = common.hash(rbStr);
let entry = {};
entry.name = r;
entry.body = rb;
entry.refs = [];
requestBodyCache[rbHash] = entry;
}
processPaths(openapi.paths, 'paths', options, requestBodyCache, openapi);
if (openapi["x-ms-paths"]) {
processPaths(openapi["x-ms-paths"], 'x-ms-paths', options, requestBodyCache, openapi);
}
if (!options.debug) {
for (let p in openapi.components.parameters) {
let param = openapi.components.parameters[p];
if (param["x-s2o-delete"]) {
delete openapi.components.parameters[p];
}
}
}
if (options.debug) {
openapi["x-s2o-consumes"] = openapi.consumes || [];
openapi["x-s2o-produces"] = openapi.produces || [];
}
delete openapi.consumes;
delete openapi.produces;
delete openapi.schemes;
let rbNamesGenerated = [];
openapi.components.requestBodies = {}; // for now as we've dereffed them
if (!options.resolveInternal) {
let counter = 1;
for (let e in requestBodyCache) {
let entry = requestBodyCache[e];
if (entry.refs.length > 1) {
// create a shared requestBody
let suffix = '';
if (!entry.name) {
entry.name = 'requestBody';
// @ts-ignore
suffix = counter++;
}
while (rbNamesGenerated.indexOf(entry.name + suffix) >= 0) {
// @ts-ignore - this can happen if descriptions are not exactly the same (e.g. bitbucket)
suffix = (suffix ? ++suffix : 2);
}
entry.name = entry.name + suffix;
rbNamesGenerated.push(entry.name);
openapi.components.requestBodies[entry.name] = clone(entry.body);
for (let r in entry.refs) {
let ref = {};
ref.$ref = '#/components/requestBodies/' + entry.name;
jptr.jptr(openapi,entry.refs[r],ref);
}
}
}
}
if (openapi.components.responses && Object.keys(openapi.components.responses).length === 0) {
delete openapi.components.responses;
}
if (openapi.components.parameters && Object.keys(openapi.components.parameters).length === 0) {
delete openapi.components.parameters;
}
if (openapi.components.examples && Object.keys(openapi.components.examples).length === 0) {
delete openapi.components.examples;
}
if (openapi.components.requestBodies && Object.keys(openapi.components.requestBodies).length === 0) {
delete openapi.components.requestBodies;
}
if (openapi.components.securitySchemes && Object.keys(openapi.components.securitySchemes).length === 0) {
delete openapi.components.securitySchemes;
}
if (openapi.components.headers && Object.keys(openapi.components.headers).length === 0) {
delete openapi.components.headers;
}
if (openapi.components.schemas && Object.keys(openapi.components.schemas).length === 0) {
delete openapi.components.schemas;
}
if (openapi.components && Object.keys(openapi.components).length === 0) {
delete openapi.components;
}
return openapi;
}
function extractServerParameters(server) {
if (!server || !server.url || (typeof server.url !== 'string')) return server;
server.url = server.url.split('{{').join('{');
server.url = server.url.split('}}').join('}');
server.url.replace(/\{(.+?)\}/g, function (match, group1) { // TODO extend to :parameters (not port)?
if (!server.variables) {
server.variables = {};
}
server.variables[group1] = { default: 'unknown' };
});
return server;
}
function fixInfo(openapi, options, reject) {
if ((typeof openapi.info === 'undefined') || (openapi.info === null)) {
if (options.patch) {
options.patches++;
openapi.info = { version: '', title: '' };
}
else {
return reject(new S2OError('(Patchable) info object is mandatory'));
}
}
if ((typeof openapi.info !== 'object') || (Array.isArray(openapi.info))) {
return reject(new S2OError('info must be an object'));
}
if ((typeof openapi.info.title === 'undefined') || (openapi.info.title === null)) {
if (options.patch) {
options.patches++;
openapi.info.title = '';
}
else {
return reject(new S2OError('(Patchable) info.title cannot be null'));
}
}
if ((typeof openapi.info.version === 'undefined') || (openapi.info.version === null)) {
if (options.patch) {
options.patches++;
openapi.info.version = '';
}
else {
return reject(new S2OError('(Patchable) info.version cannot be null'));
}
}
if (typeof openapi.info.version !== 'string') {
if (options.patch) {
options.patches++;
openapi.info.version = openapi.info.version.toString();
}
else {
return reject(new S2OError('(Patchable) info.version must be a string'));
}
}
if (typeof openapi.info.logo !== 'undefined') {
if (options.patch) {
options.patches++;
openapi.info['x-logo'] = openapi.info.logo;
delete openapi.info.logo;
}
else return reject(new S2OError('(Patchable) info should not have logo property'));
}
if (typeof openapi.info.termsOfService !== 'undefined') {
if (openapi.info.termsOfService === null) {
if (options.patch) {
options.patches++;
openapi.info.termsOfService = '';
}
else {
return reject(new S2OError('(Patchable) info.termsOfService cannot be null'));
}
}
try {
let u = new URL(openapi.info.termsOfService);
}
catch (ex) {
if (options.patch) {
options.patches++;
delete openapi.info.termsOfService;
}
else return reject(new S2OError('(Patchable) info.termsOfService must be a URL'));
}
}
}
function fixPaths(openapi, options, reject) {
if (typeof openapi.paths === 'undefined') {
if (options.patch) {
options.patches++;
openapi.paths = {};
}
else {
return reject(new S2OError('(Patchable) paths object is mandatory'));
}
}
}
function detectObjectReferences(obj, options) {
const seen = new WeakSet();
recurse(obj, {identityDetection:true}, function (obj, key, state) {
if ((typeof obj[key] === 'object') && (obj[key] !== null)) {
if (seen.has(obj[key])) {
if (options.anchors) {
obj[key] = clone(obj[key]);
}
else {
throwError('YAML anchor or merge key at '+state.path, options);
}
}
else {
seen.add(obj[key]);
}
}
});
}
function convertObj(swagger, options, callback) {
return maybe(callback, new Promise(function (resolve, reject) {
if (!swagger) swagger = {};
options.original = swagger;
if (!options.text) options.text = yaml.stringify(swagger);
options.externals = [];
options.externalRefs = {};
options.rewriteRefs = true; // avoids stack explosions
options.preserveMiro = true;
options.promise = {};
options.promise.resolve = resolve;
options.promise.reject = reject;
options.patches = 0;
if (!options.cache) options.cache = {};
if (options.source) options.cache[options.source] = options.original;
detectObjectReferences(swagger, options);
if (swagger.openapi && (typeof swagger.openapi === 'string') && swagger.openapi.startsWith('3.')) {
options.openapi = cclone(swagger);
fixInfo(options.openapi, options, reject);
fixPaths(options.openapi, options, reject);
resolver.optionalResolve(options) // is a no-op if options.resolve is not set
.then(function(){
if (options.direct) {
return resolve(options.openapi);
}
else {
return resolve(options);
}
})
.catch(function(ex){
console.warn(ex);
reject(ex);
});
return; // we should have resolved or rejected by now
}
if ((!swagger.swagger) || (swagger.swagger != "2.0")) {
return reject(new S2OError('Unsupported swagger/OpenAPI version: ' + (swagger.openapi ? swagger.openapi : swagger.swagger)));
}
let openapi = options.openapi = {};
openapi.openapi = (typeof options.targetVersion === 'string' && options.targetVersion.startsWith('3.')) ? options.targetVersion : targetVersion; // semver
if (options.origin) {
if (!openapi["x-origin"]) {
openapi["x-origin"] = [];
}
let origin = {};
origin.url = options.source||options.origin;
origin.format = 'swagger';
origin.version = swagger.swagger;
origin.converter = {};
origin.converter.url = 'https://github.com/mermade/oas-kit';
origin.converter.version = ourVersion;
openapi["x-origin"].push(origin);
}
// we want the new and existing properties to appear in a sensible order. Not guaranteed
openapi = Object.assign(openapi, cclone(swagger));
delete openapi.swagger;
recurse(openapi, {}, function(obj, key, state){
if ((obj[key] === null) && (!key.startsWith('x-')) && key !== 'default' && (state.path.indexOf('/example') < 0)) delete obj[key]; // this saves *so* much grief later
});
if (swagger.host) {
for (let s of (Array.isArray(swagger.schemes) ? swagger.schemes : [''])) {
let server = {};
let basePath = (swagger.basePath || '').replace(/\/$/, '') // Trailing slashes generally shouldn't be included
server.url = (s ? s+':' : '') + '//' + swagger.host + basePath;
extractServerParameters(server);
if (!openapi.servers) openapi.servers = [];
openapi.servers.push(server);
}
}
else if (swagger.basePath) {
let server = {};
server.url = swagger.basePath;
extractServerParameters(server);
if (!openapi.servers) openapi.servers = [];
openapi.servers.push(server);
}
delete openapi.host;
delete openapi.basePath;
if (openapi['x-servers'] && Array.isArray(openapi['x-servers'])) {
openapi.servers = openapi['x-servers'];
delete openapi['x-servers'];
}
// TODO APIMatic extensions (x-server-configuration) ?
if (swagger['x-ms-parameterized-host']) {
let xMsPHost = swagger['x-ms-parameterized-host'];
let server = {};
server.url = xMsPHost.hostTemplate + (swagger.basePath ? swagger.basePath : '');
server.variables = {};
const paramNames = server.url.match(/\{\w+\}/g);
for (let msp in xMsPHost.parameters) {
let param = xMsPHost.parameters[msp];
if (param.$ref) {
param = clone(resolveInternal(openapi, param.$ref));
}
if (!msp.startsWith('x-')) {
delete param.required; // all true
delete param.type; // all strings
delete param.in; // all 'host'
if (typeof param.default === 'undefined') {
if (param.enum) {
param.default = param.enum[0];
}
else {
param.default = 'none';
}
}
if (!param.name) {
param.name = paramNames[msp].replace('{','').replace('}','');
}
server.variables[param.name] = param;
delete param.name;
}
}
if (!openapi.servers) openapi.servers = [];
if (xMsPHost.useSchemePrefix === false) {
// The server URL already includes a protocol scheme
openapi.servers.push(server);
} else {
// Define this server once for each given protocol scheme
swagger.schemes.forEach((scheme) => {
openapi.servers.push(
Object.assign({}, server, { url: scheme + '://' + server.url })
)
});
}
delete openapi['x-ms-parameterized-host'];
}
fixInfo(openapi, options, reject);
fixPaths(openapi, options, reject);
if (typeof openapi.consumes === 'string') {
openapi.consumes = [openapi.consumes];
}
if (typeof openapi.produces === 'string') {
openapi.produces = [openapi.produces];
}
openapi.components = {};
if (openapi['x-callbacks']) {
openapi.components.callbacks = openapi['x-callbacks'];
delete openapi['x-callbacks'];
}
openapi.components.examples = {};
openapi.components.headers = {};
if (openapi['x-links']) {
openapi.components.links = openapi['x-links'];
delete openapi['x-links'];
}
openapi.components.parameters = openapi.parameters || {};
openapi.components.responses = openapi.responses || {};
openapi.components.requestBodies = {};
openapi.components.securitySchemes = openapi.securityDefinitions || {};
openapi.components.schemas = openapi.definitions || {};
delete openapi.definitions;
delete openapi.responses;
delete openapi.parameters;
delete openapi.securityDefinitions;
resolver.optionalResolve(options) // is a no-op if options.resolve is not set
.then(function(){
main(options.openapi, options);
if (options.direct) {
resolve(options.openapi);
}
else {
resolve(options);
}
})
.catch(function(ex){
console.warn(ex);
reject(ex);
});
}));
}
function convertStr(str, options, callback) {
return maybe(callback, new Promise(function (resolve, reject) {
let obj = null;
let error = null;
try {
obj = JSON.parse(str);
options.text = JSON.stringify(obj,null,2);
}
catch (ex) {
error = ex;
try {
obj = yaml.parse(str, { schema: 'core', prettyErrors: true });
options.sourceYaml = true;
options.text = str;
}
catch (ex) {
error = ex;
}
}
if (obj) {
convertObj(obj, options)
.then(options => resolve(options))
.catch(ex => reject(ex));
}
else {
reject(new S2OError(error ? error.message : 'Could not parse string'));
}
}));
}
function convertUrl(url, options, callback) {
return maybe(callback, new Promise(function (resolve, reject) {
options.origin = true;
if (!options.source) {
options.source = url;
}
if (options.verbose) {
console.warn('GET ' + url);
}
if (!options.fetch) {
options.fetch = fetch;
}
const fetchOptions = Object.assign({}, options.fetchOptions, {agent:options.agent});
options.fetch(url, fetchOptions).then(function (res) {
if (res.status !== 200) throw new S2OError(`Received status code ${res.status}: ${url}`);
return res.text();
}).then(function (body) {
convertStr(body, options)
.then(options => resolve(options))
.catch(ex => reject(ex));
}).catch(function (err) {
reject(err);
});
}));
}
function convertFile(filename, options, callback) {
return maybe(callback, new Promise(function (resolve, reject) {
fs.readFile(filename, options.encoding || 'utf8', function (err, s) {
if (err) {
reject(err);
}
else {
options.sourceFile = filename;
convertStr(s, options)
.then(options => resolve(options))
.catch(ex => reject(ex));
}
});
}));
}
function convertStream(readable, options, callback) {
return maybe(callback, new Promise(function (resolve, reject) {
let data = '';
readable.on('data', function (chunk) {
data += chunk;
})
.on('end', function () {
convertStr(data, options)
.then(options => resolve(options))
.catch(ex => reject(ex));
});
}));
}
module.exports = {
S2OError: S2OError,
targetVersion: targetVersion,
convert: convertObj,
convertObj: convertObj,
convertUrl: convertUrl,
convertStr: convertStr,
convertFile: convertFile,
convertStream: convertStream
};