feat: Automated display type checking in result

This commit is contained in:
1ambda 2017-01-21 08:22:22 +09:00
parent 5810bf1de5
commit 5c49e6e3d3
4 changed files with 213 additions and 46 deletions

View file

@ -28,22 +28,6 @@ export class AbstractFrontendInterpreter {
this.displayType = displayType;
}
/**
* @param paragraphText which includes magic
* @returns {string} if magic exists, otherwise return undefined
*/
static extractMagic(allParagraphText) {
const intpNameRegexp = /^\s*%(\S+)\s*/g;
try {
let match = intpNameRegexp.exec(allParagraphText);
if (match) { return `%${match[1].trim()}`; }
} catch (error) {
// failed to parse, ignore
}
return undefined;
}
static useInterpret(interpreter) {
return (interpreter.__proto__.hasOwnProperty('interpret'));
}

View file

@ -23,35 +23,116 @@ export const DefaultDisplayType = {
TEXT: 'TEXT',
};
export class FrontendInterpreterResult {
constructor(resultDisplayType, resultDataGenerator) {
/**
* backend interpreter uses `data` and `type` as field names.
* let's use the same field to keep consistency.
*/
this.type = resultDisplayType;
this.data = resultDataGenerator;
export const DefaultDisplayMagic = {
'%element': DefaultDisplayType.ELEMENT,
'%table': DefaultDisplayType.TABLE,
'%html': DefaultDisplayType.HTML,
'%angular': DefaultDisplayType.ANGULAR,
'%text': DefaultDisplayType.TEXT,
};
export class GeneratorWithType {
constructor(generator, type) {
// use variable name `data` to keep consistency with backend
this.data = generator;
this.type = type;
}
static isFunctionGenerator(generator) {
return (generator && typeof generator === 'function');
static handleDefaultMagic(m) {
// let's use default display type instead of magic in case of default
// to keep consistency with backend interpreter
if (DefaultDisplayMagic[m]) {
return DefaultDisplayMagic[m];
} else {
return m;
}
}
static isPromiseGenerator(generator) {
return (generator && typeof generator.then === 'function');
}
static parseStringGenerator(generator, customDisplayMagic) {
function availableMagic(magic) {
return magic && (DefaultDisplayMagic[magic] || customDisplayMagic[magic]);
}
static isObjectGenerator(generator) {
return (
!FrontendInterpreterResult.isFunctionGenerator(generator) &&
!FrontendInterpreterResult.isPromiseGenerator(generator));
const splited = generator.split("\n");
const gensWithTypes = [];
let mergedGens = [];
let previousMagic = DefaultDisplayType.TEXT;
// create `GeneratorWithType` whenever see available display type.
for(let i = 0; i < splited.length; i++) {
const g = splited[i];
const magic = FrontendInterpreterResult.extractMagic(g);
// create `GeneratorWithType` only if see new magic
if (availableMagic(magic) && mergedGens.length > 0) {
gensWithTypes.push(new GeneratorWithType(mergedGens.join(''), previousMagic));
mergedGens = [];
}
// accumulate `generator` to mergedGens
if (availableMagic(magic)) {
const withoutMagic = g.split(magic)[1];
mergedGens.push(`${withoutMagic}\n`);
previousMagic = GeneratorWithType.handleDefaultMagic(magic);
} else {
mergedGens.push(`${g}\n`);
}
}
// cleanup the last `GeneratorWithType`
if (mergedGens.length > 0) {
previousMagic = GeneratorWithType.handleDefaultMagic(previousMagic);
gensWithTypes.push(new GeneratorWithType(mergedGens.join(''), previousMagic));
}
return gensWithTypes;
}
/**
* @returns {string} display type for this frontend interpreter result.
* get 1 `GeneratorWithType` and produce multiples using available displays
* return an wrapped with a promise to generalize result output which can be
* object, function or promise
* @param generatorWithType {GeneratorWithType}
* @param availableDisplays {Object} Map for available displays
* @return {Promise<Array<GeneratorWithType>>}
*/
getType() {
return this.type;
static produceMultipleGenerator(generatorWithType, customDisplayType) {
const generator = generatorWithType.getGenerator();
const type = generatorWithType.getType();
// if the type is specified, just return it
// handle non-specified generatorWithTypes only
if (type) {
return new Promise((resolve) => { resolve([generatorWithType]); });
}
let wrapped;
if (FrontendInterpreterResult.isFunctionGenerator(generator)) {
// if generator is a function, we consider it as ELEMENT type.
wrapped = new Promise((resolve) => {
const result = [new GeneratorWithType(generator, DefaultDisplayType.ELEMENT)];
return resolve(result);
});
} else if (FrontendInterpreterResult.isPromiseGenerator(generator)) {
// if generator is a promise,
wrapped = generator.then(generated => {
const result =
GeneratorWithType.parseStringGenerator(generated, customDisplayType);
return result;
})
} else {
// if generator is a object, parse it to multiples
wrapped = new Promise((resolve) => {
const result =
GeneratorWithType.parseStringGenerator(generator, customDisplayType);
return resolve(result);
});
}
return wrapped;
}
/**
@ -63,7 +144,91 @@ export class FrontendInterpreterResult {
* will be called in `then()` of this promise.
* @returns {*} `data` which can be object, function or promise.
*/
getData() {
getGenerator() {
return this.data;
}
/**
* Value of `type` might be empty which means
* generator can be splited into multiple generators
* by `FrontendInterpreterResult.parseMultipleGenerators()`
* @returns {string}
*/
getType() {
return this.type;
}
}
export class FrontendInterpreterResult {
constructor(resultGenerator, resultType) {
this.generatorsWithTypes = [];
this.add(resultGenerator, resultType);
}
static isFunctionGenerator(generator) {
return (generator && typeof generator === 'function');
}
static isPromiseGenerator(generator) {
return (generator && typeof generator.then === 'function');
}
static isObjectGenerator(generator) {
return (generator &&
!FrontendInterpreterResult.isFunctionGenerator(generator) &&
!FrontendInterpreterResult.isPromiseGenerator(generator));
}
static extractMagic(allParagraphText) {
const intpNameRegexp = /^\s*%(\S+)\s*/g;
try {
let match = intpNameRegexp.exec(allParagraphText);
if (match) {
return `%${match[1].trim()}`;
}
} catch (error) {
// failed to parse, ignore
}
return undefined;
}
/**
* consume 1 generator and produce multiple
* @param generator {string}
* @param customDisplayType
* @return {Array<GeneratorWithType>}
*/
add(resultGenerator, resultType) {
if (resultGenerator) {
this.generatorsWithTypes.push(
new GeneratorWithType(resultGenerator, resultType));
}
return this;
}
/**
* @param customDisplayType
* @return {Promise<Array<GeneratorWithType>>}
*/
getAllParsedGeneratorsWithTypes(customDisplayType) {
const promises = this.generatorsWithTypes.map(gt => {
return GeneratorWithType.produceMultipleGenerator(gt, customDisplayType);
});
// some promises can include an array so we need to flatten them
const flatten = Promise.all(promises).then(values => {
return values.reduce((acc, cur) => {
if (Array.isArray(cur)) {
return acc.concat(cur);
} else {
return acc.concat([cur]);
}
})
});
return flatten;
}
}

View file

@ -12,7 +12,7 @@
* limitations under the License.
*/
import { AbstractFrontendInterpreter } from '../../frontend-interpreter'
import { FrontendInterpreterResult } from '../../frontend-interpreter'
angular.module('zeppelinWebApp').controller('ParagraphCtrl', ParagraphCtrl);
@ -225,9 +225,16 @@ function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $locat
websocketMsgSrv.cancelParagraphRun(paragraph.id);
};
$scope.handleFrontendInterpreterError = function(error) {
$scope.paragraph.status = 'ERROR';
$scope.paragraph.errorMessage = error.stack;
console.error('Failed to execute FrontendInterpreter.interpret\n', error);
$scope.$digest();
}
$scope.runParagraphUsingFrontendInterpreter = function(intp, paragraphText, magic) {
// clear results which is watched by `ng-repeat`. without this,
// frontend interpreter results cannot be rendered in view twice more
// frontend interpreter results cannot be rendered in view more than once
$scope.paragraph.results = {};
$scope.$digest();
@ -236,13 +243,17 @@ function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $locat
const splited = paragraphText.split(magic);
const textWithoutMagic = splited[1];
$scope.paragraph.status = 'FINISHED';
$scope.paragraph.results.msg = intp.interpret(textWithoutMagic);
$scope.paragraph.config.tableHide = false;
const frontIntpResult = intp.interpret(textWithoutMagic);
const parsed = frontIntpResult.getAllParsedGeneratorsWithTypes(
heliumService.getAvailableFrontendInterpreterDisplay());
parsed.then(resultsMsg => {
$scope.paragraph.results.msg = resultsMsg;
$scope.paragraph.config.tableHide = false;
$scope.$digest();
}).catch($scope.handleFrontendInterpreterError);
// TODO(1ambda): broadcast to other clients
} catch (error) {
$scope.paragraph.status = 'ERROR';
$scope.paragraph.errorMessage = error.stack;
console.error('Failed to execute FrontendInterpreter.interpret\n', error);
$scope.handleFrontendInterpreterError(error);
}
};
@ -271,7 +282,7 @@ function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $locat
return;
}
const magic = AbstractFrontendInterpreter.extractMagic(paragraphText);
const magic = FrontendInterpreterResult.extractMagic(paragraphText);
const frontendIntp = heliumService.getFrontendInterpreterUsingMagic(magic);
if (frontendIntp) {

View file

@ -81,10 +81,17 @@ import {
* @param displayType {string} e.g `ELEMENT`
* @returns {FrontendInterpreterBase} undefined for non-available displayType
*/
this.getFrontendInterpreterWithDisplayType = function(displayType) {
this.getFrontendInterpreterUsingDisplayType = function(displayType) {
return frontendIntpWithDisplayType[displayType];
};
/**
* @returns {Object}
*/
this.getAvailableFrontendInterpreterDisplay = function() {
return frontendIntpWithDisplayType;
}
this.getVisualizationBundles = function() {
return visualizationBundles;
};