diff --git a/language-support/ts/codegen/src/TsCodeGenMain.hs b/language-support/ts/codegen/src/TsCodeGenMain.hs index 15cabfee9b9..32f769cff77 100644 --- a/language-support/ts/codegen/src/TsCodeGenMain.hs +++ b/language-support/ts/codegen/src/TsCodeGenMain.hs @@ -344,8 +344,10 @@ data TemplateDef = TemplateDef , tplPkgId :: PackageId , tplModule :: ModuleName , tplDecoder :: Decoder + , tplEncode :: Encode , tplKeyDecoder :: Maybe Decoder -- ^ Nothing if we do not have a key. + , tplKeyEncode :: Encode , tplChoices' :: [ChoiceDef] } @@ -355,14 +357,18 @@ renderTemplateDef TemplateDef{..} = [ [ "exports." <> tplName <> " = {" , " templateId: '" <> templateId <> "'," , " keyDecoder: " <> renderDecoder (DecoderLazy keyDec) <> "," + , " keyEncode: " <> renderEncode tplKeyEncode <> "," , " decoder: " <> renderDecoder (DecoderLazy tplDecoder) <> "," + , " encode: " <> renderEncode tplEncode <> "," ] , concat [ [ " " <> chcName' <> ": {" , " template: function () { return exports." <> tplName <> "; }," , " choiceName: '" <> chcName' <> "'," , " argumentDecoder: " <> renderDecoder (DecoderLazy (DecoderRef chcArgTy)) <> "," + , " argumentEncode: " <> renderEncode (EncodeRef chcArgTy) <> "," , " resultDecoder: " <> renderDecoder (DecoderLazy (DecoderRef chcRetTy)) <> "," + , " resultEncode: " <> renderEncode (EncodeRef chcRetTy) <> "," , " }," ] | ChoiceDef{..} <- tplChoices' @@ -403,10 +409,17 @@ data SerializableDef = SerializableDef -- ^ Keys for enums. Note that enums never have type parameters -- but for simplicity we do not express this in this type. , serDecoder :: Decoder - , serNestedDecoders :: [(T.Text, Decoder)] + , serEncode :: Encode + , serNested :: [NestedSerializable] -- ^ For sums of products, e.g., `data X = Y { a : Int } } +data NestedSerializable = NestedSerializable + { field :: T.Text + , decoder :: Decoder + , encode :: Encode + } + renderSerializableDef :: SerializableDef -> (T.Text, T.Text) renderSerializableDef SerializableDef{..} | null serParams = @@ -414,7 +427,7 @@ renderSerializableDef SerializableDef{..} [ [ "export declare const " <> serName <> ":" , " damlTypes.Serializable<" <> serName <> "> & {" ] - , [ " " <> n <> ": damlTypes.Serializable<" <> serName <.> n <> ">;" | (n, _) <- serNestedDecoders ] + , [ " " <> field <> ": damlTypes.Serializable<" <> serName <.> field <> ">;" | NestedSerializable { field } <- serNested ] , [ " }" ] , [ "& { readonly keys: " <> serName <> "[] } & { readonly [e in " <> serName <> "]: e }" | notNull serKeys ] @@ -424,14 +437,16 @@ renderSerializableDef SerializableDef{..} [ ["exports." <> serName <> " = {"] , [ " " <> k <> ": " <> "'" <> k <> "'," | k <- serKeys ] , [ " keys: [" <> T.concat (map (\s -> "'" <> s <> "',") serKeys) <> "]," | notNull serKeys ] - , [ " decoder: " <> renderDecoder (DecoderLazy serDecoder) <> "," + , [ " decoder: " <> renderDecoder (DecoderLazy serDecoder) <> ",", + " encode: " <> renderEncode serEncode <> "," ] , concat $ - [ [ " " <> n <> ":({" - , " decoder: " <> renderDecoder (DecoderLazy d) <> "," + [ [ " " <> field <> ":({" + , " decoder: " <> renderDecoder (DecoderLazy decoder) <> "," + , " encode: " <> renderEncode encode <> "," , " })," ] - | (n, d) <- serNestedDecoders + | NestedSerializable { field, decoder, encode } <- serNested ] , [ "};" ] ] @@ -444,8 +459,8 @@ renderSerializableDef SerializableDef{..} [ "export declare const " <> serName <> " :" , " (" <> tyArgs <> " => damlTypes.Serializable<" <> serName <> tyParams <> ">) & {" ] ++ - [ " " <> n <> ": (" <> tyArgs <> " => damlTypes.Serializable<" <> serName <.> n <> tyParams <> ">);" - | (n, _) <- serNestedDecoders + [ " " <> field <> ": (" <> tyArgs <> " => damlTypes.Serializable<" <> serName <.> field <> tyParams <> ">);" + | NestedSerializable { field } <- serNested ] ++ [ "};" ] @@ -455,13 +470,15 @@ renderSerializableDef SerializableDef{..} -- for each nested decoder. [ "exports" <.> serName <> " = function " <> jsTyArgs <> " { return ({" , " decoder: " <> renderDecoder (DecoderLazy serDecoder) <> "," + , " encode: " <> renderEncode serEncode <> "," , "}); };" ] <> concat - [ [ "exports" <.> serName <.> n <> " = function " <> jsTyArgs <> " { return ({" - , " decoder: " <> renderDecoder (DecoderLazy d) <> "," + [ [ "exports" <.> serName <.> field <> " = function " <> jsTyArgs <> " { return ({" + , " decoder: " <> renderDecoder (DecoderLazy decoder) <> "," + , " encode: " <> renderEncode encode <> "," , "}); };" ] - | (n, d) <- serNestedDecoders + | NestedSerializable { field, encode, decoder } <- serNested ] in (jsSource, tsDecl) where tyParams = "<" <> T.intercalate ", " serParams <> ">" @@ -474,7 +491,7 @@ data TypeRef = TypeRef } data Decoder - = DecoderOneOf T.Text [Decoder] + = DecoderOneOf [Decoder] | DecoderObject [(T.Text, Decoder)] | DecoderConstant DecoderConstant | DecoderRef TypeRef -- ^ Reference to an object with a .decoder field @@ -495,7 +512,7 @@ renderDecoderConstant = \case renderDecoder :: Decoder -> T.Text renderDecoder = \case - DecoderOneOf _constr branches -> + DecoderOneOf branches -> "jtv.oneOf(" <> T.intercalate ", " (map renderDecoder branches) <> ")" @@ -507,6 +524,35 @@ renderDecoder = \case DecoderRef t -> snd (genType t) <> ".decoder" DecoderLazy d -> "damlTypes.lazyMemo(function () { return " <> renderDecoder d <> "; })" +data Encode + = EncodeRef TypeRef + | EncodeVariant T.Text [(T.Text, TypeRef)] + | EncodeAsIs + | EncodeRecord [(T.Text, TypeRef)] + | EncodeThrow + +renderEncode :: Encode -> T.Text +renderEncode = \case + EncodeRef ref -> let (_, companion) = genType ref in + "function (__typed__) { return " <> companion <> ".encode(__typed__); }" + EncodeVariant typ alts -> T.unlines $ concat + [ [ "function (__typed__) {" -- Note: switch uses === + , " switch(__typed__.tag) {" ] + , [ " case '" <> name <> "': return {tag: __typed__.tag, value: " <> companion <> ".encode(__typed__.value)};" + | (name, tr) <- alts, let (_, companion) = genType tr ] + , [ " default: throw 'unrecognized type tag: ' + __typed__.tag + ' while serializing a value of type " <> typ <> "';" + , " }" + , "}" ] ] + EncodeAsIs -> "function (__typed__) { return __typed__; }" + EncodeRecord fields -> T.unlines $ concat + [ [ "function (__typed__) {" + , " return {" ] + , [ " " <> name <> ": " <> companion <> ".encode(__typed__." <> name <> ")," + | (name, tr) <- fields, let (_, companion) = genType tr ] + , [ " };" + , "}" ] ] + EncodeThrow -> "function () { throw 'EncodeError'; }" + data TypeDef = UnionDef T.Text [T.Text] [(T.Text, TypeRef)] | ObjectDef T.Text [T.Text] [(T.Text, TypeRef)] @@ -540,14 +586,21 @@ genSerializableDef :: PackageId -> T.Text -> Module -> DefDataType -> Serializab genSerializableDef curPkgId conName mod def = case dataCons def of DataVariant bs -> - let typ = conName <> typeParams - in SerializableDef + SerializableDef { serName = conName , serParams = paramNames , serKeys = [] - , serDecoder = DecoderOneOf typ (map genBranch bs) - , serNestedDecoders = - [ (name, serDecoder (genSerializableDef curPkgId (conName <.> name) mod b)) | (name, b) <- nestedDefDataTypes ] + , serDecoder = DecoderOneOf (map genDecBranch bs) + , serEncode = EncodeVariant conName (map genEncBranch bs) + , serNested = + [ NestedSerializable + { field = name + , decoder = serDecoder nested + , encode = serEncode nested + } + | (name, b) <- nestedDefDataTypes + , let nested = genSerializableDef curPkgId (conName <.> name) mod b + ] } DataEnum enumCons -> let cs = map unVariantConName enumCons @@ -555,29 +608,30 @@ genSerializableDef curPkgId conName mod def = { serName = conName , serParams = [] , serKeys = cs - , serDecoder = DecoderOneOf conName [DecoderConstant (ConstantRef ("exports" <.> conName <.> cons)) | cons <- cs] - , serNestedDecoders = [] + , serDecoder = DecoderOneOf [DecoderConstant (ConstantRef ("exports" <.> conName <.> cons)) | cons <- cs] + , serEncode = EncodeAsIs + , serNested = [] } DataRecord fields -> let (fieldNames, fieldTypesLf) = unzip [(unFieldName x, t) | (x, t) <- fields] - fieldSers = map (\t -> TypeRef (moduleName mod) t) fieldTypesLf + fieldTypes = map (\t -> TypeRef (moduleName mod) t) fieldTypesLf in SerializableDef { serName = conName , serParams = paramNames , serKeys = [] - , serDecoder = DecoderObject [(x, DecoderRef ser) | (x, ser) <- zip fieldNames fieldSers] - , serNestedDecoders = [] + , serDecoder = DecoderObject [(x, DecoderRef ser) | (x, ser) <- zip fieldNames fieldTypes] + , serEncode = EncodeRecord $ zip fieldNames fieldTypes + , serNested = [] } where paramNames = map (unTypeVarName . fst) (dataParams def) - typeParams - | null paramNames = "" - | otherwise = "<" <> T.intercalate ", " paramNames <> ">" - genBranch (VariantConName cons, t) = + genDecBranch (VariantConName cons, t) = DecoderObject [ ("tag", DecoderConstant (ConstantString cons)) , ("value", DecoderRef $ TypeRef (moduleName mod) t) ] + genEncBranch (VariantConName cons, t) = + (cons, TypeRef (moduleName mod) t) nestedDefDataTypes = [ (sub, def) | def <- defDataTypes mod @@ -623,7 +677,7 @@ genDefDataType curPkgId conName mod tpls def = ([DeclTypeDef typeDesc, DeclSerializableDef serDesc], Set.empty) DataRecord fields -> let (fieldNames, fieldTypesLf) = unzip [(unFieldName x, t) | (x, t) <- fields] - fieldSers = map (TypeRef (moduleName mod)) fieldTypesLf + fieldTypes = map (TypeRef (moduleName mod)) fieldTypesLf fieldRefs = map (Set.setOf typeModuleRef . snd) fields typeDesc = genTypeDef conName mod def in @@ -638,18 +692,23 @@ genDefDataType curPkgId conName mod tpls def = , let argRefs = Set.setOf typeModuleRef (refType argTy) , let retRefs = Set.setOf typeModuleRef (refType rTy) ] - (keyDecoder, keyRefs) = case tplKey tpl of - Nothing -> (Nothing, Set.empty) + (keyDecoder, keyEncode, keyRefs) = case tplKey tpl of + Nothing -> (Nothing, EncodeThrow, Set.empty) Just key -> let keyType = tplKeyType key + typeRef = TypeRef (moduleName mod) keyType in - (Just (DecoderRef $ TypeRef (moduleName mod) keyType), Set.setOf typeModuleRef keyType) + ( Just $ DecoderRef typeRef + , EncodeRef typeRef + , Set.setOf typeModuleRef keyType) dict = TemplateDef { tplName = conName , tplPkgId = curPkgId , tplModule = moduleName mod - , tplDecoder = DecoderObject [(x, DecoderRef ser) | (x, ser) <- zip fieldNames fieldSers] + , tplDecoder = DecoderObject [(x, DecoderRef ser) | (x, ser) <- zip fieldNames fieldTypes] + , tplEncode = EncodeRecord $ zip fieldNames fieldTypes , tplKeyDecoder = keyDecoder + , tplKeyEncode = keyEncode , tplChoices' = chcs } associatedTypes = TemplateNamespace @@ -666,7 +725,7 @@ infixr 6 <.> -- This is the same fixity as '<>'. (<.>) u v = u <> "." <> v -- | Returns a pair of the type and a reference to the --- serializer object. +-- companion object/function. genType :: TypeRef -> (T.Text, T.Text) genType (TypeRef curModName t) = go t where diff --git a/language-support/ts/daml-ledger/index.test.ts b/language-support/ts/daml-ledger/index.test.ts index 51c881d99d7..0cef5126fea 100644 --- a/language-support/ts/daml-ledger/index.test.ts +++ b/language-support/ts/daml-ledger/index.test.ts @@ -74,7 +74,9 @@ jest.mock('isomorphic-ws', () => class { const Foo: Template = { templateId: "foo-id", keyDecoder: jtv.string(), + keyEncode: (s: string): unknown => s, decoder: jtv.object({key: jtv.string()}), + encode: (o) => o, Archive: {} as unknown as Choice, }; diff --git a/language-support/ts/daml-ledger/index.ts b/language-support/ts/daml-ledger/index.ts index 8f865f76f51..a7d5fc85181 100644 --- a/language-support/ts/daml-ledger/index.ts +++ b/language-support/ts/daml-ledger/index.ts @@ -150,6 +150,16 @@ export function assert(b: boolean, m: string): void { export type Query = T extends object ? {[K in keyof T]?: Query} : T; // TODO(MH): Support comparison queries. +/** @internal + * + */ +function encodeQuery(template: Template, query?: Query): unknown { + // TODO: actually implement this. + // I could not get the "unused" warning silenced, but this seems to count as "used" + [template]; + return query; +} + /** * Status code and result returned by a call to the ledger. @@ -316,7 +326,7 @@ class Ledger { * */ async query(template: Template, query?: Query): Promise[]> { - const payload = {templateIds: [template.templateId], query}; + const payload = {templateIds: [template.templateId], query: encodeQuery(template, query)}; const json = await this.submit('v1/query', payload); return jtv.Result.withException(jtv.array(decodeCreateEvent(template)).run(json)); } @@ -335,7 +345,7 @@ class Ledger { async fetch(template: Template, contractId: ContractId): Promise | null> { const payload = { templateId: template.templateId, - contractId, + contractId: ContractId(template).encode(contractId), }; const json = await this.submit('v1/fetch', payload); return jtv.Result.withException(jtv.oneOf(jtv.constant(null), decodeCreateEvent(template)).run(json)); @@ -360,7 +370,7 @@ class Ledger { } const payload = { templateId: template.templateId, - key, + key: template.keyEncode(key), }; const json = await this.submit('v1/fetch', payload); return jtv.Result.withException(jtv.oneOf(jtv.constant(null), decodeCreateEvent(template)).run(json)); @@ -380,7 +390,7 @@ class Ledger { async create(template: Template, payload: T): Promise> { const command = { templateId: template.templateId, - payload, + payload: template.encode(payload), }; const json = await this.submit('v1/create', command); return jtv.Result.withException(decodeCreateEvent(template).run(json)); @@ -403,9 +413,9 @@ class Ledger { async exercise(choice: Choice, contractId: ContractId, argument: C): Promise<[R , Event[]]> { const payload = { templateId: choice.template().templateId, - contractId, + contractId: ContractId(choice.template()).encode(contractId), choice: choice.choiceName, - argument, + argument: choice.argumentEncode(argument), }; const json = await this.submit('v1/exercise', payload); // Decode the server response into a tuple. @@ -441,9 +451,9 @@ class Ledger { } const payload = { templateId: choice.template().templateId, - key, + key: choice.template().keyEncode(key), choice: choice.choiceName, - argument, + argument: choice.argumentEncode(argument), }; const json = await this.submit('v1/exercise', payload); // Decode the server response into a tuple. @@ -650,7 +660,7 @@ class Ledger { ): Stream[]> { const request = queries.length == 0 ? [{templateIds: [template.templateId]}] - : queries.map(q => ({templateIds: [template.templateId], query: q})); + : queries.map(q => ({templateIds: [template.templateId], query: encodeQuery(template, q)})); const reconnectRequest = (): object[] => request; const change = (contracts: readonly CreateEvent[], events: readonly Event[]): CreateEvent[] => { const archiveEvents: Set> = new Set(); @@ -715,8 +725,8 @@ class Ledger { // given key be in output format, whereas existing implementation supports // input format. let lastContractId: ContractId | null = null; - const request = [{templateId: template.templateId, key}]; - const reconnectRequest = (): object[] => [{...request[0], 'contractIdAtOffset': lastContractId}] + const request = [{templateId: template.templateId, key: template.keyEncode(key)}]; + const reconnectRequest = (): object[] => [{...request[0], 'contractIdAtOffset': lastContractId && ContractId(template).encode(lastContractId)}] const change = (contract: CreateEvent | null, events: readonly Event[]): CreateEvent | null => { for (const event of events) { if ('created' in event) { @@ -795,8 +805,11 @@ class Ledger { const lastContractIds: (ContractId | null)[] = Array(keys.length).fill(null); const keysCopy = _.cloneDeep(keys); const initState: (CreateEvent | null)[] = Array(keys.length).fill(null); - const request = keys.map(k => ({templateId: template.templateId, key: k})); - const reconnectRequest = (): object[] => request.map((r, idx) => ({...r, 'contractIdAtOffset': lastContractIds[idx]})); + const request = keys.map(k => ({templateId: template.templateId, key: template.keyEncode(k)})); + const reconnectRequest = (): object[] => request.map((r, idx) => { + const lastId = lastContractIds[idx]; + return {...r, 'contractIdAtOffset': lastId && ContractId(template).encode(lastId)} + }); const change = (state: (CreateEvent | null)[], events: readonly Event[]): (CreateEvent | null)[] => { const newState: (CreateEvent | null)[] = Array.from(state); for (const event of events) { diff --git a/language-support/ts/daml-types/index.ts b/language-support/ts/daml-types/index.ts index f01bdc233ab..530b6e86916 100644 --- a/language-support/ts/daml-types/index.ts +++ b/language-support/ts/daml-types/index.ts @@ -4,15 +4,19 @@ import * as jtv from '@mojotech/json-type-validation'; /** * Interface for companion objects of serializable types. Its main purpose is - * to describe the JSON encoding of values of the serializable type. + * to serialize and deserialize values between raw JSON and typed values. * * @typeparam T The template type. */ export interface Serializable { /** - * @internal The decoder for a contract of template T. + * @internal */ decoder: jtv.Decoder; + /** + * @internal Encodes T in expected shape for JSON API. + */ + encode: (t: T) => unknown; } /** @@ -30,6 +34,10 @@ export interface Template; + /** + * @internal + */ + keyEncode: (k: K) => unknown; Archive: Choice; } @@ -49,12 +57,20 @@ export interface Choice { template: () => Template; /** * @internal Returns a decoder to decode the choice arguments. + * + * Note: we never need to decode the choice arguments, as they are sent over + * the API but not received. */ argumentDecoder: jtv.Decoder; + /** + * @internal + */ + argumentEncode: (c: C) => unknown; /** * @internal Returns a deocoder to decode the return value. */ resultDecoder: jtv.Decoder; + // note: no encoder for result, as they cannot be sent, only received. /** * The choice name. */ @@ -73,7 +89,7 @@ export const registerTemplate = (template: Template): void const templateId = template.templateId; const oldTemplate = registeredTemplates[templateId]; if (oldTemplate === undefined) { - registeredTemplates[templateId] = template; + registeredTemplates[templateId] = template as unknown as Template; console.debug(`Registered template ${templateId}.`); } else { console.warn(`Trying to re-register template ${templateId}.`); @@ -136,6 +152,7 @@ export interface Unit { */ export const Unit: Serializable = { decoder: jtv.object({}), + encode: (t: Unit) => t, } /** @@ -148,6 +165,7 @@ export type Bool = boolean; */ export const Bool: Serializable = { decoder: jtv.boolean(), + encode: (b: Bool) => b, } /** @@ -162,6 +180,7 @@ export type Int = string; */ export const Int: Serializable = { decoder: jtv.string(), + encode: (i: Int) => i, } /** @@ -187,6 +206,7 @@ export type Decimal = Numeric; export const Numeric = (_: number): Serializable => ({ decoder: jtv.string(), + encode: (n: Numeric): unknown => n, }) /** @@ -204,6 +224,7 @@ export type Text = string; */ export const Text: Serializable = { decoder: jtv.string(), + encode: (t: Text) => t, } /** @@ -218,6 +239,7 @@ export type Time = string; */ export const Time: Serializable