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

Fluent Builder Interface #11

Closed
ghost opened this issue Nov 22, 2018 · 3 comments
Closed

Fluent Builder Interface #11

ghost opened this issue Nov 22, 2018 · 3 comments

Comments

@ghost
Copy link

ghost commented Nov 22, 2018

I was thinking, it might be useful to apply a builder pattern to allow for a fluent interface that creates simple shortcuts?

let calcId = '';
const shortcut = shortcutBuilder()
  .comment({ text: 'Hello World' })
  .number({ number: 42 })
  .calculate({ operand: 3, operation: '/' }, id => (calcId = id))
  .showResult({ text: withVariables`Total is ${calcId}!` })
  .build();

It may not be useful for all cases but certainly some.

Unfortunately, manually maintaining a single class supporting every action being added over time wouldn't be favourable.

Thus, inspired some solutions discussed here I set about to throw together a little prototype to see if it is actually possible to simply generate one from all the actions and came up with this:

import * as Actions from '../actions';
import WFWorkflowAction from '../interfaces/WF/WFWorkflowAction';
import { buildShortcut } from './buildShortcut';

/**
 * These are just helper types
 */

// tslint:disable-next-line:no-any
type AnyFunc = (...args: any[]) => any;
type ObjectOfFuncs = Record<string, AnyFunc>;

// tslint:disable-next-line:no-any
type AsReturnType<T extends AnyFunc, R> = T extends (...args: infer P) => any
  ? (...args: P) => R
  : never;

type ActionCallback = (uuid: string) => void;
type ActionBuilder<OptionsType> = (
  options: OptionsType,
  callback?: ActionCallback,
) => WFWorkflowAction | WFWorkflowAction[];

type ActionsType = typeof Actions;

/**
 * This generates the appropriate IBuilder<T> type
 */
type IBuilder<T extends ObjectOfFuncs, R> = {
  [k in keyof T]: AsReturnType<T[k], IBuilder<T, R>>
} & { build(): R };

/**
 * This generates a proxy object that delegates down to
 * the original actions or returns a built shortcut as appropriate.
 */
type IShortcutBuilder = IBuilder<ActionsType, string>;
export const shortcutBuilder = (): IShortcutBuilder => {
  let actions: WFWorkflowAction[] = [];
  const builder = new Proxy(Actions, {
    // tslint:disable-next-line:no-any
    get(target: any, prop: PropertyKey): AnyFunc {
      if (prop === 'build') return () => buildShortcut(actions);

      return <OptionsType>(
        options: OptionsType,
        callback?: (uuid: string) => void,
      ): IShortcutBuilder => {
        const result = (target[prop] as ActionBuilder<OptionsType>)(
          options,
          callback,
        );

        if (Array.isArray(result)) {
          actions = actions.concat(result);
        } else {
          actions.push(result);
        }
        return builder;
      };
    },
  });
  return builder;
};

It's not the prettiest and there is probably room for improvement but it does prove the concept.
Apart from that, it does maintain the typings required for code completion but you do lose the code comments which isn't great.

Just a little toy example but I thought I would share it out of interest to someone, if nothing else.
It would be great to hear about other potential solutions to the problem.

@joshfarrant
Copy link
Owner

It's really an interesting idea! The current method for building a Shortcut (as an array of functions) is by no means the way it has to be done forever, so it's interesting to see options for other ways it could be done. This isn't something I'd considered before now, but it's very cool to see - Thanks so much for putting it forward!

To be honest I've been thinking about something similar, but using chained function calls rather than the builder pattern you suggested.

At this point in time, even a basic shortcut will look horribly messy in this format. You'll end up with either this:

comment({ text: 'Hello World' })(number({ number: 42 })(calculate({ operand: 3, operation: '/' }, id => (calcId = id))(showResult({ text: withVariables`Total is ${calcId}!` }))));

Or this (which is only slightly more readable).

comment({ text: 'Hello World' })(
  number({ number: 42 })(
    calculate({ operand: 3, operation: '/' }, id => (calcId = id))(
      showResult({ text: withVariables`Total is ${calcId}!` })
    )
  )
);

Hopefully we can all agree that these are both awful!

However, with the magic of the Pipeline Operator (Currently a Stage 1 proposal), we could end up with something like this:

shortcuts
  |> comment({ text: 'Hello World' })
  |> number({ number: 42 })
  |> calculate({ operand: 3, operation: '/' }, id => (calcId = id))
  |> showResult({ text: withVariables`Total is ${calcId}!` })

Granted, I've not looked into it in enough detail to know exactly how it would be implemented or used, but it's something that's been in the back of my mind that I thought I'd share. At this point it seems very likely that the Pipeline Operator will make it's way into JS (and then hopefully TypeScript), so it's another thing to think about!

@Archez
Copy link
Contributor

Archez commented Nov 23, 2018

Since Shortcuts can also take a tree path, instead of linear, (e.g. if/else, chooseMenu), I can foresee a chained/builder approach not being an elegant solution.

Secondly, one thing to keep in mind is that import questions target their corresponding action by index, so the array approach lends some credibility. However, I have not checked what index is used for a import question on an action within a branched path.

@joshfarrant
Copy link
Owner

Good point. This is something I was thinking about before I implemented if/else and chooseFromMenu, so it made a bit more sense then. The addition of those actions does make it a bit more complicated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants