cleverMerge.js 18 KB

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