diff --git a/docs/contribution-guide.md b/docs/contribution-guide.md index 18b7d98c0..762bf8205 100644 --- a/docs/contribution-guide.md +++ b/docs/contribution-guide.md @@ -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. @@ -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 diff --git a/docs/typescript.md b/docs/typescript.md new file mode 100644 index 000000000..795c61f7b --- /dev/null +++ b/docs/typescript.md @@ -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 + +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`. + +#### 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([], exampleFQName); + +Calling the constructor function is equivalent to manually constructing an object +and setting the relevant properties: + + let myReference: Morphir.IR.Value.Reference = { + 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 = { + 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']); + } diff --git a/redistributable/TypeScript/morphir/internal/Codecs.ts b/redistributable/TypeScript/morphir/internal/Codecs.ts index b7e6a031c..6eb405c0e 100644 --- a/redistributable/TypeScript/morphir/internal/Codecs.ts +++ b/redistributable/TypeScript/morphir/internal/Codecs.ts @@ -35,7 +35,9 @@ type CodecMap = Map; // * 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); } @@ -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, - 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( decodeKey: (any) => K, decodeValue: (any) => V, @@ -141,14 +94,16 @@ export function decodeDict( const inputArray: Array = 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 = item; - return [decodeKey(itemArray[0]), decodeValue(itemArray[1])]; - })); + const itemArray: Array = item; + return [decodeKey(itemArray[0]), decodeValue(itemArray[1])]; + }) + ); } export function decodeList(decodeElement: (any) => T, input: any): Array { @@ -160,7 +115,10 @@ export function decodeList(decodeElement: (any) => T, input: any): Array { return inputArray.map(decodeElement); } -export function decodeRecord(fieldDecoders: CodecMap, input: any): object { +export function decodeRecord( + fieldDecoders: CodecMap, + input: any +): recordType { if (!(input instanceof Object)) { throw new DecodeError(`Expected Object, got ${typeof input}`); } @@ -188,14 +146,14 @@ export function decodeRecord(fieldDecoders: CodecMap, input: any): object { } result[name] = decoder(inputObject[name]); }); - + // @ts-ignore return result; } -export function decodeTuple( +export function decodeTuple( elementDecoders: CodecList, input: any -): Array { +): tupleType { if (!(input instanceof Array)) { throw new DecodeError(`Expected Array, got ${typeof input}`); } @@ -205,6 +163,7 @@ export function decodeTuple( for (var i = 0; i < inputArray.length; i++) { result.push(elementDecoders[i](inputArray[i])); } + // @ts-ignore return result; } @@ -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, - argEncoders: CodecList, - value: object -): Array { - 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( encodeKey: (any) => K, encodeValue: (any) => V, @@ -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]; + 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}.` + ); +} diff --git a/src/Morphir/TypeScript/AST.elm b/src/Morphir/TypeScript/AST.elm index aa0c20f88..cd8e9a654 100644 --- a/src/Morphir/TypeScript/AST.elm +++ b/src/Morphir/TypeScript/AST.elm @@ -66,6 +66,11 @@ type Expression = ArrayLiteralExpression (List Expression) | Call CallExpression | Identifier String + | IntLiteralExpression Int + | IndexedExpression + { object : Expression + , index : Expression + } | MemberExpression { object : Expression , member : Expression @@ -108,6 +113,8 @@ parameter modifiers name typeAnnotation = type Statement = FunctionDeclaration { name : String + , typeVariables : List TypeExp + , returnType : Maybe TypeExp , scope : FunctionScope , parameters : List Parameter , body : List Statement @@ -117,6 +124,7 @@ type Statement | AssignmentStatement Expression (Maybe TypeExp) Expression | ExpressionStatement Expression | ReturnStatement Expression + | SwitchStatement Expression (List ( Expression, List Statement )) {-| Represents a type definition. @@ -164,6 +172,7 @@ Only a small subset of the type-system is currently implemented. type TypeExp = Any | Boolean + | FunctionTypeExp (List Parameter) TypeExp | List TypeExp {- Represents a Morphir 'List' type, as a Typescript 'Array' type -} | LiteralString String | Map TypeExp TypeExp diff --git a/src/Morphir/TypeScript/Backend/Types.elm b/src/Morphir/TypeScript/Backend/Types.elm index 4c57bba8b..45cf39ec2 100644 --- a/src/Morphir/TypeScript/Backend/Types.elm +++ b/src/Morphir/TypeScript/Backend/Types.elm @@ -28,6 +28,14 @@ type alias ConstructorDetail a = } +inputIndexArg : Int -> TS.Expression +inputIndexArg index = + TS.IndexedExpression + { object = TS.Identifier "input" + , index = TS.IntLiteralExpression index + } + + prependDecodeToName : Name -> String prependDecodeToName name = ("decode" :: name) |> Name.toCamelCase @@ -294,6 +302,20 @@ mapTypeExp tpe = TS.UnhandledType "Function" +genericDecoder : TS.TypeExp -> TS.TypeExp +genericDecoder typeExp = + TS.FunctionTypeExp + [ TS.Parameter [] "input" (Just TS.Any) ] + typeExp + + +genericEncoder : TS.TypeExp -> TS.TypeExp +genericEncoder typeExp = + TS.FunctionTypeExp + [ TS.Parameter [] "value" (Just typeExp) ] + TS.Any + + {-| Reference a symbol in the Morphir.Internal.Codecs module. -} codecsModule : String -> TS.Expression @@ -320,12 +342,8 @@ buildCodecMap array = } -decoderExpression : TypeVariablesList -> Type.Type a -> TS.CallExpression -decoderExpression customTypeVars typeExp = - let - inputArg = - TS.Identifier "input" - in +decoderExpression : TypeVariablesList -> Type.Type a -> TS.Expression -> TS.CallExpression +decoderExpression customTypeVars typeExp inputArg = case typeExp of Type.Reference _ ( [ [ "morphir" ], [ "s", "d", "k" ] ], [ [ "basics" ] ], [ "bool" ] ) [] -> { function = codecsModule "decodeBoolean", arguments = [ inputArg ] } @@ -441,7 +459,7 @@ specificDecoderForType : TypeVariablesList -> Type.Type ta -> TS.Expression specificDecoderForType customTypeVars typeExp = let expression = - decoderExpression customTypeVars typeExp + decoderExpression customTypeVars typeExp (TS.Identifier "input") removeInputArg arguments = arguments |> List.take (List.length arguments - 1) @@ -452,24 +470,33 @@ specificDecoderForType customTypeVars typeExp = generateDecoderFunction : TypeVariablesList -> Name -> Access -> Type.Type ta -> TS.Statement generateDecoderFunction variables typeName access typeExp = let + variableTypeExpressions : List TS.TypeExp + variableTypeExpressions = + variables |> List.map Name.toTitleCase |> List.map (\var -> TS.Variable var) + call : TS.CallExpression call = - decoderExpression variables typeExp + decoderExpression variables typeExp (TS.Identifier "input") variableParams : List TS.Parameter variableParams = variables |> List.map (\var -> - TS.parameter [] (prependDecodeToName var) Nothing + TS.parameter + [] + (prependDecodeToName var) + (Just (genericDecoder (TS.Variable (Name.toTitleCase var)))) ) inputParam : TS.Parameter inputParam = - TS.parameter [] "input" Nothing + TS.parameter [] "input" (Just TS.Any) in TS.FunctionDeclaration { name = prependDecodeToName typeName + , typeVariables = variableTypeExpressions + , returnType = Just (TS.TypeRef ( [], [], typeName ) variableTypeExpressions) , scope = TS.ModuleFunction , parameters = variableParams ++ [ inputParam ] , privacy = access |> mapPrivacy @@ -480,115 +507,149 @@ generateDecoderFunction variables typeName access typeExp = generateConstructorDecoderFunction : ConstructorDetail ta -> TS.Statement generateConstructorDecoderFunction constructor = let + variableTypeExpressions : List TS.TypeExp + variableTypeExpressions = + constructor.typeVariableNames |> List.map Name.toTitleCase |> List.map (\var -> TS.Variable var) + decoderParams : List TS.Parameter decoderParams = constructor.typeVariableNames |> List.map (\var -> - TS.parameter [] (prependDecodeToName var) Nothing + TS.parameter + [] + (prependDecodeToName var) + (Just (genericDecoder (TS.Variable (Name.toTitleCase var)))) ) inputParam : TS.Parameter inputParam = - TS.parameter [] "input" Nothing + TS.parameter [] "input" (Just TS.Any) kind = TS.StringLiteralExpression (constructor.name |> Name.toTitleCase) - argNames = - TS.ArrayLiteralExpression - (constructor.args - |> List.map (Tuple.first >> Name.toCamelCase >> TS.StringLiteralExpression) - ) - - argDecoders = - TS.ArrayLiteralExpression - (constructor.args - |> List.map Tuple.second - |> List.map (specificDecoderForType constructor.typeVariableNames) - ) - - input = - TS.Identifier "input" - - call : TS.Expression - call = + validateCall : TS.Expression + validateCall = TS.Call - { function = codecsModule "decodeCustomTypeVariant" + { function = codecsModule "validateCustomTypeVariantInput" , arguments = [ kind - , argNames - , argDecoders - , input + , constructor.args |> List.length |> TS.IntLiteralExpression + , TS.Identifier "input" ] } + + argDecoderCalls : List TS.Expression + argDecoderCalls = + constructor.args + |> List.map Tuple.second + |> List.indexedMap + (\index -> + \typExp -> + TS.Call + (decoderExpression + constructor.typeVariableNames + typExp + (inputIndexArg (index + 1)) + ) + ) + + newCall : TS.Expression + newCall = + TS.NewExpression + { constructor = constructor.name |> Name.toTitleCase + , arguments = argDecoderCalls + } in TS.FunctionDeclaration { name = prependDecodeToName constructor.name + , typeVariables = variableTypeExpressions + , returnType = Just (TS.TypeRef ( [], [], constructor.name ) variableTypeExpressions) , scope = TS.ModuleFunction , privacy = constructor.privacy , parameters = decoderParams ++ [ inputParam ] - , body = [ TS.ReturnStatement call ] + , body = + [ TS.ExpressionStatement validateCall + , TS.ReturnStatement newCall + ] } generateUnionDecoderFunction : Name -> TS.Privacy -> List Name -> List (ConstructorDetail ta) -> TS.Statement generateUnionDecoderFunction typeName privacy typeVariables constructors = let + variableTypeExpressions : List TS.TypeExp + variableTypeExpressions = + typeVariables |> List.map Name.toTitleCase |> List.map (\var -> TS.Variable var) + decoderParams : List TS.Parameter decoderParams = typeVariables |> List.map (\var -> - TS.parameter [] (prependDecodeToName var) Nothing + TS.parameter + [] + (prependDecodeToName var) + (Just (genericDecoder (TS.Variable (Name.toTitleCase var)))) ) inputParam : TS.Parameter inputParam = - TS.parameter [] "input" Nothing - - getCodecMapEntry : ConstructorDetail ta -> TS.Expression - getCodecMapEntry constructor = - TS.ArrayLiteralExpression - [ TS.StringLiteralExpression (constructor.name |> Name.toTitleCase) - , bindArgumentsToFunction - (constructor.name |> prependDecodeToName |> TS.Identifier) - (constructor.typeVariableNames |> List.map (prependDecodeToName >> TS.Identifier)) - ] + TS.parameter [] "input" (Just TS.Any) - codecMap : TS.Expression - codecMap = - constructors |> List.map getCodecMapEntry |> TS.ArrayLiteralExpression |> buildCodecMap + kindCall : TS.Statement + kindCall = + TS.LetStatement + (TS.Identifier "kind") + Nothing + (TS.Call + { function = codecsModule "parseKindFromCustomTypeInput" + , arguments = [ inputArg ] + } + ) - call : TS.Expression - call = - TS.Call - { function = - TS.MemberExpression - { object = TS.Identifier "codecs" - , member = TS.Identifier "decodeCustomType" - } + errorCall : TS.Statement + errorCall = + (TS.Call >> TS.ExpressionStatement) + { function = codecsModule "raiseDecodeErrorFromCustomType" , arguments = - [ codecMap - , TS.Identifier "input" + [ TS.StringLiteralExpression (typeName |> Name.toTitleCase) + , TS.Identifier "kind" ] } + + constructorToCase : ConstructorDetail ta -> ( TS.Expression, List TS.Statement ) + constructorToCase constructor = + ( constructor.name |> Name.toTitleCase |> TS.StringLiteralExpression + , [ TS.ReturnStatement + (TS.Call + { function = constructor.name |> prependDecodeToName |> TS.Identifier + , arguments = (constructor.typeVariableNames |> List.map (prependDecodeToName >> TS.Identifier)) ++ [ inputArg ] + } + ) + ] + ) + + switchStatement : TS.Statement + switchStatement = + TS.SwitchStatement + (TS.Identifier "kind") + (constructors |> List.map constructorToCase) in TS.FunctionDeclaration { name = prependDecodeToName typeName + , typeVariables = variableTypeExpressions + , returnType = Just (TS.TypeRef ( [], [], typeName ) variableTypeExpressions) , scope = TS.ModuleFunction , privacy = privacy , parameters = decoderParams ++ [ inputParam ] - , body = [ TS.ReturnStatement call ] + , body = [ kindCall, switchStatement, errorCall ] } -encoderExpression : TypeVariablesList -> Type.Type a -> TS.CallExpression -encoderExpression customTypeVars typeExp = - let - valueArg = - TS.Identifier "value" - in +encoderExpression : TypeVariablesList -> Type.Type a -> TS.Expression -> TS.CallExpression +encoderExpression customTypeVars typeExp valueArg = case typeExp of Type.Reference _ ( [ [ "morphir" ], [ "s", "d", "k" ] ], [ [ "basics" ] ], [ "bool" ] ) [] -> { function = codecsModule "encodeBoolean", arguments = [ valueArg ] } @@ -688,7 +749,7 @@ specificEncoderForType : TypeVariablesList -> Type.Type ta -> TS.Expression specificEncoderForType customTypeVars typeExp = let expression = - encoderExpression customTypeVars typeExp + encoderExpression customTypeVars typeExp (TS.Identifier "value") removeValueArg arguments = arguments |> List.take (List.length arguments - 1) @@ -699,23 +760,32 @@ specificEncoderForType customTypeVars typeExp = generateEncoderFunction : TypeVariablesList -> Name -> Access -> Type.Type ta -> TS.Statement generateEncoderFunction variables typeName access typeExp = let + variableTypeExpressions : List TS.TypeExp + variableTypeExpressions = + variables |> List.map Name.toTitleCase |> List.map (\var -> TS.Variable var) + call = - encoderExpression variables typeExp + encoderExpression variables typeExp (TS.Identifier "value") variableParams : List TS.Parameter variableParams = variables |> List.map (\var -> - TS.parameter [] (prependEncodeToName var) Nothing + TS.parameter + [] + (prependEncodeToName var) + (Just (genericEncoder (TS.Variable (Name.toTitleCase var)))) ) valueParam : TS.Parameter valueParam = - TS.parameter [] "value" Nothing + TS.parameter [] "value" (Just (TS.TypeRef ( [], [], typeName ) variableTypeExpressions)) in TS.FunctionDeclaration { name = prependEncodeToName typeName + , typeVariables = variableTypeExpressions + , returnType = Just TS.Any , scope = TS.ModuleFunction , parameters = variableParams ++ [ valueParam ] , privacy = access |> mapPrivacy @@ -726,102 +796,112 @@ generateEncoderFunction variables typeName access typeExp = generateConstructorEncoderFunction : ConstructorDetail ta -> TS.Statement generateConstructorEncoderFunction constructor = let + variableTypeExpressions : List TS.TypeExp + variableTypeExpressions = + constructor.typeVariableNames |> List.map Name.toTitleCase |> List.map (\var -> TS.Variable var) + encoderParams : List TS.Parameter encoderParams = constructor.typeVariableNames |> List.map (\var -> - TS.parameter [] (prependEncodeToName var) Nothing + TS.parameter + [] + (prependEncodeToName var) + (Just (genericEncoder (TS.Variable (Name.toTitleCase var)))) ) valueParam : TS.Parameter valueParam = - TS.parameter [] "value" Nothing + TS.parameter [] "value" (Just (TS.TypeRef ( [], [], constructor.name ) variableTypeExpressions)) - argNames = - TS.ArrayLiteralExpression - (constructor.args - |> List.map (Tuple.first >> Name.toCamelCase >> TS.StringLiteralExpression) + argToEncoderCall : ( Name, Type a ) -> TS.Expression + argToEncoderCall ( argName, argType ) = + TS.Call + (encoderExpression + constructor.typeVariableNames + argType + (TS.MemberExpression + { object = TS.Identifier "value" + , member = argName |> Name.toCamelCase |> TS.Identifier + } + ) ) - argEncoders = - TS.ArrayLiteralExpression - (constructor.args - |> List.map Tuple.second - |> List.map (specificEncoderForType constructor.typeVariableNames) - ) + kindExpression : TS.Expression + kindExpression = + TS.MemberExpression { object = TS.Identifier "value", member = TS.Identifier "kind" } - value = - TS.Identifier "value" + returnList : TS.Expression + returnList = + if (constructor.args |> List.length) == 0 then + kindExpression - call : TS.Expression - call = - TS.Call - { function = codecsModule "encodeCustomTypeVariant" - , arguments = - [ argNames - , argEncoders - , value - ] - } + else + TS.ArrayLiteralExpression + (kindExpression + :: (constructor.args |> List.map argToEncoderCall) + ) in TS.FunctionDeclaration { name = prependEncodeToName constructor.name + , typeVariables = variableTypeExpressions + , returnType = Just TS.Any , scope = TS.ModuleFunction , privacy = constructor.privacy , parameters = encoderParams ++ [ valueParam ] - , body = [ TS.ReturnStatement call ] + , body = [ TS.ReturnStatement returnList ] } generateUnionEncoderFunction : Name -> TS.Privacy -> List Name -> List (ConstructorDetail ta) -> TS.Statement generateUnionEncoderFunction typeName privacy typeVariables constructors = let + variableTypeExpressions : List TS.TypeExp + variableTypeExpressions = + typeVariables |> List.map Name.toTitleCase |> List.map (\var -> TS.Variable var) + encoderParams : List TS.Parameter encoderParams = typeVariables |> List.map (\var -> - TS.parameter [] (prependEncodeToName var) Nothing + TS.parameter + [] + (prependEncodeToName var) + (Just (genericEncoder (TS.Variable (Name.toTitleCase var)))) ) valueParam : TS.Parameter valueParam = - TS.parameter [] "value" Nothing - - getCodecMapEntry : ConstructorDetail ta -> TS.Expression - getCodecMapEntry constructor = - TS.ArrayLiteralExpression - [ TS.StringLiteralExpression (constructor.name |> Name.toTitleCase) - , bindArgumentsToFunction - (constructor.name |> prependEncodeToName |> TS.Identifier) - (constructor.typeVariableNames |> List.map (prependEncodeToName >> TS.Identifier)) - ] - - codecMap : TS.Expression - codecMap = - constructors |> List.map getCodecMapEntry |> TS.ArrayLiteralExpression |> buildCodecMap - - call : TS.Expression - call = - TS.Call - { function = - TS.MemberExpression - { object = TS.Identifier "codecs" - , member = TS.Identifier "encodeCustomType" + TS.parameter [] "value" (Just (TS.TypeRef ( [], [], typeName ) variableTypeExpressions)) + + constructorToCase : ConstructorDetail ta -> ( TS.Expression, List TS.Statement ) + constructorToCase constructor = + ( constructor.name |> Name.toTitleCase |> TS.StringLiteralExpression + , [ TS.ReturnStatement + (TS.Call + { function = constructor.name |> prependEncodeToName |> TS.Identifier + , arguments = (constructor.typeVariableNames |> List.map (prependEncodeToName >> TS.Identifier)) ++ [ TS.Identifier "value" ] } - , arguments = - [ codecMap - , TS.Identifier "value" - ] - } + ) + ] + ) + + switchStatement : TS.Statement + switchStatement = + TS.SwitchStatement + (TS.MemberExpression { object = TS.Identifier "value", member = TS.Identifier "kind" }) + (constructors |> List.map constructorToCase) in TS.FunctionDeclaration { name = prependEncodeToName typeName + , typeVariables = variableTypeExpressions + , returnType = Just TS.Any , scope = TS.ModuleFunction , privacy = privacy , parameters = encoderParams ++ [ valueParam ] - , body = [ TS.ReturnStatement call ] + , body = [ switchStatement ] } @@ -838,6 +918,8 @@ generateConstructorConstructorFunction { name, privacy, args, typeVariables, typ in TS.FunctionDeclaration { name = "constructor" + , typeVariables = [] + , returnType = Nothing , scope = TS.ClassMemberFunction , privacy = privacy , parameters = argParams diff --git a/src/Morphir/TypeScript/PrettyPrinter.elm b/src/Morphir/TypeScript/PrettyPrinter.elm index 9308ac077..9110f95f5 100644 --- a/src/Morphir/TypeScript/PrettyPrinter.elm +++ b/src/Morphir/TypeScript/PrettyPrinter.elm @@ -7,6 +7,7 @@ representation. -} +import Elm.Syntax.Expression exposing (Expression(..)) import Morphir.File.SourceCode exposing (Doc, concat, indentLines, newLine) import Morphir.IR.Path exposing (Path) import Morphir.TypeScript.AST exposing (CompilationUnit, Expression(..), FunctionScope(..), ImportDeclaration, NamespacePath, Parameter, Privacy(..), Statement(..), TypeDef(..), TypeExp(..)) @@ -168,6 +169,17 @@ mapExpression expression = Identifier name -> name + IntLiteralExpression num -> + String.fromInt num + + IndexedExpression { object, index } -> + concat + [ mapExpression object + , "[" + , mapExpression index + , "]" + ] + MemberExpression { object, member } -> concat [ mapExpression object @@ -224,7 +236,7 @@ mapMaybeStatement maybeStatement = mapStatement : Statement -> String mapStatement statement = case statement of - FunctionDeclaration { name, scope, parameters, body, privacy } -> + FunctionDeclaration { name, typeVariables, returnType, scope, parameters, body, privacy } -> let prefaceKeywords : String prefaceKeywords = @@ -242,13 +254,38 @@ mapStatement statement = _ -> "" + + typeVariablesString : String + typeVariablesString = + case typeVariables of + [] -> + "" + + _ -> + concat + [ "<" + , String.join ", " (typeVariables |> List.map mapTypeExp) + , ">" + ] + + returnTypeExpression : String + returnTypeExpression = + case returnType of + Nothing -> + "" + + Just typeExp -> + concat [ ": ", mapTypeExp typeExp ] in concat [ prefaceKeywords , name + , typeVariablesString , "(" , String.join ", " (parameters |> List.map mapParameter) - , ") {" + , ")" + , returnTypeExpression + , " {" , newLine , body |> List.map mapStatement |> indentLines defaultIndent , newLine @@ -280,22 +317,23 @@ mapStatement statement = ExpressionStatement expression -> concat [ mapExpression expression, ";" ] - -mapParameter : Parameter -> String -mapParameter { modifiers, name, typeAnnotation } = - concat - [ modifiers |> String.join " " - , " " - , name - , mapMaybeAnnotation typeAnnotation - ] - - -mapMaybeAnnotation : Maybe TypeExp -> String -mapMaybeAnnotation maybeTypeExp = - case maybeTypeExp of - Nothing -> - "" - - Just typeExp -> - ": " ++ mapTypeExp typeExp + SwitchStatement condition cases -> + let + mapCase : ( Expression, List Statement ) -> String + mapCase ( caseExpr, statementList ) = + concat + [ "case " + , mapExpression caseExpr + , ":" + , statementList |> List.map mapStatement |> indentLines defaultIndent + ] + in + concat + [ "switch (" + , mapExpression condition + , ") {" + , newLine + , cases |> List.map mapCase |> indentLines defaultIndent + , newLine + , "}" + ] diff --git a/src/Morphir/TypeScript/PrettyPrinter/Expressions.elm b/src/Morphir/TypeScript/PrettyPrinter/Expressions.elm index b7cad2858..f0a30a992 100644 --- a/src/Morphir/TypeScript/PrettyPrinter/Expressions.elm +++ b/src/Morphir/TypeScript/PrettyPrinter/Expressions.elm @@ -1,9 +1,9 @@ -module Morphir.TypeScript.PrettyPrinter.Expressions exposing (mapField, mapGenericVariables, mapObjectExp, mapTypeExp, namespaceNameFromPackageAndModule) +module Morphir.TypeScript.PrettyPrinter.Expressions exposing (..) import Morphir.File.SourceCode exposing (Doc, concat, indentLines, newLine) import Morphir.IR.Name as Name import Morphir.IR.Path exposing (Path) -import Morphir.TypeScript.AST exposing (ObjectExp, Privacy(..), TypeDef(..), TypeExp(..), namespaceNameFromPackageAndModule) +import Morphir.TypeScript.AST exposing (ObjectExp, Parameter, Privacy(..), TypeDef(..), TypeExp(..), namespaceNameFromPackageAndModule) defaultIndent = @@ -24,6 +24,26 @@ mapGenericVariables variables = ] +mapParameter : Parameter -> String +mapParameter { modifiers, name, typeAnnotation } = + concat + [ modifiers |> String.join " " + , " " + , name + , mapMaybeAnnotation typeAnnotation + ] + + +mapMaybeAnnotation : Maybe TypeExp -> String +mapMaybeAnnotation maybeTypeExp = + case maybeTypeExp of + Nothing -> + "" + + Just typeExp -> + ": " ++ mapTypeExp typeExp + + {-| Map a field to text (from an object or interface) -} mapField : ( String, TypeExp ) -> Doc @@ -57,6 +77,14 @@ mapTypeExp typeExp = Boolean -> "boolean" + FunctionTypeExp params rtnTypeExp -> + concat + [ "(" + , params |> List.map mapParameter |> String.join ", " + , ") => " + , mapTypeExp rtnTypeExp + ] + List listType -> "Array<" ++ mapTypeExp listType ++ ">" diff --git a/src/Morphir/Visual/Components/MultiaryDecisionTree.elm b/src/Morphir/Visual/Components/MultiaryDecisionTree.elm new file mode 100644 index 000000000..4dc73348a --- /dev/null +++ b/src/Morphir/Visual/Components/MultiaryDecisionTree.elm @@ -0,0 +1,32 @@ +module Morphir.Visual.Components.MultiaryDecisionTree exposing (..) + +import Morphir.IR.Type as Type +import Morphir.IR.Value as Value exposing (Pattern, RawValue, Value) +import Morphir.Visual.VisualTypedValue exposing (VisualTypedValue) + + +type Node + = Branch BranchNode + | Leaf VisualTypedValue + + +type alias BranchNode = + { subject : VisualTypedValue + , subjectEvaluationResult : Maybe RawValue + , branches : List ( Pattern (), Node ) + } + + +{-| Sample data structure. Should be moved into a test module. +-} +exampleTree : Node +exampleTree = + Branch + { subject = Value.Variable ( 0, Type.Unit () ) [ "foo" ] + , subjectEvaluationResult = Nothing + , branches = + [ ( Value.ConstructorPattern () ( [], [], [ "yes" ] ) [], Leaf (Value.Variable ( 0, Type.Unit () ) [ "foo" ]) ) + , ( Value.WildcardPattern (), Leaf (Value.Variable ( 0, Type.Unit () ) [ "foo" ]) ) + , ( Value.WildcardPattern (), Leaf (Value.Variable ( 0, Type.Unit () ) [ "foo" ]) ) + ] + }