Skip to content
Open
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
33 changes: 29 additions & 4 deletions docs/contribution-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,35 @@

The purpose of this document is to make it easier for new contributors to get up-to-speed on the project.

## Prerequisites
## Project Setup

### JavaScript Tooling

The project uses Node.js and NPM as the runtime and package manager. You can download them from
[this link](https://nodejs.org/en/download/).

We use [Gulp](https://gulpjs.com/) as our build tool. You can use NPM to install it:

```
npm install -g gulp
```

### Elm Tooling

The easiest way to install the Elm tooling is using NPM. Here's a list of Elm tools we use:

- `npm install -g elm`
- `npm install -g elm-test`
- `npm install -g elm-format`
- `npm install -g elm-live`

### IDE

Most contributors are using [IntelliJ](https://www.jetbrains.com/idea/download) with the
[Elm plugin](https://plugins.jetbrains.com/plugin/10268-elm). [VS Code](https://code.visualstudio.com/download) with the
[Elm plugin](https://marketplace.visualstudio.com/items?itemName=Elmtooling.elm-ls-vscode) is another popular choice.

## Learning Material

In order to contribute to this project you need to be familiar with Elm and understand some language processing / compiler concepts that are core to Morphir.
We collected a series of learning materials for you to make it easier to fill any knowledge gaps. Feel free to skip any of these if you feel like you are an expert.
Expand All @@ -24,6 +52,3 @@ If you have any doubts though it's better to glance through them. We included th
- [Deeper dive](https://www.youtube.com/watch?v=VKM1eLoN-gI) (12 mins)


## Project Setup

TODO
131 changes: 131 additions & 0 deletions docs/typescript.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# TypeScript API

The purpose of this document is describing the TypeScript API generated for Morphir
models by running `morphir-elm gen --target=TypeScript`.

## Generating TypeScript

Given a model represented in `morphir-ir.json`, you can generate a TypeScript
representation by running:

morphir-elm gen --input morphir-ir.json --output ./generated --target=TypeScript

Note that at present only the types are converted. Data values and functions
are not.

You can generate a TypeScript representation of the Morphir IR itself by
running this in the morphir-elm repo:

morphir-elm make ./morphir-make --types-only
morphir-elm gen --input=morphir-ir.json --output=./generated --target=TypeScript

## Using the generated types

The TypeScript backend outputs a top-level module per package, which your own
code should import. The namespaces correspond with the package and module names
in the IR. Only namespaces and symbols marked as public will be exported in the
TypeScript API.

For example, you can use the `IR` types from the `Morphir` package like this:

import { Morphir } from './generated/Morphir'

const myName: Morphir.IR.Name.Name = ["this", "is", "a", "great", "name"]

Internally the types map to TypeScript type definitions. This is how a Morphir
IR `Name` would be represented in `generated/morphir/ir/Name.ts`:

export type Name = Array<string>

You benefit from all the usual TypeScript type checking. For example, a Path
must be a list of Name instances, so this example will raise an error:

import { Morphir } from './generated/Morphir'

const myName: Morphir.IR.Path.Path = "This is the wrong type."

You should see this message when compiling:

test.ts:3:7 - error TS2322: Type 'string' is not assignable to type 'Path'.

Most Morphir types correspond directly to JavaScript types. The
[JSON mapping](https://github.com/finos/morphir-elm/blob/main/docs/json-mapping.md)
gives a useful reference. There are some special cases, which are documented below.

### Type mapping details

#### Dict

A `Morphir.SDK.Dict.Dict K V` maps to a TypeScript `Map<K,V>`.

#### Custom types

We follow the example
["Tagged Union Types in TypeScript"](https://mariusschulz.com/blog/tagged-union-types-in-typescript)
to implement custom types.

Each type variant is a TypeScript `interface`, with a `kind` and maybe some
fields. The fields names are defined in the IR, and if you used `morphir-elm`
make to build the IR then the names will follow the pattern `arg1`, `arg2`,
`arg3` and so on.

Constructor functions are provided for these. Here's an example using the
Morphir IR `Value` custom type, creating an instance of its `Reference`
variant:

import { Morphir } from './generated/Morphir'

const exampleFQName: Morphir.IR.FQName.FQName = [[], [[]], ["excellent", "name"]];

type AttrType = [];
let myReference = new Morphir.IR.Value.Reference<AttrType>([], exampleFQName);

Calling the constructor function is equivalent to manually constructing an object
and setting the relevant properties:

let myReference: Morphir.IR.Value.Reference<AttrType> = {
kind: "Reference",
arg1: [],
arg2: exampleFQName,
}

Constructor functions are only provided for custom types.

#### Type variables

Morphir's custom types and type aliases can use type variables. These map to
TypeScript [generics](https://www.typescriptlang.org/docs/handbook/2/generics.html).

Here's an example using Morphir IR's `AccessControlled` type, which is a type
alias that maps to a Record.

import { Morphir } from './generated/Morphir'

const myAccess = new Morphir.IR.AccessControlled.Public();

let myAccessControlled: Morphir.IR.AccessControlled.AccessControlled<String> = {
access: myAccess,
value: "I'm a string",
}

## JSON serialization and deserialization

The generated TypeScript API includes `decode` and `encode` functions for each
type, used to serialize and deserialize instances of the types according to the
[standard Morphir JSON mapping](https://github.com/finos/morphir-elm/blob/master/docs/json-mapping.md).

With the generated Morphir.IR API, this allows you to read entire `morphir-ir.json` files
into your TypeScript program and create instances of the appropriate types. Here's how you
might do that:

import { Morphir } from './generated/Morphir'

function loadMorphirIR(text) {
let data = JSON.parse(text);

if (data['format-version'] != 2) {
throw "Unsupported morphir-ir.json format";
}

return Morphir.IR.Distribution.decodeDistribution(data['distribution']);
}
154 changes: 65 additions & 89 deletions redistributable/TypeScript/morphir/internal/Codecs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ type CodecMap = Map<string, CodecFunction>;
// * https://github.com/Microsoft/TypeScript/issues/3369
// * https://stackoverflow.com/a/53136686
//
export function buildCodecMap(entries: Array<[string, CodecFunction]>): CodecMap {
export function buildCodecMap(
entries: Array<[string, CodecFunction]>
): CodecMap {
return new Map(entries);
}

Expand Down Expand Up @@ -81,55 +83,6 @@ export function decodeFloat(input: any): number {
return input;
}

export function decodeCustomType(decoderMap: CodecMap, input: any): object {
if (typeof input == "string") input = [input];
if (!(input instanceof Array)) {
throw new DecodeError(`Expected Array, got ${typeof input}`);
}
if (!(typeof input[0] == "string")) {
throw new DecodeError(`Expected String, got ${typeof input}`);
}
if (!decoderMap.has(input[0])) {
let variantNames = Array.from(decoderMap.keys());
let variantNameString = variantNames.join(", ");
throw new DecodeError(
`Expected one of "${variantNameString}", got ${input[0]}`
);
}
return decoderMap.get(input[0])(input);
}

export function decodeCustomTypeVariant(
kind: string,
argNames: Array<string>,
argDecoders: CodecList,
input: any
): object {
if (typeof input == "string") input = [input];

if (input[0] != kind) {
throw new DecodeError(`Expected kind ${kind}, got ${input[0]}`);
}

const argCount = input.length - 1;
if (argCount != argDecoders.length) {
throw new DecodeError(
`Expected ${argDecoders.length} args for custom type "${kind}", got ${argCount}`
);
}

var result = {
kind: kind,
};

for (var i = 0; i < argDecoders.length; i++) {
var paramName = argNames[i];
result[paramName] = argDecoders[i](input[i + 1]);
}

return result;
}

export function decodeDict<K, V>(
decodeKey: (any) => K,
decodeValue: (any) => V,
Expand All @@ -141,14 +94,16 @@ export function decodeDict<K, V>(

const inputArray: Array<any> = input;

return new Map(inputArray.map((item: any) => {
if (!(item instanceof Array)) {
throw new DecodeError(`Expected array, got ${typeof item}`);
}
return new Map(
inputArray.map((item: any) => {
if (!(item instanceof Array)) {
throw new DecodeError(`Expected array, got ${typeof item}`);
}

const itemArray: Array<any> = item;
return [decodeKey(itemArray[0]), decodeValue(itemArray[1])];
}));
const itemArray: Array<any> = item;
return [decodeKey(itemArray[0]), decodeValue(itemArray[1])];
})
);
}

export function decodeList<T>(decodeElement: (any) => T, input: any): Array<T> {
Expand All @@ -160,7 +115,10 @@ export function decodeList<T>(decodeElement: (any) => T, input: any): Array<T> {
return inputArray.map(decodeElement);
}

export function decodeRecord(fieldDecoders: CodecMap, input: any): object {
export function decodeRecord<recordType>(
fieldDecoders: CodecMap,
input: any
): recordType {
if (!(input instanceof Object)) {
throw new DecodeError(`Expected Object, got ${typeof input}`);
}
Expand Down Expand Up @@ -188,14 +146,14 @@ export function decodeRecord(fieldDecoders: CodecMap, input: any): object {
}
result[name] = decoder(inputObject[name]);
});

// @ts-ignore
Comment thread
This conversation was marked as resolved.
return result;
}

export function decodeTuple(
export function decodeTuple<tupleType>(
elementDecoders: CodecList,
input: any
): Array<any> {
): tupleType {
if (!(input instanceof Array)) {
throw new DecodeError(`Expected Array, got ${typeof input}`);
}
Expand All @@ -205,6 +163,7 @@ export function decodeTuple(
for (var i = 0; i < inputArray.length; i++) {
result.push(elementDecoders[i](inputArray[i]));
}
// @ts-ignore
Comment thread
This conversation was marked as resolved.
return result;
}

Expand Down Expand Up @@ -232,34 +191,6 @@ export function encodeFloat(value: number): number {
return value;
}

export function encodeCustomType(encoderMap: CodecMap, value: any): any {
if (encoderMap.has(value["kind"])) {
const encoderFn: any = encoderMap.get(value["kind"]);
return encoderFn(value);
} else {
throw new DecodeError(
`Didn't find encoder for type variant: ${value["kind"]}`
);
}
}

export function encodeCustomTypeVariant(
argNames: Array<string>,
argEncoders: CodecList,
value: object
): Array<any> {
if (argNames.length == 0) {
return value["kind"];
} else {
var result = [value["kind"]];
for (var i = 0; i < argNames.length; i++) {
const name = argNames[i];
result.push(argEncoders[i](value[name]));
}
return result;
}
}

export function encodeDict<K, V>(
encodeKey: (any) => K,
encodeValue: (any) => V,
Expand Down Expand Up @@ -292,3 +223,48 @@ export function encodeTuple(
}
return result;
}

export function validateCustomTypeVariantInput(
kindString: String,
numArgs: number,
input: any
): void {
if (typeof input == "string") input = [input];
Comment thread
This conversation was marked as resolved.
if (!(input instanceof Array)) {
throw new DecodeError(`Expected Array, got ${typeof input}`);
}
if (!(input.length == numArgs + 1)) {
throw new DecodeError(
`Expected Array of length ${numArgs + 1}, got ${input.length}`
);
}
if (typeof input[0] != "string") {
throw new DecodeError(
`Expected first argument to be ${kindString}, got ${input[0]}`
);
}
if (input[0] != kindString) {
throw new DecodeError(`Expected kind ${kindString}, got ${input[0]}`);
}
}

export function parseKindFromCustomTypeInput(input: any): string {
if (typeof input == "string") input = [input];
if (!(input instanceof Array)) {
throw new DecodeError(`Expected Array, got ${typeof input}`);
}
if (!(typeof input[0] == "string")) {
throw new DecodeError(`Expected String, got ${typeof input}`);
}
return input[0];
}

export function raiseDecodeErrorFromCustomType(
customTypeName: string,
kind: string
): void {
throw new DecodeError(
`Error while attempting to decode an instance of ${customTypeName}.` +
` "${kind}" is not a valid 'kind' field for ${customTypeName}.`
);
}
Loading