-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathserializable.ts
More file actions
167 lines (143 loc) · 5.09 KB
/
serializable.ts
File metadata and controls
167 lines (143 loc) · 5.09 KB
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
// Copyright 2018-2025 Gamebridge.ai authors. All rights reserved. MIT license.
import { SerializePropertyOptionsMap } from "./serialize_property_options_map.ts";
import { toJSONDefault } from "./strategy/to_json/default.ts";
import { fromJSONDefault } from "./strategy/from_json/default.ts";
import { toJSONRecursive } from "./strategy/to_json/recursive.ts";
import { ERROR_MISSING_PROPERTIES_MAP } from "./error_messages.ts";
/** A `JSONObject` where each `string` property value is a `JSONValue`. */
export type JSONObject = {
[key: string]: JSONValue;
};
/** A JSONValue */
export type JSONValue =
| string
| number
| boolean
| null
| JSONObject
| JSONValue[];
/** called against every property key transforming the key with the provided function */
export declare interface TransformKey {
tsTransformKey(key: string): string;
}
/** returns the object as a string with transformations */
export declare interface ToJSON {
toJSON(): string;
}
/** reutrns a new javascript object with transformations */
export declare interface FromJSON {
fromJSON(json: string | JSONObject): this;
}
/** returns the javascript object as a `JSONObject` with transformations */
export declare interface Serialize {
tsSerialize(): JSONObject;
}
/** deep copy `this`, jsonObject is a POJO of the class that overrides the cloned
* object, jsonObject keys do not need keyTransforms, and values are raw JS Objects
*/
export declare interface Clone {
clone(jsonObject: Partial<this>): this;
}
/** Recursively set default serializer logic for own class definition and parent definitions if none exists */
function getOrInitializeDefaultSerializerLogicForParents(
targetPrototype: Serializable,
): SerializePropertyOptionsMap | undefined {
// Don't create serialization logic for Serializable
if (targetPrototype === Serializable.prototype) {
return undefined;
}
if (!SERIALIZABLE_CLASS_MAP.has(targetPrototype)) {
// If the parent has a serialization map then inherit it
let parentMap = SERIALIZABLE_CLASS_MAP.get(
Object.getPrototypeOf(targetPrototype),
);
// If the parent is also missing it's map then generate it if necessary
if (!parentMap) {
parentMap = getOrInitializeDefaultSerializerLogicForParents(
Object.getPrototypeOf(targetPrototype),
);
}
return SERIALIZABLE_CLASS_MAP.set(
targetPrototype,
new SerializePropertyOptionsMap(parentMap),
).get(targetPrototype);
}
return SERIALIZABLE_CLASS_MAP.get(targetPrototype);
}
export abstract class Serializable {
constructor() {
getOrInitializeDefaultSerializerLogicForParents(this.constructor.prototype);
}
public tsTransformKey(key: string): string {
return key;
}
public toJSON(): string {
return toJSON(this);
}
public fromJSON(json: string | JSONObject): this {
return fromJSON(this, json);
}
public tsSerialize(): JSONObject {
return toPojo(this);
}
public clone(jsonObject: Partial<this> = {}): this {
const copy = Object.getPrototypeOf(this).constructor;
return Object.assign(new copy().fromJSON(this.tsSerialize()), jsonObject);
}
}
/** Options for each class */
export type SerializableMap = Map<unknown, SerializePropertyOptionsMap>;
/** Class options map */
export const SERIALIZABLE_CLASS_MAP: SerializableMap = new Map<
unknown,
SerializePropertyOptionsMap
>();
/** Converts to object using mapped keys */
export function toPojo(context: Serializable): JSONObject {
const serializablePropertyMap = SERIALIZABLE_CLASS_MAP.get(
context?.constructor?.prototype,
);
if (!serializablePropertyMap) {
throw new Error(
`${ERROR_MISSING_PROPERTIES_MAP}: ${context?.constructor?.prototype}`,
);
}
const record: JSONObject = {};
for (
let {
propertyKey,
serializedKey,
toJSONStrategy = toJSONDefault,
} of serializablePropertyMap.propertyOptions()
) {
// Assume that key is always a string, a check is done earlier in SerializeProperty
const value = context[propertyKey as keyof Serializable];
// If the value is serializable then use the recursive replacer
if (SERIALIZABLE_CLASS_MAP.get(value?.constructor?.prototype)) {
toJSONStrategy = toJSONRecursive;
}
if (value !== undefined) {
record[serializedKey] = toJSONStrategy(value);
}
}
return record;
}
/** Convert to `pojo` with our mapping logic then to string */
function toJSON(context: Serializable): string {
return JSON.stringify(toPojo(context));
}
/** Convert from object/string to mapped object on the context */
function fromJSON<T>(context: Serializable, json: JSONValue): T {
const _json = typeof json === "string" ? JSON.parse(json) : json;
const accumulator: Partial<T> = {};
const map = SERIALIZABLE_CLASS_MAP.get(context?.constructor?.prototype);
for (const [key, value] of Object.entries(_json) as [string, JSONValue][]) {
const { propertyKey, fromJSONStrategy = fromJSONDefault } =
map?.getBySerializedKey(key) || {};
if (!propertyKey) {
continue;
}
accumulator[propertyKey as keyof T] = fromJSONStrategy(value);
}
return Object.assign(context, accumulator as T);
}