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

[Canvas] Add Types to Functions #35087

Merged
merged 16 commits into from May 2, 2019

Conversation

clintandrewhall
Copy link
Contributor

@clintandrewhall clintandrewhall commented Apr 15, 2019

Summary

Depends on #35749 (merged).

Addresses #40160

This PR isn't easy. This prepares for i18n by adding strong types to Canvas Functions. I'm publishing this as a WIP to gather feedback as I go.

It's taken me several days to get Typescript working with Canvas Functions. But, as a result of that effort, we have a ton of new features to support i18n:

Define and Enforce Function Arguments

Canvas Functions have their arguments defined as individual properties of the args property. Each argument has one or more types, regardless of if it's expecting an array or single value of that type, expressed as an array of strings:

args: {
    name: {
      types: ['string'],
    },
    names: {
      types: ['string'], // Same as above, but...
      multi: true,       // ...the argument will be an array
    },
  },
},

This difference is now typed, and the type of each argument is available to fn:

Apr-15-2019 10-58-51

In addition, the presence of the resolve property set to false will now enforce that the argument types be functions:

Apr-16-2019 14-25-10

Define and Enforce Context

By providing a generic type to represent the context passed into the Function, we can enforce it in fn:

Apr-15-2019 11-11-13

@clintandrewhall clintandrewhall added WIP Work in progress Team:Presentation Presentation Team for Dashboard, Input Controls, and Canvas labels Apr 15, 2019
@clintandrewhall clintandrewhall requested a review from a team as a code owner April 15, 2019 16:18
@elasticmachine
Copy link
Contributor

Pinging @elastic/kibana-canvas

@elasticmachine
Copy link
Contributor

💔 Build Failed

@lukasolson
Copy link
Member

lukasolson commented Apr 15, 2019

Further, the types array is omitted if the argument has the resolve property set to false.

Not sure I'm understanding correctly, but does this mean things like ply will have errors?

expression: {
types: ['datatable'],
resolve: false,
multi: true,
aliases: ['fn', 'function'],
help:
'An expression to pass each resulting data table into. Tips: \n' +
' Expressions must return a datatable. Use `as` to turn literals into datatables.\n' +
' Multiple expressions must return the same number of rows.' +
' If you need to return a differing row count, pipe into another instance of ply.\n' +
' If multiple expressions return the same columns, the last one wins.',
},

resolve: false and types: [] are not supposed to be incompatible. When invoking the function to resolve the arguments, we check if the types match what the spec says. So, in something like ply where the expression argument is expected to return a datatable, if you don't actually return a datatable, we should throw an error.

@clintandrewhall
Copy link
Contributor Author

@lukasolson Nice catch. @w33ble can you explain the relationship between resolve and types? Comparing when and then from case, for example?

@clintandrewhall clintandrewhall changed the title [WIP] Type and Add i18n to Canvas Functions [WIP] Type and add i18n to Canvas Functions Apr 16, 2019
@elasticmachine
Copy link
Contributor

💔 Build Failed

@w33ble
Copy link
Contributor

w33ble commented Apr 17, 2019

Is there a reason you defined the type as FunctionFactory<Name, Arguments, Return, Context, Errors>?

Seems like it would fit better if you did context than arguments, since that's the order of the arguments in fn. ie. FunctionFactory<Name, Context, Arguments, Return, Errors>

@clintandrewhall
Copy link
Contributor Author

@w33ble I can change the order, but you'd be forced to add void in a lot of function cases, like any, which don't have a context defined. I'm honestly easy with whatever order, I was just optimizing for terseness.

@clintandrewhall
Copy link
Contributor Author

@w33ble For example:

export const any: FunctionFactory<'any', Arguments, boolean> = () => {
  const { help, args: argHelp } = getFunctionHelp().any;

would become

export const any: FunctionFactory<'any', void, Arguments, boolean> = () => {
  const { help, args: argHelp } = getFunctionHelp().any;

@elasticmachine
Copy link
Contributor

💔 Build Failed

@elasticmachine
Copy link
Contributor

💔 Build Failed

@w33ble
Copy link
Contributor

w33ble commented Apr 18, 2019

@clintandrewhall I think FunctionFactory<'any', void, Arguments, boolean> is fine, and it has the advantage of being explicit. Also, isn't there a way to define multiple possible shapes? So that both FunctionFactory<'any', void, Arguments, boolean> and FunctionFactory<'any', Arguments, boolean> are valid? Or maybe adding a related ContextlessFunctionFactory type makes sense for functions that don't use context? You're the ts guy, you'd know more than me, it just seems weird that the argument orders don't match.

@clintandrewhall clintandrewhall changed the title [WIP] Type and add i18n to Canvas Functions [WIP] Type Canvas Functions Apr 22, 2019
@clintandrewhall
Copy link
Contributor Author

As requested, I've updated this PR to only include the TS changes. I'm still making some changes around naming.

@elasticmachine
Copy link
Contributor

💔 Build Failed

@elasticmachine
Copy link
Contributor

💔 Build Failed

@elasticmachine
Copy link
Contributor

💔 Build Failed

@elasticmachine
Copy link
Contributor

💔 Build Failed

/**
* Utility type: gathers values of a collection as a type for use as a type.
*/
export type ValuesOf<T extends any[]> = T[number];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TS ramp-up (for me) question, what's the difference between this and eg. Array<T>?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should only use Array<T> for complex type expressions, (and eslint will complain if you deviate). For example: string[] and FooType[] are preferable to Array<string> or Array<FooType>, but Array<{foo: string}> is preferable in that case.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I read it - correctly? - as:

  • "You should use Array for complex type expressions only" (ie. not for simple expressions, though I'm not yet clear on what constitutes simple vs complex)
  • string[] and FooType[] are semantically, but not stylistically equivalent to Array<string> and Array<FooType>, resp.

I tried to trigger the syntax check, entering

export type ValuesOf<T extends any[]> = Array<T>;

but it didn't balk, maybe it still accepts it or I don't run the right linter. Also, ValuesOf and UnionToIntersection seem to be unused atm. All these are only curiosities, not sure if they're important at all.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think generics may be an exception...?

*/
// prettier-ignore
export type AvailableFunctionNames =
AvailableFunctions<typeof commonFunctions[number]>['name'] &
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First of all, incredible work, learning so much just by looking through this! How can AvailableFunctionNames be used? It's not being referred to now, does it have an impact without use, or if not, what can in theory be hooked up to it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is going to be used for i18n, for starters. You'll see it's usefulness in my next diff.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's a practical example. In this case, the AvailableFunctions type can be used to enforce the structure of the FunctionHelpDict type, throwing a TS error if any of the functions are missing.

/** This type uses the collection of available functions, (which are all
 * `FunctionFactory`), to derive the name and argument types from them.  This
 * allows for validation that 1/ every function available is listed, and 2/ every
 * function argument has a help string present.
 */
type FunctionHelp<T> = T extends FunctionFactory<
  infer Name,
  infer Context,
  infer Arguments
>
  ? {
      [key in Name]: {
        help: string;
        args: { [key in keyof Arguments]: string };
      }
    }
  : never;

/**
 * This type represents an exhaustive dictionary of Function help strings,
 * organized by Function and then Function Argument.
 *
 * This type indexes the existing function factories, reverses the union to an
 * intersection, and produces the dictionary of strings.
 */
type FunctionHelpDict = UnionToIntersection<FunctionHelp<typeof functions[number]>>;

/**
 * Help text for Canvas Functions should be properly localized. This function will
 * return a dictionary of help strings, organized by Canvas Function specification
 * and then by available arguments.
 */
export const getFunctionHelp = (): FunctionHelpDict => {
  return {
    alterColumn: {
      help: i18n.translate('xpack.canvas.functions.alterColumnHelpText', {
        defaultMessage:
          'Converts between core types, eg string, number, null, boolean, date and rename columns',
      }),
      args: {
        column: i18n.translate('xpack.canvas.functions.alterColumn.args.columnHelpText', {
          defaultMessage: 'The name of the column to alter',
        }),
        type: i18n.translate('xpack.canvas.functions.alterColumn.args.typeHelpText', {
          defaultMessage: 'The type to convert the column to. Leave blank to not change type',
        }),
        name: i18n.translate('xpack.canvas.functions.alterColumn.args.nameHelpText', {
          defaultMessage: 'The resultant column name. Leave blank to not rename',
        }),
      },
    },

@clintandrewhall clintandrewhall changed the title [Canvas] Add Types to Functions [WIP][Canvas] Add Types to Functions Apr 29, 2019
@clintandrewhall clintandrewhall changed the title [WIP][Canvas] Add Types to Functions [Canvas] Add Types to Functions Apr 29, 2019
@elasticmachine
Copy link
Contributor

💔 Build Failed

@elasticmachine
Copy link
Contributor

💚 Build Succeeded

Copy link
Contributor

@monfera monfera left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incredible, tight fitting types PR!

@monfera monfera self-requested a review May 2, 2019 16:07
Copy link
Contributor

@monfera monfera left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @clintandrewhall, I learnt a ton from this PR and there's still much to absorb!

@elasticmachine
Copy link
Contributor

💔 Build Failed

@elasticmachine
Copy link
Contributor

💚 Build Succeeded

@clintandrewhall clintandrewhall merged commit d2e083a into elastic:master May 2, 2019
clintandrewhall added a commit to clintandrewhall/kibana that referenced this pull request May 3, 2019
* Reducing changeset to TS-only

* Update names; fix a few bugs

* Test fixes

* Add browser and server functions

* Update ts-ignore comments; convert pie; break up TS defs

* Convert const to func

* Plot conversion

* Tweaks to documentation

* Fix deleted file

* Fix type errors

* Update yarn.lock

* Address feedback
clintandrewhall added a commit that referenced this pull request May 3, 2019
* Reducing changeset to TS-only

* Update names; fix a few bugs

* Test fixes

* Add browser and server functions

* Update ts-ignore comments; convert pie; break up TS defs

* Convert const to func

* Plot conversion

* Tweaks to documentation

* Fix deleted file

* Fix type errors

* Update yarn.lock

* Address feedback
@clintandrewhall clintandrewhall deleted the i18n-part-one branch June 6, 2019 04:24
@elasticmachine
Copy link
Contributor

💔 Build Failed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
review Team:Presentation Presentation Team for Dashboard, Input Controls, and Canvas v7.2.0 v8.0.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants