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

View File

@ -3,6 +3,8 @@ import {getDOMNode} from 'react-dom';
import CodeMirror from 'codemirror';
import classnames from 'classnames';
import JSONPath from 'jsonpath-plus';
import xml2js from 'xml2js';
import xpath from 'xml2js-xpath';
import {DEBOUNCE_MILLIS} from '../../lib/constants';
import 'codemirror/mode/css/css';
import 'codemirror/mode/htmlmixed/htmlmixed';
@ -145,6 +147,55 @@ class Editor extends Component {
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
*/
@ -202,22 +253,18 @@ class Editor extends Component {
this._originalCode = code;
this._ignoreNextChange = true;
let promise;
if (this.props.prettify) {
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
// TODO: support more than just JSON prettifying
if (this._isXML(this.props.mode)) {
promise = this._formatXML(code);
} else {
promise = this._formatJSON(code);
}
} else {
promise = Promise.resolve(code);
}
this.codeMirror.setValue(code);
promise.then(code => this.codeMirror.setValue(code));
}
_handleFilterChange (filter) {
@ -232,33 +279,51 @@ class Editor extends Component {
}
_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({
headerName: 'Response Filtering Help',
message: (
<div>
<p>
Use <Link href="http://schier.co">JSONPath</Link> to filter the
response body. Here are some examples that you might use on a
book store API.
Use {link} to filter the response body. Here are some examples that
you might use on a book store API.
</p>
<table className="pad-top-sm">
<tbody>
<tr>
<td><code className="selectable">$.store.books[*].title</code>
<td><code className="selectable">
{json ? '$.store.books[*].title' : '/store/books/title'}
</code>
</td>
<td>Get titles of all books in the store</td>
</tr>
<tr>
<td><code className="selectable">$.store.books[?(@.price &lt;
10)].title</code></td>
<td><code className="selectable">
{json ? '$.store.books[?(@.price < 10)].title' : '/store/books[price < 10]'}
</code></td>
<td>Get books costing more than $10</td>
</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>
</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>
</tr>
</tbody>
@ -286,14 +351,14 @@ class Editor extends Component {
);
let filterElement = null;
if (this.props.updateFilter && this._isJSON(mode)) {
if (this.props.updateFilter && (this._isJSON(mode) || this._isXML(mode))) {
filterElement = (
<div className="editor__filter">
<div className="form-control form-control--outlined">
<input
type="text"
defaultValue={filter || ''}
placeholder="$.store.book[*].author"
placeholder={this._isJSON(mode) ? '$.store.books[*].author' : '/store/books/author'}
onChange={e => this._handleFilterChange(e.target.value)}
/>
</div>

View File

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

View File

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

View File

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