mirror of
https://github.com/Kong/insomnia
synced 2024-11-08 06:39:48 +00:00
New Template Tag to read file from filesystem (#557)
* Template tag to read from file * Add tests
This commit is contained in:
parent
ce9b72bf07
commit
f9521a3456
@ -153,7 +153,7 @@ describe('tokenizeTag()', () => {
|
||||
|
||||
describe('unTokenizeTag()', () => {
|
||||
beforeEach(globalBeforeEach);
|
||||
it('untokenizes a tag', () => {
|
||||
it('handles the default case', () => {
|
||||
const tagStr = `{% name bar, "baz \\"qux\\"" , 1 + 5, 'hi' %}`;
|
||||
|
||||
const tagData = utils.tokenizeTag(tagStr);
|
||||
@ -161,4 +161,18 @@ describe('unTokenizeTag()', () => {
|
||||
|
||||
expect(result).toEqual(`{% name bar, "baz \\"qux\\"", 1 + 5, 'hi' %}`);
|
||||
});
|
||||
|
||||
it('fixes missing quotedBy attribute', () => {
|
||||
const tagData = {
|
||||
name: 'name',
|
||||
args: [
|
||||
{type: 'file', value: 'foo/bar/baz'},
|
||||
{type: 'model', value: 'foo'}
|
||||
]
|
||||
};
|
||||
|
||||
const result = utils.unTokenizeTag(tagData);
|
||||
|
||||
expect(result).toEqual(`{% name 'foo/bar/baz', 'foo' %}`);
|
||||
});
|
||||
});
|
||||
|
32
app/templating/extensions/__tests__/file-extension.test.js
Normal file
32
app/templating/extensions/__tests__/file-extension.test.js
Normal file
@ -0,0 +1,32 @@
|
||||
import path from 'path';
|
||||
import * as templating from '../../index';
|
||||
import {globalBeforeEach} from '../../../__jest__/before-each';
|
||||
|
||||
function assertTemplate (txt, context, expected) {
|
||||
return async function () {
|
||||
const result = await templating.render(txt, {context});
|
||||
expect(result).toMatch(expected);
|
||||
};
|
||||
}
|
||||
|
||||
function assertTemplateFails (txt, context, expected) {
|
||||
return async function () {
|
||||
try {
|
||||
await templating.render(txt, {context});
|
||||
fail(`Render should have thrown ${expected}`);
|
||||
} catch (err) {
|
||||
expect(err.message).toContain(expected);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
describe('FileExtension', () => {
|
||||
beforeEach(globalBeforeEach);
|
||||
const ctx = {path: path.resolve(__dirname, path.join('./test.txt'))};
|
||||
const escapedPath = ctx.path.replace(/\\/g, '\\\\');
|
||||
it('reads from string', assertTemplate(`{% file "${escapedPath}" %}`, ctx, 'Hello World'));
|
||||
it('reads a file correctly', assertTemplate('{% file path %}', ctx, 'Hello World!'));
|
||||
it('fails on missing file', assertTemplateFails('{% file "/foo" %}', ctx, `ENOENT: no such file or directory, open '${path.resolve('/foo')}'`));
|
||||
it('fails on no 2nd param', assertTemplateFails('{% file %}', ctx, 'No file selected'));
|
||||
it('fails on unknown variable', assertTemplateFails('{% file foo %}', ctx, 'No file selected'));
|
||||
});
|
1
app/templating/extensions/__tests__/test.txt
Normal file
1
app/templating/extensions/__tests__/test.txt
Normal file
@ -0,0 +1 @@
|
||||
Hello World!
|
21
app/templating/extensions/file-extension.js
Normal file
21
app/templating/extensions/file-extension.js
Normal file
@ -0,0 +1,21 @@
|
||||
// @flow
|
||||
import fs from 'fs';
|
||||
|
||||
export default {
|
||||
name: 'file',
|
||||
displayName: 'File',
|
||||
description: 'read contents from a file',
|
||||
args: [
|
||||
{
|
||||
displayName: 'Choose File',
|
||||
type: 'file'
|
||||
}
|
||||
],
|
||||
run (context: Object, path: string): string {
|
||||
if (!path) {
|
||||
throw new Error('No file selected');
|
||||
}
|
||||
|
||||
return fs.readFileSync(path, 'utf8');
|
||||
}
|
||||
};
|
@ -3,7 +3,8 @@ import * as plugins from '../../plugins/index';
|
||||
|
||||
import timestampExtension from './timestamp-extension';
|
||||
import uuidExtension from './uuid-extension';
|
||||
import NowExtension from './now-extension';
|
||||
import nowExtension from './now-extension';
|
||||
import fileExtension from './file-extension';
|
||||
import responseExtension from './response-extension';
|
||||
import base64Extension from './base-64-extension';
|
||||
import requestExtension from './request-extension';
|
||||
@ -29,28 +30,32 @@ export type PluginArgumentEnumOption = {
|
||||
}
|
||||
|
||||
export type PluginArgumentEnum = PluginArgumentBase & {
|
||||
type: 'enum';
|
||||
type: 'enum',
|
||||
options: Array<PluginArgumentEnumOption>,
|
||||
defaultValue?: PluginArgumentValue
|
||||
};
|
||||
|
||||
export type PluginArgumentModel = PluginArgumentBase & {
|
||||
type: 'model';
|
||||
type: 'model',
|
||||
model: string,
|
||||
defaultValue?: string
|
||||
};
|
||||
|
||||
export type PluginArgumentString = PluginArgumentBase & {
|
||||
type: 'string';
|
||||
type: 'string',
|
||||
placeholder?: string,
|
||||
defaultValue?: string
|
||||
};
|
||||
|
||||
export type PluginArgumentBoolean = PluginArgumentBase & {
|
||||
type: 'boolean';
|
||||
type: 'boolean',
|
||||
defaultValue?: boolean
|
||||
};
|
||||
|
||||
export type PluginArgumentFile = PluginArgumentBase & {
|
||||
type: 'file'
|
||||
};
|
||||
|
||||
export type PluginArgumentNumber = PluginArgumentBase & {
|
||||
type: 'number';
|
||||
placeholder?: string,
|
||||
@ -62,6 +67,7 @@ export type PluginArgument =
|
||||
| PluginArgumentModel
|
||||
| PluginArgumentString
|
||||
| PluginArgumentBoolean
|
||||
| PluginArgumentFile
|
||||
| PluginArgumentNumber;
|
||||
|
||||
export type PluginTemplateTagContext = {
|
||||
@ -90,9 +96,10 @@ export type PluginTemplateTag = {
|
||||
|
||||
const DEFAULT_EXTENSIONS: Array<PluginTemplateTag> = [
|
||||
timestampExtension,
|
||||
NowExtension,
|
||||
nowExtension,
|
||||
uuidExtension,
|
||||
base64Extension,
|
||||
fileExtension,
|
||||
requestExtension,
|
||||
responseExtension
|
||||
];
|
||||
|
@ -145,7 +145,7 @@ export function tokenizeTag (tagStr: string): NunjucksParsedTag {
|
||||
export function unTokenizeTag (tagData: NunjucksParsedTag): string {
|
||||
const args = [];
|
||||
for (const arg of tagData.args) {
|
||||
if (arg.type === 'string') {
|
||||
if (['string', 'model', 'file'].includes(arg.type)) {
|
||||
const q = arg.quotedBy || "'";
|
||||
const re = new RegExp(`([^\\\\])${q}`, 'g');
|
||||
const str = arg.value.toString().replace(re, `$1\\${q}`);
|
||||
@ -174,6 +174,8 @@ export function getDefaultFill (name: string, args: Array<NunjucksParsedTagArg>)
|
||||
case 'boolean':
|
||||
return argDefinition.defaultValue ? 'true' : 'false';
|
||||
case 'string':
|
||||
case 'file':
|
||||
case 'model':
|
||||
return `'${(argDefinition.defaultValue: any) || ''}'`;
|
||||
default:
|
||||
return "''";
|
||||
|
@ -1,20 +1,32 @@
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import autobind from 'autobind-decorator';
|
||||
import {basename as pathBasename} from 'path';
|
||||
import {remote} from 'electron';
|
||||
|
||||
type Props = {
|
||||
// Required
|
||||
onChange: Function,
|
||||
path: string,
|
||||
|
||||
// Optional
|
||||
showFileName?: boolean,
|
||||
showFileIcon?: boolean,
|
||||
name?: string
|
||||
};
|
||||
|
||||
@autobind
|
||||
class FileInputButton extends PureComponent {
|
||||
class FileInputButton extends React.PureComponent<Props> {
|
||||
_button: ?HTMLButtonElement;
|
||||
focus () {
|
||||
this._button.focus();
|
||||
this._button && this._button.focus();
|
||||
}
|
||||
|
||||
focusEnd () {
|
||||
this._button.focus();
|
||||
this._button && this._button.focus();
|
||||
}
|
||||
|
||||
_setRef (n) {
|
||||
_setRef (n: ?HTMLButtonElement) {
|
||||
this._button = n;
|
||||
}
|
||||
|
||||
@ -52,15 +64,4 @@ class FileInputButton extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
FileInputButton.propTypes = {
|
||||
// Required
|
||||
onChange: PropTypes.func.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
|
||||
// Optional
|
||||
showFileName: PropTypes.bool,
|
||||
showFileIcon: PropTypes.bool,
|
||||
name: PropTypes.string
|
||||
};
|
||||
|
||||
export default FileInputButton;
|
||||
|
@ -15,6 +15,7 @@ import type {BaseModel} from '../../../models/index';
|
||||
import type {Workspace} from '../../../models/workspace';
|
||||
import type {PluginArgumentEnumOption} from '../../../templating/extensions/index';
|
||||
import {Dropdown, DropdownButton, DropdownDivider, DropdownItem} from '../base/dropdown/index';
|
||||
import FileInputButton from '../base/file-input-button';
|
||||
|
||||
type Props = {
|
||||
handleRender: Function,
|
||||
@ -116,7 +117,8 @@ class TagEditor extends React.PureComponent<Props, State> {
|
||||
_updateArg (
|
||||
argValue: string | number | boolean,
|
||||
argIndex: number,
|
||||
forceNewType: string | null = null
|
||||
forceNewType: string | null = null,
|
||||
patch: Object = {}
|
||||
) {
|
||||
const {tagDefinitions, activeTagData, activeTagDefinition} = this.state;
|
||||
|
||||
@ -130,6 +132,11 @@ class TagEditor extends React.PureComponent<Props, State> {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fix strings
|
||||
if (typeof argValue === 'string') {
|
||||
argValue = argValue.replace(/\\/g, '\\\\');
|
||||
}
|
||||
|
||||
// Ensure all arguments exist
|
||||
const defaultArgs = this._getDefaultTagData(activeTagDefinition).args;
|
||||
for (let i = 0; i < defaultArgs.length; i++) {
|
||||
@ -154,7 +161,7 @@ class TagEditor extends React.PureComponent<Props, State> {
|
||||
// Update type if we need to
|
||||
if (forceNewType) {
|
||||
// Ugh, what a hack (because it's enum)
|
||||
(argData: any).type = forceNewType;
|
||||
Object.assign((argData: any), {type: forceNewType}, patch);
|
||||
}
|
||||
|
||||
this._update(tagDefinitions, activeTagDefinition, tagData, false);
|
||||
@ -182,10 +189,14 @@ class TagEditor extends React.PureComponent<Props, State> {
|
||||
const initialType = argDef ? argDef.type : 'string';
|
||||
const variable = variables.find(v => v.name === existingValue);
|
||||
const value = variable ? variable.value : '';
|
||||
return this._updateArg(value, argIndex, initialType);
|
||||
return this._updateArg(value, argIndex, initialType, {quotedBy: "'"});
|
||||
}
|
||||
}
|
||||
|
||||
_handleChangeFile (path: string, argIndex: number) {
|
||||
return this._updateArg(path, argIndex);
|
||||
}
|
||||
|
||||
_handleChange (e: SyntheticEvent<HTMLInputElement>, forceVariable: boolean = false) {
|
||||
const parent = e.currentTarget.parentNode;
|
||||
let argIndex = -1;
|
||||
@ -315,6 +326,9 @@ class TagEditor extends React.PureComponent<Props, State> {
|
||||
|
||||
return (
|
||||
<select value={path || ''} onChange={this._handleChange}>
|
||||
<option key="n/a" value="NO_VARIABLE">
|
||||
-- Select Variable --
|
||||
</option>
|
||||
{variables.map((v, i) => (
|
||||
<option key={`${i}::${v.name}`} value={v.name}>
|
||||
{v.name}
|
||||
@ -352,6 +366,18 @@ class TagEditor extends React.PureComponent<Props, State> {
|
||||
);
|
||||
}
|
||||
|
||||
renderArgFile (value: string, argIndex: number) {
|
||||
return (
|
||||
<FileInputButton
|
||||
showFileIcon
|
||||
showFileName
|
||||
className="btn btn--clicky btn--super-compact"
|
||||
onChange={path => this._handleChangeFile(path, argIndex)}
|
||||
path={value}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderArgEnum (value: string, options: Array<PluginArgumentEnumOption>) {
|
||||
const argDatas = this.state.activeTagData ? this.state.activeTagData.args : [];
|
||||
return (
|
||||
@ -439,9 +465,8 @@ class TagEditor extends React.PureComponent<Props, State> {
|
||||
const argInputVariable = isVariable ? this.renderArgVariable(strValue) : null;
|
||||
|
||||
let argInput;
|
||||
let isVariableAllowed = false;
|
||||
let isVariableAllowed = true;
|
||||
if (argDefinition.type === 'string') {
|
||||
isVariableAllowed = true;
|
||||
const placeholder = typeof argDefinition.placeholder === 'string'
|
||||
? argDefinition.placeholder
|
||||
: '';
|
||||
@ -449,14 +474,16 @@ class TagEditor extends React.PureComponent<Props, State> {
|
||||
} else if (argDefinition.type === 'enum') {
|
||||
const {options} = argDefinition;
|
||||
argInput = this.renderArgEnum(strValue, options);
|
||||
} else if (argDefinition.type === 'file') {
|
||||
argInput = this.renderArgFile(strValue, argIndex);
|
||||
} else if (argDefinition.type === 'model') {
|
||||
isVariableAllowed = false;
|
||||
const model = typeof argDefinition.model === 'string' ? argDefinition.model : 'unknown';
|
||||
const modelId = typeof strValue === 'string' ? strValue : 'unknown';
|
||||
argInput = this.renderArgModel(modelId, model);
|
||||
} else if (argDefinition.type === 'boolean') {
|
||||
argInput = this.renderArgBoolean(strValue.toLowerCase() === 'true');
|
||||
} else if (argDefinition.type === 'number') {
|
||||
isVariableAllowed = true;
|
||||
const placeholder = typeof argDefinition.placeholder === 'string' ? argDefinition.placeholder : '';
|
||||
argInput = this.renderArgNumber(strValue, placeholder || '');
|
||||
} else {
|
||||
|
Loading…
Reference in New Issue
Block a user