New Template Tag to read file from filesystem (#557)

* Template tag to read from file

* Add tests
This commit is contained in:
Gregory Schier 2017-11-01 10:57:07 +01:00 committed by GitHub
parent ce9b72bf07
commit f9521a3456
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 136 additions and 31 deletions

View File

@ -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' %}`);
});
});

View 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'));
});

View File

@ -0,0 +1 @@
Hello World!

View 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');
}
};

View File

@ -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
];

View File

@ -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 "''";

View File

@ -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;

View File

@ -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 {