@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:
Gary Verhaegen 2020-11-12 12:00:38 +01:00 committed by GitHub
parent abd61b7429
commit 4b191f8154
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 183 additions and 50 deletions

View File

@ -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

View File

@ -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>,
};

View File

@ -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) {

View File

@ -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.