-
Notifications
You must be signed in to change notification settings - Fork 4
/
polymorphic.ts
240 lines (207 loc) · 6.99 KB
/
polymorphic.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
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
// Copyright 2018-2022 Gamebridge.ai authors. All rights reserved. MIT license.
import {
JSONObject,
Serializable,
SERIALIZABLE_CLASS_MAP,
} from "./serializable.ts";
import { ERROR_FAILED_TO_RESOLVE_POLYMORPHIC_CLASS } from "./error_messages.ts";
import { fromJSONDefault } from "./strategy/from_json/default.ts";
/**
* Function type to ensure that initializers take no arguments and return a valid serializable
*/
export type InitializerFunction = () => Serializable;
/** Polymorphic class deserializer
*
* There are currently 2 ways of doing polymorphic deserialization:
* 1. Manually using \@PolymorphicResolver on a static method on the parent class
*
* This works by keeping a map of target 'parent' classes to resolver functions.
* These are set when a static method is annotated with @PolymorphicResolver.
* You can then call `serializePolymorphicClass` with the parent class and an
* input the input is passed to whatever the corresponding resolver function,
* which will make a determination and returns an instance of a 'child' class
*
* 2. Implicitly using \@PolymorphicSwitch instance property on a child class.
*
* This works by mapping the decorated class' parent prototype to the child
* class, property key, property value (or property value test), and initializer
* function
*/
/** \@PolymorphicResolver method decorator */
export function PolymorphicResolver(): PropertyDecorator {
return (
target: unknown,
propertyKey: string | symbol,
): void => {
registerPolymorphicResolver(
target,
(target as Record<typeof propertyKey, () => Serializable>)[
propertyKey as string
],
);
};
}
export type ResolverFunction = (
json: string | JSONObject,
) => Serializable | null;
/** Map of parent class constructors to functions that take in a JSON input and output a class instance that inherits Serializable */
const POLYMORPHIC_RESOLVER_MAP = new Map<unknown, ResolverFunction>();
/** Adds a class and a resolver function to the resolver map */
function registerPolymorphicResolver(
classPrototype: unknown,
resolver: ResolverFunction,
): void {
POLYMORPHIC_RESOLVER_MAP.set(classPrototype, resolver);
}
/**
* \@PolymorphicSwitch property decorator.
*
* Maps the provided initializer function and value or propertyValueTest to the parent class
*/
export function PolymorphicSwitch(
initializerFunction: InitializerFunction,
propertyValueTest: PropertyValueTest,
): PropertyDecorator;
export function PolymorphicSwitch<T>(
initializerFunction: InitializerFunction,
value: Exclude<T, PropertyValueTest>,
): PropertyDecorator;
export function PolymorphicSwitch(
initializerFunction: InitializerFunction,
valueOrTest: PropertyValueTest | unknown,
): PropertyDecorator {
return (
target: unknown, // The class it's self
propertyKey: string | symbol,
) => {
registerPolymorphicSwitch(
Object.getPrototypeOf(target).constructor, // Parent's prototype
target,
propertyKey,
valueOrTest,
initializerFunction,
);
};
}
const POLYMORPHIC_SWITCH_MAP = new Map<unknown, Set<PolymorphicClassOptions>>();
type PolymorphicClassOptions = {
classDefinition: unknown;
propertyKey: string | symbol;
propertyValueTest: PropertyValueTest;
initializer: InitializerFunction;
};
export type PropertyValueTest = (propertyValue: unknown) => boolean;
/**
* Registers a set of polymorphic class options with a parent class
*/
function registerPolymorphicSwitch<T>(
parentClassConstructor: unknown,
classDefinition: unknown,
propertyKey: string | symbol,
propertyValueTest: PropertyValueTest,
initializer: InitializerFunction,
): void;
function registerPolymorphicSwitch<T>(
parentClassConstructor: unknown,
classDefinition: unknown,
propertyKey: string | symbol,
propertyValue: Exclude<T, PropertyValueTest>,
initializer: InitializerFunction,
): void;
function registerPolymorphicSwitch(
parentClassConstructor: unknown,
classDefinition: unknown,
propertyKey: string | symbol,
valueOrTest: PropertyValueTest | unknown,
initializer: InitializerFunction,
): void {
let classPropertiesSet = POLYMORPHIC_SWITCH_MAP.get(parentClassConstructor);
if (!classPropertiesSet) {
classPropertiesSet = new Set<PolymorphicClassOptions>();
POLYMORPHIC_SWITCH_MAP.set(parentClassConstructor, classPropertiesSet);
}
if (valueOrTest instanceof Function) {
classPropertiesSet.add({
classDefinition,
propertyKey,
propertyValueTest: valueOrTest as PropertyValueTest,
initializer,
});
} else {
// If a value was provided set the property value test to be a simple equality check
classPropertiesSet.add({
classDefinition,
propertyKey,
propertyValueTest: (propertyValue) => propertyValue === valueOrTest,
initializer,
});
}
}
/** Return a resolved class type by testing the value of a property key */
function resolvePolymorphicSwitch(
parentClassConstructor: unknown,
json: string | JSONObject,
): Serializable | null {
const classOptionsSet = POLYMORPHIC_SWITCH_MAP.get(
parentClassConstructor,
);
if (!classOptionsSet) {
return null;
}
const _json = typeof json === "string" ? JSON.parse(json) : json;
for (
const {
classDefinition,
propertyKey,
propertyValueTest,
initializer,
} of classOptionsSet.values()
) {
const classMap = SERIALIZABLE_CLASS_MAP.get(
classDefinition,
);
if (!classMap) {
continue;
}
const serializePropertyOptions = classMap.getByPropertyKey(propertyKey);
if (!serializePropertyOptions) {
continue;
}
const fromJSONStrategy = serializePropertyOptions.fromJSONStrategy ||
fromJSONDefault;
const deserializedValue = fromJSONStrategy(
_json[serializePropertyOptions.serializedKey],
);
if (propertyValueTest(deserializedValue)) {
return initializer();
}
}
// Return null if no child could be matched to this value
return null;
}
/** Uses either the polymorphic resolver or the polymorphic switch resolver to determine the
* appropriate class, then deserialize the input using Serializable#fromJSON, returning the result
*/
export function polymorphicClassFromJSON<T extends Serializable>(
classPrototype: unknown & { prototype: T },
json: string | JSONObject,
): T {
return resolvePolymorphicClass(classPrototype, json).fromJSON(json);
}
/** Calls the polymorphic resolver or polymorphic switch resolver for the provided class prototype
* and input, and returns the initialized child class. Throws an exception if no class can be resolved
*/
function resolvePolymorphicClass<T extends Serializable>(
classPrototype: unknown & { prototype: T },
json: string | JSONObject,
): T {
const classResolver = POLYMORPHIC_RESOLVER_MAP.get(classPrototype);
if (classResolver) {
return classResolver(json) as T;
}
const resolvedClass = resolvePolymorphicSwitch(classPrototype, json);
if (resolvedClass) {
return resolvedClass as T;
}
throw new Error(ERROR_FAILED_TO_RESOLVE_POLYMORPHIC_CLASS);
}