Repository: char0n/ramda-adjunct (js)

Issue:
renameKeyWith
**Is your feature request related to a problem? Please describe.**

Renames single key of an object.

**Describe the solution you'd like**
```js
const obj = {A: 1};
RA.renameKeyWith(R.concat('a'), 'A', obj); //=> {aA: 1}
```

**Describe alternatives you've considered**

--

**Additional context**

This function should be implemented as specialization of [R.renameKeysWith](https://char0n.github.io/ramda-adjunct/2.23.0/RA.html#.renameKeysWith). 

Related to https://github.com/char0n/ramda-adjunct/issues/1227

- [ ] implement the function
- [ ] add ref to ramda cookbook


Pull request description:
Add renameKeyWith
Closes #1228

Hi there! Some questions:

1. I'm not sure I got the signature in the documentation correct, could you please check it? Currently it is `(a -> b) -> c -> {a: *} -> {b: *}`
2. I did two versions, one is a bit simpler (the latest commit), but the other one (before the latest commit) has the advantage of being stable when changing to an existing key name. Which one do you prefer?
3. For typing I just went with `object` as it is used for `renameKeysWith`, but actually I would prefer to limit the input to a `Record` and then limit the key to be a key of said record. The more complicated version has the disadvantage that it relies on `mergeRight` which we would have to use from `ramda-adjunct` even though it is deprecated to keep compatibilty with older `ramda` versions that do not include `mergeRight` yet. What do you think?

PS: There is no mention that PhantomJS is needed to run tests locally. I could either add it as devDependency or to the documentation in a separate PR if you want.

Patch:
diff --git a/package-lock.json b/package-lock.json
index e3b435314e..8caee0723c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3435,12 +3435,12 @@
       },
       "dependencies": {
         "debug": {
-          "version": "4.1.1",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
-          "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
+          "version": "4.2.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
+          "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
           "dev": true,
           "requires": {
-            "ms": "^2.1.1"
+            "ms": "2.1.2"
           }
         },
         "ms": {
@@ -4869,16 +4869,28 @@
       "dev": true
     },
     "codecov": {
-      "version": "3.7.2",
-      "resolved": "https://registry.npmjs.org/codecov/-/codecov-3.7.2.tgz",
-      "integrity": "sha512-fmCjAkTese29DUX3GMIi4EaKGflHa4K51EoMc29g8fBHawdk/+KEq5CWOeXLdd9+AT7o1wO4DIpp/Z1KCqCz1g==",
+      "version": "3.8.0",
+      "resolved": "https://registry.npmjs.org/codecov/-/codecov-3.8.0.tgz",
+      "integrity": "sha512-7E/S7hmq2CJvCMBMu+aRACO9jxQX1HJug/M3ub8+t84R+5Ai2T5sFMxS3W8P41m2A63+VSAAL4U0aBlqZXkJPw==",
       "dev": true,
       "requires": {
         "argv": "0.0.2",
         "ignore-walk": "3.0.3",
-        "js-yaml": "3.13.1",
-        "teeny-request": "6.0.1",
+        "js-yaml": "3.14.0",
+        "teeny-request": "7.0.1",
         "urlgrey": "0.4.4"
+      },
+      "dependencies": {
+        "js-yaml": {
+          "version": "3.14.0",
+          "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz",
+          "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==",
+          "dev": true,
+          "requires": {
+            "argparse": "^1.0.7",
+            "esprima": "^4.0.0"
+          }
+        }
       }
     },
     "collection-visit": {
@@ -9789,12 +9801,12 @@
       },
       "dependencies": {
         "debug": {
-          "version": "4.1.1",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
-          "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
+          "version": "4.2.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
+          "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
           "dev": true,
           "requires": {
-            "ms": "^2.1.1"
+            "ms": "2.1.2"
           }
         },
         "ms": {
@@ -9823,28 +9835,22 @@
       "dev": true
     },
     "https-proxy-agent": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz",
-      "integrity": "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==",
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz",
+      "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==",
       "dev": true,
       "requires": {
-        "agent-base": "5",
+        "agent-base": "6",
         "debug": "4"
       },
       "dependencies": {
-        "agent-base": {
-          "version": "5.1.1",
-          "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz",
-          "integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==",
-          "dev": true
-        },
         "debug": {
-          "version": "4.1.1",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
-          "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
+          "version": "4.2.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
+          "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
           "dev": true,
           "requires": {
-            "ms": "^2.1.1"
+            "ms": "2.1.2"
           }
         },
         "ms": {
@@ -10769,9 +10775,9 @@
       }
     },
     "jest-worker": {
-      "version": "26.3.0",
-      "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.3.0.tgz",
-      "integrity": "sha512-Vmpn2F6IASefL+DVBhPzI2J9/GJUsqzomdeN+P+dK8/jKxbh8R3BtFnx3FIta7wYlPU62cpJMJQo4kuOowcMnw==",
+      "version": "26.5.0",
+      "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.5.0.tgz",
+      "integrity": "sha512-kTw66Dn4ZX7WpjZ7T/SUDgRhapFRKWmisVAF0Rv4Fu8SLFD7eLbqpLvbxVqYhSgaWa7I+bW7pHnbyfNsH6stug==",
       "dev": true,
       "requires": {
         "@types/node": "*",
@@ -15445,16 +15451,24 @@
       }
     },
     "teeny-request": {
-      "version": "6.0.1",
-      "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-6.0.1.tgz",
-      "integrity": "sha512-TAK0c9a00ELOqLrZ49cFxvPVogMUFaWY8dUsQc/0CuQPGF+BOxOQzXfE413BAk2kLomwNplvdtMpeaeGWmoc2g==",
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-7.0.1.tgz",
+      "integrity": "sha512-sasJmQ37klOlplL4Ia/786M5YlOcoLGQyq2TE4WHSRupbAuDaQW0PfVxV4MtdBtRJ4ngzS+1qim8zP6Zp35qCw==",
       "dev": true,
       "requires": {
         "http-proxy-agent": "^4.0.0",
-        "https-proxy-agent": "^4.0.0",
-        "node-fetch": "^2.2.0",
+        "https-proxy-agent": "^5.0.0",
+        "node-fetch": "^2.6.1",
         "stream-events": "^1.0.5",
-        "uuid": "^3.3.2"
+        "uuid": "^8.0.0"
+      },
+      "dependencies": {
+        "uuid": {
+          "version": "8.3.1",
+          "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz",
+          "integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==",
+          "dev": true
+        }
       }
     },
     "temp-dir": {
@@ -15499,19 +15513,19 @@
       }
     },
     "terser-webpack-plugin": {
-      "version": "4.2.2",
-      "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-4.2.2.tgz",
-      "integrity": "sha512-3qAQpykRTD5DReLu5/cwpsg7EZFzP3Q0Hp2XUWJUw2mpq2jfgOKTZr8IZKKnNieRVVo1UauROTdhbQJZveGKtQ==",
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-4.2.3.tgz",
+      "integrity": "sha512-jTgXh40RnvOrLQNgIkwEKnQ8rmHjHK4u+6UBEi+W+FPmvb+uo+chJXntKe7/3lW5mNysgSWD60KyesnhW8D6MQ==",
       "dev": true,
       "requires": {
         "cacache": "^15.0.5",
         "find-cache-dir": "^3.3.1",
-        "jest-worker": "^26.3.0",
+        "jest-worker": "^26.5.0",
         "p-limit": "^3.0.2",
-        "schema-utils": "^2.7.1",
+        "schema-utils": "^3.0.0",
         "serialize-javascript": "^5.0.1",
         "source-map": "^0.6.1",
-        "terser": "^5.3.2",
+        "terser": "^5.3.4",
         "webpack-sources": "^1.4.3"
       },
       "dependencies": {
@@ -15690,13 +15704,13 @@
           }
         },
         "schema-utils": {
-          "version": "2.7.1",
-          "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz",
-          "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==",
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz",
+          "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==",
           "dev": true,
           "requires": {
-            "@types/json-schema": "^7.0.5",
-            "ajv": "^6.12.4",
+            "@types/json-schema": "^7.0.6",
+            "ajv": "^6.12.5",
             "ajv-keywords": "^3.5.2"
           }
         },
@@ -15715,6 +15729,16 @@
           "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
           "dev": true
         },
+        "source-map-support": {
+          "version": "0.5.19",
+          "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz",
+          "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==",
+          "dev": true,
+          "requires": {
+            "buffer-from": "^1.0.0",
+            "source-map": "^0.6.0"
+          }
+        },
         "ssri": {
           "version": "8.0.0",
           "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.0.tgz",
@@ -15739,14 +15763,22 @@
           }
         },
         "terser": {
-          "version": "5.3.2",
-          "resolved": "https://registry.npmjs.org/terser/-/terser-5.3.2.tgz",
-          "integrity": "sha512-H67sydwBz5jCUA32ZRL319ULu+Su1cAoZnnc+lXnenGRYWyLE3Scgkt8mNoAsMx0h5kdo758zdoS0LG9rYZXDQ==",
+          "version": "5.3.4",
+          "resolved": "https://registry.npmjs.org/terser/-/terser-5.3.4.tgz",
+          "integrity": "sha512-dxuB8KQo8Gt6OVOeLg/rxfcxdNZI/V1G6ze1czFUzPeCFWZRtvZMgSzlZZ5OYBZ4HoG607F6pFPNLekJyV+yVw==",
           "dev": true,
           "requires": {
             "commander": "^2.20.0",
-            "source-map": "~0.6.1",
-            "source-map-support": "~0.5.12"
+            "source-map": "~0.7.2",
+            "source-map-support": "~0.5.19"
+          },
+          "dependencies": {
+            "source-map": {
+              "version": "0.7.3",
+              "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
+              "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==",
+              "dev": true
+            }
           }
         },
         "yallist": {
diff --git a/package.json b/package.json
index 336db92564..061da285b4 100644
--- a/package.json
+++ b/package.json
@@ -152,7 +152,7 @@
     "babel-plugin-istanbul": "6.0.0",
     "better-npm-run": "0.1.1",
     "chai": "4.2.0",
-    "codecov": "3.7.2",
+    "codecov": "3.8.0",
     "conventional-changelog-cli": "2.1.0",
     "docdash": "git+https://github.com/char0n/docdash.git#534b44382138a55dd8d93642c979e51e46471185",
     "dtslint": "=4.0.4",
@@ -184,7 +184,7 @@
     "rimraf": "3.0.2",
     "sanctuary-show": "2.0.0",
     "sinon": "9.1.0",
-    "terser-webpack-plugin": "4.2.2",
+    "terser-webpack-plugin": "4.2.3",
     "testem": "=3.2.0",
     "tslint": "=6.1.3",
     "typescript": "=4.0.3",
diff --git a/src/fantasy-land/Identity.js b/src/fantasy-land/Identity.js
index 166281fd33..f471d30253 100644
--- a/src/fantasy-land/Identity.js
+++ b/src/fantasy-land/Identity.js
@@ -66,7 +66,6 @@ class Identity {
   /**
    * Private constructor. Use {@link RA.Identity.of|Identity.of} instead.
    *
-   * @private
    * @param {*} value
    * @return {RA.Identity}
    */
diff --git a/src/index.js b/src/index.js
index cb3a54b746..cb3d694b13 100644
--- a/src/index.js
+++ b/src/index.js
@@ -159,6 +159,7 @@ export { default as invokeArgs } from './invokeArgs';
 export { default as paths } from './paths';
 export { default as renameKeys } from './renameKeys';
 export { default as renameKeysWith } from './renameKeysWith';
+export { default as renameKeyWith } from './renameKeyWith';
 export { default as mergeRight } from './mergeRight';
 export { default as mergeLeft } from './mergeRight';
 export { default as resetToDefault } from './mergeRight';
diff --git a/src/renameKeyWith.js b/src/renameKeyWith.js
new file mode 100644
index 0000000000..539aadfd75
--- /dev/null
+++ b/src/renameKeyWith.js
@@ -0,0 +1,30 @@
+import { curry, equals, when } from 'ramda';
+
+import renameKeysWith from './renameKeysWith';
+
+/**
+ * Creates a new object with the own properties of the provided object, but the
+ * key `key` renamed according to logic of renaming function.
+ *
+ * Keep in mind that in case the new key name already existed on the object,
+ * the behaviour is undefined and the result may vary between various JS engines!
+ *
+ * @func renameKeyWith
+ * @memberOf RA
+ * @since {@link https://char0n.github.io/ramda-adjunct/2.29.0|v2.29.0}
+ * @category Object
+ * @sig (k -> k) -> k -> {k: v} -> {k: v}
+ * @param {Function} fn Function that renames the keys
+ * @param {!string} key Key to rename
+ * @param {!Object} obj Provided object
+ * @return {!Object} New object with renamed key
+ * @see {@link RA.renameKeysWith|renameKeysWith}
+ * @example
+ *
+ * RA.renameKeyWith(R.concat('a'), 'A', { A: 1 }) //=> { aA: 1 }
+ */
+const renameKeyWith = curry((fn, key, obj) =>
+  renameKeysWith(when(equals(key), fn), obj)
+);
+
+export default renameKeyWith;
diff --git a/types/index.d.ts b/types/index.d.ts
index a1c91709e4..96cb6e9b10 100644
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -561,6 +561,26 @@ declare namespace RamdaAdjunct {
         renameKeysWith(renameFn: (key: string) => string, obj: object): object;
         renameKeysWith(renameFn: (key: string) => string): (obj: object) => object;
 
+        /**
+         * Creates a new object with the own properties of the provided object, but the
+         * key `key` renamed according to logic of renaming function.
+         */
+        renameKeyWith(
+            renameFn: (key: string) => string,
+            key: string,
+            obj: object
+        ): object;
+        renameKeyWith(
+            renameFn: (key: string) => string,
+            key: string
+        ): (obj: object) => object;
+        renameKeyWith(
+            renameFn: (key: string) => string
+        ): {
+            (key: string, obj: object): object;
+            (key: string): (obj: object) => object;
+        };
+
         /**
          * Create a new object with the own properties of the second object merged with
          * the own properties of the first object. If a key exists in both objects,


Test patch:
diff --git a/test/renameKeyWith.js b/test/renameKeyWith.js
new file mode 100644
index 0000000000..9b1f2f71e1
--- /dev/null
+++ b/test/renameKeyWith.js
@@ -0,0 +1,42 @@
+import { assert } from 'chai';
+import { concat } from 'ramda';
+
+import * as RA from '../src';
+
+describe('renameKeyWith', function () {
+  it('should rename object keys', function () {
+    assert.deepEqual(RA.renameKeyWith(concat('a'), 'A', { A: 1 }), {
+      aA: 1,
+    });
+  });
+
+  it('should not change other key-value pairs', function () {
+    assert.deepEqual(RA.renameKeyWith(concat('a'), 'A', { A: 1, B: 2 }), {
+      aA: 1,
+      B: 2,
+    });
+  });
+
+  it('should return an empty object when renames keys on null and undefined', function () {
+    assert.deepEqual(RA.renameKeyWith(concat('a'), 'A', null), {});
+    assert.deepEqual(RA.renameKeyWith(concat('a'), 'A', undefined), {});
+  });
+
+  it('should return the original object when renaming a non-existant key', function () {
+    assert.deepEqual(RA.renameKeyWith(concat('a'), 'B', { A: 1 }), {
+      A: 1,
+    });
+  });
+
+  it('should be curried', function () {
+    assert.deepEqual(RA.renameKeyWith(concat('a'))('A')({ A: 1 }), {
+      aA: 1,
+    });
+    assert.deepEqual(RA.renameKeyWith(concat('a'), 'A')({ A: 1 }), {
+      aA: 1,
+    });
+    assert.deepEqual(RA.renameKeyWith(concat('a'))('A', { A: 1 }), {
+      aA: 1,
+    });
+  });
+});
diff --git a/types/test/renameKeyWith.ts b/types/test/renameKeyWith.ts
new file mode 100644
index 0000000000..a224334159
--- /dev/null
+++ b/types/test/renameKeyWith.ts
@@ -0,0 +1,10 @@
+import * as RA from 'ramda-adjunct';
+
+RA.renameKeyWith((str) => 'a' + str, 'A', { A: 1 }); // $ExpectType object
+RA.renameKeyWith((str) => 'a' + str)('A', { A: 1 }); // $ExpectType object
+RA.renameKeyWith((str) => 'a' + str)('A')({ A: 1 }); // $ExpectType object
+RA.renameKeyWith((str) => 'a' + str, 'A')({ A: 1 }); // $ExpectType object
+
+RA.renameKeyWith((str) => 1, 'A', { A: 1 }); // $ExpectError
+RA.renameKeyWith((str) => 'a' + str, 1, { A: 1 }); // $ExpectError
+RA.renameKeyWith((str) => 'a' + str, 'A', 1); // $ExpectError