Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expression forks #57491

Merged
merged 4 commits into from
Feb 18, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions src/plugins/expressions/common/executor/executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,4 +209,34 @@ export class Executor<Context extends Record<string, unknown> = Record<string, u

return execution;
}

public fork(): Executor<Context> {
const initialState = this.state.get();
const fork = new Executor<Context>(initialState);

/**
* Synchronize registry state - make any new types, functions and context
* also available in the forked instance of `Executor`.
*/
this.state.state$.subscribe(({ types, functions, context }) => {
const state = fork.state.get();
fork.state.set({
...state,
types: {
...types,
...state.types,
},
functions: {
...functions,
...state.functions,
},
context: {
...context,
...state.context,
},
});
});

return fork;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,83 @@ describe('ExpressionsService', () => {

expect(typeof expressions.setup().getFunctions().var_set).toBe('object');
});

describe('.fork()', () => {
test('returns a new ExpressionsService instance', () => {
const service = new ExpressionsService();
const fork = service.fork();

expect(fork).not.toBe(service);
expect(fork).toBeInstanceOf(ExpressionsService);
});

test('fork keeps all types of the origin service', () => {
const service = new ExpressionsService();
const fork = service.fork();

expect(fork.executor.state.get().types).toEqual(service.executor.state.get().types);
});

test('fork keeps all functions of the origin service', () => {
const service = new ExpressionsService();
const fork = service.fork();

expect(fork.executor.state.get().functions).toEqual(service.executor.state.get().functions);
});

test('fork keeps context of the origin service', () => {
const service = new ExpressionsService();
const fork = service.fork();

expect(fork.executor.state.get().context).toEqual(service.executor.state.get().context);
});

test('newly registered functions in origin are also available in fork', () => {
const service = new ExpressionsService();
const fork = service.fork();

service.registerFunction({
name: '__test__',
args: {},
help: '',
fn: () => {},
});

expect(fork.executor.state.get().functions).toEqual(service.executor.state.get().functions);
});

test('newly registered functions in fork are NOT available in origin', () => {
const service = new ExpressionsService();
const fork = service.fork();

fork.registerFunction({
name: '__test__',
args: {},
help: '',
fn: () => {},
});

expect(Object.values(fork.executor.state.get().functions)).toHaveLength(
Object.values(service.executor.state.get().functions).length + 1
);
});

test('fork can execute an expression with newly registered function', async () => {
const service = new ExpressionsService();
const fork = service.fork();

service.registerFunction({
name: '__test__',
args: {},
help: '',
fn: () => {
return '123';
},
});

const result = await fork.run('__test__', null);

expect(result).toBe('123');
});
});
});
37 changes: 34 additions & 3 deletions src/plugins/expressions/common/service/expressions_services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ import { ExecutionContract } from '../execution/execution_contract';
export type ExpressionsServiceSetup = ReturnType<ExpressionsService['setup']>;
export type ExpressionsServiceStart = ReturnType<ExpressionsService['start']>;

export interface ExpressionServiceParams {
executor?: Executor;
renderers?: ExpressionRendererRegistry;
}

/**
* `ExpressionsService` class is used for multiple purposes:
*
Expand All @@ -45,8 +50,16 @@ export type ExpressionsServiceStart = ReturnType<ExpressionsService['start']>;
* so that JSDoc appears in developers IDE when they use those `plugins.expressions.registerFunction(`.
*/
export class ExpressionsService {
public readonly executor = Executor.createWithDefaults();
public readonly renderers = new ExpressionRendererRegistry();
public readonly executor: Executor;
public readonly renderers: ExpressionRendererRegistry;

constructor({
executor = Executor.createWithDefaults(),
renderers = new ExpressionRendererRegistry(),
}: ExpressionServiceParams = {}) {
this.executor = executor;
this.renderers = renderers;
}

/**
* Register an expression function, which will be possible to execute as
Expand Down Expand Up @@ -118,6 +131,23 @@ export class ExpressionsService {
context?: ExtraContext
): Promise<Output> => this.executor.run<Input, Output, ExtraContext>(ast, input, context);

/**
* Create a new instance of `ExpressionsService`. The new instance inherits
* all state of the original `ExpressionsService`, including all expression
* types, expression functions and context. Also, all new types and functions
* registered in the original services AFTER the forking event will be
* available in the forked instance. However, all new types and functions
* registered in the forked instances will NOT be available to the original
* service.
*/
public readonly fork = (): ExpressionsService => {
const executor = this.executor.fork();
const renderers = this.renderers;
const fork = new ExpressionsService({ executor, renderers });

return fork;
};

/**
* Starts expression execution and immediately returns `ExecutionContract`
* instance that tracks the progress of the execution and can be used to
Expand All @@ -139,7 +169,7 @@ export class ExpressionsService {
};

public setup() {
const { executor, renderers, registerFunction, run } = this;
const { executor, renderers, registerFunction, run, fork } = this;

const getFunction = executor.getFunction.bind(executor);
const getFunctions = executor.getFunctions.bind(executor);
Expand All @@ -151,6 +181,7 @@ export class ExpressionsService {
const registerType = executor.registerType.bind(executor);

return {
fork,
getFunction,
getFunctions,
getRenderer,
Expand Down
1 change: 1 addition & 0 deletions src/plugins/expressions/public/mocks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export type Start = jest.Mocked<ExpressionsStart>;

const createSetupContract = (): Setup => {
const setupContract: Setup = {
fork: jest.fn(),
getFunction: jest.fn(),
getFunctions: jest.fn(),
getRenderer: jest.fn(),
Expand Down
8 changes: 8 additions & 0 deletions src/plugins/expressions/public/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import { expressionsPluginMock } from './mocks';
import { add } from '../common/test_helpers/expression_functions/add';
import { ExpressionsService } from '../common';

describe('ExpressionsPublicPlugin', () => {
test('can instantiate from mocks', async () => {
Expand All @@ -27,6 +28,13 @@ describe('ExpressionsPublicPlugin', () => {
});

describe('setup contract', () => {
test('.fork() method returns ExpressionsService', async () => {
const { setup } = await expressionsPluginMock.createPlugin();
const fork = setup.fork();

expect(fork).toBeInstanceOf(ExpressionsService);
});

describe('.registerFunction()', () => {
test('can register a function', async () => {
const { setup } = await expressionsPluginMock.createPlugin();
Expand Down