@@ -25,7 +25,7 @@ export type Serialized<T> = {
25
25
* const config = new MyConfig()
26
26
* console.log(config.toJSON()) // {} - Default values are excluded from the JSON
27
27
* 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
29
29
* // Only explicitly set values are included in the JSON
30
30
* config.foo = 'baz'
31
31
* console.log(config.toJSON()) // { foo: 'baz' }
@@ -35,21 +35,47 @@ export type Serialized<T> = {
35
35
* console.log(config.foo) // 'string'
36
36
* ```
37
37
*/
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
+ > {
39
45
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 >
41
50
private __parent__ ?: Config
42
51
43
52
constructor ( ) {
44
- const scope = this
53
+ // @ts -expect-error
54
+ this . __origin__ = this
55
+ const origin = this
45
56
return new Proxy ( this , {
46
57
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
51
59
// @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
53
79
} ,
54
80
set ( target , key : string , newValue ) {
55
81
// @ts -expect-error
@@ -59,7 +85,7 @@ export class SerializableConfig<Config, T extends Record<string, any> = Serializ
59
85
} )
60
86
}
61
87
62
- private postInitialize ( ) {
88
+ private __postInitialize ( ) {
63
89
for ( const key of Object . getOwnPropertyNames ( this ) ) {
64
90
if ( key . startsWith ( '_' ) ) continue
65
91
// @ts -expect-error
@@ -69,29 +95,45 @@ export class SerializableConfig<Config, T extends Record<string, any> = Serializ
69
95
}
70
96
}
71
97
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
75
104
for ( const key of Object . getOwnPropertyNames ( this ) ) {
76
105
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 ) {
79
108
// @ts -expect-error
80
- result [ key ] = this [ key ]
109
+ result [ key ] = value
81
110
}
82
111
}
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 ) )
85
116
}
86
117
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
+ }
88
130
for ( const key of Object . getOwnPropertyNames ( this ) ) {
89
131
if ( key . startsWith ( '_' ) ) continue
90
132
// Explicitly check for nullish, and use undefined if the key is not present in the JSON.
91
133
if ( json [ key ] != undefined ) {
92
134
// @ts -expect-error
93
135
this [ key ] = json [ key ]
94
- } else {
136
+ } else if ( ! partial ) {
95
137
// @ts -expect-error
96
138
this [ key ] = undefined
97
139
}
@@ -108,23 +150,18 @@ export class SerializableConfig<Config, T extends Record<string, any> = Serializ
108
150
}
109
151
110
152
/**
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.
120
154
*
121
- * Note that this is different from setting it to undefined.
155
+ * Note that this is different from setting it to ` undefined` .
122
156
*/
123
- makeDefaultValue < Key extends keyof T > ( key : Key ) : void {
157
+ makeDefault < Key extends keyof T > ( key : Key ) : void {
124
158
// @ts -expect-error
125
159
this [ key ] = this . __defaultValues__ [ key ]
126
160
}
127
161
162
+ /**
163
+ * Checks whether two configs are equal.
164
+ */
128
165
equalTo ( other : Config ) : boolean {
129
166
for ( const key of Object . getOwnPropertyNames ( this ) ) {
130
167
if ( key . startsWith ( '_' ) ) continue
@@ -135,23 +172,133 @@ export class SerializableConfig<Config, T extends Record<string, any> = Serializ
135
172
}
136
173
137
174
/**
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.
139
200
*/
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
+
142
212
// @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
146
239
}
147
240
148
241
/**
149
242
* Set the parent of this config.
150
243
*
151
244
* Any properties that are not set in this config will be fetched from the parent.
152
245
*/
153
- setParent ( parent : Config ) {
246
+ setParent ( parent : Config | undefined ) : this {
154
247
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
155
302
}
156
303
157
304
/**
@@ -164,7 +311,7 @@ export class SerializableConfig<Config, T extends Record<string, any> = Serializ
164
311
return new Proxy ( settingClass , {
165
312
construct ( target , args ) {
166
313
const instance = new target ( ...( args as [ ] ) )
167
- instance . postInitialize ( )
314
+ instance . __postInitialize ( )
168
315
return instance
169
316
} ,
170
317
} )
0 commit comments