[79a0317] | 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;
|
---|