mirror of
https://github.com/usebruno/bruno.git
synced 2024-12-23 15:19:01 +01:00
firstpass at adding openapi v3 import json
This commit is contained in:
parent
816496246b
commit
7b55d52c11
@ -2,6 +2,7 @@ import React from 'react';
|
||||
import importBrunoCollection from 'utils/importers/bruno-collection';
|
||||
import importPostmanCollection from 'utils/importers/postman-collection';
|
||||
import importInsomniaCollection from 'utils/importers/insomnia-collection';
|
||||
import importOpenapiCollection from 'utils/importers/openapi-collection';
|
||||
import { toastError } from 'utils/common/error';
|
||||
import Modal from 'components/Modal';
|
||||
|
||||
@ -30,6 +31,14 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
|
||||
.catch((err) => toastError(err, 'Insomnia Import collection failed'));
|
||||
};
|
||||
|
||||
const handleImportOpenapiCollection = () => {
|
||||
importOpenapiCollection()
|
||||
.then((collection) => {
|
||||
handleSubmit(collection);
|
||||
})
|
||||
.catch((err) => toastError(err, 'OpenAPI v3 Import collection failed'));
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal size="sm" title="Import Collection" hideFooter={true} handleConfirm={onClose} handleCancel={onClose}>
|
||||
<div>
|
||||
@ -42,6 +51,9 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
|
||||
<div className="text-link hover:underline cursor-pointer mt-2" onClick={handleImportInsomniaCollection}>
|
||||
Insomnia Collection
|
||||
</div>
|
||||
<div className="text-link hover:underline cursor-pointer mt-2" onClick={handleImportOpenapiCollection}>
|
||||
OpenAPI Collection
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
|
346
packages/bruno-app/src/utils/importers/openapi-collection.js
Normal file
346
packages/bruno-app/src/utils/importers/openapi-collection.js
Normal file
@ -0,0 +1,346 @@
|
||||
import each from 'lodash/each';
|
||||
import get from 'lodash/get';
|
||||
import fileDialog from 'file-dialog';
|
||||
import { uuid } from 'utils/common';
|
||||
import { BrunoError } from 'utils/common/error';
|
||||
import { validateSchema, transformItemsInCollection, hydrateSeqInCollection } from './common';
|
||||
|
||||
const readFile = (files) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = (e) => resolve(e.target.result);
|
||||
fileReader.onerror = (err) => reject(err);
|
||||
fileReader.readAsText(files[0]);
|
||||
});
|
||||
};
|
||||
|
||||
const ensureUrl = (url) => {
|
||||
let protUrl = url.startsWith('http') ? url : `http://${url}`;
|
||||
// replace any double or triple slashes
|
||||
return protUrl.replace(/([^:]\/)\/+/g, '$1');
|
||||
};
|
||||
|
||||
const transformOpenapiRequestItem = (request) => {
|
||||
console.log(request);
|
||||
let _operationObject = request.operationObject;
|
||||
const brunoRequestItem = {
|
||||
uid: uuid(),
|
||||
name: _operationObject.operationId,
|
||||
type: 'http-request',
|
||||
request: {
|
||||
url: ensureUrl(request.global.server + '/' + request.path),
|
||||
method: request.method.toUpperCase(),
|
||||
auth: {
|
||||
mode: 'none',
|
||||
basic: null,
|
||||
bearer: null
|
||||
},
|
||||
headers: [],
|
||||
params: [],
|
||||
body: {
|
||||
mode: 'none',
|
||||
json: null,
|
||||
text: null,
|
||||
xml: null,
|
||||
formUrlEncoded: [],
|
||||
multipartForm: []
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
each(_operationObject.parameters || [], (param) => {
|
||||
if (param.in === 'query') {
|
||||
brunoRequestItem.request.params.push({
|
||||
uid: uuid(),
|
||||
name: param.name,
|
||||
value: '',
|
||||
description: param.description || '',
|
||||
enabled: param.required
|
||||
});
|
||||
} else if (param.in === 'header') {
|
||||
brunoRequestItem.request.headers.push({
|
||||
uid: uuid(),
|
||||
name: param.name,
|
||||
value: '',
|
||||
description: param.description || '',
|
||||
enabled: param.required
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let auth;
|
||||
// allow operation override
|
||||
if (_operationObject.security) {
|
||||
let schemeName = Object.keys(_operationObject.security[0])[0];
|
||||
auth = request.global.security.getScheme(schemeName);
|
||||
} else if (request.global.security.supported.length > 0) {
|
||||
auth = request.global.security.supported[0];
|
||||
}
|
||||
|
||||
if (auth) {
|
||||
if (auth.type === 'http' && auth.scheme === 'basic') {
|
||||
brunoRequestItem.request.auth.mode = 'basic';
|
||||
brunoRequestItem.request.auth.basic = {
|
||||
username: '{{username}}',
|
||||
password: '{{password}}'
|
||||
};
|
||||
} else if (auth.type === 'http' && auth.scheme === 'bearer') {
|
||||
brunoRequestItem.request.auth.mode = 'bearer';
|
||||
brunoRequestItem.request.auth.bearer = {
|
||||
token: '{{token}}'
|
||||
};
|
||||
} else if (auth.type === 'apiKey' && auth.in === 'header') {
|
||||
brunoRequestItem.request.headers.push({
|
||||
uid: uuid(),
|
||||
name: auth.name,
|
||||
value: '{{apiKey}}',
|
||||
description: 'Authentication header',
|
||||
enabled: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: handle allOf/anyOf/oneOf
|
||||
if (_operationObject.requestBody) {
|
||||
let content = get(_operationObject, 'requestBody.content', {});
|
||||
Object.entries(content).forEach(([mimeType, body]) => {
|
||||
if (mimeType === 'application/json') {
|
||||
brunoRequestItem.request.body.mode = 'json';
|
||||
if (body.schema && body.schema.type === 'object') {
|
||||
let _jsonBody = {};
|
||||
each(body.schema.properties || {}, (prop, name) => {
|
||||
_jsonBody[name] = '';
|
||||
});
|
||||
brunoRequestItem.request.body.json = JSON.stringify(_jsonBody, null, 2);
|
||||
}
|
||||
} else if (mimeType === 'application/x-www-form-urlencoded') {
|
||||
brunoRequestItem.request.body.mode = 'formUrlEncoded';
|
||||
if (body.schema && body.schema.type === 'object') {
|
||||
each(body.schema.properties || {}, (prop, name) => {
|
||||
brunoRequestItem.request.body.formUrlEncoded.push({
|
||||
uid: uuid(),
|
||||
name: name,
|
||||
value: '',
|
||||
description: prop.description || '',
|
||||
enabled: true
|
||||
});
|
||||
});
|
||||
}
|
||||
} else if (mimeType === 'multipart/form-data') {
|
||||
brunoRequestItem.request.body.mode = 'multipartForm';
|
||||
if (body.schema && body.schema.type === 'object') {
|
||||
each(body.schema.properties || {}, (prop, name) => {
|
||||
brunoRequestItem.request.body.multipartForm.push({
|
||||
uid: uuid(),
|
||||
name: name,
|
||||
value: '',
|
||||
description: prop.description || '',
|
||||
enabled: true
|
||||
});
|
||||
});
|
||||
}
|
||||
} else if (mimeType === 'text/plain') {
|
||||
brunoRequestItem.request.body.mode = 'text';
|
||||
brunoRequestItem.request.body.text = '';
|
||||
} else if (mimeType === 'text/xml') {
|
||||
brunoRequestItem.request.body.mode = 'xml';
|
||||
brunoRequestItem.request.body.xml = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return brunoRequestItem;
|
||||
};
|
||||
|
||||
const resolveRefs = (spec, components = spec.components) => {
|
||||
if (!spec || typeof spec !== 'object') {
|
||||
return spec;
|
||||
}
|
||||
|
||||
if (Array.isArray(spec)) {
|
||||
return spec.map((item) => resolveRefs(item, components));
|
||||
}
|
||||
|
||||
if ('$ref' in spec) {
|
||||
const refPath = spec.$ref;
|
||||
|
||||
if (refPath.startsWith('#/components/')) {
|
||||
// Local reference within components
|
||||
const refKeys = refPath.replace('#/components/', '').split('/');
|
||||
let ref = components;
|
||||
|
||||
for (const key of refKeys) {
|
||||
if (ref[key]) {
|
||||
ref = ref[key];
|
||||
} else {
|
||||
// Handle invalid references gracefully?
|
||||
return spec;
|
||||
}
|
||||
}
|
||||
|
||||
return resolveRefs(ref, components);
|
||||
} else {
|
||||
// Handle external references (not implemented here)
|
||||
// You would need to fetch the external reference and resolve it.
|
||||
// Example: Fetch and resolve an external reference from a URL.
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively resolve references in nested objects
|
||||
for (const prop in spec) {
|
||||
spec[prop] = resolveRefs(spec[prop], components);
|
||||
}
|
||||
|
||||
return spec;
|
||||
};
|
||||
|
||||
const groupRequestsByTags = (requests) => {
|
||||
let _groups = {};
|
||||
let ungrouped = [];
|
||||
each(requests, (request) => {
|
||||
let tags = request.operationObject.tags || [];
|
||||
if (tags.length > 0) {
|
||||
let tag = tags[0]; // take first tag
|
||||
if (!_groups[tag]) {
|
||||
_groups[tag] = [];
|
||||
}
|
||||
_groups[tag].push(request);
|
||||
} else {
|
||||
ungrouped.push(request);
|
||||
}
|
||||
});
|
||||
|
||||
let groups = Object.keys(_groups).map((groupName) => {
|
||||
return {
|
||||
name: groupName,
|
||||
requests: _groups[groupName]
|
||||
};
|
||||
});
|
||||
|
||||
return [groups, ungrouped];
|
||||
};
|
||||
|
||||
const getDefaultUrl = (serverObject) => {
|
||||
let url = serverObject.url;
|
||||
if (serverObject.variables) {
|
||||
each(serverObject.variables, (variable, variableName) => {
|
||||
let sub = variable.default || (variable.enum ? variable.enum[0] : `{{${variableName}}}`);
|
||||
url = url.replace(`{${variableName}}`, sub);
|
||||
});
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
const getSecurity = (apiSpec) => {
|
||||
let supportedSchemes = apiSpec.security || [];
|
||||
if (supportedSchemes.length === 0) {
|
||||
return {
|
||||
supported: []
|
||||
};
|
||||
}
|
||||
|
||||
let securitySchemes = get(apiSpec, 'components.securitySchemes', {});
|
||||
if (Object.keys(securitySchemes) === 0) {
|
||||
return {
|
||||
supported: []
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
supported: supportedSchemes.map((scheme) => {
|
||||
var schemeName = Object.keys(scheme)[0];
|
||||
return securitySchemes[schemeName];
|
||||
}),
|
||||
schemes: securitySchemes,
|
||||
getScheme: (schemeName) => {
|
||||
return securitySchemes[schemeName];
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const parseOpenapiCollection = (data) => {
|
||||
const brunoCollection = {
|
||||
name: '',
|
||||
uid: uuid(),
|
||||
version: '1',
|
||||
items: [],
|
||||
environments: []
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const collectionData = resolveRefs(JSON.parse(data));
|
||||
if (!collectionData) {
|
||||
reject(new BrunoError('Invalid OpenAPI collection. Failed to resolve refs.'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Currently parsing of openapi spec is "do your best", that is
|
||||
// allows "invalid" openapi spec
|
||||
|
||||
// assumes v3 if not defined. v2 no supported yet
|
||||
if (collectionData.openapi && !collectionData.openapi.startsWith('3')) {
|
||||
reject(new BrunoError('Only OpenAPI v3 is supported currently.'));
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO what if info.title not defined?
|
||||
brunoCollection.name = collectionData.info.title;
|
||||
let servers = collectionData.servers || [];
|
||||
let baseUrl = servers[0] ? getDefaultUrl(servers[0]) : '';
|
||||
let securityConfig = getSecurity(collectionData);
|
||||
|
||||
let allRequests = Object.entries(collectionData.paths)
|
||||
.map(([path, methods]) => {
|
||||
return Object.entries(methods).map(([method, operationObject]) => {
|
||||
return {
|
||||
method: method,
|
||||
path: path,
|
||||
operationObject: operationObject,
|
||||
global: {
|
||||
server: baseUrl,
|
||||
security: securityConfig
|
||||
}
|
||||
};
|
||||
});
|
||||
})
|
||||
.reduce((acc, val) => acc.concat(val), []); // flatten
|
||||
|
||||
let [groups, ungroupedRequests] = groupRequestsByTags(allRequests);
|
||||
let brunoFolders = groups.map((group) => {
|
||||
return {
|
||||
uid: uuid(),
|
||||
name: group.name,
|
||||
type: 'folder',
|
||||
items: group.requests.map(transformOpenapiRequestItem)
|
||||
};
|
||||
});
|
||||
|
||||
let ungroupedItems = ungroupedRequests.map(transformOpenapiRequestItem);
|
||||
let brunoCollectionItems = brunoFolders.concat(ungroupedItems);
|
||||
brunoCollection.items = brunoCollectionItems;
|
||||
resolve(brunoCollection);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
reject(new BrunoError('An error occurred while parsing the OpenAPI collection'));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const importCollection = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fileDialog({ accept: 'application/json' })
|
||||
.then(readFile)
|
||||
.then(parseOpenapiCollection)
|
||||
.then(transformItemsInCollection)
|
||||
.then(hydrateSeqInCollection)
|
||||
.then(validateSchema)
|
||||
.then((collection) => resolve(collection))
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
reject(new BrunoError('Import collection failed: ' + err.message));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export default importCollection;
|
Loading…
Reference in New Issue
Block a user