mirror of
https://github.com/digital-asset/daml.git
synced 2024-11-10 00:35:25 +03:00
@daml/types: add encoders (#7801)
This PR adds encoders to the various types defined in `@daml/types`. The serde mechanism did not need one so far because all of the types we're currently exposing map one-to-one to an appropriate (or, I suppose, tolerable) JS equivalent. This will not be the case anymore with generic maps, which means that if we want to provide our users with decent types (I do), we'll need some real encoding/decoding moving forward. CHANGELOG_BEGIN CHANGELOG_END
This commit is contained in:
parent
abd61b7429
commit
4b191f8154
@ -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
|
||||
|
@ -74,7 +74,9 @@ jest.mock('isomorphic-ws', () => class {
|
||||
const Foo: Template<Foo, string, "foo-id"> = {
|
||||
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<Foo, {}, {}, string>,
|
||||
};
|
||||
|
||||
|
@ -150,6 +150,16 @@ export function assert(b: boolean, m: string): void {
|
||||
export type Query<T> = T extends object ? {[K in keyof T]?: Query<T[K]>} : T;
|
||||
// TODO(MH): Support comparison queries.
|
||||
|
||||
/** @internal
|
||||
*
|
||||
*/
|
||||
function encodeQuery<T extends object, K, I extends string>(template: Template<T, K, I>, query?: Query<T>): 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<T extends object, K, I extends string>(template: Template<T, K, I>, query?: Query<T>): Promise<CreateEvent<T, K, I>[]> {
|
||||
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<T extends object, K, I extends string>(template: Template<T, K, I>, contractId: ContractId<T>): Promise<CreateEvent<T, K, I> | 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<T extends object, K, I extends string>(template: Template<T, K, I>, payload: T): Promise<CreateEvent<T, K, I>> {
|
||||
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<T extends object, C, R>(choice: Choice<T, C, R>, contractId: ContractId<T>, argument: C): Promise<[R , Event<object>[]]> {
|
||||
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<T, K, I, readonly CreateEvent<T, K, I>[]> {
|
||||
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<T, K, I>[], events: readonly Event<T, K, I>[]): CreateEvent<T, K, I>[] => {
|
||||
const archiveEvents: Set<ContractId<T>> = new Set();
|
||||
@ -715,8 +725,8 @@ class Ledger {
|
||||
// given key be in output format, whereas existing implementation supports
|
||||
// input format.
|
||||
let lastContractId: ContractId<T> | 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<T, K, I> | null, events: readonly Event<T, K, I>[]): CreateEvent<T, K, I> | null => {
|
||||
for (const event of events) {
|
||||
if ('created' in event) {
|
||||
@ -795,8 +805,11 @@ class Ledger {
|
||||
const lastContractIds: (ContractId<T> | null)[] = Array(keys.length).fill(null);
|
||||
const keysCopy = _.cloneDeep(keys);
|
||||
const initState: (CreateEvent<T, K, I> | 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<T, K, I> | null)[], events: readonly Event<T, K, I>[]): (CreateEvent<T, K, I> | null)[] => {
|
||||
const newState: (CreateEvent<T, K, I> | null)[] = Array.from(state);
|
||||
for (const event of events) {
|
||||
|
@ -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<T> {
|
||||
/**
|
||||
* @internal The decoder for a contract of template T.
|
||||
* @internal
|
||||
*/
|
||||
decoder: jtv.Decoder<T>;
|
||||
/**
|
||||
* @internal Encodes T in expected shape for JSON API.
|
||||
*/
|
||||
encode: (t: T) => unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -30,6 +34,10 @@ export interface Template<T extends object, K = unknown, I extends string = stri
|
||||
* @internal
|
||||
*/
|
||||
keyDecoder: jtv.Decoder<K>;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
keyEncode: (k: K) => unknown;
|
||||
Archive: Choice<T, {}, {}, K>;
|
||||
}
|
||||
|
||||
@ -49,12 +57,20 @@ export interface Choice<T extends object, C, R, K = unknown> {
|
||||
template: () => Template<T, K>;
|
||||
/**
|
||||
* @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<C>;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
argumentEncode: (c: C) => unknown;
|
||||
/**
|
||||
* @internal Returns a deocoder to decode the return value.
|
||||
*/
|
||||
resultDecoder: jtv.Decoder<R>;
|
||||
// note: no encoder for result, as they cannot be sent, only received.
|
||||
/**
|
||||
* The choice name.
|
||||
*/
|
||||
@ -73,7 +89,7 @@ export const registerTemplate = <T extends object>(template: Template<T>): void
|
||||
const templateId = template.templateId;
|
||||
const oldTemplate = registeredTemplates[templateId];
|
||||
if (oldTemplate === undefined) {
|
||||
registeredTemplates[templateId] = template;
|
||||
registeredTemplates[templateId] = template as unknown as Template<object, unknown, string>;
|
||||
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<Unit> = {
|
||||
decoder: jtv.object({}),
|
||||
encode: (t: Unit) => t,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -148,6 +165,7 @@ export type Bool = boolean;
|
||||
*/
|
||||
export const Bool: Serializable<Bool> = {
|
||||
decoder: jtv.boolean(),
|
||||
encode: (b: Bool) => b,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -162,6 +180,7 @@ export type Int = string;
|
||||
*/
|
||||
export const Int: Serializable<Int> = {
|
||||
decoder: jtv.string(),
|
||||
encode: (i: Int) => i,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -187,6 +206,7 @@ export type Decimal = Numeric;
|
||||
export const Numeric = (_: number): Serializable<Numeric> =>
|
||||
({
|
||||
decoder: jtv.string(),
|
||||
encode: (n: Numeric): unknown => n,
|
||||
})
|
||||
|
||||
/**
|
||||
@ -204,6 +224,7 @@ export type Text = string;
|
||||
*/
|
||||
export const Text: Serializable<Text> = {
|
||||
decoder: jtv.string(),
|
||||
encode: (t: Text) => t,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -218,6 +239,7 @@ export type Time = string;
|
||||
*/
|
||||
export const Time: Serializable<Time> = {
|
||||
decoder: jtv.string(),
|
||||
encode: (t: Time) => t,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -232,6 +254,7 @@ export type Party = string;
|
||||
*/
|
||||
export const Party: Serializable<Party> = {
|
||||
decoder: jtv.string(),
|
||||
encode: (p: Party) => p,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -248,6 +271,7 @@ export type List<T> = T[];
|
||||
*/
|
||||
export const List = <T>(t: Serializable<T>): Serializable<T[]> => ({
|
||||
decoder: jtv.array(t.decoder),
|
||||
encode: (l: List<T>): unknown => l.map((element: T) => t.encode(element)),
|
||||
});
|
||||
|
||||
/**
|
||||
@ -262,6 +286,7 @@ export type Date = string;
|
||||
*/
|
||||
export const Date: Serializable<Date> = {
|
||||
decoder: jtv.string(),
|
||||
encode: (d: Date) => d,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -290,6 +315,7 @@ export type ContractId<T> = string & { [ContractIdBrand]: T }
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export const ContractId = <T>(_t: Serializable<T>): Serializable<ContractId<T>> => ({
|
||||
decoder: jtv.string() as jtv.Decoder<ContractId<T>>,
|
||||
encode: (c: ContractId<T>): unknown => c,
|
||||
});
|
||||
|
||||
/**
|
||||
@ -314,6 +340,7 @@ type OptionalInner<T> = null extends T ? [] | [Exclude<T, null>] : T
|
||||
class OptionalWorker<T> implements Serializable<Optional<T>> {
|
||||
decoder: jtv.Decoder<Optional<T>>;
|
||||
private innerDecoder: jtv.Decoder<OptionalInner<T>>;
|
||||
encode: (o: Optional<T>) => unknown;
|
||||
|
||||
constructor(payload: Serializable<T>) {
|
||||
if (payload instanceof OptionalWorker) {
|
||||
@ -329,10 +356,35 @@ class OptionalWorker<T> implements Serializable<Optional<T>> {
|
||||
jtv.constant<[]>([]),
|
||||
jtv.tuple([payloadInnerDecoder]),
|
||||
) as jtv.Decoder<OptionalInner<T>>;
|
||||
this.encode = (o: Optional<T>): unknown => {
|
||||
if (o === null) {
|
||||
// Top-level enclosing Optional where the type argument is also
|
||||
// Optional and we represent None.
|
||||
return null;
|
||||
} else {
|
||||
// The current type is Optional<Optional<...>> and the current value
|
||||
// is Some x. Therefore the nested value is represented as [] for
|
||||
// x = None or as [y] for x = Some y. In both cases mapping the
|
||||
// encoder of the type parameter does the right thing.
|
||||
return (o as unknown as T[]).map(nested => payload.encode(nested));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// NOTE(MH): `T` is not of the form `Optional<U>` here and hence `null`
|
||||
// does not extend `T`. Thus, `OptionalInner<T> = T`.
|
||||
this.innerDecoder = payload.decoder as jtv.Decoder<OptionalInner<T>>;
|
||||
this.encode = (o: Optional<T>): unknown => {
|
||||
if (o === null) {
|
||||
// This branch is only reached if we are at the top-level and the
|
||||
// entire type is a non-nested Optional, i.e. Optional<U> where U is
|
||||
// not Optional. Recursive calls from the other branch would stop
|
||||
// before reaching this case, as nested None are empty lists and
|
||||
// never null.
|
||||
return null;
|
||||
} else {
|
||||
return payload.encode(o as unknown as T);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.decoder = jtv.oneOf(jtv.constant(null), this.innerDecoder);
|
||||
}
|
||||
@ -357,7 +409,14 @@ export type TextMap<T> = { [key: string]: T };
|
||||
* Companion object of the [[TextMap]] type.
|
||||
*/
|
||||
export const TextMap = <T>(t: Serializable<T>): Serializable<TextMap<T>> => ({
|
||||
decoder: jtv.dict(t.decoder),
|
||||
decoder: jtv.dict(t.decoder),
|
||||
encode: (tm: TextMap<T>): unknown => {
|
||||
const out: {[key: string]: unknown} = {};
|
||||
Object.keys(tm).forEach((k) => {
|
||||
out[k] = t.encode(tm[k]);
|
||||
});
|
||||
return out;
|
||||
}
|
||||
});
|
||||
|
||||
// TODO(MH): `Map` type.
|
||||
|
Loading…
Reference in New Issue
Block a user