Better XPath handling (Fixes #492)

This commit is contained in:
Gregory Schier 2017-09-22 15:48:47 +02:00
parent 4b7fe5c190
commit 8eecb55515
7 changed files with 74 additions and 27 deletions

31
app/common/xpath.js Normal file
View File

@ -0,0 +1,31 @@
// @flow
import xpath from 'xpath';
import {DOMParser} from 'xmldom';
export function query (xml: string, query: string): Array<{outer: string, inner: string}> {
const dom = new DOMParser().parseFromString(xml);
let rawResults = [];
try {
rawResults = xpath.select(query, dom);
} catch (err) {
throw new Error(`Invalid XPath query: ${query}`);
}
const results = [];
for (const result of rawResults || []) {
if (result.constructor.name === 'Attr') {
results.push({
outer: result.toString().trim(),
inner: result.nodeValue
});
} else if (result.constructor.name === 'Element') {
results.push({
outer: result.toString().trim(),
inner: result.childNodes.toString()
});
}
}
return results;
}

View File

@ -115,7 +115,7 @@ describe('ResponseExtension JSONPath', async () => {
describe('ResponseExtension XPath', async () => {
beforeEach(globalBeforeEach);
it('renders basic response "body", query', async () => {
it('renders basic response "body" query', async () => {
const request = await models.request.create({parentId: 'foo'});
await models.response.create({
parentId: request._id,
@ -127,6 +127,18 @@ describe('ResponseExtension XPath', async () => {
expect(result).toBe('Hello World!');
});
it('renders basic response "body" attribute query', async () => {
const request = await models.request.create({parentId: 'foo'});
await models.response.create({
parentId: request._id,
statusCode: 200
}, '<foo><bar hello="World!">Hello World!</bar></foo>');
const result = await templating.render(`{% response "body", "${request._id}", "/foo/bar/@hello" %}`);
expect(result).toBe('World!');
});
it('no results on invalid XML', async () => {
const request = await models.request.create({parentId: 'foo'});
await models.response.create({

View File

@ -1,6 +1,7 @@
// @flow
import jq from 'jsonpath';
import {DOMParser} from 'xmldom';
import xpath from 'xpath';
import * as xpath from '../../common/xpath';
import type {ResponseHeader} from '../../models/response';
export default {
name: 'response',
@ -23,8 +24,8 @@ export default {
},
{
type: 'string',
hide: args => args[0].value === 'raw',
displayName: args => {
hide: (args: Array<Object>): boolean => args[0].value === 'raw',
displayName: (args: Array<Object>): string => {
switch (args[0].value) {
case 'body':
return 'Filter (JSONPath or XPath)';
@ -37,7 +38,7 @@ export default {
}
],
async run (context, field, id, filter) {
async run (context: Object, field: string, id: string, filter: string) {
if (!['body', 'header', 'raw'].includes(field)) {
throw new Error(`Invalid response field ${field}`);
}
@ -83,7 +84,7 @@ export default {
}
};
function matchJSONPath (bodyStr, query) {
function matchJSONPath (bodyStr: string, query: string): string {
let body;
let results;
@ -108,17 +109,8 @@ function matchJSONPath (bodyStr, query) {
return results[0];
}
function matchXPath (bodyStr, query) {
let results;
// This will never throw
const dom = new DOMParser().parseFromString(bodyStr);
try {
results = xpath.select(query, dom);
} catch (err) {
throw new Error(`Invalid XPath query: ${query}`);
}
function matchXPath (bodyStr: string, query: string): string {
const results = xpath.query(bodyStr, query);
if (results.length === 0) {
throw new Error(`Returned no results: ${query}`);
@ -126,10 +118,10 @@ function matchXPath (bodyStr, query) {
throw new Error(`Returned more than one result: ${query}`);
}
return results[0].childNodes.toString();
return results[0].inner;
}
function matchHeader (headers, name) {
function matchHeader (headers: Array<ResponseHeader>, name: string): string {
const header = headers.find(
h => h.name.toLowerCase() === name.toLowerCase()
);

View File

@ -6,8 +6,6 @@ import classnames from 'classnames';
import clone from 'clone';
import jq from 'jsonpath';
import vkBeautify from 'vkbeautify';
import {DOMParser} from 'xmldom';
import xpath from 'xpath';
import {showModal} from '../modals/index';
import FilterHelpModal from '../modals/filter-help-modal';
import * as misc from '../../../common/misc';
@ -19,6 +17,7 @@ import {getTagDefinitions} from '../../../templating/index';
import Dropdown from '../base/dropdown/dropdown';
import DropdownButton from '../base/dropdown/dropdown-button';
import DropdownItem from '../base/dropdown/dropdown-item';
import * as xpath2 from '../../../common/xpath';
const TAB_KEY = 9;
const TAB_SIZE = 4;
@ -322,11 +321,9 @@ class CodeEditor extends PureComponent {
_prettifyXML (code) {
if (this.props.updateFilter && this.state.filter) {
try {
const dom = new DOMParser().parseFromString(code);
const nodes = xpath.select(this.state.filter, dom);
const inner = nodes.map(n => n.toString()).join('\n');
code = `<result>${inner}</result>`;
} catch (e) {
const results = xpath2.query(code, this.state.filter);
code = `<result>${results.map(r => r.outer).join('\n')}</result>`;
} catch (err) {
// Failed to parse filter (that's ok)
code = `<result></result>`;
}

5
flow-typed/jsonpath.js vendored Normal file
View File

@ -0,0 +1,5 @@
declare module 'jsonpath' {
declare module.exports: {
query: Function
}
}

5
flow-typed/xmldom.js vendored Normal file
View File

@ -0,0 +1,5 @@
declare module 'xmldom' {
declare module.exports: {
DOMParser: Function
}
}

5
flow-typed/xpath.js vendored Normal file
View File

@ -0,0 +1,5 @@
declare module 'xpath' {
declare module.exports: {
select: Function
}
}