XML and XPath support

This commit is contained in:
Gregory Schier 2016-09-09 18:51:49 -07:00
parent b51fb24e53
commit 46d56804e6
6 changed files with 121 additions and 52 deletions

View File

@ -20,6 +20,8 @@
"raven": "^0.12.1", "raven": "^0.12.1",
"request": "^2.71.0", "request": "^2.71.0",
"tough-cookie": "^2.3.1", "tough-cookie": "^2.3.1",
"traverse": "^0.6.6" "traverse": "^0.6.6",
"xml2js": "^0.4.17",
"xml2js-xpath": "^0.7.0"
} }
} }

View File

@ -1,12 +1,10 @@
import React, {Component, PropTypes} from 'react'; import React, {Component, PropTypes} from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import Dropdown from './base/Dropdown'; import Dropdown from './base/Dropdown';
import MethodTag from './tags/MethodTag'; import MethodTag from './tags/MethodTag';
import {METHODS} from '../lib/constants'; import {METHODS, DEBOUNCE_MILLIS} from '../lib/constants';
import Mousetrap from '../lib/mousetrap'; import Mousetrap from '../lib/mousetrap';
import {trackEvent} from '../lib/analytics'; import {trackEvent} from '../lib/analytics';
import {DEBOUNCE_MILLIS} from '../lib/constants';
class RequestUrlBar extends Component { class RequestUrlBar extends Component {
@ -23,7 +21,10 @@ class RequestUrlBar extends Component {
} }
componentDidMount () { componentDidMount () {
Mousetrap.bindGlobal('mod+l', () => {this.input.focus(); this.input.select()}); Mousetrap.bindGlobal('mod+l', () => {
this.input.focus();
this.input.select()
});
} }
render () { render () {
@ -33,9 +34,10 @@ class RequestUrlBar extends Component {
const hasError = !url; const hasError = !url;
return ( return (
<div className={classnames({'urlbar': true, 'urlbar--error': hasError})}> <form className={classnames({'urlbar': true, 'urlbar--error': hasError})}
onSubmit={this._handleFormSubmit.bind(this)}>
<Dropdown> <Dropdown>
<button> <button type="button">
<div className="tall"> <div className="tall">
<span>{method}</span> <span>{method}</span>
<i className="fa fa-caret-down"/> <i className="fa fa-caret-down"/>
@ -54,18 +56,16 @@ class RequestUrlBar extends Component {
))} ))}
</ul> </ul>
</Dropdown> </Dropdown>
<form onSubmit={this._handleFormSubmit.bind(this)}> <div className="form-control">
<div className="form-control"> <input
<input ref={n => this.input = n}
ref={n => this.input = n} type="text"
type="text" placeholder="https://api.myproduct.com/v1/users"
placeholder="https://api.myproduct.com/v1/users" defaultValue={url}
defaultValue={url} onChange={e => this._handleUrlChange(e.target.value)}/>
onChange={e => this._handleUrlChange(e.target.value)}/> </div>
</div> <button type="submit" className="urlbar__send-button">Send</button>
<button>Send</button> </form>
</form>
</div>
); );
} }
} }

View File

@ -3,6 +3,8 @@ import {getDOMNode} from 'react-dom';
import CodeMirror from 'codemirror'; import CodeMirror from 'codemirror';
import classnames from 'classnames'; import classnames from 'classnames';
import JSONPath from 'jsonpath-plus'; import JSONPath from 'jsonpath-plus';
import xml2js from 'xml2js';
import xpath from 'xml2js-xpath';
import {DEBOUNCE_MILLIS} from '../../lib/constants'; import {DEBOUNCE_MILLIS} from '../../lib/constants';
import 'codemirror/mode/css/css'; import 'codemirror/mode/css/css';
import 'codemirror/mode/htmlmixed/htmlmixed'; import 'codemirror/mode/htmlmixed/htmlmixed';
@ -145,6 +147,55 @@ class Editor extends Component {
return mode.indexOf('json') !== -1 return mode.indexOf('json') !== -1
} }
_isXML (mode) {
if (!mode) {
return false;
}
return mode.indexOf('xml') !== -1
}
_formatJSON (code) {
try {
let obj = JSON.parse(code);
if (this.props.updateFilter && this.state.filter) {
obj = JSONPath({json: obj, path: this.state.filter});
}
code = JSON.stringify(obj, null, '\t');
} catch (e) {
// That's Ok, just leave it
}
return Promise.resolve(code);
}
_formatXML (code) {
return new Promise(resolve => {
xml2js.parseString(code, (err, obj) => {
if (err) {
resolve(code);
return;
}
if (this.props.updateFilter && this.state.filter) {
obj = xpath.find(obj, this.state.filter);
}
const builder = new xml2js.Builder({
renderOpts: {
pretty: true,
indent: '\t'
}
});
const xml = builder.buildObject(obj);
resolve(xml);
});
})
}
/** /**
* Sets options on the CodeMirror editor while also sanitizing them * Sets options on the CodeMirror editor while also sanitizing them
*/ */
@ -202,22 +253,18 @@ class Editor extends Component {
this._originalCode = code; this._originalCode = code;
this._ignoreNextChange = true; this._ignoreNextChange = true;
let promise;
if (this.props.prettify) { if (this.props.prettify) {
try { if (this._isXML(this.props.mode)) {
let obj = JSON.parse(code); promise = this._formatXML(code);
} else {
if (this.props.updateFilter && this.state.filter) { promise = this._formatJSON(code);
obj = JSONPath({json: obj, path: this.state.filter});
}
code = JSON.stringify(obj, null, '\t');
} catch (e) {
// That's Ok, just leave it
// TODO: support more than just JSON prettifying
} }
} else {
promise = Promise.resolve(code);
} }
this.codeMirror.setValue(code); promise.then(code => this.codeMirror.setValue(code));
} }
_handleFilterChange (filter) { _handleFilterChange (filter) {
@ -232,33 +279,51 @@ class Editor extends Component {
} }
_showFilterHelp () { _showFilterHelp () {
const json = this._isJSON(this.props.mode);
const link = json ? (
<Link href="http://goessner.net/articles/JsonPath/">
JSONPath
</Link>
) : (
<Link
href="https://www.w3.org/TR/xpath/">
XPath
</Link>
);
getModal(AlertModal).show({ getModal(AlertModal).show({
headerName: 'Response Filtering Help', headerName: 'Response Filtering Help',
message: ( message: (
<div> <div>
<p> <p>
Use <Link href="http://schier.co">JSONPath</Link> to filter the Use {link} to filter the response body. Here are some examples that
response body. Here are some examples that you might use on a you might use on a book store API.
book store API.
</p> </p>
<table className="pad-top-sm"> <table className="pad-top-sm">
<tbody> <tbody>
<tr> <tr>
<td><code className="selectable">$.store.books[*].title</code> <td><code className="selectable">
{json ? '$.store.books[*].title' : '/store/books/title'}
</code>
</td> </td>
<td>Get titles of all books in the store</td> <td>Get titles of all books in the store</td>
</tr> </tr>
<tr> <tr>
<td><code className="selectable">$.store.books[?(@.price &lt; <td><code className="selectable">
10)].title</code></td> {json ? '$.store.books[?(@.price < 10)].title' : '/store/books[price < 10]'}
</code></td>
<td>Get books costing more than $10</td> <td>Get books costing more than $10</td>
</tr> </tr>
<tr> <tr>
<td><code className="selectable">$.store.books[-1:]</code></td> <td><code className="selectable">
{json ? '$.store.books[-1:]' : '/store/books[last()]'}
</code></td>
<td>Get the last book in the store</td> <td>Get the last book in the store</td>
</tr> </tr>
<tr> <tr>
<td><code className="selectable">$.store.books.length</code></td> <td><code className="selectable">
{json ? '$.store.books.length' : 'count(/store/books)'}
</code></td>
<td>Get the number of books in the store</td> <td>Get the number of books in the store</td>
</tr> </tr>
</tbody> </tbody>
@ -286,14 +351,14 @@ class Editor extends Component {
); );
let filterElement = null; let filterElement = null;
if (this.props.updateFilter && this._isJSON(mode)) { if (this.props.updateFilter && (this._isJSON(mode) || this._isXML(mode))) {
filterElement = ( filterElement = (
<div className="editor__filter"> <div className="editor__filter">
<div className="form-control form-control--outlined"> <div className="form-control form-control--outlined">
<input <input
type="text" type="text"
defaultValue={filter || ''} defaultValue={filter || ''}
placeholder="$.store.book[*].author" placeholder={this._isJSON(mode) ? '$.store.books[*].author' : '/store/books/author'}
onChange={e => this._handleFilterChange(e.target.value)} onChange={e => this._handleFilterChange(e.target.value)}
/> />
</div> </div>

View File

@ -3,9 +3,8 @@
.urlbar { .urlbar {
width: 100%; width: 100%;
display: grid; display: flex;
grid-template-columns: auto 1fr; flex-direction: row;
grid-template-rows: 1fr;
align-items: stretch; align-items: stretch;
align-self: stretch; align-self: stretch;
@ -51,21 +50,22 @@
} }
} }
form { input {
display: grid; min-width: 0;
grid-template-columns: 1fr auto;
} }
form button { .urlbar__send-button {
padding-right: @padding-md; padding-right: @padding-md;
padding-left: @padding-md; padding-left: @padding-md;
} }
form, .form-control { .form-control {
width: 100%;
height: 100%; height: 100%;
display: inline-block;
} }
form button, .urlbar__send-button,
& > .dropdown { & > .dropdown {
height: 100%; height: 100%;

View File

@ -28,7 +28,7 @@
@sidebar-width: 19rem; @sidebar-width: 19rem;
/* Scrollbars */ /* Scrollbars */
@scrollbar-width: 0.8rem; @scrollbar-width: 0.75rem;
/* Borders */ /* Borders */
@radius-sm: 0.15rem; @radius-sm: 0.15rem;

View File

@ -80,7 +80,9 @@
"redux-thunk": "^2.0.1", "redux-thunk": "^2.0.1",
"request": "^2.74.0", "request": "^2.74.0",
"tough-cookie": "^2.3.1", "tough-cookie": "^2.3.1",
"traverse": "^0.6.6" "traverse": "^0.6.6",
"xml2js": "^0.4.17",
"xml2js-xpath": "^0.7.0"
}, },
"devDependencies": { "devDependencies": {
"babel-cli": "^6.11.4", "babel-cli": "^6.11.4",