-
Notifications
You must be signed in to change notification settings - Fork 4
/
serializable.ts
219 lines (191 loc) · 6.31 KB
/
serializable.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
// Copyright 2018-2020 Gamebridge.ai authors. All rights reserved. MIT license.
import { SerializePropertyOptionsMap } from "./serialize_property_options_map.ts";
import { defaultToJson } from "./to_json/default.ts";
import { defaultFromJson } from "./from_json/default.ts";
import { recursiveToJson } from "./to_json/recursive.ts";
/** A JSON object where each property value is a simple JSON value. */
export type JsonObject = { [Key in string]?: JsonValue };
/** A JSON array where each value is a simple JSON value. */
export interface JsonArray extends Array<JsonValue> {}
/** A property value in a JSON object. */
export type JsonValue =
| string
| number
| boolean
| null
| JsonObject
| JsonArray;
/** to be implemented by external authors on their models */
export declare interface TransformKey {
/** a function that will be called against
* every property key transforming the key
* with the provided function
*/
tsTransformKey(key: string): string;
}
/** Adds methods for serialization */
export abstract class Serializable {
/** Default transform functionality */
public tsTransformKey?(key: string): string {
return key;
}
/** Serializable to Json String */
public toJson(): string {
return toJson(this);
}
/** Deserialize to Serializable */
public fromJson(json: string | JsonValue): this {
return fromJson(this, json);
}
}
/** Functions used when hydrating data */
export type FromJsonStrategy = (value: JsonValue) => any;
export type FromJsonStrategyArgument =
(FromJsonStrategy | FromJsonStrategy[])[];
/** Functions used when dehydrating data */
export type ToJsonStrategy = (value: any) => JsonValue;
export type ToJsonStrategyArgument = (ToJsonStrategy | ToJsonStrategy[])[];
/** options to use when (de)serializing values */
export class SerializePropertyOptions {
public fromJsonStrategy?: FromJsonStrategy;
public toJsonStrategy?: ToJsonStrategy;
constructor(
public propertyKey: string | symbol,
public serializedKey: string,
fromJsonStrategy?: FromJsonStrategy | FromJsonStrategyArgument,
toJsonStrategy?: ToJsonStrategy | ToJsonStrategyArgument,
) {
if (Array.isArray(fromJsonStrategy)) {
this.fromJsonStrategy = composeStrategy(...fromJsonStrategy);
} else if (fromJsonStrategy) {
this.fromJsonStrategy = fromJsonStrategy;
}
if (Array.isArray(toJsonStrategy)) {
this.toJsonStrategy = composeStrategy(...toJsonStrategy);
} else if (toJsonStrategy) {
this.toJsonStrategy = toJsonStrategy;
}
}
}
/** list of FromJsonStrategy to one FromJsonStrategy composition */
export function composeStrategy(
...fns: FromJsonStrategyArgument
): FromJsonStrategy;
/** list of ToJsonStrategy to one ToJsonStrategy composition */
export function composeStrategy(
...fns: ToJsonStrategyArgument
): ToJsonStrategy;
/** Function to build a `fromJsonStrategy` or `toJsonStrategy`.
* Converts value from functions provided as parameters
*/
export function composeStrategy(
...fns:
| FromJsonStrategyArgument
| ToJsonStrategyArgument
): FromJsonStrategy | ToJsonStrategy {
return function _composeStrategy(val: any): any {
return fns.flat().reduce(
(acc: any, fn: FromJsonStrategy | ToJsonStrategy) => fn(acc),
val,
);
};
}
/** Options for each class */
export type SerializableMap = Map<unknown, SerializePropertyOptionsMap>;
/** Class options map */
export const SERIALIZABLE_CLASS_MAP: SerializableMap = new Map<
unknown,
SerializePropertyOptionsMap
>();
const ERROR_MESSAGE_MISSING_PROPERTIES_MAP =
"Unable to load serializer properties for the given context";
/** Converts to object using mapped keys */
export function toPojo(
context: any,
): JsonObject {
const serializablePropertyMap = SERIALIZABLE_CLASS_MAP.get(
context?.constructor?.prototype,
);
if (!serializablePropertyMap) {
throw new Error(
`${ERROR_MESSAGE_MISSING_PROPERTIES_MAP}: ${context?.constructor
?.prototype}`,
);
}
const record: JsonObject = {};
for (
let {
propertyKey,
serializedKey,
toJsonStrategy = defaultToJson,
} of serializablePropertyMap.propertyOptions()
) {
// Assume that key is always a string, a check is done earlier in SerializeProperty
const value = context[propertyKey as string];
// If the value is serializable then use the recursive replacer
if (
SERIALIZABLE_CLASS_MAP.get(
(value as Serializable)?.constructor?.prototype,
)
) {
toJsonStrategy = recursiveToJson;
}
if (Array.isArray(value)) {
record[serializedKey] = value.map((item: any) => {
if (item instanceof Serializable) {
return toPojo(item);
}
return toJsonStrategy(item);
});
} else if (value !== undefined) {
record[serializedKey] = toJsonStrategy(value);
}
}
return record;
}
/** Convert to `pojo` with our mapping logic then to string */
function toJson<T>(context: T): string {
return JSON.stringify(toPojo(context));
}
/** Convert from object/string to mapped object on the context */
function fromJson<T>(
context: Serializable,
json: string | JsonValue,
): T {
const serializablePropertyMap = SERIALIZABLE_CLASS_MAP.get(
context?.constructor?.prototype,
);
if (!serializablePropertyMap) {
throw new Error(
`${ERROR_MESSAGE_MISSING_PROPERTIES_MAP}: ${context?.constructor
?.prototype}`,
);
}
const _json = typeof json === "string" ? json : JSON.stringify(json);
return Object.assign(
context,
JSON.parse(
_json,
/** Processes the value through the provided or default `fromJsonStrategy` */
function revive(key: string, value: JsonValue): unknown {
// After the last iteration of the fromJsonStrategy a function
// will be called one more time with a empty string key
if (key === "") {
return value;
}
const {
propertyKey,
fromJsonStrategy = defaultFromJson,
} = serializablePropertyMap.getBySerializedKey(key) || {};
const processedValue: unknown = Array.isArray(value)
? value.map((v) => fromJsonStrategy(v))
: fromJsonStrategy(value);
if (propertyKey) {
context[propertyKey as keyof Serializable] = processedValue as any;
return;
}
return processedValue;
},
),
);
}