Skip to content

Commit fb47343

Browse files
committed
✨ Fully functional Serializable Config
1 parent cae3f77 commit fb47343

File tree

1 file changed

+186
-39
lines changed

1 file changed

+186
-39
lines changed

src/systems/node-configs/serializableConfig.ts

Lines changed: 186 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export type Serialized<T> = {
2525
* const config = new MyConfig()
2626
* console.log(config.toJSON()) // {} - Default values are excluded from the JSON
2727
* console.log(config.foo) // 'string' - But they are still accessible when reading
28-
* console.log(config.getLocalValue('foo')) // undefined - Unless you explicitly ask for the local value
28+
* console.log(config.get('foo', 'local')) // undefined - Unless you explicitly ask for the local value
2929
* // Only explicitly set values are included in the JSON
3030
* config.foo = 'baz'
3131
* console.log(config.toJSON()) // { foo: 'baz' }
@@ -35,21 +35,47 @@ export type Serialized<T> = {
3535
* console.log(config.foo) // 'string'
3636
* ```
3737
*/
38-
export class SerializableConfig<Config, T extends Record<string, any> = Serialized<Config>> {
38+
export class SerializableConfig<
39+
Config,
40+
T extends Record<string, any> = Serialized<Config>,
41+
JSON extends T & { __inheritedKeys__: Array<keyof T> } = T & {
42+
__inheritedKeys__: Array<keyof T>
43+
},
44+
> {
3945
private __defaultValues__ = {} as Record<string, any>
40-
private __returnDefaults__ = true
46+
private __inheritedKeys__ = new Set<keyof T>()
47+
private __getLocal__ = false
48+
/** The actual instance of {@link Config} (No proxies) */
49+
private __origin__: SerializableConfig<Config>
4150
private __parent__?: Config
4251

4352
constructor() {
44-
const scope = this
53+
// @ts-expect-error
54+
this.__origin__ = this
55+
const origin = this
4556
return new Proxy(this, {
4657
get(target, key: string) {
47-
if (key === '$local') return
48-
49-
// @ts-expect-error
50-
if (!scope.__returnDefaults__) return target[key] ?? scope.__parent__?.[key]
58+
// Return the value if it's set in this config
5159
// @ts-expect-error
52-
return target[key] ?? scope.__parent__?.[key] ?? scope.__defaultValues__[key]
60+
if (target[key] != undefined) {
61+
// @ts-expect-error
62+
return target[key]
63+
}
64+
// Check inheritance / default value if we're not expecting a local value.
65+
if (!origin.__getLocal__) {
66+
// If the key is inherited, return the value from the parent if it's set there
67+
if (origin.__inheritedKeys__.has(key)) {
68+
// @ts-expect-error
69+
const parentValue = target.__parent__?.[key]
70+
if (parentValue != undefined) {
71+
return parentValue
72+
}
73+
}
74+
// Default
75+
return origin.__defaultValues__[key]
76+
}
77+
// Return undefined if non of the above
78+
return undefined
5379
},
5480
set(target, key: string, newValue) {
5581
// @ts-expect-error
@@ -59,7 +85,7 @@ export class SerializableConfig<Config, T extends Record<string, any> = Serializ
5985
})
6086
}
6187

62-
private postInitialize() {
88+
private __postInitialize() {
6389
for (const key of Object.getOwnPropertyNames(this)) {
6490
if (key.startsWith('_')) continue
6591
// @ts-expect-error
@@ -69,29 +95,45 @@ export class SerializableConfig<Config, T extends Record<string, any> = Serializ
6995
}
7096
}
7197

72-
toJSON(): T {
73-
this.__returnDefaults__ = false
74-
const result = {} as Record<string, any>
98+
/**
99+
* Serialize the config to a JSON object.
100+
* @param metaData If true, include metadata such as inherited keys.
101+
*/
102+
toJSON(metaData = true): JSON {
103+
const result = {} as JSON
75104
for (const key of Object.getOwnPropertyNames(this)) {
76105
if (key.startsWith('_')) continue
77-
// @ts-expect-error
78-
if (this[key] != undefined) {
106+
const value = this.get(key, 'local-inherited')
107+
if (value != undefined) {
79108
// @ts-expect-error
80-
result[key] = this[key]
109+
result[key] = value
81110
}
82111
}
83-
this.__returnDefaults__ = true
84-
return result as T
112+
if (metaData && this.__inheritedKeys__.size > 0) {
113+
result.__inheritedKeys__ = Array.from(this.__inheritedKeys__)
114+
}
115+
return JSON.parse(JSON.stringify(result))
85116
}
86117

87-
fromJSON(json: Partial<T>) {
118+
/**
119+
* Initialize the config from a JSON object.
120+
* @param json The JSON object to initialize the config from.
121+
* @param partial If true, only set the properties that are present in the JSON object. Otherwise, clear all properties that are not present in the JSON object.
122+
*/
123+
fromJSON(json: Partial<JSON>, partial = false): this {
124+
json = JSON.parse(JSON.stringify(json))
125+
if (json.__inheritedKeys__) {
126+
this.__inheritedKeys__ = new Set(json.__inheritedKeys__)
127+
} else if (!partial) {
128+
this.__inheritedKeys__ = new Set()
129+
}
88130
for (const key of Object.getOwnPropertyNames(this)) {
89131
if (key.startsWith('_')) continue
90132
// Explicitly check for nullish, and use undefined if the key is not present in the JSON.
91133
if (json[key] != undefined) {
92134
// @ts-expect-error
93135
this[key] = json[key]
94-
} else {
136+
} else if (!partial) {
95137
// @ts-expect-error
96138
this[key] = undefined
97139
}
@@ -108,23 +150,18 @@ export class SerializableConfig<Config, T extends Record<string, any> = Serializ
108150
}
109151

110152
/**
111-
* Set the parent of this config.
112-
*/
113-
getDefaultValue<Key extends keyof T>(key: Key): NonNullable<T[Key]> {
114-
// @ts-expect-error
115-
return this.__defaultValues__[key]
116-
}
117-
118-
/**
119-
* Set the value of {@link key} to its default value.
153+
* Explicitly set the value of {@link key} to its default.
120154
*
121-
* Note that this is different from setting it to undefined.
155+
* Note that this is different from setting it to `undefined`.
122156
*/
123-
makeDefaultValue<Key extends keyof T>(key: Key): void {
157+
makeDefault<Key extends keyof T>(key: Key): void {
124158
// @ts-expect-error
125159
this[key] = this.__defaultValues__[key]
126160
}
127161

162+
/**
163+
* Checks whether two configs are equal.
164+
*/
128165
equalTo(other: Config): boolean {
129166
for (const key of Object.getOwnPropertyNames(this)) {
130167
if (key.startsWith('_')) continue
@@ -135,23 +172,133 @@ export class SerializableConfig<Config, T extends Record<string, any> = Serializ
135172
}
136173

137174
/**
138-
* Get the local value of {@link key}, ignoring the default and parent values.
175+
* Set the value of {@link key} to be (or not to be) inherited from the parent if it's not set locally.
176+
* @param key The key to set the inheritance of.
177+
* @param inherit Whether or not to inherit the value from the parent.
178+
*/
179+
setKeyInheritance<Key extends keyof T>(key: Key, inherit = true): this {
180+
if (inherit) {
181+
this.__inheritedKeys__.add(key)
182+
} else {
183+
this.__inheritedKeys__.delete(key)
184+
}
185+
return this
186+
}
187+
188+
/**
189+
* Get the inheritance status of {@link key}.
190+
* @returns Whether or not the key is inherited from the parent.
191+
*/
192+
getKeyInheritance<Key extends keyof T>(key: Key): boolean {
193+
return this.__inheritedKeys__.has(key)
194+
}
195+
196+
/**
197+
* Gets the value of {@link key} based on {@link getMode}
198+
* @returns If {@link getMode} is `local`, returns the explicitly set value of property {@link key} on this instance. If {@link getMode} is `local-inherited`
199+
* and the local value is `undefined`, it attempts to return the parent's explicitly set `local-inherited` value. If {@link getMode} is `default` it will return the `key`'s default value.
139200
*/
140-
getLocalValue<Key extends keyof T>(key: Key): T[Key] | undefined {
141-
this.__returnDefaults__ = false
201+
get<Key extends keyof T>(key: Key, getMode: 'local' | 'local-inherited'): T[Key] | undefined
202+
get<Key extends keyof T>(key: Key, getMode: 'default'): T[Key]
203+
get<Key extends keyof T>(
204+
key: Key,
205+
getMode: 'local' | 'local-inherited' | 'default'
206+
): T[Key] | undefined {
207+
if (getMode === 'default') {
208+
// @ts-expect-error
209+
return this.__defaultValues__[key]
210+
}
211+
142212
// @ts-expect-error
143-
const result = this[key]
144-
this.__returnDefaults__ = true
145-
return result
213+
const local = this.__origin__[key]
214+
if (local != undefined) {
215+
// Return the explicitly set local value
216+
return local
217+
}
218+
if (getMode === 'local') return undefined
219+
220+
if (this.__inheritedKeys__.has(key) && this.__parent__) {
221+
// Return the inherited value if it's explicitly set.
222+
// @ts-expect-error
223+
const inherited = this.__parent__.get(key)
224+
if (inherited != undefined) {
225+
return inherited
226+
}
227+
}
228+
}
229+
230+
/**
231+
* Set the value of {@link key} to {@link value}.
232+
*
233+
* Convenience method for `config[key] = value` typing issues.
234+
*/
235+
set<Key extends keyof T>(key: Key, value: T[Key]): this {
236+
// @ts-expect-error
237+
this[key] = value
238+
return this
146239
}
147240

148241
/**
149242
* Set the parent of this config.
150243
*
151244
* Any properties that are not set in this config will be fetched from the parent.
152245
*/
153-
setParent(parent: Config) {
246+
setParent(parent: Config | undefined): this {
154247
this.__parent__ = parent
248+
return this
249+
}
250+
251+
/**
252+
* @param sort If true, sort the keys alphabetically. If a function, sort the keys using the function. The function
253+
* uses the same implementation as {@link Array.prototype.sort}.
254+
*/
255+
keys(sort?: boolean): Array<keyof T>
256+
keys(sort?: (a: string, b: string) => number): Array<keyof T>
257+
keys(sort?: boolean | ((a: string, b: string) => number)): Array<keyof T> {
258+
const keys: Array<keyof T> = []
259+
const names = Object.getOwnPropertyNames(this)
260+
if (sort === true) {
261+
names.sort()
262+
} else if (typeof sort === 'function') {
263+
names.sort(sort)
264+
}
265+
for (const key of names) {
266+
if (key.startsWith('_')) continue
267+
keys.push(key as keyof T)
268+
}
269+
return keys
270+
}
271+
272+
values(): Array<T[keyof T]> {
273+
const values: Array<T[keyof T]> = []
274+
for (const key of Object.getOwnPropertyNames(this)) {
275+
if (key.startsWith('_')) continue
276+
// @ts-expect-error
277+
values.push(this[key])
278+
}
279+
return values
280+
}
281+
282+
/**
283+
* @param sort If true, sort the entries alphabetically. If a function, sort the entries using the function. The function
284+
* uses the same implementation as {@link Array.prototype.sort}.
285+
*/
286+
entries(sort?: boolean): Array<[keyof T, T[keyof T]]>
287+
entries(sort?: (a: string, b: string) => number): Array<[keyof T, T[keyof T]]>
288+
entries(sort?: boolean | ((a: string, b: string) => number)): Array<[keyof T, T[keyof T]]> {
289+
const entries: Array<[keyof T, T[keyof T]]> = []
290+
const names = Object.getOwnPropertyNames(this)
291+
if (sort === true) {
292+
names.sort()
293+
} else if (typeof sort === 'function') {
294+
names.sort(sort)
295+
}
296+
for (const key of names) {
297+
if (key.startsWith('_')) continue
298+
// @ts-expect-error
299+
entries.push([key, this[key]])
300+
}
301+
return entries
155302
}
156303

157304
/**
@@ -164,7 +311,7 @@ export class SerializableConfig<Config, T extends Record<string, any> = Serializ
164311
return new Proxy(settingClass, {
165312
construct(target, args) {
166313
const instance = new target(...(args as []))
167-
instance.postInitialize()
314+
instance.__postInitialize()
168315
return instance
169316
},
170317
})

0 commit comments

Comments
 (0)