1 | /*
|
---|
2 | MIT License http://www.opensource.org/licenses/mit-license.php
|
---|
3 | Author Tobias Koppers @sokra
|
---|
4 | */
|
---|
5 |
|
---|
6 | "use strict";
|
---|
7 |
|
---|
8 | /** @type {WeakMap<object, WeakMap<object, object>>} */
|
---|
9 | const mergeCache = new WeakMap();
|
---|
10 | /** @type {WeakMap<object, Map<string, Map<string|number|boolean, object>>>} */
|
---|
11 | const setPropertyCache = new WeakMap();
|
---|
12 | const DELETE = Symbol("DELETE");
|
---|
13 | const DYNAMIC_INFO = Symbol("cleverMerge dynamic info");
|
---|
14 |
|
---|
15 | /**
|
---|
16 | * Merges two given objects and caches the result to avoid computation if same objects passed as arguments again.
|
---|
17 | * @template T
|
---|
18 | * @template O
|
---|
19 | * @example
|
---|
20 | * // performs cleverMerge(first, second), stores the result in WeakMap and returns result
|
---|
21 | * cachedCleverMerge({a: 1}, {a: 2})
|
---|
22 | * {a: 2}
|
---|
23 | * // when same arguments passed, gets the result from WeakMap and returns it.
|
---|
24 | * cachedCleverMerge({a: 1}, {a: 2})
|
---|
25 | * {a: 2}
|
---|
26 | * @param {T | null | undefined} first first object
|
---|
27 | * @param {O | null | undefined} second second object
|
---|
28 | * @returns {T & O | T | O} merged object of first and second object
|
---|
29 | */
|
---|
30 | const cachedCleverMerge = (first, second) => {
|
---|
31 | if (second === undefined) return /** @type {T} */ (first);
|
---|
32 | if (first === undefined) return /** @type {O} */ (second);
|
---|
33 | if (typeof second !== "object" || second === null)
|
---|
34 | return /** @type {O} */ (second);
|
---|
35 | if (typeof first !== "object" || first === null)
|
---|
36 | return /** @type {T} */ (first);
|
---|
37 |
|
---|
38 | let innerCache = mergeCache.get(first);
|
---|
39 | if (innerCache === undefined) {
|
---|
40 | innerCache = new WeakMap();
|
---|
41 | mergeCache.set(first, innerCache);
|
---|
42 | }
|
---|
43 | const prevMerge = /** @type {T & O} */ (innerCache.get(second));
|
---|
44 | if (prevMerge !== undefined) return prevMerge;
|
---|
45 | const newMerge = _cleverMerge(first, second, true);
|
---|
46 | innerCache.set(second, newMerge);
|
---|
47 | return /** @type {T & O} */ (newMerge);
|
---|
48 | };
|
---|
49 |
|
---|
50 | /**
|
---|
51 | * @template T
|
---|
52 | * @param {Partial<T>} obj object
|
---|
53 | * @param {string} property property
|
---|
54 | * @param {string|number|boolean} value assignment value
|
---|
55 | * @returns {T} new object
|
---|
56 | */
|
---|
57 | const cachedSetProperty = (obj, property, value) => {
|
---|
58 | let mapByProperty = setPropertyCache.get(obj);
|
---|
59 |
|
---|
60 | if (mapByProperty === undefined) {
|
---|
61 | mapByProperty = new Map();
|
---|
62 | setPropertyCache.set(obj, mapByProperty);
|
---|
63 | }
|
---|
64 |
|
---|
65 | let mapByValue = mapByProperty.get(property);
|
---|
66 |
|
---|
67 | if (mapByValue === undefined) {
|
---|
68 | mapByValue = new Map();
|
---|
69 | mapByProperty.set(property, mapByValue);
|
---|
70 | }
|
---|
71 |
|
---|
72 | let result = mapByValue.get(value);
|
---|
73 |
|
---|
74 | if (result) return /** @type {T} */ (result);
|
---|
75 |
|
---|
76 | result = {
|
---|
77 | ...obj,
|
---|
78 | [property]: value
|
---|
79 | };
|
---|
80 | mapByValue.set(value, result);
|
---|
81 |
|
---|
82 | return /** @type {T} */ (result);
|
---|
83 | };
|
---|
84 |
|
---|
85 | /** @typedef {Map<string, any>} ByValues */
|
---|
86 |
|
---|
87 | /**
|
---|
88 | * @typedef {object} ObjectParsedPropertyEntry
|
---|
89 | * @property {any | undefined} base base value
|
---|
90 | * @property {string | undefined} byProperty the name of the selector property
|
---|
91 | * @property {ByValues} byValues value depending on selector property, merged with base
|
---|
92 | */
|
---|
93 |
|
---|
94 | /**
|
---|
95 | * @typedef {object} ParsedObject
|
---|
96 | * @property {Map<string, ObjectParsedPropertyEntry>} static static properties (key is property name)
|
---|
97 | * @property {{ byProperty: string, fn: Function } | undefined} dynamic dynamic part
|
---|
98 | */
|
---|
99 |
|
---|
100 | /** @type {WeakMap<object, ParsedObject>} */
|
---|
101 | const parseCache = new WeakMap();
|
---|
102 |
|
---|
103 | /**
|
---|
104 | * @param {object} obj the object
|
---|
105 | * @returns {ParsedObject} parsed object
|
---|
106 | */
|
---|
107 | const cachedParseObject = obj => {
|
---|
108 | const entry = parseCache.get(obj);
|
---|
109 | if (entry !== undefined) return entry;
|
---|
110 | const result = parseObject(obj);
|
---|
111 | parseCache.set(obj, result);
|
---|
112 | return result;
|
---|
113 | };
|
---|
114 |
|
---|
115 | /**
|
---|
116 | * @template {object} T
|
---|
117 | * @param {T} obj the object
|
---|
118 | * @returns {ParsedObject} parsed object
|
---|
119 | */
|
---|
120 | const parseObject = obj => {
|
---|
121 | const info = new Map();
|
---|
122 | let dynamicInfo;
|
---|
123 | /**
|
---|
124 | * @param {string} p path
|
---|
125 | * @returns {Partial<ObjectParsedPropertyEntry>} object parsed property entry
|
---|
126 | */
|
---|
127 | const getInfo = p => {
|
---|
128 | const entry = info.get(p);
|
---|
129 | if (entry !== undefined) return entry;
|
---|
130 | const newEntry = {
|
---|
131 | base: undefined,
|
---|
132 | byProperty: undefined,
|
---|
133 | byValues: undefined
|
---|
134 | };
|
---|
135 | info.set(p, newEntry);
|
---|
136 | return newEntry;
|
---|
137 | };
|
---|
138 | for (const key of Object.keys(obj)) {
|
---|
139 | if (key.startsWith("by")) {
|
---|
140 | const byProperty = /** @type {keyof T} */ (key);
|
---|
141 | const byObj = /** @type {object} */ (obj[byProperty]);
|
---|
142 | if (typeof byObj === "object") {
|
---|
143 | for (const byValue of Object.keys(byObj)) {
|
---|
144 | const obj = byObj[/** @type {keyof (keyof T)} */ (byValue)];
|
---|
145 | for (const key of Object.keys(obj)) {
|
---|
146 | const entry = getInfo(key);
|
---|
147 | if (entry.byProperty === undefined) {
|
---|
148 | entry.byProperty = /** @type {string} */ (byProperty);
|
---|
149 | entry.byValues = new Map();
|
---|
150 | } else if (entry.byProperty !== byProperty) {
|
---|
151 | throw new Error(
|
---|
152 | `${/** @type {string} */ (byProperty)} and ${entry.byProperty} for a single property is not supported`
|
---|
153 | );
|
---|
154 | }
|
---|
155 | /** @type {ByValues} */
|
---|
156 | (entry.byValues).set(
|
---|
157 | byValue,
|
---|
158 | obj[/** @type {keyof (keyof T)} */ (key)]
|
---|
159 | );
|
---|
160 | if (byValue === "default") {
|
---|
161 | for (const otherByValue of Object.keys(byObj)) {
|
---|
162 | if (
|
---|
163 | !(/** @type {ByValues} */ (entry.byValues).has(otherByValue))
|
---|
164 | )
|
---|
165 | /** @type {ByValues} */
|
---|
166 | (entry.byValues).set(otherByValue, undefined);
|
---|
167 | }
|
---|
168 | }
|
---|
169 | }
|
---|
170 | }
|
---|
171 | } else if (typeof byObj === "function") {
|
---|
172 | if (dynamicInfo === undefined) {
|
---|
173 | dynamicInfo = {
|
---|
174 | byProperty: key,
|
---|
175 | fn: byObj
|
---|
176 | };
|
---|
177 | } else {
|
---|
178 | throw new Error(
|
---|
179 | `${key} and ${dynamicInfo.byProperty} when both are functions is not supported`
|
---|
180 | );
|
---|
181 | }
|
---|
182 | } else {
|
---|
183 | const entry = getInfo(key);
|
---|
184 | entry.base = obj[/** @type {keyof T} */ (key)];
|
---|
185 | }
|
---|
186 | } else {
|
---|
187 | const entry = getInfo(key);
|
---|
188 | entry.base = obj[/** @type {keyof T} */ (key)];
|
---|
189 | }
|
---|
190 | }
|
---|
191 | return {
|
---|
192 | static: info,
|
---|
193 | dynamic: dynamicInfo
|
---|
194 | };
|
---|
195 | };
|
---|
196 |
|
---|
197 | /**
|
---|
198 | * @template {object} T
|
---|
199 | * @param {Map<string, ObjectParsedPropertyEntry>} info static properties (key is property name)
|
---|
200 | * @param {{ byProperty: string, fn: Function } | undefined} dynamicInfo dynamic part
|
---|
201 | * @returns {T} the object
|
---|
202 | */
|
---|
203 | const serializeObject = (info, dynamicInfo) => {
|
---|
204 | const obj = /** @type {T} */ ({});
|
---|
205 | // Setup byProperty structure
|
---|
206 | for (const entry of info.values()) {
|
---|
207 | if (entry.byProperty !== undefined) {
|
---|
208 | const byObj = (obj[entry.byProperty] = obj[entry.byProperty] || {});
|
---|
209 | for (const byValue of entry.byValues.keys()) {
|
---|
210 | byObj[byValue] = byObj[byValue] || {};
|
---|
211 | }
|
---|
212 | }
|
---|
213 | }
|
---|
214 | for (const [key, entry] of info) {
|
---|
215 | if (entry.base !== undefined) {
|
---|
216 | obj[/** @type {keyof T} */ (key)] = entry.base;
|
---|
217 | }
|
---|
218 | // Fill byProperty structure
|
---|
219 | if (entry.byProperty !== undefined) {
|
---|
220 | const byObj = (obj[entry.byProperty] = obj[entry.byProperty] || {});
|
---|
221 | for (const byValue of Object.keys(byObj)) {
|
---|
222 | const value = getFromByValues(entry.byValues, byValue);
|
---|
223 | if (value !== undefined) byObj[byValue][key] = value;
|
---|
224 | }
|
---|
225 | }
|
---|
226 | }
|
---|
227 | if (dynamicInfo !== undefined) {
|
---|
228 | obj[dynamicInfo.byProperty] = dynamicInfo.fn;
|
---|
229 | }
|
---|
230 | return obj;
|
---|
231 | };
|
---|
232 |
|
---|
233 | const VALUE_TYPE_UNDEFINED = 0;
|
---|
234 | const VALUE_TYPE_ATOM = 1;
|
---|
235 | const VALUE_TYPE_ARRAY_EXTEND = 2;
|
---|
236 | const VALUE_TYPE_OBJECT = 3;
|
---|
237 | const VALUE_TYPE_DELETE = 4;
|
---|
238 |
|
---|
239 | /**
|
---|
240 | * @param {any} value a single value
|
---|
241 | * @returns {VALUE_TYPE_UNDEFINED | VALUE_TYPE_ATOM | VALUE_TYPE_ARRAY_EXTEND | VALUE_TYPE_OBJECT | VALUE_TYPE_DELETE} value type
|
---|
242 | */
|
---|
243 | const getValueType = value => {
|
---|
244 | if (value === undefined) {
|
---|
245 | return VALUE_TYPE_UNDEFINED;
|
---|
246 | } else if (value === DELETE) {
|
---|
247 | return VALUE_TYPE_DELETE;
|
---|
248 | } else if (Array.isArray(value)) {
|
---|
249 | if (value.includes("...")) return VALUE_TYPE_ARRAY_EXTEND;
|
---|
250 | return VALUE_TYPE_ATOM;
|
---|
251 | } else if (
|
---|
252 | typeof value === "object" &&
|
---|
253 | value !== null &&
|
---|
254 | (!value.constructor || value.constructor === Object)
|
---|
255 | ) {
|
---|
256 | return VALUE_TYPE_OBJECT;
|
---|
257 | }
|
---|
258 | return VALUE_TYPE_ATOM;
|
---|
259 | };
|
---|
260 |
|
---|
261 | /**
|
---|
262 | * Merges two objects. Objects are deeply clever merged.
|
---|
263 | * Arrays might reference the old value with "...".
|
---|
264 | * Non-object values take preference over object values.
|
---|
265 | * @template T
|
---|
266 | * @template O
|
---|
267 | * @param {T} first first object
|
---|
268 | * @param {O} second second object
|
---|
269 | * @returns {T & O | T | O} merged object of first and second object
|
---|
270 | */
|
---|
271 | const cleverMerge = (first, second) => {
|
---|
272 | if (second === undefined) return first;
|
---|
273 | if (first === undefined) return second;
|
---|
274 | if (typeof second !== "object" || second === null) return second;
|
---|
275 | if (typeof first !== "object" || first === null) return first;
|
---|
276 |
|
---|
277 | return /** @type {T & O} */ (_cleverMerge(first, second, false));
|
---|
278 | };
|
---|
279 |
|
---|
280 | /**
|
---|
281 | * Merges two objects. Objects are deeply clever merged.
|
---|
282 | * @param {object} first first object
|
---|
283 | * @param {object} second second object
|
---|
284 | * @param {boolean} internalCaching should parsing of objects and nested merges be cached
|
---|
285 | * @returns {object} merged object of first and second object
|
---|
286 | */
|
---|
287 | const _cleverMerge = (first, second, internalCaching = false) => {
|
---|
288 | const firstObject = internalCaching
|
---|
289 | ? cachedParseObject(first)
|
---|
290 | : parseObject(first);
|
---|
291 | const { static: firstInfo, dynamic: firstDynamicInfo } = firstObject;
|
---|
292 |
|
---|
293 | // If the first argument has a dynamic part we modify the dynamic part to merge the second argument
|
---|
294 | if (firstDynamicInfo !== undefined) {
|
---|
295 | let { byProperty, fn } = firstDynamicInfo;
|
---|
296 | const fnInfo = fn[DYNAMIC_INFO];
|
---|
297 | if (fnInfo) {
|
---|
298 | second = internalCaching
|
---|
299 | ? cachedCleverMerge(fnInfo[1], second)
|
---|
300 | : cleverMerge(fnInfo[1], second);
|
---|
301 | fn = fnInfo[0];
|
---|
302 | }
|
---|
303 | const newFn = (...args) => {
|
---|
304 | const fnResult = fn(...args);
|
---|
305 | return internalCaching
|
---|
306 | ? cachedCleverMerge(fnResult, second)
|
---|
307 | : cleverMerge(fnResult, second);
|
---|
308 | };
|
---|
309 | newFn[DYNAMIC_INFO] = [fn, second];
|
---|
310 | return serializeObject(firstObject.static, { byProperty, fn: newFn });
|
---|
311 | }
|
---|
312 |
|
---|
313 | // If the first part is static only, we merge the static parts and keep the dynamic part of the second argument
|
---|
314 | const secondObject = internalCaching
|
---|
315 | ? cachedParseObject(second)
|
---|
316 | : parseObject(second);
|
---|
317 | const { static: secondInfo, dynamic: secondDynamicInfo } = secondObject;
|
---|
318 | /** @type {Map<string, ObjectParsedPropertyEntry>} */
|
---|
319 | const resultInfo = new Map();
|
---|
320 | for (const [key, firstEntry] of firstInfo) {
|
---|
321 | const secondEntry = secondInfo.get(key);
|
---|
322 | const entry =
|
---|
323 | secondEntry !== undefined
|
---|
324 | ? mergeEntries(firstEntry, secondEntry, internalCaching)
|
---|
325 | : firstEntry;
|
---|
326 | resultInfo.set(key, entry);
|
---|
327 | }
|
---|
328 | for (const [key, secondEntry] of secondInfo) {
|
---|
329 | if (!firstInfo.has(key)) {
|
---|
330 | resultInfo.set(key, secondEntry);
|
---|
331 | }
|
---|
332 | }
|
---|
333 | return serializeObject(resultInfo, secondDynamicInfo);
|
---|
334 | };
|
---|
335 |
|
---|
336 | /**
|
---|
337 | * @param {ObjectParsedPropertyEntry} firstEntry a
|
---|
338 | * @param {ObjectParsedPropertyEntry} secondEntry b
|
---|
339 | * @param {boolean} internalCaching should parsing of objects and nested merges be cached
|
---|
340 | * @returns {ObjectParsedPropertyEntry} new entry
|
---|
341 | */
|
---|
342 | const mergeEntries = (firstEntry, secondEntry, internalCaching) => {
|
---|
343 | switch (getValueType(secondEntry.base)) {
|
---|
344 | case VALUE_TYPE_ATOM:
|
---|
345 | case VALUE_TYPE_DELETE:
|
---|
346 | // No need to consider firstEntry at all
|
---|
347 | // second value override everything
|
---|
348 | // = second.base + second.byProperty
|
---|
349 | return secondEntry;
|
---|
350 | case VALUE_TYPE_UNDEFINED:
|
---|
351 | if (!firstEntry.byProperty) {
|
---|
352 | // = first.base + second.byProperty
|
---|
353 | return {
|
---|
354 | base: firstEntry.base,
|
---|
355 | byProperty: secondEntry.byProperty,
|
---|
356 | byValues: secondEntry.byValues
|
---|
357 | };
|
---|
358 | } else if (firstEntry.byProperty !== secondEntry.byProperty) {
|
---|
359 | throw new Error(
|
---|
360 | `${firstEntry.byProperty} and ${secondEntry.byProperty} for a single property is not supported`
|
---|
361 | );
|
---|
362 | } else {
|
---|
363 | // = first.base + (first.byProperty + second.byProperty)
|
---|
364 | // need to merge first and second byValues
|
---|
365 | const newByValues = new Map(firstEntry.byValues);
|
---|
366 | for (const [key, value] of secondEntry.byValues) {
|
---|
367 | const firstValue = getFromByValues(firstEntry.byValues, key);
|
---|
368 | newByValues.set(
|
---|
369 | key,
|
---|
370 | mergeSingleValue(firstValue, value, internalCaching)
|
---|
371 | );
|
---|
372 | }
|
---|
373 | return {
|
---|
374 | base: firstEntry.base,
|
---|
375 | byProperty: firstEntry.byProperty,
|
---|
376 | byValues: newByValues
|
---|
377 | };
|
---|
378 | }
|
---|
379 | default: {
|
---|
380 | if (!firstEntry.byProperty) {
|
---|
381 | // The simple case
|
---|
382 | // = (first.base + second.base) + second.byProperty
|
---|
383 | return {
|
---|
384 | base: mergeSingleValue(
|
---|
385 | firstEntry.base,
|
---|
386 | secondEntry.base,
|
---|
387 | internalCaching
|
---|
388 | ),
|
---|
389 | byProperty: secondEntry.byProperty,
|
---|
390 | byValues: secondEntry.byValues
|
---|
391 | };
|
---|
392 | }
|
---|
393 | let newBase;
|
---|
394 | const intermediateByValues = new Map(firstEntry.byValues);
|
---|
395 | for (const [key, value] of intermediateByValues) {
|
---|
396 | intermediateByValues.set(
|
---|
397 | key,
|
---|
398 | mergeSingleValue(value, secondEntry.base, internalCaching)
|
---|
399 | );
|
---|
400 | }
|
---|
401 | if (
|
---|
402 | Array.from(firstEntry.byValues.values()).every(value => {
|
---|
403 | const type = getValueType(value);
|
---|
404 | return type === VALUE_TYPE_ATOM || type === VALUE_TYPE_DELETE;
|
---|
405 | })
|
---|
406 | ) {
|
---|
407 | // = (first.base + second.base) + ((first.byProperty + second.base) + second.byProperty)
|
---|
408 | newBase = mergeSingleValue(
|
---|
409 | firstEntry.base,
|
---|
410 | secondEntry.base,
|
---|
411 | internalCaching
|
---|
412 | );
|
---|
413 | } else {
|
---|
414 | // = first.base + ((first.byProperty (+default) + second.base) + second.byProperty)
|
---|
415 | newBase = firstEntry.base;
|
---|
416 | if (!intermediateByValues.has("default"))
|
---|
417 | intermediateByValues.set("default", secondEntry.base);
|
---|
418 | }
|
---|
419 | if (!secondEntry.byProperty) {
|
---|
420 | // = first.base + (first.byProperty + second.base)
|
---|
421 | return {
|
---|
422 | base: newBase,
|
---|
423 | byProperty: firstEntry.byProperty,
|
---|
424 | byValues: intermediateByValues
|
---|
425 | };
|
---|
426 | } else if (firstEntry.byProperty !== secondEntry.byProperty) {
|
---|
427 | throw new Error(
|
---|
428 | `${firstEntry.byProperty} and ${secondEntry.byProperty} for a single property is not supported`
|
---|
429 | );
|
---|
430 | }
|
---|
431 | const newByValues = new Map(intermediateByValues);
|
---|
432 | for (const [key, value] of secondEntry.byValues) {
|
---|
433 | const firstValue = getFromByValues(intermediateByValues, key);
|
---|
434 | newByValues.set(
|
---|
435 | key,
|
---|
436 | mergeSingleValue(firstValue, value, internalCaching)
|
---|
437 | );
|
---|
438 | }
|
---|
439 | return {
|
---|
440 | base: newBase,
|
---|
441 | byProperty: firstEntry.byProperty,
|
---|
442 | byValues: newByValues
|
---|
443 | };
|
---|
444 | }
|
---|
445 | }
|
---|
446 | };
|
---|
447 |
|
---|
448 | /**
|
---|
449 | * @param {Map<string, any>} byValues all values
|
---|
450 | * @param {string} key value of the selector
|
---|
451 | * @returns {any | undefined} value
|
---|
452 | */
|
---|
453 | const getFromByValues = (byValues, key) => {
|
---|
454 | if (key !== "default" && byValues.has(key)) {
|
---|
455 | return byValues.get(key);
|
---|
456 | }
|
---|
457 | return byValues.get("default");
|
---|
458 | };
|
---|
459 |
|
---|
460 | /**
|
---|
461 | * @param {any} a value
|
---|
462 | * @param {any} b value
|
---|
463 | * @param {boolean} internalCaching should parsing of objects and nested merges be cached
|
---|
464 | * @returns {any} value
|
---|
465 | */
|
---|
466 | const mergeSingleValue = (a, b, internalCaching) => {
|
---|
467 | const bType = getValueType(b);
|
---|
468 | const aType = getValueType(a);
|
---|
469 | switch (bType) {
|
---|
470 | case VALUE_TYPE_DELETE:
|
---|
471 | case VALUE_TYPE_ATOM:
|
---|
472 | return b;
|
---|
473 | case VALUE_TYPE_OBJECT: {
|
---|
474 | return aType !== VALUE_TYPE_OBJECT
|
---|
475 | ? b
|
---|
476 | : internalCaching
|
---|
477 | ? cachedCleverMerge(a, b)
|
---|
478 | : cleverMerge(a, b);
|
---|
479 | }
|
---|
480 | case VALUE_TYPE_UNDEFINED:
|
---|
481 | return a;
|
---|
482 | case VALUE_TYPE_ARRAY_EXTEND:
|
---|
483 | switch (
|
---|
484 | aType !== VALUE_TYPE_ATOM
|
---|
485 | ? aType
|
---|
486 | : Array.isArray(a)
|
---|
487 | ? VALUE_TYPE_ARRAY_EXTEND
|
---|
488 | : VALUE_TYPE_OBJECT
|
---|
489 | ) {
|
---|
490 | case VALUE_TYPE_UNDEFINED:
|
---|
491 | return b;
|
---|
492 | case VALUE_TYPE_DELETE:
|
---|
493 | return /** @type {any[]} */ (b).filter(item => item !== "...");
|
---|
494 | case VALUE_TYPE_ARRAY_EXTEND: {
|
---|
495 | const newArray = [];
|
---|
496 | for (const item of b) {
|
---|
497 | if (item === "...") {
|
---|
498 | for (const item of a) {
|
---|
499 | newArray.push(item);
|
---|
500 | }
|
---|
501 | } else {
|
---|
502 | newArray.push(item);
|
---|
503 | }
|
---|
504 | }
|
---|
505 | return newArray;
|
---|
506 | }
|
---|
507 | case VALUE_TYPE_OBJECT:
|
---|
508 | return /** @type {any[]} */ (b).map(item =>
|
---|
509 | item === "..." ? a : item
|
---|
510 | );
|
---|
511 | default:
|
---|
512 | throw new Error("Not implemented");
|
---|
513 | }
|
---|
514 | default:
|
---|
515 | throw new Error("Not implemented");
|
---|
516 | }
|
---|
517 | };
|
---|
518 |
|
---|
519 | /**
|
---|
520 | * @template {object} T
|
---|
521 | * @param {T} obj the object
|
---|
522 | * @param {(keyof T)[]=} keysToKeepOriginalValue keys to keep original value
|
---|
523 | * @returns {T} the object without operations like "..." or DELETE
|
---|
524 | */
|
---|
525 | const removeOperations = (obj, keysToKeepOriginalValue = []) => {
|
---|
526 | const newObj = /** @type {T} */ ({});
|
---|
527 | for (const key of Object.keys(obj)) {
|
---|
528 | const value = obj[/** @type {keyof T} */ (key)];
|
---|
529 | const type = getValueType(value);
|
---|
530 | if (
|
---|
531 | type === VALUE_TYPE_OBJECT &&
|
---|
532 | keysToKeepOriginalValue.includes(/** @type {keyof T} */ (key))
|
---|
533 | ) {
|
---|
534 | newObj[/** @type {keyof T} */ (key)] = value;
|
---|
535 | continue;
|
---|
536 | }
|
---|
537 | switch (type) {
|
---|
538 | case VALUE_TYPE_UNDEFINED:
|
---|
539 | case VALUE_TYPE_DELETE:
|
---|
540 | break;
|
---|
541 | case VALUE_TYPE_OBJECT:
|
---|
542 | newObj[/** @type {keyof T} */ (key)] =
|
---|
543 | /** @type {T[keyof T]} */
|
---|
544 | (
|
---|
545 | removeOperations(
|
---|
546 | /** @type {TODO} */ (value),
|
---|
547 | keysToKeepOriginalValue
|
---|
548 | )
|
---|
549 | );
|
---|
550 | break;
|
---|
551 | case VALUE_TYPE_ARRAY_EXTEND:
|
---|
552 | newObj[/** @type {keyof T} */ (key)] =
|
---|
553 | /** @type {T[keyof T]} */
|
---|
554 | (
|
---|
555 | /** @type {any[]} */
|
---|
556 | (value).filter(i => i !== "...")
|
---|
557 | );
|
---|
558 | break;
|
---|
559 | default:
|
---|
560 | newObj[/** @type {keyof T} */ (key)] = value;
|
---|
561 | break;
|
---|
562 | }
|
---|
563 | }
|
---|
564 | return newObj;
|
---|
565 | };
|
---|
566 |
|
---|
567 | /**
|
---|
568 | * @template T
|
---|
569 | * @template {string} P
|
---|
570 | * @param {T} obj the object
|
---|
571 | * @param {P} byProperty the by description
|
---|
572 | * @param {...any} values values
|
---|
573 | * @returns {Omit<T, P>} object with merged byProperty
|
---|
574 | */
|
---|
575 | const resolveByProperty = (obj, byProperty, ...values) => {
|
---|
576 | if (typeof obj !== "object" || obj === null || !(byProperty in obj)) {
|
---|
577 | return obj;
|
---|
578 | }
|
---|
579 | const { [byProperty]: _byValue, ..._remaining } = obj;
|
---|
580 | const remaining = /** @type {T} */ (_remaining);
|
---|
581 | const byValue =
|
---|
582 | /** @type {Record<string, T> | function(...any[]): T} */
|
---|
583 | (_byValue);
|
---|
584 | if (typeof byValue === "object") {
|
---|
585 | const key = values[0];
|
---|
586 | if (key in byValue) {
|
---|
587 | return cachedCleverMerge(remaining, byValue[key]);
|
---|
588 | } else if ("default" in byValue) {
|
---|
589 | return cachedCleverMerge(remaining, byValue.default);
|
---|
590 | }
|
---|
591 | return remaining;
|
---|
592 | } else if (typeof byValue === "function") {
|
---|
593 | // eslint-disable-next-line prefer-spread
|
---|
594 | const result = byValue.apply(null, values);
|
---|
595 | return cachedCleverMerge(
|
---|
596 | remaining,
|
---|
597 | resolveByProperty(result, byProperty, ...values)
|
---|
598 | );
|
---|
599 | }
|
---|
600 | };
|
---|
601 |
|
---|
602 | module.exports.cachedSetProperty = cachedSetProperty;
|
---|
603 | module.exports.cachedCleverMerge = cachedCleverMerge;
|
---|
604 | module.exports.cleverMerge = cleverMerge;
|
---|
605 | module.exports.resolveByProperty = resolveByProperty;
|
---|
606 | module.exports.removeOperations = removeOperations;
|
---|
607 | module.exports.DELETE = DELETE;
|
---|