Skip to content

Commit

Permalink
[RFC] Allow 'leasing' of Expression constructs
Browse files Browse the repository at this point in the history
  • Loading branch information
clintandrewhall committed Jul 9, 2021
1 parent 48fa754 commit 22046d6
Show file tree
Hide file tree
Showing 8 changed files with 235 additions and 68 deletions.
80 changes: 78 additions & 2 deletions src/plugins/expressions/common/executor/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,89 @@ export const defaultState: ExecutorState<any> = {

export interface ExecutorPureTransitions {
addFunction: (state: ExecutorState) => (fn: ExpressionFunction) => ExecutorState;
addFunctions: (state: ExecutorState) => (fns: ExpressionFunction[]) => ExecutorState;
removeFunction: (state: ExecutorState) => (fnName: ExpressionFunction['name']) => ExecutorState;
removeFunctions: (
state: ExecutorState
) => (fnNames: Array<ExpressionFunction['name']>) => ExecutorState;
addType: (state: ExecutorState) => (type: ExpressionType) => ExecutorState;
addTypes: (state: ExecutorState) => (types: ExpressionType[]) => ExecutorState;
removeType: (state: ExecutorState) => (typeName: ExpressionType['name']) => ExecutorState;
removeTypes: (state: ExecutorState) => (typeName: Array<ExpressionType['name']>) => ExecutorState;
extendContext: (state: ExecutorState) => (extraContext: Record<string, unknown>) => ExecutorState;
}

const addFunctions: ExecutorPureTransitions['addFunctions'] = (state) => (fns) => {
const functions = {} as Record<string, ExpressionFunction>;

fns.forEach((fn) => {
functions[fn.name] = fn;
});

return {
...state,
functions: {
...state.functions,
...functions,
},
};
};

const removeFunctions: ExecutorPureTransitions['removeFunctions'] = (state) => (names) => {
const functions = {} as Record<string, ExpressionFunction>;

for (const name in state.functions) {
if (!names.includes(name)) {
functions[name] = state.functions[name];
}
}

return {
...state,
functions,
};
};

const addTypes: ExecutorPureTransitions['addTypes'] = (state) => (typesToAdd) => {
const types = {} as Record<string, ExpressionType>;

typesToAdd.forEach((type) => {
types[type.name] = type;
});

return {
...state,
types: {
...state.types,
...types,
},
};
};

const removeTypes: ExecutorPureTransitions['removeTypes'] = (state) => (typesToRemove) => {
const types = {} as Record<string, ExpressionType>;

for (const name in state.types) {
if (!typesToRemove.includes(name)) {
types[name] = state.types[name];
}
}

return {
...state,
types,
};
};

export const pureTransitions: ExecutorPureTransitions = {
addFunction: (state) => (fn) => ({ ...state, functions: { ...state.functions, [fn.name]: fn } }),
addType: (state) => (type) => ({ ...state, types: { ...state.types, [type.name]: type } }),
addFunction: (state) => (fn) => addFunctions(state)([fn]),
addFunctions,
removeFunction: (state) => (fnName) => removeFunctions(state)([fnName]),
removeFunctions,
addType: (state) => (type) => addTypes(state)([type]),
addTypes,
removeType: (state) => (typeName) => removeTypes(state)([typeName]),
removeTypes,
extendContext: (state) => (extraContext) => ({
...state,
context: { ...state.context, ...extraContext },
Expand Down
34 changes: 34 additions & 0 deletions src/plugins/expressions/common/executor/executor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,18 @@ describe('Executor', () => {
expressionTypes.typeSpecs.map((spec) => spec.name).sort()
);
});

test('can lease all types', () => {
const executor = new Executor();
const release = executor.leaseTypes(expressionTypes.typeSpecs);
let types = executor.getTypes();
expect(Object.keys(types).sort()).toEqual(
expressionTypes.typeSpecs.map((spec) => spec.name).sort()
);
release();
types = executor.getTypes();
expect(Object.keys(types).length).toBe(0);
});
});

describe('function registry', () => {
Expand Down Expand Up @@ -80,6 +92,28 @@ describe('Executor', () => {

expect(Object.keys(functions).sort()).toEqual(functionSpecs.map((spec) => spec.name).sort());
});

test('can lease functions', () => {
const executor = new Executor();
const functionSpecs = [
expressionFunctions.clog,
expressionFunctions.font,
expressionFunctions.variableSet,
expressionFunctions.variable,
expressionFunctions.theme,
expressionFunctions.cumulativeSum,
expressionFunctions.derivative,
expressionFunctions.movingAverage,
expressionFunctions.mapColumn,
expressionFunctions.math,
];
const release = executor.leaseFunctions(functionSpecs);
let functions = executor.getFunctions();
expect(Object.keys(functions).sort()).toEqual(functionSpecs.map((spec) => spec.name).sort());
release();
functions = executor.getFunctions();
expect(Object.keys(functions).length).toBe(0);
});
});

describe('context', () => {
Expand Down
60 changes: 46 additions & 14 deletions src/plugins/expressions/common/executor/executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,16 @@ export class FunctionsRegistry implements IRegistry<ExpressionFunction> {
}
}

type FunctionDefinition = AnyExpressionFunctionDefinition | (() => AnyExpressionFunctionDefinition);
type TypeDefinition = AnyExpressionTypeDefinition | (() => AnyExpressionTypeDefinition);

export class Executor<Context extends Record<string, unknown> = Record<string, unknown>>
implements PersistableStateService<ExpressionAstExpression> {
static createWithDefaults<Ctx extends Record<string, unknown> = Record<string, unknown>>(
state?: ExecutorState<Ctx>
): Executor<Ctx> {
const executor = new Executor<Ctx>(state);
for (const type of typeSpecs) executor.registerType(type);

executor.registerTypes(typeSpecs);
return executor;
}

Expand All @@ -106,13 +108,27 @@ export class Executor<Context extends Record<string, unknown> = Record<string, u
this.types = new TypesRegistry(this);
}

public registerFunction(
functionDefinition: AnyExpressionFunctionDefinition | (() => AnyExpressionFunctionDefinition)
) {
const fn = new ExpressionFunction(
typeof functionDefinition === 'object' ? functionDefinition : functionDefinition()
public registerFunction(functionDefinition: FunctionDefinition) {
this.registerFunctions([functionDefinition]);
}

public registerFunctions(functionDefinitions: FunctionDefinition[]) {
const fns = functionDefinitions.map(
(fn) => new ExpressionFunction(typeof fn === 'object' ? fn : fn())
);
this.state.transitions.addFunction(fn);

this.state.transitions.addFunctions(fns);
}

public leaseFunctions(functionDefinitions: FunctionDefinition[]) {
const fns = functionDefinitions.map(
(fn) => new ExpressionFunction(typeof fn === 'object' ? fn : fn())
);

this.state.transitions.addFunctions(fns);
const names = fns.map((fn) => fn.name);

return () => this.state.transitions.removeFunctions(names);
}

public getFunction(name: string): ExpressionFunction | undefined {
Expand All @@ -123,13 +139,29 @@ export class Executor<Context extends Record<string, unknown> = Record<string, u
return { ...this.state.get().functions };
}

public registerType(
typeDefinition: AnyExpressionTypeDefinition | (() => AnyExpressionTypeDefinition)
) {
const type = new ExpressionType(
typeof typeDefinition === 'object' ? typeDefinition : typeDefinition()
public registerType(typeDefinition: TypeDefinition) {
this.registerTypes([typeDefinition]);
}

public registerTypes(typeDefinitions: TypeDefinition[]) {
const types = typeDefinitions.map(
(typeDefinition) =>
new ExpressionType(typeof typeDefinition === 'object' ? typeDefinition : typeDefinition())
);
this.state.transitions.addType(type);

this.state.transitions.addTypes(types);
}

public leaseTypes(typeDefinitions: TypeDefinition[]) {
const types = typeDefinitions.map(
(typeDefinition) =>
new ExpressionType(typeof typeDefinition === 'object' ? typeDefinition : typeDefinition())
);

this.state.transitions.addTypes(types);
const names = types.map((type) => type.name);

return () => this.state.transitions.removeTypes(names);
}

public getType(name: string): ExpressionType | undefined {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ export class ExpressionRendererRegistry implements IRegistry<ExpressionRenderer>
this.renderers.set(renderer.name, renderer);
}

remove(name: string) {
this.renderers.delete(name);
}

public get(id: string): ExpressionRenderer | null {
return this.renderers.get(id) || null;
}
Expand Down
54 changes: 43 additions & 11 deletions src/plugins/expressions/common/service/expressions_services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ import { Executor } from '../executor';
import { AnyExpressionRenderDefinition, ExpressionRendererRegistry } from '../expression_renderers';
import { ExpressionAstExpression } from '../ast';
import { ExecutionContract, ExecutionResult } from '../execution';
import { AnyExpressionTypeDefinition, ExpressionValueError } from '../expression_types';
import { AnyExpressionFunctionDefinition } from '../expression_functions';
import { ExpressionValueError } from '../expression_types';
import { SavedObjectReference } from '../../../../core/types';
import { PersistableStateService, SerializableState } from '../../../kibana_utils/common';
import { Adapters } from '../../../inspector/common/adapters';
Expand Down Expand Up @@ -43,13 +42,19 @@ export type ExpressionsServiceSetup = Pick<
ExpressionsService,
| 'getFunction'
| 'getFunctions'
| 'leaseFunctions'
| 'getRenderer'
| 'getRenderers'
| 'getType'
| 'getTypes'
| 'leaseTypes'
| 'registerFunction'
| 'registerFunctions'
| 'registerRenderer'
| 'registerRenderers'
| 'leaseRenderers'
| 'registerType'
| 'registerTypes'
| 'run'
| 'fork'
>;
Expand Down Expand Up @@ -231,17 +236,44 @@ export class ExpressionsService implements PersistableStateService<ExpressionAst
* be edited by user (e.g in case of Canvas); (3) `context` is a shared object
* passed to all functions that can be used for side-effects.
*/
public readonly registerFunction = (
functionDefinition: AnyExpressionFunctionDefinition | (() => AnyExpressionFunctionDefinition)
): void => this.executor.registerFunction(functionDefinition);
public readonly registerFunction: Executor['registerFunction'] = (functionDefinition) =>
this.executor.registerFunction(functionDefinition);

public readonly registerType = (
typeDefinition: AnyExpressionTypeDefinition | (() => AnyExpressionTypeDefinition)
): void => this.executor.registerType(typeDefinition);
public readonly registerFunctions: Executor['registerFunctions'] = (functionDefinitions) =>
this.executor.registerFunctions(functionDefinitions);

public readonly registerRenderer = (
definition: AnyExpressionRenderDefinition | (() => AnyExpressionRenderDefinition)
): void => this.renderers.register(definition);
public readonly leaseFunctions: Executor['leaseFunctions'] = (fns) =>
this.executor.leaseFunctions(fns);

public readonly registerType: Executor['registerType'] = (typeDefinition) =>
this.executor.registerType(typeDefinition);

public readonly registerTypes: Executor['registerTypes'] = (typeDefinitions) =>
this.executor.registerTypes(typeDefinitions);

public readonly leaseTypes: Executor['leaseTypes'] = (types) => this.executor.leaseTypes(types);

public readonly registerRenderer: ExpressionRendererRegistry['register'] = (definition): void =>
this.renderers.register(definition);

public readonly registerRenderers = (
definitions: Array<AnyExpressionRenderDefinition | (() => AnyExpressionRenderDefinition)>
) => {
definitions.forEach(this.registerRenderer);
};

public readonly leaseRenderers = (
definitions: Array<AnyExpressionRenderDefinition | (() => AnyExpressionRenderDefinition)>
) => {
const names = definitions.map((definition) => {
this.renderers.register(definition);
return definition.name;
});

return () => {
names.forEach((name) => this.renderers.remove(name));
};
};

public readonly run: ExpressionsServiceStart['run'] = (ast, input, params) =>
this.executor.run(ast, input, params);
Expand Down
30 changes: 25 additions & 5 deletions x-pack/plugins/canvas/canvas_plugin_src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,20 @@

import { CoreSetup, CoreStart, Plugin } from 'src/core/public';
import { ChartsPluginStart } from 'src/plugins/charts/public';
import { ExpressionsSetup } from 'src/plugins/expressions/public';

import { CanvasSetup } from '../public';
import { EmbeddableStart } from '../../../../src/plugins/embeddable/public';
import { UiActionsStart } from '../../../../src/plugins/ui_actions/public';
import { Start as InspectorStart } from '../../../../src/plugins/inspector/public';
import { AnyExpressionRenderDefinition } from '../types';

import { functions } from './functions/browser';
import { typeFunctions } from './expression_types';
import { renderFunctions, renderFunctionFactories } from './renderers';
interface SetupDeps {
canvas: CanvasSetup;
expressions: ExpressionsSetup;
}

export interface StartDeps {
Expand All @@ -32,15 +36,24 @@ export type StartInitializer<T> = (core: CoreStart, plugins: StartDeps) => T;
/** @internal */
export class CanvasSrcPlugin implements Plugin<void, void, SetupDeps, StartDeps> {
public setup(core: CoreSetup<StartDeps>, plugins: SetupDeps) {
plugins.canvas.addFunctions(functions);
plugins.canvas.addTypes(typeFunctions);
const { expressions } = plugins;
const releaseFunctions = expressions.leaseFunctions(functions);
const releaseTypes = expressions.leaseTypes(typeFunctions);

// There is an issue of the canvas render definition not matching the expression render definition
// due to our handlers needing additional methods. For now, we are going to cast to get to the proper
// type, but we should work with AppArch to figure out how the Handlers can be genericized
const releaseRenderers = expressions.leaseRenderers(
(renderFunctions as unknown) as AnyExpressionRenderDefinition[]
);

plugins.canvas.addRenderers(renderFunctions);
let releaseFactories = () => {};

core.getStartServices().then(([coreStart, depsStart]) => {
plugins.canvas.addRenderers(
renderFunctionFactories.map((factory: any) => factory(coreStart, depsStart))
const renderers = renderFunctionFactories.map((factory: any) =>
factory(coreStart, depsStart)
);
releaseFactories = expressions.leaseRenderers(renderers);
});

plugins.canvas.addDatasourceUIs(async () => {
Expand Down Expand Up @@ -81,6 +94,13 @@ export class CanvasSrcPlugin implements Plugin<void, void, SetupDeps, StartDeps>
const { transformSpecs } = await import('./canvas_addons');
return transformSpecs;
});

return () => {
releaseFunctions();
releaseTypes();
releaseRenderers();
releaseFactories();
};
}

public start(core: CoreStart, plugins: StartDeps) {}
Expand Down
Loading

0 comments on commit 22046d6

Please sign in to comment.