From 9587f66e93981fce37a6b4de7d69cd28d2d8ca3c Mon Sep 17 00:00:00 2001 From: Karsten Schmidt Date: Sat, 20 Jul 2024 11:44:18 +0200 Subject: [PATCH] feat(object-utils): import as new package (#486) - extract `BidirIndex` from thi.ng/associative --- packages/object-utils/LICENSE | 201 ++++++++++++++++++++ packages/object-utils/README.md | 99 ++++++++++ packages/object-utils/api-extractor.json | 3 + packages/object-utils/package.json | 121 ++++++++++++ packages/object-utils/src/common-keys.ts | 46 +++++ packages/object-utils/src/copy.ts | 22 +++ packages/object-utils/src/dissoc.ts | 11 ++ packages/object-utils/src/empty.ts | 6 + packages/object-utils/src/from-keys.ts | 42 ++++ packages/object-utils/src/index.ts | 14 ++ packages/object-utils/src/invert.ts | 58 ++++++ packages/object-utils/src/merge-apply.ts | 81 ++++++++ packages/object-utils/src/merge-deep.ts | 25 +++ packages/object-utils/src/merge-with.ts | 67 +++++++ packages/object-utils/src/merge.ts | 34 ++++ packages/object-utils/src/partition-keys.ts | 55 ++++++ packages/object-utils/src/rename-keys.ts | 110 +++++++++++ packages/object-utils/src/select-keys.ts | 76 ++++++++ packages/object-utils/src/without-keys.ts | 30 +++ packages/object-utils/test/merge.test.ts | 93 +++++++++ packages/object-utils/test/object.test.ts | 16 ++ packages/object-utils/test/tsconfig.json | 8 + packages/object-utils/tpl.readme.md | 38 ++++ packages/object-utils/tsconfig.json | 9 + 24 files changed, 1265 insertions(+) create mode 100644 packages/object-utils/LICENSE create mode 100644 packages/object-utils/README.md create mode 100644 packages/object-utils/api-extractor.json create mode 100644 packages/object-utils/package.json create mode 100644 packages/object-utils/src/common-keys.ts create mode 100644 packages/object-utils/src/copy.ts create mode 100644 packages/object-utils/src/dissoc.ts create mode 100644 packages/object-utils/src/empty.ts create mode 100644 packages/object-utils/src/from-keys.ts create mode 100644 packages/object-utils/src/index.ts create mode 100644 packages/object-utils/src/invert.ts create mode 100644 packages/object-utils/src/merge-apply.ts create mode 100644 packages/object-utils/src/merge-deep.ts create mode 100644 packages/object-utils/src/merge-with.ts create mode 100644 packages/object-utils/src/merge.ts create mode 100644 packages/object-utils/src/partition-keys.ts create mode 100644 packages/object-utils/src/rename-keys.ts create mode 100644 packages/object-utils/src/select-keys.ts create mode 100644 packages/object-utils/src/without-keys.ts create mode 100644 packages/object-utils/test/merge.test.ts create mode 100644 packages/object-utils/test/object.test.ts create mode 100644 packages/object-utils/test/tsconfig.json create mode 100644 packages/object-utils/tpl.readme.md create mode 100644 packages/object-utils/tsconfig.json diff --git a/packages/object-utils/LICENSE b/packages/object-utils/LICENSE new file mode 100644 index 0000000000..8dada3edaf --- /dev/null +++ b/packages/object-utils/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/object-utils/README.md b/packages/object-utils/README.md new file mode 100644 index 0000000000..f0d6c09a37 --- /dev/null +++ b/packages/object-utils/README.md @@ -0,0 +1,99 @@ + + +# ![@thi.ng/object-utils](https://media.thi.ng/umbrella/banners-20230807/thing-object-utils.svg?ede54c24) + +[![npm version](https://img.shields.io/npm/v/@thi.ng/object-utils.svg)](https://www.npmjs.com/package/@thi.ng/object-utils) +![npm downloads](https://img.shields.io/npm/dm/@thi.ng/object-utils.svg) +[![Mastodon Follow](https://img.shields.io/mastodon/follow/109331703950160316?domain=https%3A%2F%2Fmastodon.thi.ng&style=social)](https://mastodon.thi.ng/@toxi) + +> [!NOTE] +> This is one of 192 standalone projects, maintained as part +> of the [@thi.ng/umbrella](https://github.com/thi-ng/umbrella/) monorepo +> and anti-framework. +> +> 🚀 Please help me to work full-time on these projects by [sponsoring me on +> GitHub](https://github.com/sponsors/postspectacular). Thank you! ❤️ + +- [About](#about) +- [Status](#status) +- [Related packages](#related-packages) +- [Installation](#installation) +- [Dependencies](#dependencies) +- [API](#api) +- [Authors](#authors) +- [License](#license) + +## About + +Utilities for manipulating plain JS objects & maps. + +This package contains functionality which was previously part of and has been +extracted from the [@thi.ng/associative](https://thi.ng/associative) package. + +## Status + +**STABLE** - used in production + +[Search or submit any issues for this package](https://github.com/thi-ng/umbrella/issues?q=%5Bobject-utils%5D+in%3Atitle) + +## Related packages + +- [@thi.ng/associative](https://github.com/thi-ng/umbrella/tree/develop/packages/associative) - Alternative Map and Set implementations with customizable equality semantics & supporting operations, plain object utilities + +## Installation + +```bash +yarn add @thi.ng/object-utils +``` + +ESM import: + +```ts +import * as ou from "@thi.ng/object-utils"; +``` + +Browser ESM import: + +```html + +``` + +[JSDelivr documentation](https://www.jsdelivr.com/) + +For Node.js REPL: + +```js +const ou = await import("@thi.ng/object-utils"); +``` + +Package sizes (brotli'd, pre-treeshake): ESM: 1.35 KB + +## Dependencies + +- [@thi.ng/api](https://github.com/thi-ng/umbrella/tree/develop/packages/api) +- [@thi.ng/checks](https://github.com/thi-ng/umbrella/tree/develop/packages/checks) + +## API + +[Generated API docs](https://docs.thi.ng/umbrella/object-utils/) + +TODO + +## Authors + +- [Karsten Schmidt](https://thi.ng) + +If this project contributes to an academic publication, please cite it as: + +```bibtex +@misc{thing-object-utils, + title = "@thi.ng/object-utils", + author = "Karsten Schmidt", + note = "https://thi.ng/object-utils", + year = 2017 +} +``` + +## License + +© 2017 - 2024 Karsten Schmidt // Apache License 2.0 diff --git a/packages/object-utils/api-extractor.json b/packages/object-utils/api-extractor.json new file mode 100644 index 0000000000..bc73f2cc02 --- /dev/null +++ b/packages/object-utils/api-extractor.json @@ -0,0 +1,3 @@ +{ + "extends": "../../api-extractor.json" +} diff --git a/packages/object-utils/package.json b/packages/object-utils/package.json new file mode 100644 index 0000000000..3bf6b06ec1 --- /dev/null +++ b/packages/object-utils/package.json @@ -0,0 +1,121 @@ +{ + "name": "@thi.ng/object-utils", + "version": "1.0.0", + "description": "Utilities for manipulating plain JS objects & maps", + "type": "module", + "module": "./index.js", + "typings": "./index.d.ts", + "sideEffects": false, + "repository": { + "type": "git", + "url": "https://github.com/thi-ng/umbrella.git" + }, + "homepage": "https://thi.ng/object-utils", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/postspectacular" + }, + { + "type": "patreon", + "url": "https://patreon.com/thing_umbrella" + } + ], + "author": "Karsten Schmidt (https://thi.ng)", + "license": "Apache-2.0", + "scripts": { + "build": "yarn build:esbuild && yarn build:decl", + "build:decl": "tsc --declaration --emitDeclarationOnly", + "build:esbuild": "esbuild --format=esm --platform=neutral --target=es2022 --tsconfig=tsconfig.json --outdir=. src/**/*.ts", + "clean": "rimraf --glob '*.js' '*.d.ts' '*.map' doc", + "doc": "typedoc --excludePrivate --excludeInternal --out doc src/index.ts", + "doc:ae": "mkdir -p .ae/doc .ae/temp && api-extractor run --local --verbose", + "doc:readme": "bun ../../tools/src/module-stats.ts && bun ../../tools/src/readme.ts", + "pub": "yarn npm publish --access public", + "test": "bun test", + "tool:tangle": "../../node_modules/.bin/tangle src/**/*.ts" + }, + "dependencies": { + "@thi.ng/api": "^8.11.6", + "@thi.ng/checks": "^3.6.8" + }, + "devDependencies": { + "@microsoft/api-extractor": "^7.47.0", + "esbuild": "^0.23.0", + "typedoc": "^0.26.3", + "typescript": "^5.5.3" + }, + "keywords": [ + "associative", + "object", + "map", + "typescript" + ], + "publishConfig": { + "access": "public" + }, + "browser": { + "process": false, + "setTimeout": false + }, + "engines": { + "node": ">=18" + }, + "files": [ + "./*.js", + "./*.d.ts" + ], + "exports": { + ".": { + "default": "./index.js" + }, + "./common-keys": { + "default": "./common-keys.js" + }, + "./copy": { + "default": "./copy.js" + }, + "./dissoc": { + "default": "./dissoc.js" + }, + "./empty": { + "default": "./empty.js" + }, + "./from-keys": { + "default": "./from-keys.js" + }, + "./invert": { + "default": "./invert.js" + }, + "./merge-apply": { + "default": "./merge-apply.js" + }, + "./merge-deep": { + "default": "./merge-deep.js" + }, + "./merge-with": { + "default": "./merge-with.js" + }, + "./merge": { + "default": "./merge.js" + }, + "./partition-keys": { + "default": "./partition-keys.js" + }, + "./rename-keys": { + "default": "./rename-keys.js" + }, + "./select-keys": { + "default": "./select-keys.js" + }, + "./without-keys": { + "default": "./without-keys.js" + } + }, + "thi.ng": { + "related": [ + "associative" + ], + "year": 2017 + } +} diff --git a/packages/object-utils/src/common-keys.ts b/packages/object-utils/src/common-keys.ts new file mode 100644 index 0000000000..bbfef61fcf --- /dev/null +++ b/packages/object-utils/src/common-keys.ts @@ -0,0 +1,46 @@ +/** + * Like {@link commonKeysObj}, but for ES6 Maps. + * + * @param a - first map + * @param b - other map + * @param out - result array + */ +export const commonKeysMap = ( + a: Map, + b: Map, + out: K[] = [] +) => { + for (let k of a.keys()) { + b.has(k) && out.push(k); + } + return out; +}; + +/** + * Returns array of keys present in both args, i.e. the set intersection + * of the given objects' key / property sets. + * + * @example + * ```ts tangle:../export/common-keys.ts + * import { commonKeysObj } from "@thi.ng/object-utils"; + * + * console.log( + * commonKeys({ a: 1, b: 2 }, { c: 10, b: 20, a: 30 }) + * ); + * // [ "a", "b" ] + * ``` + * + * @param a - first object + * @param b - other object + * @param out - result array + */ +export const commonKeysObj = ( + a: A, + b: B, + out: string[] = [] +): (keyof A & keyof B)[] => { + for (let k in a) { + b.hasOwnProperty(k) && out.push(k); + } + return out; +}; diff --git a/packages/object-utils/src/copy.ts b/packages/object-utils/src/copy.ts new file mode 100644 index 0000000000..84924ecc8c --- /dev/null +++ b/packages/object-utils/src/copy.ts @@ -0,0 +1,22 @@ +import { implementsFunction } from "@thi.ng/checks/implements-function"; +import { isIllegalKey } from "@thi.ng/checks/is-proto-path"; + +export const copy = (x: any, ctor: Function) => + implementsFunction(x, "copy") + ? x.copy() + : new (x[Symbol.species] || ctor)(x); + +/** + * Creates shallow copy of `src` object without any properties for which + * [`isIllegalKey()`](https://docs.thi.ng/umbrella/checks/functions/isIllegalKey.html) + * returns true. + * + * @param src + */ +export const copyObj = (src: any) => { + const res: any = {}; + for (let k in src) { + !isIllegalKey(k) && (res[k] = src[k]); + } + return res; +}; diff --git a/packages/object-utils/src/dissoc.ts b/packages/object-utils/src/dissoc.ts new file mode 100644 index 0000000000..0a33bbf670 --- /dev/null +++ b/packages/object-utils/src/dissoc.ts @@ -0,0 +1,11 @@ +import type { IObjectOf } from "@thi.ng/api"; + +export const dissocObj = ( + obj: IObjectOf, + keys: Iterable +) => { + for (let k of keys) { + delete obj[k]; + } + return obj; +}; diff --git a/packages/object-utils/src/empty.ts b/packages/object-utils/src/empty.ts new file mode 100644 index 0000000000..3c3a8a4f0c --- /dev/null +++ b/packages/object-utils/src/empty.ts @@ -0,0 +1,6 @@ +import { implementsFunction } from "@thi.ng/checks/implements-function"; + +export const empty = (x: any, ctor: Function) => + implementsFunction(x, "empty") + ? x.empty() + : new (x[Symbol.species] || ctor)(); diff --git a/packages/object-utils/src/from-keys.ts b/packages/object-utils/src/from-keys.ts new file mode 100644 index 0000000000..c517b0184f --- /dev/null +++ b/packages/object-utils/src/from-keys.ts @@ -0,0 +1,42 @@ +import type { Fn } from "@thi.ng/api"; +import { isFunction } from "@thi.ng/checks/is-function"; + +/** + * Similar to (but much faster than) `Object.fromEntries()`. Takes an array of + * property keys and an `init` value or function. If the latter, the `init` + * function is called for each key and its results are used as values. Otherwise + * the `init` value is used homogeneously for all keys. + * + * @example + * ```ts tangle:../export/from-keys.ts + * import { objectFromKeys } from "@thi.ng/object-utils"; + * + * console.log( + * objectFromKeys(["a", "b", "c"], 1) + * ); + * // { a: 1, b: 1, c: 1 } + * + * console.log( + * objectFromKeys(["a", "b", "c"], () => []) + * ); + * // { a: [], b: [], c: [] } + * + * console.log( + * objectFromKeys(["a", "b", "c"], (k) => `${k}-${(Math.random()*100)|0}`) + * ); + * // { a: 'a-54', b: 'b-8', c: 'c-61' } + * ``` + * + * @param keys + * @param init + */ +export const objectFromKeys = ( + keys: K[], + init: V | Fn +) => + keys.reduce( + isFunction(init) + ? (acc, k) => ((acc[k] = init(k)), acc) + : (acc, k) => ((acc[k] = init), acc), + >{} + ); diff --git a/packages/object-utils/src/index.ts b/packages/object-utils/src/index.ts new file mode 100644 index 0000000000..88bf417437 --- /dev/null +++ b/packages/object-utils/src/index.ts @@ -0,0 +1,14 @@ +export * from "./common-keys.js"; +export * from "./copy.js"; +export * from "./dissoc.js"; +export * from "./empty.js"; +export * from "./from-keys.js"; +export * from "./invert.js"; +export * from "./merge.js"; +export * from "./merge-apply.js"; +export * from "./merge-deep.js"; +export * from "./merge-with.js"; +export * from "./partition-keys.js"; +export * from "./rename-keys.js"; +export * from "./select-keys.js"; +export * from "./without-keys.js"; diff --git a/packages/object-utils/src/invert.ts b/packages/object-utils/src/invert.ts new file mode 100644 index 0000000000..16e38fafcc --- /dev/null +++ b/packages/object-utils/src/invert.ts @@ -0,0 +1,58 @@ +import type { IObjectOf } from "@thi.ng/api"; + +/** + * Returns a new map in which the original values are used as keys and + * original keys as values. If `dest` is given, writes results in that + * map instead. Depending on the value type of `src` and/or if the + * inverted map should use custom key equality semantics as provided by + * the Map types in this package, you MUST provide a `dest` map, since + * the default `dest` will only be a standard ES6 Map. + * + * @example + * ```ts tangle:../export/invert.ts + * import { invertMap } from "@thi.ng/object-utils"; + * + * console.log( + * invertMap(new Map([["a", 1], ["b", 2]])) + * ); + * // Map { 1 => 'a', 2 => 'b' } + * ``` + * + * @param src - map to invert + * @param dest - result map + */ +export const invertMap = (src: Map, dest?: Map) => { + dest = dest || new Map(); + for (let p of src) { + dest.set(p[1], p[0]); + } + return dest; +}; + +/** + * Returns a new object in which the original values are used as keys + * and original keys as values. If `dest` is given, writes results in + * that object instead. + * + * @example + * ```ts tangle:../export/invert.ts + * import { invertObj } from "@thi.ng/object-utils"; + * + * console.log( + * invertObj({ a: 1, b: 2 }) + * ); + * // { '1': 'a', '2': 'b' } + * ``` + * + * @param src - object to invert + * @param dest - result object + */ +export const invertObj = ( + src: IObjectOf, + dest: IObjectOf = {} +) => { + for (let k in src) { + dest[src[k]] = k; + } + return dest; +}; diff --git a/packages/object-utils/src/merge-apply.ts b/packages/object-utils/src/merge-apply.ts new file mode 100644 index 0000000000..61f6f92a5a --- /dev/null +++ b/packages/object-utils/src/merge-apply.ts @@ -0,0 +1,81 @@ +import type { Fn, IObjectOf } from "@thi.ng/api"; +import { isFunction } from "@thi.ng/checks/is-function"; +import { isIllegalKey } from "@thi.ng/checks/is-proto-path"; +import { copy, copyObj } from "./copy.js"; + +/** + * Similar to {@link mergeApplyObj}, but for ES6 Maps instead of plain objects. + * + * @param src - source map + * @param xforms - map w/ transformation functions + */ +export const mergeApplyMap = ( + src: Map, + xforms: Map> +): Map => { + const res: Map = copy(src, Map); + for (let [k, v] of xforms) { + res.set(k, isFunction(v) ? v(res.get(k)) : v); + } + return res; +}; + +/** + * Similar to {@link mergeObjWith}, but only supports 2 args and any function + * values in `xforms` will be called with respective value in `src` to produce a + * new / derived value for that key, i.e. + * + * @remarks + * Since v4.4.0, the `__proto__` property will be ignored to avoid prototype + * pollution. + * + * @example + * ```ts + * dest[k] = xforms[k](src[k]) + * ``` + * + * Returns new merged object and does not modify any of the inputs. + * + * @example + * ```ts tangle:../export/merge-apply.ts + * import { mergeApplyObj } from "@thi.ng/object-utils"; + * + * console.log( + * mergeApplyObj( + * { a: "hello", b: 23, c: 12 }, + * { a: (x) => x + " world", b: 42 } + * ) + * ); + * // { a: 'hello world', b: 42, c: 12 } + * ``` + * + * @param src - source object + * @param xforms - object w/ transformation functions + */ +export const mergeApplyObj = ( + src: IObjectOf, + xforms: IObjectOf> +) => meldApplyObj(copyObj(src), xforms); + +/** + * Mutable version of {@link mergeApplyObj}. Returns modified `src` + * object. + * + * @remarks + * Since v4.4.0, the `__proto__` property will be ignored to avoid + * prototype pollution. + * + * @param src - + * @param xforms - + */ +export const meldApplyObj = ( + src: IObjectOf, + xforms: IObjectOf> +) => { + for (let k in xforms) { + if (isIllegalKey(k)) continue; + const v = xforms[k]; + src[k] = isFunction(v) ? v(src[k]) : v; + } + return src; +}; diff --git a/packages/object-utils/src/merge-deep.ts b/packages/object-utils/src/merge-deep.ts new file mode 100644 index 0000000000..2b53392cab --- /dev/null +++ b/packages/object-utils/src/merge-deep.ts @@ -0,0 +1,25 @@ +import type { IObjectOf, Nullable } from "@thi.ng/api"; +import { isPlainObject } from "@thi.ng/checks/is-plain-object"; +import { meldObjWith, mergeObjWith } from "./merge-with.js"; + +export const mergeDeepObj = ( + dest: IObjectOf, + ...objects: Nullable>[] +): any => + mergeObjWith( + (a, b) => + isPlainObject(a) && isPlainObject(b) ? mergeDeepObj(a, b) : b, + dest, + ...objects + ); + +export const meldDeepObj = ( + dest: IObjectOf, + ...objects: Nullable>[] +): any => + meldObjWith( + (a, b) => + isPlainObject(a) && isPlainObject(b) ? meldDeepObj(a, b) : b, + dest, + ...objects + ); diff --git a/packages/object-utils/src/merge-with.ts b/packages/object-utils/src/merge-with.ts new file mode 100644 index 0000000000..bab70a2cb4 --- /dev/null +++ b/packages/object-utils/src/merge-with.ts @@ -0,0 +1,67 @@ +import type { Fn2, IObjectOf, Nullable } from "@thi.ng/api"; +import { isIllegalKey } from "@thi.ng/checks/is-proto-path"; +import { copy, copyObj } from "./copy.js"; + +export const mergeMapWith = ( + f: Fn2, + dest: Map, + ...maps: Nullable>[] +) => { + const res: Map = copy(dest, Map); + for (let x of maps) { + if (x != null) { + for (let [k, v] of x) { + res.set(k, res.has(k) ? f(res.get(k)!, v) : v); + } + } + } + return res; +}; + +/** + * Immutably merges given objects in a pairwise manner. Applies function + * `f` if the same key exists in both objects and uses that function's + * return value as new value for that key. + * + * @remarks + * Since v4.4.0, the `__proto__` property will be ignored to avoid + * prototype pollution. + * + * @param f - + * @param dest - + * @param objects - + */ +export const mergeObjWith = ( + f: Fn2, + dest: IObjectOf, + ...objects: Nullable>[] +) => meldObjWith(f, copyObj(dest), ...objects); + +/** + * Mutable version of {@link mergeObjWith}. Returns modified `dest` + * object. + * + * @remarks + * Since v4.4.0, the `__proto__` property will be ignored to avoid + * prototype pollution. + * + * @param f - + * @param dest - + * @param objects - + */ +export const meldObjWith = ( + f: Fn2, + dest: IObjectOf, + ...objects: Nullable>[] +) => { + for (let x of objects) { + if (x != null) { + for (let k in x) { + if (isIllegalKey(k)) continue; + const v = x[k]; + dest[k] = dest.hasOwnProperty(k) ? f(dest[k], v) : v; + } + } + } + return dest; +}; diff --git a/packages/object-utils/src/merge.ts b/packages/object-utils/src/merge.ts new file mode 100644 index 0000000000..d702198112 --- /dev/null +++ b/packages/object-utils/src/merge.ts @@ -0,0 +1,34 @@ +import type { IObjectOf, Nullable } from "@thi.ng/api"; + +/** + * Merges all given maps in left-to-right order into `dest`. + * Returns `dest`. + * + * @param dest - target map + * @param maps - input maps + */ +export const mergeMap = ( + dest: Map, + ...maps: Nullable>[] +) => { + for (let x of maps) { + if (x != null) { + for (let pair of x) { + dest.set(pair[0], pair[1]); + } + } + } + return dest; +}; + +/** + * Merges all given objects in left-to-right order into `dest`. + * Returns `dest`. + * + * @param dest - target object + * @param objects - input objects + */ +export const mergeObj = ( + dest: IObjectOf, + ...objects: Nullable>[] +): IObjectOf => Object.assign(dest, ...objects); diff --git a/packages/object-utils/src/partition-keys.ts b/packages/object-utils/src/partition-keys.ts new file mode 100644 index 0000000000..df7d906686 --- /dev/null +++ b/packages/object-utils/src/partition-keys.ts @@ -0,0 +1,55 @@ +import type { Keys } from "@thi.ng/api"; +import { empty } from "./empty.js"; +import { selectKeysObj } from "./select-keys.js"; +import { withoutKeysObj } from "./without-keys.js"; + +/** + * Same as {@link partitionKeysObj}, but for maps. + * + * @param src + * @param ks + */ +export const partitionKeysMap = ( + src: Map, + ks: Iterable +): [Map, Map] => { + const destA = empty(src, Map); + const destB = empty(src, Map); + for (let k of ks) { + (src.has(k) ? destA : destB).set(k, src.get(k)); + } + return [destA, destB]; +}; + +/** + * Takes an object and a number of its property keys, returns a 2-tuple of + * segmented versions of the original object: `[obj-with-only-selected-keys, + * obj-without-selected-keys]`. + * + * @remarks + * The segmented versions are produced by {@link selectKeysObj} and + * {@link withoutKeysObj} respectively. + * + * @example + * ```ts tangle:../export/partition-keys.ts + * import { partitionKeysObj } from "@thi.ng/object-utils"; + * + * console.log( + * partitionKeysObj({ a: 1, b: 2, c: 3, d: 4 }, ["a", "c", "e"]) + * ); + * // [ + * // { a: 1, c: 3 }, + * // { b: 2, d: 4 } + * // ] + * ``` + * + * @param obj + * @param keys + */ +export const partitionKeysObj = ( + obj: T, + keys: Iterable> +): [Partial, Partial] => { + const $keys = Array.isArray(keys) ? keys : [...keys]; + return [selectKeysObj(obj, $keys), withoutKeysObj(obj, $keys)]; +}; diff --git a/packages/object-utils/src/rename-keys.ts b/packages/object-utils/src/rename-keys.ts new file mode 100644 index 0000000000..9fb9ec32e6 --- /dev/null +++ b/packages/object-utils/src/rename-keys.ts @@ -0,0 +1,110 @@ +import type { Fn2, Nullable } from "@thi.ng/api"; +import { isArray } from "@thi.ng/checks/is-array"; +import { empty } from "./empty.js"; + +/** + * Renames keys in `src` using mapping provided by key map `km`. Does + * support key swapping / swizzling. Does not modify original. + * + * @remarks + * Also see {@link renameKeysObj} for related functionality & example. + * + * @param src - source map + * @param km - key mappings + * @param out - result map + */ +export const renameKeysMap = ( + src: Map, + km: Map, + out?: Map +) => { + out = out || empty(src, Map); + for (let [k, v] of src) { + out!.set(km.has(k) ? km.get(k)! : k, v); + } + return out; +}; + +/** + * Renames keys in `src` using mapping provided by key map `km`. Does + * support key swapping / swizzling. Does not modify original. + * + * ```ts tangle:../export/rename-keys.ts + * import { renameKeysObj } from "@thi.ng/object-utils"; + * + * // swap a & b, rename c + * console.log( + * renameKeysObj({a: 1, b: 2, c: 3}, {a: "b", b: "a", c: "cc"}) + * ); + * // {b: 1, a: 2, cc: 3} + * ``` + * + * @param src - source object + * @param km - key mappings + * @param out - result object + */ +export const renameKeysObj = ( + src: T, + km: { [id in keyof T]?: PropertyKey }, + out: any = {} +) => { + for (let k in src) { + out[km.hasOwnProperty(k) ? km[k] : k] = src[k]; + } + return out; +}; + +/** + * Similar to (combination of) {@link renameKeysObj} and + * {@link selectDefinedKeysObj}. Takes a `src` object and `keys`, an object of + * mappings to rename given keys and (optionally) transform their values. + * Returns new object. If `src` is nullish itself, returns an empty object. + * + * @remarks + * Only keys with non-nullish values (in `src`) are being processed. The `keys` + * object uses the original key names as keys and the new keys as their values + * (like {@link renameKeysObj}). If a transformation of a key's value is + * desired, the format is `{ oldname: [newname, xform] }`, where `xform` is a + * 2-arg function, receiving the original value of `oldname` and the entire + * `src` object as 2nd arg. The return value of that function will be used as + * the value of `newname`. + * + * @example + * ```ts tangle:../export/rename-transformed.ts + * import { renameTransformedKeys } from "@thi.ng/object-utils"; + * + * console.log( + * renameTransformedKeys( + * // source object + * { a: 1, b: 2, c: null }, + * // mappings + * { + * // rename a => aa + * a: "aa", + * // rename & transform + * b: ["bb", (x, src) => x * 10 + src.a] + * // ignored, since original c is null + * c: "cc" + * } + * ) + * ); + * // { aa: 1, bb: 21 } + * ``` + * + * @param src - + * @param keys - + */ +export const renameTransformedKeys = ( + src: Nullable, + keys: Record]> +) => { + if (!src) return {}; + const res: any = {}; + for (let $k in keys) { + const spec = keys[$k]; + const [k, fn] = isArray(spec) ? spec : [spec]; + const val = src[$k]; + if (val != null) res[k] = fn ? fn(val, src) : val; + } + return res; +}; diff --git a/packages/object-utils/src/select-keys.ts b/packages/object-utils/src/select-keys.ts new file mode 100644 index 0000000000..4fa65e76e5 --- /dev/null +++ b/packages/object-utils/src/select-keys.ts @@ -0,0 +1,76 @@ +import type { Keys } from "@thi.ng/api"; +import { empty } from "./empty.js"; + +/** + * Returns a new map of same type as input only containing given keys + * (and only if they existed in the original map). + * + * @param src - source map + * @param ks - selected keys + */ +export const selectKeysMap = ( + src: Map, + ks: Iterable +): Map => { + const dest = empty(src, Map); + for (let k of ks) { + src.has(k) && dest.set(k, src.get(k)); + } + return dest; +}; + +/** + * Similar to {@link selectKeysMap}, but only selects keys if their value is + * defined (i.e. non-nullish). + * + * @param src - + * @param ks - + */ +export const selectDefinedKeysMap = ( + src: Map, + ks: Iterable +): Map => { + const dest = empty(src, Map); + for (let k of ks) { + const val = src.get(k); + if (val != null) dest.set(k, val); + } + return dest; +}; + +/** + * Returns a new object only containing given keys (and only if they + * existed in the original). + * + * @param src - source object + * @param ks - selected keys + */ +export const selectKeysObj = ( + src: T, + ks: Iterable> +): Partial => { + const dest: Partial = {}; + for (let k of ks) { + src.hasOwnProperty(k) && (dest[k] = src[k]); + } + return dest; +}; + +/** + * Similar to {@link selectKeysObj}, but only selects keys if their value is + * defined (i.e. non-nullish). + * + * @param src - + * @param ks - + */ +export const selectDefinedKeysObj = ( + src: T, + ks: Iterable +) => { + const res: Partial = {}; + for (let k of ks) { + const val = src[k]; + if (val != null) res[k] = val; + } + return res; +}; diff --git a/packages/object-utils/src/without-keys.ts b/packages/object-utils/src/without-keys.ts new file mode 100644 index 0000000000..022a8ab9db --- /dev/null +++ b/packages/object-utils/src/without-keys.ts @@ -0,0 +1,30 @@ +import type { Keys } from "@thi.ng/api"; +import { isSet } from "@thi.ng/checks/is-set"; +import { empty } from "./empty.js"; + +const __ensureSet = (x: Iterable) => + isSet(x) ? >x : new Set(x); + +export const withoutKeysMap = (src: Map, keys: Iterable) => { + const ks = __ensureSet(keys); + const dest = empty(src, Map); + for (let p of src.entries()) { + const k = p[0]; + !ks.has(k) && dest.set(k, p[1]); + } + return dest; +}; + +export const withoutKeysObj = ( + src: T, + keys: Iterable> +) => { + const ks = __ensureSet(keys); + const dest: Partial = {}; + for (let k in src) { + src.hasOwnProperty(k) && + !ks.has(>k) && + (dest[>k] = src[>k]); + } + return dest; +}; diff --git a/packages/object-utils/test/merge.test.ts b/packages/object-utils/test/merge.test.ts new file mode 100644 index 0000000000..21eef15b05 --- /dev/null +++ b/packages/object-utils/test/merge.test.ts @@ -0,0 +1,93 @@ +import type { Fn, FnN } from "@thi.ng/api"; +import { expect, test } from "bun:test"; +import { + meldApplyObj, + meldDeepObj, + mergeApplyMap, + mergeApplyObj, + mergeDeepObj, +} from "../src/index.js"; + +test("mergeApply map", () => { + expect( + mergeApplyMap( + new Map([ + ["a", 1], + ["b", 2], + ["c", 3], + ]), + new Map>([ + ["a", (x) => x + 10], + ["b", 20], + ["d", 40], + ]) + ) + ).toEqual( + new Map([ + ["a", 11], + ["b", 20], + ["c", 3], + ["d", 40], + ]) + ); +}); + +test("mergeApply object", () => { + const orig = { a: 1, b: 2, c: 3 }; + const src = { ...orig }; + expect(mergeApplyObj(src, { a: (x) => x + 10, b: 20, d: 40 })).toEqual({ + a: 11, + b: 20, + c: 3, + d: 40, + }); + expect(src).toEqual(orig); +}); + +test("mergeApply pollute", () => { + const inc: FnN = (x) => x + 1; + expect( + mergeApplyObj( + { a: 1, ["__proto__"]: 1 }, + { a: inc, ["__proto__"]: inc } + ) + ).toEqual({ a: 2 }); + expect( + meldApplyObj({ a: 1, ["__proto__"]: 1 }, { a: inc, ["__proto__"]: inc }) + ).toEqual({ + a: 2, + ["__proto__"]: 1, + }); +}); + +test("mergeDeepObj basic", () => { + const orig = { a: { b: { c: 1 } } }; + const src = { ...orig }; + expect( + mergeDeepObj(src, { a: { b: { d: 2 }, e: { f: 3 } }, g: 4 }) + ).toEqual({ a: { b: { c: 1, d: 2 }, e: { f: 3 } }, g: 4 }); + expect(src).toEqual(orig); +}); + +test("meldDeepObj basic", () => { + const orig = { a: { b: { c: 1 } } }; + const src = { ...orig }; + const dest = meldDeepObj(src, { + a: { b: { d: 2 }, e: { f: 3 } }, + g: 4, + }); + expect(dest).toEqual({ + a: { b: { c: 1, d: 2 }, e: { f: 3 } }, + g: 4, + }); + expect(src === dest).toBeTrue(); + expect(src).not.toEqual(orig); +}); + +test("meldDeepObj pollute", () => { + const p1 = JSON.parse(`{ "a": 1, "__proto__": { "eek": 2 } }`); + const p2 = JSON.parse(`{ "a": 1, "b": { "__proto__": { "eek": 2 } } }`); + expect(meldDeepObj({}, p1)).toEqual({ a: 1 }); + expect(meldDeepObj({}, p2)).toEqual(p2); + expect(meldDeepObj({ b: { c: 1 } }, p2)).toEqual({ a: 1, b: { c: 1 } }); +}); diff --git a/packages/object-utils/test/object.test.ts b/packages/object-utils/test/object.test.ts new file mode 100644 index 0000000000..baa83561c9 --- /dev/null +++ b/packages/object-utils/test/object.test.ts @@ -0,0 +1,16 @@ +import { expect, test } from "bun:test"; +import { renameTransformedKeys } from "../src/index.js"; + +test("renameTransformedKeys", () => { + expect( + renameTransformedKeys( + { a: 1, b: 2, c: null }, + { + a: "aa", + b: ["bb", (x, src) => x * 10 + src.a], + c: "cc", + } + ) + ).toEqual({ aa: 1, bb: 21 }); + expect(renameTransformedKeys(null, { a: "aa" })).toEqual({}); +}); diff --git a/packages/object-utils/test/tsconfig.json b/packages/object-utils/test/tsconfig.json new file mode 100644 index 0000000000..10a781ee02 --- /dev/null +++ b/packages/object-utils/test/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "types": ["bun-types"], + "noEmit": true + }, + "include": ["./**/*.ts"] +} diff --git a/packages/object-utils/tpl.readme.md b/packages/object-utils/tpl.readme.md new file mode 100644 index 0000000000..726cfe4562 --- /dev/null +++ b/packages/object-utils/tpl.readme.md @@ -0,0 +1,38 @@ + + + + +## About + +{{pkg.description}} + +This package contains functionality which was previously part of and has been +extracted from the [@thi.ng/associative](https://thi.ng/associative) package. + +{{meta.status}} + +{{repo.supportPackages}} + +{{repo.relatedPackages}} + +{{meta.blogPosts}} + +## Installation + +{{pkg.install}} + +{{pkg.size}} + +## Dependencies + +{{pkg.deps}} + +{{repo.examples}} + +## API + +{{pkg.docs}} + +TODO + + diff --git a/packages/object-utils/tsconfig.json b/packages/object-utils/tsconfig.json new file mode 100644 index 0000000000..1cd5465cf2 --- /dev/null +++ b/packages/object-utils/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "." + }, + "include": [ + "./src/**/*.ts" + ] +}