New 'request' tag and a lot of improvements (#296)

* New 'request' tag and a lot of improvements

* Update request extension to render all values

* Custom value of tag editor now inherits current
This commit is contained in:
Gregory Schier 2017-06-08 18:10:12 -07:00 committed by GitHub
parent 1d7090e36c
commit fd7a25e1ac
19 changed files with 303 additions and 73 deletions

View File

@ -270,3 +270,11 @@ export function clickLink (href) {
shell.openExternal(href);
}
}
export function fnOrString (v, ...args) {
if (typeof v === 'string') {
return v;
} else {
return v(...args);
}
}

View File

@ -154,7 +154,15 @@ export async function getRenderContext (request, environmentId, ancestors = null
const subEnvironment = await models.environment.getById(environmentId);
// Generate the context we need to render
return buildRenderContext(ancestors, rootEnvironment, subEnvironment, variablesOnly);
const context = await buildRenderContext(ancestors, rootEnvironment, subEnvironment, variablesOnly);
// Add meta data
context.getMeta = () => ({
requestId: request._id,
workspaceId: workspace._id
});
return context;
}
export async function getRenderedRequest (request, environmentId) {

View File

@ -426,14 +426,6 @@ export function _actuallySend (renderedRequest, workspace, settings) {
}
const cookies = await cookiesFromJar(jar);
// Make sure domains are prefixed with dots (Curl does this)
for (const cookie of cookies) {
if (cookie.domain && cookie.domain[0] !== '.') {
cookie.domain = `.${cookie.domain}`;
}
}
models.cookieJar.update(renderedRequest.cookieJar, {cookies});
const n = setCookieHeaders.length;

View File

@ -1,5 +1,6 @@
const EMPTY_ARG = '__EMPTY_NUNJUCKS_ARG__';
import * as models from '../models/index';
import * as templating from './index';
export default class BaseExtension {
constructor (ext) {
@ -19,24 +20,6 @@ export default class BaseExtension {
return this._ext.displayName || this.getTag();
}
getDefaultFill () {
const args = this.getArgs().map(argDefinition => {
if (argDefinition.type === 'enum') {
const {defaultValue, options} = argDefinition;
const value = defaultValue !== undefined ? defaultValue : options[0].value;
return `'${value}'`;
} else if (argDefinition.type === 'number') {
const {defaultValue} = argDefinition;
return defaultValue !== undefined ? defaultValue : 0;
} else {
const {defaultValue} = argDefinition;
return defaultValue !== undefined ? `'${defaultValue}'` : "''";
}
});
return `${this.getTag()} ${args.join(', ')}`;
}
getDescription () {
return this._ext.description || 'no description';
}
@ -69,30 +52,37 @@ export default class BaseExtension {
return new nodes.CallExtensionAsync(this, 'asyncRun', args);
}
asyncRun (...runArgs) {
asyncRun ({ctx: renderContext}, ...runArgs) {
// Pull the callback off the end
const callback = runArgs[runArgs.length - 1];
// Only pass render context, not the entire Nunjucks instance
const renderContext = runArgs[0].ctx;
// Pull out the meta
const renderMeta = renderContext.getMeta ? renderContext.getMeta() : {};
delete renderContext.getMeta;
// Extract the rest of the args
const args = runArgs
.slice(1, runArgs.length - 1)
.slice(0, runArgs.length - 1)
.filter(a => a !== EMPTY_ARG);
// Define a plugin context with helpers
const pluginContext = {
// Define a helper context with utils
const helperContext = {
context: renderContext,
models: {
request: {getById: models.request.getById},
response: {getLatestForRequestId: models.response.getLatestForRequest}
meta: renderMeta,
util: {
render: str => templating.render(str, {context: renderContext}),
models: {
request: {getById: models.request.getById},
workspace: {getById: models.workspace.getById},
cookieJar: {getOrCreateForWorkspace: models.cookieJar.getOrCreateForWorkspace},
response: {getLatestForRequestId: models.response.getLatestForRequest}
}
}
};
let result;
try {
result = this.run(pluginContext, ...args);
result = this.run(helperContext, ...args);
} catch (err) {
callback(err);
return;

View File

@ -0,0 +1,88 @@
import * as templating from '../../index';
import * as db from '../../../common/database';
import * as models from '../../../models';
import {cookiesFromJar, jarFromCookies} from '../../../common/cookies';
import {getRenderContext} from '../../../common/render';
describe('RequestExtension cookie', async () => {
beforeEach(() => db.init(models.types(), {inMemoryOnly: true}, true));
it('should get cookie by name', async () => {
// Create necessary models
const workspace = await models.workspace.create({name: 'Workspace'});
const request = await models.request.create({
parentId: workspace._id,
url: 'https://insomnia.rest/foo/bar'
});
const cookieJar = await models.cookieJar.getOrCreateForWorkspace(workspace);
const jar = jarFromCookies(cookieJar.cookies);
jar.setCookieSync([
'foo=bar',
'path=/',
'domain=.insomnia.rest',
'HttpOnly Cache-Control: public, no-cache'
].join('; '), request.url);
const cookies = await cookiesFromJar(jar);
await models.cookieJar.update(cookieJar, {cookies});
const context = await getRenderContext(request);
const result = await templating.render(`{% request 'cookie', 'foo' %}`, {context});
expect(result).toBe('bar');
});
});
describe('RequestExtension url', async () => {
beforeEach(() => db.init(models.types(), {inMemoryOnly: true}, true));
it('should get url', async () => {
// Create necessary models
const workspace = await models.workspace.create({name: 'Workspace'});
const request = await models.request.create({
parentId: workspace._id,
url: 'https://insomnia.rest/foo/bar',
parameters: [{name: 'foo', value: 'bar'}]
});
const context = await getRenderContext(request);
const result = await templating.render(`{% request 'url' %}`, {context});
expect(result).toBe('https://insomnia.rest/foo/bar?foo=bar');
});
it('should get rendered url', async () => {
// Create necessary models
const workspace = await models.workspace.create({name: 'Workspace'});
const request = await models.request.create({
parentId: workspace._id,
url: 'https://insomnia.rest/foo/bar',
parameters: [{name: 'foo', value: '{{ foo }}'}]
});
const context = await getRenderContext(request);
context.foo = 'Hello World!';
const result = await templating.render(`{% request 'url' %}`, {context});
expect(result).toBe('https://insomnia.rest/foo/bar?foo=Hello%20World!');
});
});
describe('RequestExtension header', async () => {
beforeEach(() => db.init(models.types(), {inMemoryOnly: true}, true));
it('should get url', async () => {
// Create necessary models
const workspace = await models.workspace.create({name: 'Workspace'});
const request = await models.request.create({
parentId: workspace._id,
url: 'https://insomnia.rest/foo/bar',
headers: [{name: 'foo', value: '{{ foo }}'}]
});
const context = await getRenderContext(request);
context.foo = 'Hello World!';
const result = await templating.render(`{% request 'header', 'foo' %}`, {context});
expect(result).toBe('Hello World!');
});
});

View File

@ -2,7 +2,6 @@ export default {
name: 'base64',
displayName: 'Base64',
description: 'encode or decode values',
defaultFill: "base64 'encode', ''",
args: [
{
displayName: 'Action',

View File

@ -5,12 +5,14 @@ import uuidExtension from './uuid-extension';
import NowExtension from './now-extension';
import responseExtension from './response-extension';
import base64Extension from './base-64-extension';
import requestExtension from './request-extension';
const DEFAULT_EXTENSIONS = [
timestampExtension,
NowExtension,
uuidExtension,
base64Extension,
requestExtension,
responseExtension
];

View File

@ -1,8 +1,7 @@
export default {
name: 'now',
displayName: 'Now Timestamp',
displayName: 'Timestamp',
description: 'get the current time',
defaultFill: "now 'iso-8601'",
args: [{
displayName: 'Timestamp Format',
type: 'enum',

View File

@ -0,0 +1,108 @@
import * as querystring from '../../common/querystring';
import {prepareUrlForSending} from '../../common/misc';
import {jarFromCookies} from '../../common/cookies';
export default {
name: 'request',
displayName: 'Request',
description: 'reference value from current request',
args: [
{
displayName: 'Attribute',
type: 'enum',
options: [
{displayName: 'URL', value: 'url', description: 'fully qualified URL'},
{displayName: 'Cookie', value: 'cookie', description: 'cookie value by name'},
{displayName: 'Header', value: 'header', description: 'header value by name'}
]
},
{
type: 'string',
hide: args => ['url'].includes(args[0].value),
displayName: args => {
switch (args[0].value) {
case 'cookie':
return 'Cookie Name';
case 'header':
return 'Header Name';
default:
return 'Name';
}
}
}
],
async run (context, attribute, name) {
const {meta} = context;
if (!meta.requestId || !meta.workspaceId) {
return null;
}
const request = await context.util.models.request.getById(meta.requestId);
const workspace = await context.util.models.workspace.getById(meta.workspaceId);
if (!request) {
throw new Error(`Request not found for ${meta.requestId}`);
}
if (!workspace) {
throw new Error(`Workspace not found for ${meta.workspaceId}`);
}
switch (attribute) {
case 'url':
return getRequestUrl(context, request);
case 'cookie':
const cookieJar = await context.util.models.cookieJar.getOrCreateForWorkspace(workspace);
const url = await getRequestUrl(context, request);
const value = await getCookieValue(cookieJar, url, name);
return value;
case 'header':
for (const header of request.headers) {
const currentName = await context.util.render(name);
if (currentName.toLowerCase() === name.toLowerCase()) {
return context.util.render(header.value);
}
}
throw new Error(`No header for name "${name}"`);
}
return null;
}
};
async function getRequestUrl (context, request) {
const url = await context.util.render(request.url);
const parameters = [];
for (const p of request.parameters) {
parameters.push({
name: await context.util.render(p.name),
value: await context.util.render(p.value)
});
}
const qs = querystring.buildFromParams(parameters);
const finalUrl = querystring.joinUrl(url, qs);
return prepareUrlForSending(finalUrl, request.settingEncodeUrl);
}
function getCookieValue (cookieJar, url, name) {
return new Promise((resolve, reject) => {
const jar = jarFromCookies(cookieJar.cookies);
jar.getCookies(url, {}, (err, cookies) => {
if (err) {
console.warn(`Failed to find cookie for ${url}`, err);
}
const cookie = cookies.find(cookie => cookie.key === name);
if (!cookie) {
reject(new Error(`No cookie found with name "${name}"`));
} else {
resolve(cookie ? cookie.value : null);
}
});
});
}

View File

@ -4,9 +4,8 @@ import xpath from 'xpath';
export default {
name: 'response',
displayName: 'Response Value',
displayName: 'Response',
description: 'reference values from other requests',
defaultFill: "response 'body', '', ''",
args: [
{
displayName: 'Attribute',
@ -47,12 +46,12 @@ export default {
throw new Error(`No ${field} filter specified`);
}
const request = await context.models.request.getById(id);
const request = await context.util.models.request.getById(id);
if (!request) {
throw new Error(`Could not find request ${id}`);
}
const response = await context.models.response.getLatestForRequestId(id);
const response = await context.util.models.response.getLatestForRequestId(id);
if (!response) {
throw new Error('No responses for request');

View File

@ -3,7 +3,6 @@ export default {
name: 'timestamp',
displayName: 'Timestamp',
description: 'generate timestamp in milliseconds',
defaultFill: 'timestamp',
run (context) {
return Date.now();
}

View File

@ -3,7 +3,6 @@ import uuid from 'uuid';
export default {
displayName: 'UUID',
name: 'uuid',
defaultFill: "uuid 'v4'",
description: 'generate v1 or v4 UUIDs',
args: [{
displayName: 'Version',

View File

@ -76,7 +76,6 @@ export function getTagDefinitions () {
.map(ext => ({
name: ext.getTag(),
displayName: ext.getName(),
defaultFill: ext.getDefaultFill(),
description: ext.getDescription(),
args: ext.getArgs()
}));
@ -125,7 +124,7 @@ function getNunjucks (variablesOnly) {
const ext = allExtensions[i];
ext.priority = ext.priority || i * 100;
const instance = new BaseExtension(ext);
nj.addExtension(instance.getName(), instance);
nj.addExtension(instance.getTag(), instance);
}
// ~~~~~~~~~~~~~~~~~~~~ //

View File

@ -143,3 +143,27 @@ export function unTokenizeTag (tagData) {
const argsStr = args.join(', ');
return `{% ${tagData.name} ${argsStr} %}`;
}
/**
* Get the default Nunjucks string for an extension
* @param {string} name
* @param {object[]} args
* @returns {string}
*/
export function getDefaultFill (name, args) {
const stringArgs = (args || []).map(argDefinition => {
if (argDefinition.type === 'enum') {
const {defaultValue, options} = argDefinition;
const value = defaultValue !== undefined ? defaultValue : options[0].value;
return `'${value}'`;
} else if (argDefinition.type === 'number') {
const {defaultValue} = argDefinition;
return defaultValue !== undefined ? defaultValue : 0;
} else {
const {defaultValue} = argDefinition;
return defaultValue !== undefined ? `'${defaultValue}'` : "''";
}
});
return `${name} ${stringArgs.join(', ')}`;
}

View File

@ -2,6 +2,7 @@ import React, {PureComponent, PropTypes} from 'react';
import autobind from 'autobind-decorator';
import CodeMirror from 'codemirror';
import classnames from 'classnames';
import clone from 'clone';
import jq from 'jsonpath';
import vkBeautify from 'vkbeautify';
import {DOMParser} from 'xmldom';
@ -409,7 +410,25 @@ class CodeEditor extends PureComponent {
};
// Only allow tags if we have variables too
getTags = getTagDefinitions;
getTags = () => {
const expandedTags = [];
for (const tagDef of getTagDefinitions()) {
if (tagDef.args[0].type !== 'enum') {
expandedTags.push(tagDef);
continue;
}
for (const option of tagDef.args[0].options) {
const optionName = misc.fnOrString(option.displayName, tagDef.args) || option.name;
const newDef = clone(tagDef);
newDef.displayName = `${tagDef.displayName}${optionName}`;
newDef.args[0].defaultValue = option.value;
expandedTags.push(newDef);
}
}
return expandedTags;
};
}
options.environmentAutocomplete = {
getVariables,

View File

@ -1,6 +1,7 @@
import CodeMirror from 'codemirror';
import 'codemirror/addon/mode/overlay';
import * as misc from '../../../../common/misc';
import {getDefaultFill} from '../../../../templating/utils';
const NAME_MATCH_FLEXIBLE = /[\w.\][\-/]+$/;
const NAME_MATCH = /[\w.\][]+$/;
@ -298,7 +299,7 @@ function matchSegments (listOfThings, segment, type, limit = -1) {
const name = typeof t === 'string' ? t : t.name;
const value = typeof t === 'string' ? '' : t.value;
const displayName = t.displayName || name;
const defaultFill = t.defaultFill || name;
const defaultFill = getDefaultFill(t.name, t.args);
const matchSegment = segment.toLowerCase();
const matchName = displayName.toLowerCase();

View File

@ -5,6 +5,7 @@ import {createPlugin, getPlugins} from '../../../plugins/index';
import Button from '../base/button';
import CopyButton from '../base/copy-button';
import {showPrompt} from '../modals/index';
import {trackEvent} from '../../../analytics/index';
@autobind
class Plugins extends PureComponent {
@ -30,12 +31,14 @@ class Plugins extends PureComponent {
onComplete: async name => {
await createPlugin(name);
this._handleRefreshPlugins();
trackEvent('Plugins', 'Generate');
}
});
}
_handleRefreshPlugins () {
this.setState({plugins: getPlugins(true)});
trackEvent('Plugins', 'Refresh');
}
render () {

View File

@ -6,6 +6,8 @@ import * as templateUtils from '../../../templating/utils';
import * as db from '../../../common/database';
import * as models from '../../../models';
import HelpTooltip from '../help-tooltip';
import {fnOrString} from '../../../common/misc';
import {trackEvent} from '../../../analytics/index';
@autobind
class TagEditor extends PureComponent {
@ -91,6 +93,7 @@ class TagEditor extends PureComponent {
const name = e.target.value;
const tagDefinition = templating.getTagDefinitions().find(d => d.name === name);
this._update(tagDefinition, false);
trackEvent('Tag Editor', 'Change Tag', name);
}
_setSelectRef (n) {
@ -102,19 +105,6 @@ class TagEditor extends PureComponent {
}, 100);
}
_buildArgFromDefinition (argDefinition) {
if (argDefinition.type === 'enum') {
const {defaultValue, options} = argDefinition;
return {type: 'string', value: defaultValue || options[0].value};
} else if (argDefinition.type === 'number') {
const {defaultValue} = argDefinition;
const value = defaultValue !== undefined ? defaultValue : 0;
return {type: 'number', value};
} else {
return {type: 'string', value: argDefinition.defaultValue || ''};
}
}
async _update (tagDefinition, tagData, noCallback = false) {
const {handleRender} = this.props;
@ -123,13 +113,13 @@ class TagEditor extends PureComponent {
let activeTagData = tagData;
if (!activeTagData && tagDefinition) {
activeTagData = {
name: tagDefinition.name,
rawValue: null,
args: tagDefinition.args.map(this._buildArgFromDefinition)
};
const defaultFill = templateUtils.getDefaultFill(tagDefinition.name, tagDefinition.args);
activeTagData = templateUtils.tokenizeTag(defaultFill);
} else if (!activeTagData && !tagDefinition) {
activeTagData = {name: 'custom', rawValue: "{% tag 'arg1', 'arg2' %}"};
activeTagData = {
name: 'custom',
rawValue: templateUtils.unTokenizeTag(this.state.activeTagData)
};
}
let template;
@ -219,7 +209,7 @@ class TagEditor extends PureComponent {
const parentId = request ? request.parentId : 'n/a';
const requestGroups = allDocs[models.requestGroup.type] || [];
const requestGroup = requestGroups.find(rg => rg._id === parentId);
namePrefix = requestGroup ? `${requestGroup.name} ` : null;
namePrefix = requestGroup ? `[${requestGroup.name}] ` : null;
}
return (
@ -260,7 +250,7 @@ class TagEditor extends PureComponent {
return (
<div key={argIndex} className="form-control form-control--outlined">
<label>
{typeof displayName === 'function' ? displayName(args) : displayName}
{fnOrString(displayName, args)}
{argDefinition.help && <HelpTooltip>{argDefinition.help}</HelpTooltip>}
<div data-arg-index={argIndex}>
{argInput}
@ -285,7 +275,7 @@ class TagEditor extends PureComponent {
{tagDefinition.displayName} {tagDefinition.description}
</option>
))}
<option value="">-- Custom --</option>
<option value="custom">-- Custom --</option>
</select>
</label>
</div>

View File

@ -1,5 +1,6 @@
import React, {PropTypes, PureComponent} from 'react';
import autobind from 'autobind-decorator';
import {trackEvent} from '../../../analytics/index';
@autobind
class VariableEditor extends PureComponent {
@ -23,7 +24,9 @@ class VariableEditor extends PureComponent {
}
_handleChange (e) {
this._update(e.target.value);
const name = e.target.value;
trackEvent('Variable Editor', 'Change Variable', name);
this._update(name);
}
_setSelectRef (n) {