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