Compare commits

..

1 Commits

Author SHA1 Message Date
Anoop M D
e6265db353 chore: backstage catalog 2022-12-16 19:18:16 +05:30
55 changed files with 101 additions and 1948 deletions

View File

@@ -1,19 +0,0 @@
name: Unit Tests
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
- name: Install dependencies
run: npm i --legacy-peer-deps
- name: Test Package bruno-lang
run: npm run test --workspace=packages/bruno-lang

16
catalog-info.yaml Normal file
View File

@@ -0,0 +1,16 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: "bruno"
links:
- url: https://example.com/user
title: Examples Users
icon: user
- url: https://example.com/group
title: Example Group
icon: group
spec:
type: component
lifecycle: production
owner: anoop
system: tech-docs

View File

@@ -6,7 +6,6 @@
"packages/bruno-electron",
"packages/bruno-tauri",
"packages/bruno-schema",
"packages/bruno-lang",
"packages/bruno-testbench",
"packages/bruno-graphql-docs"
],

View File

@@ -1,39 +0,0 @@
{
"type": "http-request",
"name": "Send Bulk SMS",
"request": {
"method": "GET",
"url": "https://api.textlocal.in/bulk_json?apiKey=secret=&numbers=998877665&message=hello&sender=600010",
"params": [
{
"name": "apiKey",
"value": "secret",
"enabled": true
},
{
"name": "numbers",
"value": "998877665",
"enabled": true
},
{
"name": "message",
"value": "hello",
"enabled": true
},
{
"name": "sender",
"value": "600010",
"enabled": true
}
],
"headers": [],
"body": {
"mode": "json",
"json": "{\n apikey: \"secret\",\n numbers: \"+919988776655\",\n data: {\n sender: \"TXTLCL\",\n messages: [{\n numbers: \"+919988776655\",\n message: \"Hello World\"\n }]\n }\n}",
"text": null,
"xml": null,
"multipartForm": null,
"formUrlEncoded": null
}
}
}

View File

@@ -1,102 +0,0 @@
ver 1.0
type http-request
name Send Bulk SMS
method GET
url https://api.textlocal.in/bulk_json?apiKey=secret=&numbers=919988776655&message=hello&sender=600010
params
1 apiKey secret
1 numbers 998877665
1 message hello
/params
headers
1 content-type application/json
1 accept-language en-US,en;q=0.9,hi;q=0.8
0 transaction-id {{transactionId}}
/headers
body-mode json
body(type=json)
{
apikey: "secret",
numbers: "+91998877665",
data: {
sender: "TXTLCL",
messages: [{
numbers: "+91998877665",
message: "Hello World"
}]
}
}
/body
body(type=graphql)
{
launchesPast {
launch_site {
site_name
}
launch_success
}
}
/body
script
let user = 'John Doe';
function onRequest(request) {
request.body.user = user;
}
function onResponse(request, response) {
expect(response.status).to.equal(200);
}
/script
assert
{
"$res.data.order.items.length": 1,
"$res.data.orderNumber.isDefined": true
}
/assert
vars
1 petId $res.data.id
/vars
readme
Documentation about the request
/readme
response-example
name Created
status 201
headers
1 content-type application/json
1 accept-language en-US,en;q=0.9,hi;q=0.8
0 transaction-id {{transactionId}}
/headers
body
{
"data": {
"launchesPast": [
{
"launch_site": {
"site_name": "CCAFS SLC 40"
},
"launch_success": true
},
{
"launch_site": {
"site_name": "VAFB SLC 4E"
},
"launch_success": true
}
]
}
}
/body
/response-example

View File

@@ -1,101 +0,0 @@
ver 1.0
type http-request
name Send Bulk SMS
method GET
url https://api.textlocal.in/bulk_json?apiKey=secret=&numbers=919988776655&message=hello&sender=600010
body-mode json
params
1 apiKey secret
1 numbers 998877665
1 message hello
/params
headers
1 content-type application/json
1 accept-language en-US,en;q=0.9,hi;q=0.8
0 transaction-id {{transactionId}}
/headers
body(type=json)
{
apikey: "secret",
numbers: "+91998877665",
data: {
sender: "TXTLCL",
messages: [{
numbers: "+91998877665",
message: "Hello World"
}]
}
}
/body
body(type=graphql)
{
launchesPast {
launch_site {
site_name
}
launch_success
}
}
/body
script
let user = 'John Doe';
function onRequest(request) {
request.body.user = user;
}
function onResponse(request, response) {
expect(response.status).to.equal(200);
}
/script
assert
{
"$res.data.order.items.length": 1,
"$res.data.orderNumber.isDefined": true
}
/assert
vars
1 petId $res.data.id
/vars
readme
Documentation about the request
/readme
response-example
name Created
status 201
headers
1 content-type application/json
1 accept-language en-US,en;q=0.9,hi;q=0.8
0 transaction-id {{transactionId}}
/headers
body
{
"data": {
"launchesPast": [
{
"launch_site": {
"site_name": "CCAFS SLC 40"
},
"launch_success": true
},
{
"launch_site": {
"site_name": "VAFB SLC 4E"
},
"launch_success": true
}
]
}
}
/body
/response-example

View File

@@ -1,5 +1,5 @@
module.exports = {
reactStrictMode: false,
reactStrictMode: true,
publicRuntimeConfig: {
CI: process.env.CI,
PLAYWRIGHT: process.env.PLAYWRIGHT

View File

@@ -1,6 +1,5 @@
{
"name": "@usebruno/app",
"version": "0.3.0",
"private": true,
"scripts": {
"dev": "next dev",
@@ -16,8 +15,8 @@
"@reduxjs/toolkit": "^1.8.0",
"@tabler/icons": "^1.46.0",
"@tippyjs/react": "^4.2.6",
"@usebruno/graphql-docs": "0.1.0",
"@usebruno/schema": "0.2.0",
"@usebruno/graphql-docs": "0.1.0",
"axios": "^0.26.0",
"classnames": "^2.3.1",
"codemirror": "^5.65.2",
@@ -35,17 +34,16 @@
"markdown-it": "^13.0.1",
"mousetrap": "^1.6.5",
"nanoid": "3.3.4",
"next": "12.3.3",
"next": "12.3.1",
"path": "^0.12.7",
"platform": "^1.3.6",
"posthog-node": "^2.1.0",
"qs": "^6.11.0",
"react": "18.2.0",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "18.2.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-hot-toast": "^2.4.0",
"react-redux": "^7.2.6",
"react-tabs": "^3.2.3",
"reckonjs": "^0.1.2",
"sass": "^1.46.0",
"split-on-first": "^3.0.0",

View File

@@ -70,7 +70,7 @@ const Wrapper = styled.div`
}
}
&.is-sidebar-dragging .collection-item-name {
&.is-dragging .collection-item-name {
cursor: inherit;
}
`;

View File

@@ -2,12 +2,10 @@ import React, { useState, useRef, forwardRef, useEffect } from 'react';
import range from 'lodash/range';
import filter from 'lodash/filter';
import classnames from 'classnames';
import { useDrag, useDrop } from 'react-dnd';
import { IconChevronRight, IconDots } from '@tabler/icons';
import { useSelector, useDispatch } from 'react-redux';
import { addTab, focusTab } from 'providers/ReduxStore/slices/tabs';
import { collectionFolderClicked } from 'providers/ReduxStore/slices/collections';
import { moveItem } from 'providers/ReduxStore/slices/collections/actions';
import Dropdown from 'components/Dropdown';
import NewRequest from 'components/Sidebar/NewRequest';
import NewFolder from 'components/Sidebar/NewFolder';
@@ -25,7 +23,7 @@ import StyledWrapper from './StyledWrapper';
const CollectionItem = ({ item, collection, searchText }) => {
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
const isSidebarDragging = useSelector((state) => state.app.isDragging);
const isDragging = useSelector((state) => state.app.isDragging);
const dispatch = useDispatch();
const [renameItemModalOpen, setRenameItemModalOpen] = useState(false);
@@ -35,29 +33,6 @@ const CollectionItem = ({ item, collection, searchText }) => {
const [newFolderModalOpen, setNewFolderModalOpen] = useState(false);
const [itemIsCollapsed, setItemisCollapsed] = useState(item.collapsed);
// const [{ isDragging }, drag] = useDrag({
// type: 'COLLECTION_ITEM',
// item: item,
// collect: (monitor) => ({
// isDragging: monitor.isDragging()
// })
// });
// const [{ isOver }, drop] = useDrop({
// accept: 'COLLECTION_ITEM',
// drop: (draggedItem) => {
// if (draggedItem.uid !== item.uid) {
// dispatch(moveItem(collection.uid, draggedItem.uid, item.uid));
// }
// },
// canDrop: (draggedItem) => {
// return draggedItem.uid !== item.uid;
// },
// collect: (monitor) => ({
// isOver: monitor.isOver()
// })
// });
useEffect(() => {
if (searchText && searchText.length) {
setItemisCollapsed(false);
@@ -116,7 +91,7 @@ const CollectionItem = ({ item, collection, searchText }) => {
const isFolder = isItemAFolder(item);
const className = classnames('flex flex-col w-full', {
'is-sidebar-dragging': isSidebarDragging
'is-dragging': isDragging
});
if (searchText && searchText.length) {

View File

@@ -2,11 +2,9 @@ import React, { useState, forwardRef, useRef, useEffect } from 'react';
import classnames from 'classnames';
import filter from 'lodash/filter';
import cloneDeep from 'lodash/cloneDeep';
import { useDrop } from 'react-dnd';
import { IconChevronRight, IconDots } from '@tabler/icons';
import Dropdown from 'components/Dropdown';
import { collectionClicked } from 'providers/ReduxStore/slices/collections';
import { moveItemToRootOfCollection } from 'providers/ReduxStore/slices/collections/actions';
import { useDispatch } from 'react-redux';
import NewRequest from 'components/Sidebar/NewRequest';
import NewFolder from 'components/Sidebar/NewFolder';
@@ -73,21 +71,6 @@ const Collection = ({ collection, searchText }) => {
const isLocal = isLocalCollection(collection);
// const [{ isOver }, drop] = useDrop({
// accept: 'COLLECTION_ITEM',
// drop: (draggedItem) => {
// console.log('drop', draggedItem);
// dispatch(moveItemToRootOfCollection(collection.uid, draggedItem.uid));
// },
// canDrop: (draggedItem) => {
// // todo need to make sure that draggedItem belongs to the collection
// return true;
// },
// collect: (monitor) => ({
// isOver: monitor.isOver()
// })
// });
return (
<StyledWrapper className="flex flex-col">
{showNewRequestModal && <NewRequest collection={collection} onClose={() => setShowNewRequestModal(false)} />}

View File

@@ -1,7 +1,5 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import find from 'lodash/find';
import filter from 'lodash/filter';
import Collection from './Collection';
@@ -28,11 +26,7 @@ const Collections = ({ searchText }) => {
<div className="mt-4 flex flex-col">
{collectionToDisplay && collectionToDisplay.length
? collectionToDisplay.map((c) => {
return (
<DndProvider backend={HTML5Backend} key={c.uid}>
<Collection searchText={searchText} collection={c} />
</DndProvider>
);
return <Collection searchText={searchText} collection={c} key={c.uid} />;
})
: null}
</div>

View File

@@ -61,7 +61,7 @@ const Welcome = () => {
<Bruno width={50} />
</div>
<div className="text-xl font-semibold select-none">bruno</div>
<div className="mt-4">Opensource IDE for exploring and testing api's</div>
<div className="mt-4">Local-first, Opensource API Client.</div>
<div className="uppercase font-semibold heading mt-10">Collections</div>
<div className="mt-4 flex items-center collection-options select-none">

View File

@@ -21,6 +21,17 @@ const Wrapper = styled.div`
.fw-600 {
font-weight: 600;
}
.react-tabs {
.react-tabs__tab-list {
padding-left: 1rem;
border-bottom: 1px solid #cfcfcf;
.react-tabs__tab--selected {
border-color: #cfcfcf;
}
}
}
`;
export default Wrapper;

View File

@@ -166,7 +166,7 @@ const Login = () => {
<div className="font-semibold" style={{ fontSize: '2rem' }}>
bruno
</div>
<div className="mt-1">Opensource IDE for exploring and testing api's.</div>
<div className="mt-1">Opensource API Collection Collaboration Platform</div>
</div>
<form onSubmit={formik.handleSubmit}>
<div className="flex justify-center flex-col form-container mx-auto mt-10 p-5">

View File

@@ -171,7 +171,7 @@ const SignUp = () => {
<div className="font-semibold" style={{ fontSize: '2rem' }}>
bruno
</div>
<div className="mt-1">Opensource IDE for exploring and testing api's.</div>
<div className="mt-1">Opensource API Collection Collaboration Platform</div>
</div>
<form onSubmit={formik.handleSubmit}>

View File

@@ -1,4 +1,3 @@
import { useState, useEffect } from 'react';
import { Provider } from 'react-redux';
import { AppProvider } from 'providers/App';
import { ToastProvider } from 'providers/Toaster';
@@ -10,6 +9,7 @@ import ThemeProvider from 'providers/Theme/index';
import '../styles/app.scss';
import '../styles/globals.css';
import 'tailwindcss/dist/tailwind.min.css';
import 'react-tabs/style/react-tabs.css';
import 'codemirror/lib/codemirror.css';
import 'graphiql/graphiql.min.css';
@@ -28,16 +28,6 @@ function NoSsr({ children }) {
}
function MyApp({ Component, pageProps }) {
const [domLoaded, setDomLoaded] = useState(false);
useEffect(() => {
setDomLoaded(true);
}, []);
if(!domLoaded) {
return null;
}
return (
<SafeHydrate>
<NoSsr>

View File

@@ -7,8 +7,6 @@ import { uuid } from 'utils/common';
import cloneDeep from 'lodash/cloneDeep';
import {
findItemInCollection,
moveCollectionItem,
moveCollectionItemToRootOfCollection,
findCollectionByUid,
recursivelyGetAllItemUids,
transformCollectionToSaveToIdb,
@@ -35,8 +33,6 @@ import {
renameItem as _renameItem,
cloneItem as _cloneItem,
deleteItem as _deleteItem,
moveItem as _moveItem,
moveItemToRootOfCollection as _moveItemToRootOfCollection,
saveRequest as _saveRequest,
addEnvironment as _addEnvironment,
renameEnvironment as _renameEnvironment,
@@ -554,121 +550,6 @@ export const deleteItem = (itemUid, collectionUid) => (dispatch, getState) => {
});
};
export const moveItem = (collectionUid, draggedItemUid, targetItemUid) => (dispatch, getState) => {
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
return new Promise((resolve, reject) => {
if (!collection) {
return reject(new Error('Collection not found'));
}
if (isLocalCollection(collection)) {
const draggedItem = findItemInCollection(collection, draggedItemUid);
const targetItem = findItemInCollection(collection, targetItemUid);
if (!draggedItem) {
return reject(new Error('Dragged item not found'));
}
if (!targetItem) {
return reject(new Error('Target item not found'));
}
const { ipcRenderer } = window;
ipcRenderer
.invoke('renderer:move-item', draggedItem.pathname, targetItem.pathname)
.then(() => resolve())
.catch((error) => reject(error));
return;
}
const collectionCopy = cloneDeep(collection);
const draggedItem = findItemInCollection(collectionCopy, draggedItemUid);
const targetItem = findItemInCollection(collectionCopy, targetItemUid);
if (!draggedItem) {
return reject(new Error('Dragged item not found'));
}
if (!targetItem) {
return reject(new Error('Target item not found'));
}
moveCollectionItem(collectionCopy, draggedItem, targetItem);
const collectionToSave = transformCollectionToSaveToIdb(collectionCopy);
collectionSchema
.validate(collectionToSave)
.then(() => saveCollectionToIdb(window.__idb, collectionToSave))
.then(() => {
dispatch(
_moveItem({
collectionUid: collectionUid,
draggedItemUid: draggedItemUid,
targetItemUid: targetItemUid
})
);
})
.then(() => resolve())
.catch((error) => reject(error));
});
};
export const moveItemToRootOfCollection = (collectionUid, draggedItemUid) => (dispatch, getState) => {
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
return new Promise((resolve, reject) => {
if (!collection) {
return reject(new Error('Collection not found'));
}
if (isLocalCollection(collection)) {
const draggedItem = findItemInCollection(collection, draggedItemUid);
if (!draggedItem) {
return reject(new Error('Dragged item not found'));
}
const { ipcRenderer } = window;
ipcRenderer
.invoke('renderer:move-item-to-root-of-collection', draggedItem.pathname)
.then(() => resolve())
.catch((error) => reject(error));
return;
}
const collectionCopy = cloneDeep(collection);
const draggedItem = findItemInCollection(collectionCopy, draggedItemUid);
if (!draggedItem) {
return reject(new Error('Dragged item not found'));
}
moveCollectionItemToRootOfCollection(collectionCopy, draggedItem);
const collectionToSave = transformCollectionToSaveToIdb(collectionCopy);
collectionSchema
.validate(collectionToSave)
.then(() => saveCollectionToIdb(window.__idb, collectionToSave))
.then(() => {
dispatch(
_moveItemToRootOfCollection({
collectionUid: collectionUid,
draggedItemUid: draggedItemUid
})
);
})
.then(() => resolve())
.catch((error) => reject(error));
});
};
export const newHttpRequest = (params) => (dispatch, getState) => {
const { requestName, requestType, requestUrl, requestMethod, collectionUid, itemUid } = params;

View File

@@ -14,7 +14,6 @@ import {
findEnvironmentInCollection,
findItemInCollectionByPathname,
addDepth,
moveCollectionItem,
collapseCollection,
deleteItemInCollection,
isItemARequest
@@ -182,38 +181,6 @@ export const collectionsSlice = createSlice({
}
}
},
moveItem: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
const draggedItemUid = action.payload.draggedItemUid;
const targetItemUid = action.payload.targetItemUid;
if (collection) {
const draggedItem = findItemInCollection(collection, draggedItemUid);
const targetItem = findItemInCollection(collection, targetItemUid);
if (!draggedItem || !targetItem) {
return;
}
moveCollectionItem(collection, draggedItem, targetItem);
addDepth(collection.items);
}
},
moveItemToRootOfCollection: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
const draggedItemUid = action.payload.draggedItemUid;
if (collection) {
const draggedItem = findItemInCollection(collection, draggedItemUid);
if (!draggedItem) {
return;
}
moveCollectionItemToRootOfCollection(collection, draggedItem);
addDepth(collection.items);
}
},
requestSent: (state, action) => {
const { itemUid, collectionUid, cancelTokenUid } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
@@ -818,8 +785,6 @@ export const {
deleteItem,
renameItem,
cloneItem,
moveItem,
moveItemToRootOfCollection,
requestSent,
requestCancelled,
responseReceived,

View File

@@ -2,7 +2,6 @@ import reckon from 'reckonjs';
import get from 'lodash/get';
import each from 'lodash/each';
import find from 'lodash/find';
import findIndex from 'lodash/findIndex';
import isString from 'lodash/isString';
import map from 'lodash/map';
import filter from 'lodash/filter';
@@ -123,43 +122,6 @@ export const findEnvironmentInCollection = (collection, envUid) => {
return find(collection.environments, (e) => e.uid === envUid);
};
export const moveCollectionItem = (collection, draggedItem, targetItem) => {
let draggedItemParent = findParentItemInCollection(collection, draggedItem.uid);
if (draggedItemParent) {
draggedItemParent.items = filter(draggedItemParent.items, (i) => i.uid !== draggedItem.uid);
} else {
collection.items = filter(collection.items, (i) => i.uid !== draggedItem.uid);
}
if (targetItem.type === 'folder') {
targetItem.items = targetItem.items || [];
targetItem.items.push(draggedItem);
} else {
let targetItemParent = findParentItemInCollection(collection, targetItem.uid);
if (targetItemParent) {
let targetItemIndex = findIndex(targetItemParent.items, (i) => i.uid === targetItem.uid);
targetItemParent.items.splice(targetItemIndex + 1, 0, draggedItem);
} else {
let targetItemIndex = findIndex(collection.items, (i) => i.uid === targetItem.uid);
collection.items.splice(targetItemIndex + 1, 0, draggedItem);
}
}
};
export const moveCollectionItemToRootOfCollection = (collection, draggedItem) => {
let draggedItemParent = findParentItemInCollection(collection, draggedItem.uid);
if (draggedItemParent) {
draggedItemParent.items = filter(draggedItemParent.items, (i) => i.uid !== draggedItem.uid);
} else {
collection.items = filter(collection.items, (i) => i.uid !== draggedItem.uid);
}
collection.items.push(draggedItem);
};
export const transformCollectionToSaveToIdb = (collection, options = {}) => {
const copyHeaders = (headers) => {
return map(headers, (header) => {
@@ -414,6 +376,7 @@ export const interpolateEnvironmentVars = (item, variables) => {
};
request.url = interpolate(request.url);
console.log(request.url);
each(request.headers, (header) => {
header.value = interpolate(header.value);

View File

@@ -14,7 +14,7 @@ export const isLocalCollection = (collection) => {
};
export const resolveRequestFilename = (name) => {
return `${trim(name)}.bru`;
return `${trim(name)}.json`;
};
export const getSubdirectoriesFromRoot = (rootPath, pathname) => {

View File

@@ -13,8 +13,6 @@
"pack-app": "electron-builder --dir"
},
"dependencies": {
"@usebruno/bruno-lang": "0.1.0",
"@usebruno/schema": "0.1.0",
"axios": "^0.26.0",
"chokidar": "^3.5.3",
"electron-is-dev": "^2.0.0",
@@ -32,7 +30,7 @@
},
"devDependencies": {
"electron": "^21.1.1",
"electron-builder": "23.0.2",
"electron-builder": "23.3.3",
"electron-icon-maker": "^0.0.5"
}
}

View File

@@ -3,7 +3,6 @@ const path = require('path');
const { dialog, ipcMain } = require('electron');
const Yup = require('yup');
const { isDirectory, normalizeAndResolvePath } = require('../utils/filesystem');
const { generateUidBasedOnHash } = require('../utils/common');
const uidSchema = Yup.string()
.length(21, 'uid must be 21 characters in length')
@@ -12,6 +11,7 @@ const uidSchema = Yup.string()
.strict();
const configSchema = Yup.object({
uid: uidSchema,
name: Yup.string().nullable().max(256, 'name must be 256 characters or less'),
type: Yup.string().oneOf(['collection']).required('type is required'),
version: Yup.string().oneOf(['1']).required('type is required')
@@ -65,9 +65,12 @@ const openCollection = async (win, watcher, collectionPath, options = {}) => {
if(!watcher.hasWatcher(collectionPath)) {
try {
const {
uid,
name
} = await getCollectionConfigFile(collectionPath);
const uid = generateUidBasedOnHash(collectionPath);
console.log(uid);
console.log(name);
win.webContents.send('main:collection-opened', collectionPath, uid, name);
ipcMain.emit('main:collection-opened', win, collectionPath, uid);

View File

@@ -1,14 +1,7 @@
const _ = require('lodash');
const fs = require('fs');
const path = require('path');
const chokidar = require('chokidar');
const { hasJsonExtension, hasBruExtension, writeFile } = require('../utils/filesystem');
const {
bruToJson,
jsonToBru
} = require('@usebruno/bruno-lang');
const { itemSchema } = require('@usebruno/schema');
const { generateUidBasedOnHash, uuid } = require('../utils/common');
const { hasJsonExtension } = require('../utils/filesystem');
const isEnvironmentConfig = (pathname, collectionPath) => {
const dirname = path.dirname(pathname);
@@ -17,22 +10,6 @@ const isEnvironmentConfig = (pathname, collectionPath) => {
return dirname === collectionPath && basename === 'environments.json';
}
const hydrateRequestWithUuid = (request, pathname) => {
request.uid = generateUidBasedOnHash(pathname);
const params = _.get(request, 'request.params', []);
const headers = _.get(request, 'request.headers', []);
const bodyFormUrlEncoded = _.get(request, 'request.body.formUrlEncoded', []);
const bodyMultipartForm = _.get(request, 'request.body.multipartForm', []);
params.forEach((param) => param.uid = uuid());
headers.forEach((header) => header.uid = uuid());
bodyFormUrlEncoded.forEach((param) => param.uid = uuid());
bodyMultipartForm.forEach((param) => param.uid = uuid());
return request;
}
const addEnvironmentFile = async (win, pathname, collectionUid) => {
try {
const file = {
@@ -94,30 +71,7 @@ const add = async (win, pathname, collectionUid, collectionPath) => {
if(isEnvironmentConfig(pathname, collectionPath)) {
return addEnvironmentFile(win, pathname, collectionUid);
}
}
// migrate old json files to bru
if(hasJsonExtension(pathname)) {
try {
const json = fs.readFileSync(pathname, 'utf8');
const jsonData = JSON.parse(json);
await itemSchema.validate(jsonData);
const content = jsonToBru(jsonData);
const re = /(.*)\.json$/;
const subst = `$1.bru`;
const bruFilename = pathname.replace(re, subst);
await writeFile(bruFilename, content);
await fs.unlinkSync(pathname);
} catch (err) {
// do nothing
}
}
if(hasBruExtension(pathname)) {
const file = {
meta: {
collectionUid,
@@ -127,9 +81,8 @@ const add = async (win, pathname, collectionUid, collectionPath) => {
}
try {
const bru = fs.readFileSync(pathname, 'utf8');
file.data = bruToJson(bru);
hydrateRequestWithUuid(file.data, pathname);
const jsonData = fs.readFileSync(pathname, 'utf8');
file.data = JSON.parse(jsonData);
win.webContents.send('main:collection-tree-updated', 'addFile', file);
} catch (err) {
console.error(err)
@@ -151,30 +104,25 @@ const addDirectory = (win, pathname, collectionUid) => {
const change = async (win, pathname, collectionUid, collectionPath) => {
console.log(`watcher change: ${pathname}`);
if(isEnvironmentConfig(pathname, collectionPath)) {
return changeEnvironmentFile(win, pathname, collectionUid);
}
if(hasBruExtension(pathname)) {
try {
const file = {
meta: {
collectionUid,
pathname,
name: path.basename(pathname),
}
};
const bru = fs.readFileSync(pathname, 'utf8');
file.data = bruToJson(bru);
hydrateRequestWithUuid(file.data, pathname);
win.webContents.send('main:collection-tree-updated', 'change', file);
} catch (err) {
console.error(err)
try {
if(isEnvironmentConfig(pathname, collectionPath)) {
return changeEnvironmentFile(win, pathname, collectionUid);
}
}
const file = {
meta: {
collectionUid,
pathname,
name: path.basename(pathname),
}
};
const jsonData = fs.readFileSync(pathname, 'utf8');
file.data = await JSON.parse(jsonData);
win.webContents.send('main:collection-tree-updated', 'change', file);
} catch (err) {
console.error(err)
}
};
const unlink = (win, pathname, collectionUid, collectionPath) => {
@@ -182,19 +130,19 @@ const unlink = (win, pathname, collectionUid, collectionPath) => {
return unlinkEnvironmentFile(win, pathname, collectionUid);
}
if(hasBruExtension(pathname)) {
const file = {
meta: {
collectionUid,
pathname,
name: path.basename(pathname)
}
};
win.webContents.send('main:collection-tree-updated', 'unlink', file);
}
console.log(`watcher unlink: ${pathname}`);
const file = {
meta: {
collectionUid,
pathname,
name: path.basename(pathname)
}
};
win.webContents.send('main:collection-tree-updated', 'unlink', file);
}
const unlinkDir = (win, pathname, collectionUid) => {
console.log(`watcher unlinkDir: ${pathname}`);
const directory = {
meta: {
collectionUid,
@@ -215,11 +163,6 @@ class Watcher {
this.watchers[watchPath].close();
}
// todo
// enable this in a future release
// once we can confirm all older json based files have been auto migrated to .bru format
// watchPath = path.join(watchPath, '**/*.bru');
const self = this;
setTimeout(() => {
const watcher = chokidar.watch(watchPath, {

View File

@@ -2,19 +2,15 @@ const _ = require('lodash');
const fs = require('fs');
const path = require('path');
const { ipcMain } = require('electron');
const {
jsonToBru,
bruToJson,
} = require('@usebruno/bruno-lang');
const {
isValidPathname,
writeFile,
hasBruExtension,
hasJsonExtension,
isDirectory,
browseDirectory,
createDirectory
} = require('../utils/filesystem');
const { uuid, stringifyJson } = require('../utils/common');
const { uuid, stringifyJson, parseJson } = require('../utils/common');
const { openCollectionDialog, openCollection } = require('../app/collections');
const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollections) => {
@@ -68,7 +64,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
throw new Error(`path: ${pathname} already exists`);
}
const content = jsonToBru(request);
const content = await stringifyJson(request);
await writeFile(pathname, content);
} catch (error) {
return Promise.reject(error);
@@ -82,7 +78,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
throw new Error(`path: ${pathname} does not exist`);
}
const content = jsonToBru(request);
const content = await stringifyJson(request);
await writeFile(pathname, content);
} catch (error) {
return Promise.reject(error);
@@ -116,18 +112,18 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
return fs.renameSync(oldPath, newPath);
}
const isBru = hasBruExtension(oldPath);
if(!isBru) {
throw new Error(`path: ${oldPath} is not a bru file`);
const isJson = hasJsonExtension(oldPath);
if(!isJson) {
throw new Error(`path: ${oldPath} is not a json file`);
}
// update name in file and save new copy, then delete old copy
const data = fs.readFileSync(oldPath, 'utf8');
const jsonData = bruToJson(data);
const jsonData = await parseJson(data);
jsonData.name = newName;
const content = jsonToBru(jsonData);
const content = await stringifyJson(jsonData);
await writeFile(newPath, content);
await fs.unlinkSync(oldPath);
} catch (error) {

View File

@@ -25,26 +25,8 @@ const parseJson = async (obj) => {
}
}
const simpleHash = (str) => {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash &= hash; // Convert to 32bit integer
}
return new Uint32Array([hash])[0].toString(36);
};
const generateUidBasedOnHash = (str) => {
const hash = simpleHash(str);
return `${hash}`.padEnd(21, '0');
}
module.exports = {
uuid,
stringifyJson,
parseJson,
simpleHash,
generateUidBasedOnHash
parseJson
};

View File

@@ -65,11 +65,6 @@ const hasJsonExtension = filename => {
return ['json'].some(ext => filename.toLowerCase().endsWith(`.${ext}`))
}
const hasBruExtension = filename => {
if (!filename || typeof filename !== 'string') return false
return ['bru'].some(ext => filename.toLowerCase().endsWith(`.${ext}`))
}
const createDirectory = async (dir) => {
if(!dir) {
throw new Error(`directory: path is null`);
@@ -104,7 +99,6 @@ module.exports = {
normalizeAndResolvePath,
writeFile,
hasJsonExtension,
hasBruExtension,
createDirectory,
browseDirectory
};

View File

@@ -19,8 +19,8 @@
"graphql": "^16.6.0",
"markdown-it": "^13.0.1",
"postcss": "^8.4.18",
"react": "18.2.0",
"react-dom": "18.2.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"rollup": "3.2.5",
"rollup-plugin-dts": "^5.0.0",
"rollup-plugin-peer-deps-external": "^2.2.4",
@@ -30,7 +30,8 @@
},
"peerDependencies": {
"graphql": "^16.6.0",
"markdown-it": "^13.0.1"
"markdown-it": "^13.0.1",
"react": "^17.0.2"
},
"overrides": {
"rollup": "3.2.5"

View File

@@ -1,7 +0,0 @@
node_modules
web
out
pnpm-lock.yaml
package-lock.json
yarn.lock

View File

@@ -1,55 +0,0 @@
ver 1.0
type http-request
name Send Bulk SMS
method GET
url https://api.textlocal.in/bulk_json?apiKey=secret=&numbers=919988776655&message=hello&sender=600010
body-mode json
params
1 apiKey secret
1 numbers 998877665
1 message hello
/params
headers
1 content-type application/json
1 accept-language en-US,en;q=0.9,hi;q=0.8
0 transaction-id {{transactionId}}
/headers
body(type=json)
{
"apikey": "secret",
"numbers": "+91998877665",
"data": {
"sender": "TXTLCL",
"messages": [{
"numbers": "+91998877665",
"message": "Hello World"
}]
}
}
/body
body(type=graphql)
{
launchesPast {
launch_site {
site_name
}
launch_success
}
}
/body
script
let user = 'John Doe';
function onRequest(request) {
request.body.user = user;
}
function onResponse(request, response) {
expect(response.status).to.equal(200);
}
/script

View File

@@ -1,39 +0,0 @@
{
"type": "http-request",
"name": "Send Bulk SMS",
"request": {
"method": "GET",
"url": "https://api.textlocal.in/bulk_json?apiKey=secret=&numbers=998877665&message=hello&sender=600010",
"params": [
{
"name": "apiKey",
"value": "secret",
"enabled": true
},
{
"name": "numbers",
"value": "998877665",
"enabled": true
},
{
"name": "message",
"value": "hello",
"enabled": true
},
{
"name": "sender",
"value": "600010",
"enabled": true
}
],
"headers": [],
"body": {
"mode": "json",
"json": "{\n apikey: \"secret\",\n numbers: \"+919988776655\",\n data: {\n sender: \"TXTLCL\",\n messages: [{\n numbers: \"+919988776655\",\n message: \"Hello World\"\n }]\n }\n}",
"text": null,
"xml": null,
"multipartForm": null,
"formUrlEncoded": null
}
}
}

View File

@@ -1,95 +0,0 @@
ver 1.0
type http-request
name Send Bulk SMS
method GET
url https://api.textlocal.in/bulk_json?apiKey=secret=&numbers=919988776655&message=hello&sender=600010
body-mode json
params
1 apiKey secret
1 numbers 998877665
1 message hello
/params
headers
1 content-type application/json
1 accept-language en-US,en;q=0.9,hi;q=0.8
0 transaction-id {{transactionId}}
/headers
body(type=json)
{
apikey: "secret",
numbers: "+91998877665",
data: {
sender: "TXTLCL",
messages: [{
numbers: "+91998877665",
message: "Hello World"
}]
}
}
/body
body(type=graphql)
{
launchesPast {
launch_site {
site_name
}
launch_success
}
}
/body
script
let user = 'John Doe';
function onRequest(request) {
request.body.user = user;
}
function onResponse(request, response) {
expect(response.status).to.equal(200);
}
/script
assert
{
"$res.data.order.items.length": 1,
"$res.data.orderNumber.isDefined": true
}
/assert
docs
Documentation about the request
/docs
response-example(name="Created", status=201)
headers
1 content-type application/json
1 accept-language en-US,en;q=0.9,hi;q=0.8
0 transaction-id {{transactionId}}
/headers
body
{
"data": {
"launchesPast": [
{
"launch_site": {
"site_name": "CCAFS SLC 40"
},
"launch_success": true
},
{
"launch_site": {
"site_name": "VAFB SLC 4E"
},
"launch_success": true
}
]
}
}
/body
/response-example

View File

@@ -1,15 +0,0 @@
{
"name": "@usebruno/bruno-lang",
"version": "0.1.0",
"main": "src/index.js",
"files": [
"src",
"package.json"
],
"scripts": {
"test": "jest"
},
"dependencies": {
"arcsecond": "^5.0.0"
}
}

View File

@@ -1,3 +0,0 @@
# bruno-lang
The language utils for working with `.bru` files

View File

@@ -1,121 +0,0 @@
const {
between,
regex,
everyCharUntil,
digit,
whitespace,
optionalWhitespace,
endOfInput,
choice,
many,
sepBy,
sequenceOf
} = require("arcsecond");
// body(type=json)
const bodyJsonBegin = regex(/^body\s*\(\s*type\s*=\s*json\s*\)\s*\r?\n/);
// body(type=graphql)
const bodyGraphqlBegin = regex(/^body\s*\(\s*type\s*=\s*graphql\s*\)\s*\r?\n/);
// body(type=text)
const bodyTextBegin = regex(/^body\s*\(\s*type\s*=\s*text\s*\)\s*\r?\n/);
// body(type=xml)
const bodyXmlBegin = regex(/^body\s*\(\s*type\s*=\s*xml\s*\)\s*\r?\n/);
const bodyEnd = regex(/^[\r?\n]+\/body\s*[\r?\n]*/);
const bodyJsonTag = between(bodyJsonBegin)(bodyEnd)(everyCharUntil(bodyEnd)).map((bodyJson) => {
return {
body: {
json: bodyJson
}
};
});
const bodyGraphqlTag = between(bodyGraphqlBegin)(bodyEnd)(everyCharUntil(bodyEnd)).map((bodyGraphql) => {
return {
body: {
graphql: {
query: bodyGraphql
}
}
}
});
const bodyTextTag = between(bodyTextBegin)(bodyEnd)(everyCharUntil(bodyEnd)).map((bodyText) => {
return {
body: {
text: bodyText
}
}
});
const bodyXmlTag = between(bodyXmlBegin)(bodyEnd)(everyCharUntil(bodyEnd)).map((bodyXml) => {
return {
body: {
xml: bodyXml
}
}
});
// generic key value parser
const newline = regex(/^\r?\n/);
const newLineOrEndOfInput = choice([newline, endOfInput]);
const word = regex(/^[^\s\t\n]+/g);
const line = sequenceOf([
optionalWhitespace,
digit,
whitespace,
word,
whitespace,
word,
newLineOrEndOfInput
]).map(([_, enabled, __, key, ___, value]) => {
return {
"enabled": Number(enabled) ? true : false,
"name": key,
"value": value
};
});
const lines = many(line);
const keyvalLines = sepBy(newline)(lines);
// body(type=form-url-encoded)
const bodyFormUrlEncoded = regex(/^body\s*\(\s*type\s*=\s*form-url-encoded\s*\)\s*\r?\n/);
// body(type=multipart-form)
const bodyMultipartForm = regex(/^body\s*\(\s*type\s*=\s*multipart-form\s*\)\s*\r?\n/);
// this regex allows the body end tag to start without a newline
// currently the line parser consumes the last newline
// todo: fix this
const bodyEndRelaxed = regex(/^[\r?\n]*\/body\s*[\r?\n]*/);
const bodyFormUrlEncodedTag = between(bodyFormUrlEncoded)(bodyEndRelaxed)(keyvalLines).map(([result]) => {
return {
body: {
formUrlEncoded: result
}
}
});
const bodyMultipartFormTag = between(bodyMultipartForm)(bodyEndRelaxed)(keyvalLines).map(([result]) => {
return {
body: {
multipartForm: result
}
}
});
module.exports = {
bodyJsonTag,
bodyGraphqlTag,
bodyTextTag,
bodyXmlTag,
bodyFormUrlEncodedTag,
bodyMultipartFormTag
};

View File

@@ -1,45 +0,0 @@
const {
sequenceOf,
whitespace,
optionalWhitespace,
choice,
endOfInput,
between,
digit,
many,
regex,
sepBy
} = require("arcsecond");
const newline = regex(/^\r?\n/);
const newLineOrEndOfInput = choice([newline, endOfInput]);
const begin = regex(/^headers\s*\r?\n/);
const end = regex(/^[\r?\n]*\/headers\s*[\r?\n]*/);
const word = regex(/^[^\s\t\n]+/g);
const line = sequenceOf([
optionalWhitespace,
digit,
whitespace,
word,
whitespace,
word,
newLineOrEndOfInput
]).map(([_, enabled, __, key, ___, value]) => {
return {
"enabled": Number(enabled) ? true : false,
"name": key,
"value": value
};
});
const lines = many(line);
const headersLines = sepBy(newline)(lines);
const headersTag = between(begin)(end)(headersLines).map(([headers]) => {
return {
headers
};
});
module.exports = headersTag;

View File

@@ -1,167 +0,0 @@
const {
many,
choice,
anyChar
} = require("arcsecond");
const _ = require('lodash');
const {
indentString,
outdentString,
get
} = require('./utils');
const inlineTag = require('./inline-tag');
const paramsTag = require('./params-tag');
const headersTag = require('./headers-tag');
const {
bodyJsonTag,
bodyGraphqlTag,
bodyTextTag,
bodyXmlTag,
bodyFormUrlEncodedTag,
bodyMultipartFormTag
} = require('./body-tag');
const bruToJson = (fileContents) => {
const parser = many(choice([
inlineTag,
paramsTag,
headersTag,
bodyJsonTag,
bodyGraphqlTag,
bodyTextTag,
bodyXmlTag,
bodyFormUrlEncodedTag,
bodyMultipartFormTag,
anyChar
]));
const parsed = parser
.run(fileContents)
.result
.reduce((acc, item) => _.merge(acc, item), {});
const json = {
type: parsed.type || '',
name: parsed.name || '',
request: {
method: parsed.method || '',
url: parsed.url || '',
params: parsed.params || [],
headers: parsed.headers || [],
body: parsed.body || {mode: 'none'}
}
}
const body = get(json, 'request.body');
if(body && body.text) {
body.text = outdentString(body.text);
}
if(body && body.json) {
body.json = outdentString(body.json);
}
if(body && body.xml) {
body.xml = outdentString(body.xml);
}
if(body && body.graphql && body.graphql.query) {
body.graphql.query = outdentString(body.graphql.query);
}
return json;
};
const jsonToBru = (json) => {
const {
type,
name,
request: {
method,
url,
params,
headers,
body
}
} = json;
let bru = `name ${name}
method ${method}
url ${url}
type ${type}
body-mode ${body ? body.mode : 'none'}
`;
if(params && params.length) {
bru += `
params
${params.map(param => ` ${param.enabled ? 1 : 0} ${param.name} ${param.value}`).join('\n')}
/params
`;
}
if(headers && headers.length) {
bru += `
headers
${headers.map(header => ` ${header.enabled ? 1 : 0} ${header.name} ${header.value}`).join('\n')}
/headers
`;
}
if(body && body.json && body.json.length) {
bru += `
body(type=json)
${indentString(body.json)}
/body
`;
}
if(body && body.graphql && body.graphql.query) {
bru += `
body(type=graphql)
${indentString(body.graphql.query)}
/body
`;
}
if(body && body.text && body.text.length) {
bru += `
body(type=text)
${indentString(body.text)}
/body
`;
}
if(body && body.xml && body.xml.length) {
bru += `
body(type=xml)
${indentString(body.xml)}
/body
`;
}
if(body && body.formUrlEncoded && body.formUrlEncoded.length) {
bru += `
body(type=form-url-encoded)
${body.formUrlEncoded.map(item => ` ${item.enabled ? 1 : 0} ${item.name} ${item.value}`).join('\n')}
/body
`;
}
if(body && body.multipartForm && body.multipartForm.length) {
bru += `
body(type=multipart-form)
${body.multipartForm.map(item => ` ${item.enabled ? 1 : 0} ${item.name} ${item.value}`).join('\n')}
/body
`;
}
return bru;
};
module.exports = {
bruToJson,
jsonToBru
};

View File

@@ -1,36 +0,0 @@
const {
sequenceOf,
whitespace,
str,
lookAhead,
choice,
endOfInput,
everyCharUntil
} = require("arcsecond");
const newline = lookAhead(str("\n"));
const newLineOrEndOfInput = choice([endOfInput, newline]);
const inlineTag = sequenceOf([
choice([
str('type'),
str('name'),
str('method'),
str('url'),
str('body-mode')
]),
whitespace,
everyCharUntil(newLineOrEndOfInput)
]).map(([key, _, val]) => {
if(key === 'body-mode') {
return {
body: {
mode: val
}
};
}
return { [key]: val };
});
module.exports = inlineTag;

View File

@@ -1,46 +0,0 @@
const {
sequenceOf,
whitespace,
optionalWhitespace,
choice,
endOfInput,
everyCharUntil,
between,
digit,
many,
regex,
sepBy
} = require("arcsecond");
const newline = regex(/^\r?\n/);
const newLineOrEndOfInput = choice([newline, endOfInput]);
const begin = regex(/^params\s*\r?\n/);
const end = regex(/^[\r?\n]*\/params\s*[\r?\n]*/);
const word = regex(/^[^\s\t\n]+/g);
const line = sequenceOf([
optionalWhitespace,
digit,
whitespace,
word,
whitespace,
word,
newLineOrEndOfInput
]).map(([_, enabled, __, key, ___, value]) => {
return {
"enabled": Number(enabled) ? true : false,
"name": key,
"value": value
};
});
const lines = many(line);
const paramsLines = sepBy(newline)(lines);
const paramsTag = between(begin)(end)(paramsLines).map(([params]) => {
return {
params
};
});
module.exports = paramsTag;

View File

@@ -1,44 +0,0 @@
// safely parse json
const safeParseJson = (json) => {
try {
return JSON.parse(json);
} catch (e) {
return null;
}
};
const indentString = (str) => {
if(!str || !str.length) {
return str;
}
return str.split("\n").map(line => " " + line).join("\n");
};
const outdentString = (str) => {
if(!str || !str.length) {
return str;
}
return str.split("\n").map(line => line.replace(/^ /, '')).join("\n");
};
// implement lodash _.get functionality
const get = (obj, path, defaultValue) => {
const pathParts = path.split('.');
let current = obj;
for(let i = 0; i < pathParts.length; i++) {
if(current[pathParts[i]] === undefined) {
return defaultValue;
}
current = current[pathParts[i]];
}
return current;
};
module.exports = {
get,
safeParseJson,
indentString,
outdentString
};

View File

@@ -1,46 +0,0 @@
const { bodyJsonTag } = require('../src/body-tag');
describe('bodyJsonTag', () => {
const testbodyJson = (input, expected) => {
const result = bodyJsonTag.run(input);
expect(result.isError).toBe(false);
expect(result.result.body.json).toEqual('{ "foo": "bar" }');
};
// simple case
it('should parse json body tag - 1', () => {
const input = 'body(type=json)\n{ "foo": "bar" }\n/body';
testbodyJson(input, '{ "foo": "bar" }\n');
});
// space between body and args
it('should parse json body tag - 2', () => {
const input = 'body (type = json)\n{ "foo": "bar" }\n/body';
testbodyJson(input, '{ "foo": "bar" }\n');
});
// space after body tag
it('should parse json body tag - 3', () => {
const input = 'body (type = json) \n{ "foo": "bar" }\n/body';
testbodyJson(input, '{ "foo": "bar" }\n');
});
// space after body tag
it('should parse json body tag - 4', () => {
const input = 'body (type = json) \n{ "foo": "bar" }\n/body ';
testbodyJson(input, '{ "foo": "bar" }\n');
});
it('should fail to parse when body tag is missing', () => {
const input = '{ "foo": "bar" }\n/body';
const result = bodyJsonTag.run(input);
expect(result.isError).toBe(true);
});
it('should fail to parse when body end tag is missing', () => {
const input = 'body (type = json)\n{ "foo": "bar" }';
const result = bodyJsonTag.run(input);
expect(result.isError).toBe(true);
});
});

View File

@@ -1,91 +0,0 @@
const fs = require('fs');
const path = require('path');
const {
bruToJson
} = require('../src');
describe('bruToJson', () => {
it('should parse .bru file contents', () => {
const requestFile = fs.readFileSync(path.join(__dirname, 'fixtures', 'request.bru'), 'utf8');
const result = bruToJson(requestFile);
expect(result).toEqual({
"type": "http-request",
"name": "Send Bulk SMS",
"request": {
"method": "GET",
"url": "https://api.textlocal.in/bulk_json?apiKey=secret=&numbers=919988776655&message=hello&sender=600010",
"params": [
{
"enabled": true,
"name": "apiKey",
"value": "secret"
},
{
"enabled": true,
"name": "numbers",
"value": "998877665"
},
{
"enabled": true,
"name": "message",
"value": "hello"
}
],
"headers": [
{
"enabled": true,
"name": "content-type",
"value": "application/json"
},
{
"enabled": true,
"name": "accept-language",
"value": "en-US,en;q=0.9,hi;q=0.8"
},
{
"enabled": false,
"name": "transaction-id",
"value": "{{transactionId}}"
}
],
"body": {
"mode": "json",
"json": '{\n "apikey": "secret",\n "numbers": "+91998877665"\n}',
"graphql": {
"query": "{\n launchesPast {\n launch_success\n }\n}"
},
"text": "Hello, there. You must be from the past",
"xml": "<body>back to the ice age</body>",
"formUrlEncoded": [
{
"enabled": true,
"name": "username",
"value": "john"
},
{
"enabled": false,
"name": "password",
"value": "{{password}}"
}
],
"multipartForm": [
{
"enabled": true,
"name": "username",
"value": "nash"
},
{
"enabled": false,
"name": "password",
"value": "governingdynamics"
}
]
}
}
});
});
});

View File

@@ -1,50 +0,0 @@
name Send Bulk SMS
method GET
url https://api.textlocal.in/bulk_json?apiKey=secret=&numbers=919988776655&message=hello&sender=600010
type http-request
body-mode json
params
1 apiKey secret
1 numbers 998877665
1 message hello
/params
headers
1 content-type application/json
1 accept-language en-US,en;q=0.9,hi;q=0.8
0 transaction-id {{transactionId}}
/headers
body(type=json)
{
"apikey": "secret",
"numbers": "+91998877665"
}
/body
body(type=graphql)
{
launchesPast {
launch_success
}
}
/body
body(type=text)
Hello, there. You must be from the past
/body
body(type=xml)
<body>back to the ice age</body>
/body
body(type=form-url-encoded)
1 username john
0 password {{password}}
/body
body(type=multipart-form)
1 username nash
0 password governingdynamics
/body

View File

@@ -1,58 +0,0 @@
const inlineTag = require('../src/inline-tag');
const {
sepBy,
char,
many
} = require('arcsecond');
describe('type', () => {
it('should parse the type', () => {
const input = 'type http-request';
const result = inlineTag.run(input);
expect(result.isError).toBe(false);
expect(result.result).toEqual({ type: 'http-request' });
});
it('should allow whitespaces while parsing the type', () => {
const input = 'type http-request';
const result = inlineTag.run(input);
expect(result.isError).toBe(false);
expect(result.result).toEqual({ type: 'http-request' });
});
it('should fail to parse when type is missing', () => {
const input = 'type';
const result = inlineTag.run(input);
expect(result.isError).toBe(true);
});
});
describe('multiple inline tags', () => {
it('should parse the multiple inline tags', () => {
const input = `
type http-request
name Send Bulk SMS
method GET
url https://api.textlocal.in/bulk_json?apiKey=secret=&numbers=919988776655&message=hello&sender=600010
body-mode json
`;
const newline = char('\n');
const line = inlineTag;
const lines = many(line);
const parser = sepBy(newline)(lines);
const result = parser.run(input);
expect(result.isError).toBe(false);
expect(result.result).toEqual([
[],
[{ type: 'http-request' }],
[{ name: 'Send Bulk SMS' }],
[{ method: 'GET' }],
[{ url: 'https://api.textlocal.in/bulk_json?apiKey=secret=&numbers=919988776655&message=hello&sender=600010' }],
[{ body: { mode: 'json' } }],
[]
]);
})
});

View File

@@ -1,93 +0,0 @@
const fs = require('fs');
const path = require('path');
const {
jsonToBru
} = require('../src');
describe('bruToJson', () => {
it('should convert json file into .bru file', () => {
const request = {
"type": "http-request",
"name": "Send Bulk SMS",
"request": {
"method": "GET",
"url": "https://api.textlocal.in/bulk_json?apiKey=secret=&numbers=919988776655&message=hello&sender=600010",
"params": [
{
"enabled": true,
"name": "apiKey",
"value": "secret"
},
{
"enabled": true,
"name": "numbers",
"value": "998877665"
},
{
"enabled": true,
"name": "message",
"value": "hello"
}
],
"headers": [
{
"enabled": true,
"name": "content-type",
"value": "application/json"
},
{
"enabled": true,
"name": "accept-language",
"value": "en-US,en;q=0.9,hi;q=0.8"
},
{
"enabled": false,
"name": "transaction-id",
"value": "{{transactionId}}"
}
],
"body": {
"mode": "json",
"json": '{\n "apikey": "secret",\n "numbers": "+91998877665"\n}',
"graphql": {
"query": "{\n launchesPast {\n launch_success\n }\n}"
},
"text": "Hello, there. You must be from the past",
"xml": "<body>back to the ice age</body>",
"formUrlEncoded": [
{
"enabled": true,
"name": "username",
"value": "john"
},
{
"enabled": false,
"name": "password",
"value": "{{password}}"
}
],
"multipartForm": [
{
"enabled": true,
"name": "username",
"value": "nash"
},
{
"enabled": false,
"name": "password",
"value": "governingdynamics"
}
]
}
}
};
const expectedBruFile = fs.readFileSync(path.join(__dirname, 'fixtures', 'request.bru'), 'utf8');
const actualBruFile = jsonToBru(request);
expect(expectedBruFile).toEqual(actualBruFile);
});
});

View File

@@ -1,50 +0,0 @@
const {
safeParseJson,
indentString,
outdentString,
get
} = require('../src/utils');
describe('utils', () => {
describe('safeParseJson', () => {
it('should parse valid json', () => {
const input = '{"a": 1}';
const result = safeParseJson(input);
expect(result).toEqual({ a: 1 });
});
it('should return null for invalid json', () => {
const input = '{"a": 1';
const result = safeParseJson(input);
expect(result).toBeNull();
});
});
describe('indentString', () => {
it('correctly indents a multiline string', () => {
const input = "line1\nline2\nline3";
const expectedOutput = " line1\n line2\n line3";
expect(indentString(input)).toBe(expectedOutput);
});
});
describe('outdentString', () => {
it('correctly outdents a multiline string', () => {
const input = " line1\n line2\n line3";
const expectedOutput = "line1\nline2\nline3";
expect(outdentString(input)).toBe(expectedOutput);
});
});
describe('get', () => {
it('returns the value at the given path', () => {
const input = { a: { b: { c: 1 } } };
expect(get(input, 'a.b.c')).toBe(1);
});
it('returns the defaultValue if the path does not exist', () => {
const input = { a: { b: { c: 1 } } };
expect(get(input, 'a.b.d', 2)).toBe(2);
});
});
});

View File

@@ -1,17 +0,0 @@
// A launch configuration that launches the extension inside a new window
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
{
"version": "0.2.0",
"configurations": [
{
"name": "Extension",
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}"
]
}
]
}

View File

@@ -1,4 +0,0 @@
.vscode/**
.vscode-test/**
.gitignore
vsc-extension-quickstart.md

View File

@@ -1,9 +0,0 @@
# Change Log
All notable changes to the "bruno" extension will be documented in this file.
Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.
## [Unreleased]
- Initial release

View File

@@ -1,65 +0,0 @@
# bruno README
This is the README for your extension "bruno". After writing up a brief description, we recommend including the following sections.
## Features
Describe specific features of your extension including screenshots of your extension in action. Image paths are relative to this README file.
For example if there is an image subfolder under your extension project workspace:
\!\[feature X\]\(images/feature-x.png\)
> Tip: Many popular extensions utilize animations. This is an excellent way to show off your extension! We recommend short, focused animations that are easy to follow.
## Requirements
If you have any requirements or dependencies, add a section describing those and how to install and configure them.
## Extension Settings
Include if your extension adds any VS Code settings through the `contributes.configuration` extension point.
For example:
This extension contributes the following settings:
* `myExtension.enable`: Enable/disable this extension.
* `myExtension.thing`: Set to `blah` to do something.
## Known Issues
Calling out known issues can help limit users opening duplicate issues against your extension.
## Release Notes
Users appreciate release notes as you update your extension.
### 1.0.0
Initial release of ...
### 1.0.1
Fixed issue #.
### 1.1.0
Added features X, Y, and Z.
---
## Working with Markdown
You can author your README using Visual Studio Code. Here are some useful editor keyboard shortcuts:
* Split the editor (`Cmd+\` on macOS or `Ctrl+\` on Windows and Linux).
* Toggle preview (`Shift+Cmd+V` on macOS or `Shift+Ctrl+V` on Windows and Linux).
* Press `Ctrl+Space` (Windows, Linux, macOS) to see a list of Markdown snippets.
## For more information
* [Visual Studio Code's Markdown Support](http://code.visualstudio.com/docs/languages/markdown)
* [Markdown Syntax Reference](https://help.github.com/articles/markdown-basics/)
**Enjoy!**

View File

@@ -1,30 +0,0 @@
{
"comments": {
// symbol used for single line comment. Remove this entry if your language does not support line comments
"lineComment": "//",
// symbols used for start and end a block comment. Remove this entry if your language does not support block comments
"blockComment": [ "/*", "*/" ]
},
// symbols used as brackets
"brackets": [
["{", "}"],
["[", "]"],
["(", ")"]
],
// symbols that are auto closed when typing
"autoClosingPairs": [
["{", "}"],
["[", "]"],
["(", ")"],
["\"", "\""],
["'", "'"]
],
// symbols that can be used to surround a selection
"surroundingPairs": [
["{", "}"],
["[", "]"],
["(", ")"],
["\"", "\""],
["'", "'"]
]
}

View File

@@ -1,25 +0,0 @@
{
"name": "bruno",
"displayName": "Bruno",
"description": "Bruno support for Visual Studio Code.",
"version": "0.0.1",
"engines": {
"vscode": "^1.74.0"
},
"categories": [
"Programming Languages"
],
"contributes": {
"languages": [{
"id": "bruno",
"aliases": ["bruno", "bruno"],
"extensions": [".bru"],
"configuration": "./language-configuration.json"
}],
"grammars": [{
"language": "bruno",
"scopeName": "source.bru",
"path": "./syntaxes/bruno.tmLanguage.json"
}]
}
}

View File

@@ -1,45 +0,0 @@
{
"$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
"name": "bruno",
"patterns": [
{
"include": "#keywords"
},
{
"include": "#strings"
},
{
"include": "#script-block"
}
],
"repository": {
"keywords": {
"patterns": [{
"name": "keyword.control.bruno",
"match": "\\b(ver|type|name|method|url|params|body-mode|body|script|assert|vars|response-example|readme)\\b"
}]
},
"strings": {
"name": "string.quoted.double.bruno",
"begin": "\"",
"end": "\"",
"patterns": [
{
"name": "constant.character.escape.bruno",
"match": "\\\\."
}
]
},
"script-block": {
"name": "meta.script-block.bruno",
"begin": "script",
"end": "/script",
"patterns": [
{
"include": "source.js"
}
]
}
},
"scopeName": "source.bru"
}

View File

@@ -1,29 +0,0 @@
# Welcome to your VS Code Extension
## What's in the folder
* This folder contains all of the files necessary for your extension.
* `package.json` - this is the manifest file in which you declare your language support and define the location of the grammar file that has been copied into your extension.
* `syntaxes/bruno.tmLanguage.json` - this is the Text mate grammar file that is used for tokenization.
* `language-configuration.json` - this is the language configuration, defining the tokens that are used for comments and brackets.
## Get up and running straight away
* Make sure the language configuration settings in `language-configuration.json` are accurate.
* Press `F5` to open a new window with your extension loaded.
* Create a new file with a file name suffix matching your language.
* Verify that syntax highlighting works and that the language configuration settings are working.
## Make changes
* You can relaunch the extension from the debug toolbar after making changes to the files listed above.
* You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes.
## Add more language features
* To add features such as IntelliSense, hovers and validators check out the VS Code extenders documentation at https://code.visualstudio.com/docs
## Install your extension
* To start using your extension with Visual Studio Code copy it into the `<user home>/.vscode/extensions` folder and restart Code.
* To share your extension with the world, read on https://code.visualstudio.com/docs about publishing an extension.

View File

@@ -1,8 +1,15 @@
# bruno
Opensource IDE for exploring and testing api's.
Local-first, Opensource API Client.
### Live Demo 🏂
Woof! Lets play with some api's [here](https://play.usebruno.com).
Your api must allow CORS for it to work in the browser, else checkout the chrome extension ot the desktop app
You can visit the [Website](https://www.usebruno.com) or watch a [4 min demo](https://www.youtube.com/watch?v=wwXJW7_qyLA)
![bruno](assets/images/landing.png)
### Bring Your Own Version Control ✨
Bruno is built from the ground up with the Local-first paradigm in mind. This allows developers to directly store there collections on top of their local filesystem. The collections are mirrored as folders and files on the filesystem.
![bruno](assets/images/local-collections.png)