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

sync up to latest #131

Merged
merged 1 commit into from
Jan 27, 2023
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
73 changes: 70 additions & 3 deletions core/make-flow/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import type { Flow, Asset, AssetWrapper } from '@player-ui/types';
import type {
Flow,
Asset,
AssetWrapper,
NavigationFlow,
NavigationFlowEndState,
} from '@player-ui/types';
import identify, { ObjType } from './identify';

export * from './identify';
Expand Down Expand Up @@ -27,10 +33,71 @@ function unwrapJSend(obj: object) {
return obj;
}

interface NavOptions {
/** An optional expression to run when this Flow starts */
onStart?: NavigationFlow['onStart'];
/** An optional expression to run when this Flow ends */
onEnd?: NavigationFlow['onEnd'];
/**
* A description of _how_ the flow ended.
* If this is a flow started from another flow, the outcome determines the flow transition
*/
outcome?: NavigationFlowEndState['outcome'];
}

/**
* create a default navigation if the flow was exactly one view and there is no navigation already
*/
const createDefaultNav = (flow: Flow, options?: NavOptions): Flow => {
if (
(flow.navigation === undefined || flow.navigation === null) &&
Array.isArray(flow.views) &&
flow.views.length === 1
) {
const navFlow: NavigationFlow = {
startState: 'VIEW_0',
VIEW_0: {
state_type: 'VIEW',
ref: flow.views[0].id ?? `${flow.id}-views-0`,
transitions: {
'*': 'END_done',
Prev: 'END_back',
},
},
END_done: {
state_type: 'END',
outcome: options?.outcome ?? 'doneWithFlow',
},
END_back: {
state_type: 'END',
outcome: 'BACK',
},
};

if (options?.onStart !== undefined) {
navFlow.onStart = options.onStart;
}

if (options?.onEnd !== undefined) {
navFlow.onEnd = options.onEnd;
}

return {
...flow,
navigation: {
BEGIN: 'Flow',
Flow: navFlow,
},
};
}

return flow;
};

/**
* Take any given object and try to convert it to a flow
*/
export function makeFlow(obj: any): Flow {
export function makeFlow(obj: any, args?: NavOptions): Flow {
const objified = unwrapJSend(typeof obj === 'string' ? JSON.parse(obj) : obj);

if (Array.isArray(objified)) {
Expand Down Expand Up @@ -60,7 +127,7 @@ export function makeFlow(obj: any): Flow {
}

if (type === ObjType.FLOW) {
return obj;
return createDefaultNav(obj, args);
}

if (type === ObjType.ASSET_WRAPPER) {
Expand Down
153 changes: 153 additions & 0 deletions core/player/src/__tests__/validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2269,3 +2269,156 @@ describe('weak binding edge cases', () => {
});
});
});

test('updating a binding only updates its data and not other bindings due to weak binding connections', async () => {
const flow = makeFlow({
id: 'view-1',
type: 'view',
thing1: {
asset: {
id: 'thing-1',
binding: 'input.text',
},
},
thing2: {
asset: {
id: 'thing-2',
binding: 'input.check',
},
},
validation: [
{
type: 'requiredIf',
ref: 'input.text',
param: 'input.check',
},
],
});

flow.data = {
someOtherParam: 'notFoo',
};

flow.schema = {
ROOT: {
input: {
type: 'InputType',
},
},
InputType: {
text: {
type: 'DateType',
validation: [
{
type: 'paramIsFoo',
param: 'someOtherParam',
},
],
},
check: {
type: 'BooleanType',
validation: [
{
type: 'required',
},
],
},
},
};

const basicValidationPlugin = {
name: 'basic-validation',
apply: (player: Player) => {
player.hooks.schema.tap('basic-validation', (schema) => {
schema.addDataTypes([
{
type: 'DateType',
validation: [{ type: 'date' }],
},
{
type: 'BooleanType',
validation: [{ type: 'boolean' }],
},
]);
});

player.hooks.validationController.tap('basic-validation', (vc) => {
vc.hooks.createValidatorRegistry.tap('basic-validation', (registry) => {
registry.register('date', (ctx, value) => {
if (value === undefined) {
return;
}

return value.match(/^\d{4}-\d{2}-\d{2}$/)
? undefined
: { message: 'Not a date' };
});
registry.register('boolean', (ctx, value) => {
if (value === undefined || value === true || value === false) {
return;
}

return {
message: 'Not a boolean',
};
});

registry.register('required', (ctx, value) => {
if (value === undefined) {
return {
message: 'Required',
};
}
});

registry.register<any>('requiredIf', (ctx, value, { param }) => {
const paramValue = ctx.model.get(param);
if (paramValue === undefined) {
return;
}

if (value === undefined) {
return {
message: 'Required',
};
}
});

registry.register<any>('paramIsFoo', (ctx, value, { param }) => {
const paramValue = ctx.model.get(param);
if (paramValue === 'foo') {
return;
}

if (value === undefined) {
return {
message: 'Must be foo',
};
}
});
});
});
},
};

const player = new Player({
plugins: [new TrackBindingPlugin(), basicValidationPlugin],
});
player.start(flow);
const state = player.getState() as InProgressState;

state.controllers.flow.transition('next');
waitFor(() => {
state.controllers.data.set([['input.text', '']]);
});

waitFor(() => {
state.controllers.data.set([['input.check', true]]);
});

waitFor(() => {
const finalState = player.getState() as InProgressState;
const otherParam = finalState.controllers.data.get('someOtherParam');
expect(otherParam).toBe('notFoo');
});
});
2 changes: 0 additions & 2 deletions core/player/src/controllers/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,6 @@ export class DataController implements DataModelWithParser<DataModelOptions> {

this.hooks.onSet.call(normalizedTransaction);

this.hooks.onSet.call(normalizedTransaction);

if (setUpdates.length > 0) {
this.hooks.onUpdate.call(setUpdates, options);
}
Expand Down
25 changes: 6 additions & 19 deletions core/player/src/player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
FlowController,
} from './controllers';
import { FlowExpPlugin } from './plugins/flow-exp-plugin';
import { DefaultExpPlugin } from './plugins/default-exp-plugin';
import type {
PlayerFlowState,
InProgressState,
Expand Down Expand Up @@ -117,17 +118,16 @@ export class Player {
};

constructor(config?: PlayerConfigOptions) {
const initialPlugins: PlayerPlugin[] = [];
const flowExpPlugin = new FlowExpPlugin();

initialPlugins.push(flowExpPlugin);

if (config?.logger) {
this.logger.addHandler(config.logger);
}

this.config = config || {};
this.config.plugins = [...(this.config.plugins || []), ...initialPlugins];
this.config.plugins = [
new DefaultExpPlugin(),
...(this.config.plugins || []),
new FlowExpPlugin(),
];
this.config.plugins?.forEach((plugin) => {
plugin.apply(this);
});
Expand Down Expand Up @@ -414,19 +414,6 @@ export class Player {
});
this.hooks.viewController.call(viewController);

/** Gets formatter for given formatName and formats value if found, returns value otherwise */
const formatFunction: ExpressionHandler<[unknown, string], any> = (
ctx,
value,
formatName
) => {
return (
schema.getFormatterForType({ type: formatName })?.format(value) ?? value
);
};

expressionEvaluator.addExpressionFunction('format', formatFunction);

return {
start: () => {
flowController
Expand Down
57 changes: 57 additions & 0 deletions core/player/src/plugins/default-exp-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { ExpressionHandler, ExpressionType } from '../expressions';
import type { SchemaController } from '../schema';
import type { Player, PlayerPlugin } from '../player';

/** Gets formatter for given formatName and formats value if found, returns value otherwise */
const createFormatFunction = (schema: SchemaController) => {
/**
* The generated handler for the given schema
*/
const handler: ExpressionHandler<[unknown, string], any> = (
ctx,
value,
formatName
) => {
return (
schema.getFormatterForType({ type: formatName })?.format(value) ?? value
);
};

return handler;
};

/**
* A plugin that provides the out-of-the-box expressions for player
*/
export class DefaultExpPlugin implements PlayerPlugin {
name = 'flow-exp-plugin';

apply(player: Player) {
let formatFunction: ExpressionHandler<[unknown, string]> | undefined;

player.hooks.schema.tap(this.name, (schemaController) => {
formatFunction = createFormatFunction(schemaController);
});

player.hooks.expressionEvaluator.tap(this.name, (expEvaluator) => {
if (formatFunction) {
expEvaluator.addExpressionFunction('format', formatFunction);
}

expEvaluator.addExpressionFunction('log', (ctx, ...args) => {
player.logger.info(...args);
});

expEvaluator.addExpressionFunction('debug', (ctx, ...args) => {
player.logger.debug(...args);
});

expEvaluator.addExpressionFunction(
'eval',
(ctx, ...args: [ExpressionType]) => {
return ctx.evaluate(...args);
}
);
});
}
}
Loading