cleverMerge.js 16 KB

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