@inglorious/utils 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inglorious/utils",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "A set of general-purpose utility functions designed with functional programming principles in mind.",
5
5
  "author": "IceOnFire <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
6
6
  "license": "MIT",
@@ -24,7 +24,7 @@
24
24
  "./*": "./src/*"
25
25
  },
26
26
  "files": [
27
- "src/*"
27
+ "src"
28
28
  ],
29
29
  "publishConfig": {
30
30
  "access": "public"
@@ -29,7 +29,17 @@ export function contains(arr, item) {
29
29
  * @returns {Array} - The original array or the value wrapped in a new array.
30
30
  */
31
31
  export function ensureArray(arr) {
32
- return Array.isArray(arr) ? arr : [arr]
32
+ return isArray(arr) ? arr : [arr]
33
+ }
34
+
35
+ /**
36
+ * Checks if a value is an array.
37
+ * @param {*} arr - The value to check.
38
+ *
39
+ * @returns {boolean} True if the value is an array, false otherwise.
40
+ */
41
+ export function isArray(arr) {
42
+ return Array.isArray(arr)
33
43
  }
34
44
 
35
45
  /**
@@ -1,6 +1,15 @@
1
1
  import { expect, test } from "vitest"
2
2
 
3
- import { contains, ensureArray, max, min, pop, push, remove } from "./array.js"
3
+ import {
4
+ contains,
5
+ ensureArray,
6
+ isArray,
7
+ max,
8
+ min,
9
+ pop,
10
+ push,
11
+ remove,
12
+ } from "./array.js"
4
13
 
5
14
  test("it should check if an array contains a value", () => {
6
15
  const arr = [3, 2, 6, 1, 7, 4, 5]
@@ -38,6 +47,18 @@ test("it should wrap the value in an array if it is not an array", () => {
38
47
  expect(ensureArray(str)).toStrictEqual([str])
39
48
  })
40
49
 
50
+ test("it should correctly identify an array", () => {
51
+ const arr = [1, "two", { three: 3 }]
52
+
53
+ expect(isArray(arr)).toBe(true)
54
+ })
55
+
56
+ test("it should correctly identify a non-array", () => {
57
+ const str = "hello"
58
+
59
+ expect(isArray(str)).toBe(false)
60
+ })
61
+
41
62
  test("it should find the maximum value of an array", () => {
42
63
  const arr = [3, 2, 6, 1, 7, 4, 5]
43
64
  const expectedResult = 7
@@ -3,6 +3,7 @@ const NEXT_LEVEL = 2
3
3
 
4
4
  /**
5
5
  * Creates a deep clone of the given object.
6
+ *
6
7
  * @param {Object} obj - The object to clone.
7
8
  * @returns {Object} A deep clone of the input object.
8
9
  */
@@ -12,6 +13,7 @@ export function clone(obj) {
12
13
 
13
14
  /**
14
15
  * Filters the properties of an object based on a callback function.
16
+ *
15
17
  * @param {Object} obj - The object to filter.
16
18
  * @param {Function} callback - A function that determines whether a property should be included.
17
19
  * Receives (key, value, obj) as arguments.
@@ -27,6 +29,7 @@ export function filter(obj, callback) {
27
29
 
28
30
  /**
29
31
  * Finds the first property in an object that satisfies the callback function.
32
+ *
30
33
  * @param {Object} obj - The object to search.
31
34
  * @param {Function} callback - A function that determines whether a property matches.
32
35
  * Receives (key, value, obj) as arguments.
@@ -40,6 +43,7 @@ export function find(obj, callback) {
40
43
 
41
44
  /**
42
45
  * Checks if a value is a plain object.
46
+ *
43
47
  * @param {*} obj - The value to check.
44
48
  * @returns {boolean} True if the value is a plain object, false otherwise.
45
49
  */
@@ -49,6 +53,7 @@ export function isObject(obj) {
49
53
 
50
54
  /**
51
55
  * Maps the properties of an object using a callback function.
56
+ *
52
57
  * @param {Object} obj - The object to map.
53
58
  * @param {Function} callback - A function that transforms each property.
54
59
  * Receives (key, value, obj) as arguments.
@@ -99,6 +104,7 @@ export function produce(baseState, recipe, ...args) {
99
104
 
100
105
  /**
101
106
  * Converts an object or array to a formatted string representation.
107
+ *
102
108
  * @param {*} obj - The object or array to convert.
103
109
  * @param {number} [indentationLevel=INITIAL_LEVEL] - The current indentation level (used for nested structures).
104
110
  * @returns {string} A string representation of the input object or array.
@@ -1,45 +1,92 @@
1
+ import { isFunction } from "../functions/function.js"
2
+ import { isArray } from "./array.js"
1
3
  import { isObject } from "./object.js"
2
4
 
3
5
  /**
4
- * Extends a target object by merging it with one or more source objects.
5
- * Performs a deep merge for nested objects and arrays.
6
+ * Creates a new object by deeply merging a target object with one or more source objects.
7
+ * This function is immutable; it does not modify the original `target` or `sources`.
8
+ * Nested objects are merged recursively.
9
+ * Arrays from a source object will overwrite arrays in the target object.
6
10
  *
7
- * @param {Object} target - The target object to extend.
11
+ * @param {Object} target - The base object.
8
12
  * @param {...Object} sources - The source objects to merge into the target object.
9
- * @returns {Object} - The extended target object.
13
+ * @returns {Object} - A new object containing the merged properties.
14
+ * @see {@link merge} for a mutable version.
10
15
  */
11
16
  export function extend(target, ...sources) {
12
- return merge({}, target, ...sources)
17
+ return extendWith(undefined, target, ...sources)
13
18
  }
14
19
 
15
20
  /**
16
21
  * Merges multiple source objects into a target object.
17
- * Performs a deep merge for nested objects and arrays.
22
+ * This function is mutable; it modifies the `target` object in place.
23
+ * Nested objects are merged recursively.
24
+ * Arrays from a source object will overwrite arrays in the target object.
18
25
  *
19
26
  * @param {Object} target - The target object to merge into.
20
27
  * @param {...Object} sources - The source objects to merge from.
21
28
  * @returns {Object} - The merged target object.
29
+ * @see {@link extend} for an immutable version.
22
30
  */
23
31
  export function merge(target, ...sources) {
32
+ return mergeWith(undefined, target, ...sources)
33
+ }
34
+
35
+ /**
36
+ * Creates a new object by deeply merging a target object with one or more source objects,
37
+ * using a custom merger function to handle specific properties.
38
+ * This function is immutable.
39
+ *
40
+ * @param {Function} [merger] - A function to customize merging behavior. It receives `(targetValue, sourceValue)`. If it returns `undefined`, the default merge logic is used.
41
+ * @param {Object} target - The base object.
42
+ * @param {...Object} sources - The source objects to merge.
43
+ * @returns {Object} A new object with the merged properties.
44
+ */
45
+ export function extendWith(merger, target, ...sources) {
46
+ return mergeWith(merger, {}, target, ...sources)
47
+ }
48
+
49
+ /**
50
+ * Merges multiple source objects into a target object, using a custom merger function.
51
+ * This function is mutable; it modifies the `target` object in place.
52
+ *
53
+ * @param {Function} [merger] - A function to customize merging behavior. It receives `(targetValue, sourceValue)`. If it returns `undefined`, the default merge logic is used.
54
+ * @param {Object} target - The target object to merge into.
55
+ * @param {...Object} sources - The source objects to merge from.
56
+ * @returns {Object} The merged target object.
57
+ */
58
+ export function mergeWith(merger, target, ...sources) {
24
59
  return sources
25
60
  .filter((source) => source != null)
26
- .reduce((acc, source) => deepMerge(acc, source), target)
61
+ .reduce((acc, source) => deepMerge(acc, source, merger), target)
27
62
  }
28
63
 
29
64
  /**
30
- * Recursively merges the properties of the source object into the target object.
65
+ * Recursively merges properties from a source object into a target object.
66
+ * This is a helper function for `merge` and `extend`.
31
67
  *
32
68
  * @param {Object} target - The target object to merge into.
33
69
  * @param {Object} source - The source object to merge from.
34
- * @returns {Object} - The merged target object.
70
+ * @param {Function} [merger] - An optional function to customize merging behavior for specific keys.
71
+ * @returns {Object} - The modified target object.
35
72
  */
36
- function deepMerge(target, source) {
73
+ function deepMerge(target, source, merger) {
37
74
  for (const [key, value] of Object.entries(source)) {
38
- if (Array.isArray(value) || isObject(value)) {
39
- if (target[key] === undefined) {
40
- target[key] = new value.__proto__.constructor()
75
+ if (isFunction(merger)) {
76
+ const mergedValue = merger(target[key], value)
77
+ if (mergedValue !== undefined) {
78
+ target[key] = mergedValue
79
+ continue
80
+ }
81
+ }
82
+
83
+ if (isArray(value)) {
84
+ target[key] = value
85
+ } else if (isObject(value)) {
86
+ if (!isObject(target[key])) {
87
+ target[key] = {}
41
88
  }
42
- target[key] = deepMerge(target[key], value)
89
+ target[key] = deepMerge(target[key], value, merger)
43
90
  } else {
44
91
  target[key] = value
45
92
  }
@@ -1,6 +1,7 @@
1
1
  import { expect, test } from "vitest"
2
2
 
3
- import { extend, merge } from "./objects.js"
3
+ import { isArray } from "./array.js"
4
+ import { extend, extendWith, merge, mergeWith } from "./objects.js"
4
5
 
5
6
  test("it should extend an object with another, producing a new object as a result", () => {
6
7
  const obj1 = {
@@ -32,13 +33,13 @@ test("it should extend an object with another, producing a new object as a resul
32
33
  primitiveMerged: 3,
33
34
  primitiveAdded: 4,
34
35
  primitiveArrayKept: [1, 2],
35
- primitiveArrayMerged: [5, 4],
36
+ primitiveArrayMerged: [5],
36
37
  primitiveArrayAdded: [6, 7],
37
38
  primitiveObjectKept: { a: 1 },
38
39
  primitiveObjectMerged: { b: 2, c: 4 },
39
40
  primitiveObjectAdded: { d: 4 },
40
41
  nestedArrayKept: [{ a: 1 }],
41
- nestedArrayMerged: [{ b: 2, c: 3, d: 4 }],
42
+ nestedArrayMerged: [{ d: 4 }],
42
43
  nestedArrayAdded: [{ e: 5 }],
43
44
  nestedObjectKept: { a: { b: 2 } },
44
45
  nestedObjectMerged: { c: { d: 4, e: 5 } },
@@ -80,13 +81,13 @@ test("it should deep merge an two objects, changing the first object in place",
80
81
  primitiveMerged: 3,
81
82
  primitiveAdded: 4,
82
83
  primitiveArrayKept: [1, 2],
83
- primitiveArrayMerged: [5, 4],
84
+ primitiveArrayMerged: [5],
84
85
  primitiveArrayAdded: [6, 7],
85
86
  primitiveObjectKept: { a: 1 },
86
87
  primitiveObjectMerged: { b: 2, c: 4 },
87
88
  primitiveObjectAdded: { d: 4 },
88
89
  nestedArrayKept: [{ a: 1 }],
89
- nestedArrayMerged: [{ b: 2, c: 3, d: 4 }],
90
+ nestedArrayMerged: [{ d: 4 }],
90
91
  nestedArrayAdded: [{ e: 5 }],
91
92
  nestedObjectKept: { a: { b: 2 } },
92
93
  nestedObjectMerged: { c: { d: 4, e: 5 } },
@@ -97,3 +98,99 @@ test("it should deep merge an two objects, changing the first object in place",
97
98
  expect(result).toStrictEqual(expectedResult)
98
99
  expect(result).toBe(obj1)
99
100
  })
101
+
102
+ test("it should extend an object with a custom merger, producing a new object", () => {
103
+ const obj1 = {
104
+ primitiveArrayMerged: [3, 4],
105
+ nestedArrayMerged: [{ b: 2, c: 3 }],
106
+ nestedObjectMerged: { c: { d: 4 } },
107
+ }
108
+ const obj2 = {
109
+ primitiveArrayMerged: [5],
110
+ nestedArrayMerged: [{ d: 4 }],
111
+ nestedObjectMerged: { c: { e: 5 } },
112
+ }
113
+
114
+ const merger = (targetValue, sourceValue) => {
115
+ if (isArray(targetValue) && isArray(sourceValue)) {
116
+ return targetValue.concat(sourceValue)
117
+ }
118
+ return undefined // Use default logic for non-arrays
119
+ }
120
+
121
+ const expectedResult = {
122
+ primitiveArrayMerged: [3, 4, 5], // Concatenated
123
+ nestedArrayMerged: [{ b: 2, c: 3 }, { d: 4 }], // Concatenated
124
+ nestedObjectMerged: { c: { d: 4, e: 5 } }, // Default deep merge
125
+ }
126
+
127
+ const result = extendWith(merger, obj1, obj2)
128
+ expect(result).toStrictEqual(expectedResult)
129
+ expect(result).not.toBe(obj1)
130
+ })
131
+
132
+ test("it should merge an object with a custom merger, changing the first object in place", () => {
133
+ const obj1 = {
134
+ primitiveArrayMerged: [3, 4],
135
+ nestedArrayMerged: [{ b: 2, c: 3 }],
136
+ nestedObjectMerged: { c: { d: 4 } },
137
+ }
138
+ const obj2 = {
139
+ primitiveArrayMerged: [5],
140
+ nestedArrayMerged: [{ d: 4 }],
141
+ nestedObjectMerged: { c: { e: 5 } },
142
+ }
143
+
144
+ const merger = (targetValue, sourceValue) => {
145
+ if (isArray(targetValue) && isArray(sourceValue)) {
146
+ return targetValue.concat(sourceValue)
147
+ }
148
+ return undefined // Use default logic for non-arrays
149
+ }
150
+
151
+ const expectedResult = {
152
+ primitiveArrayMerged: [3, 4, 5], // Concatenated
153
+ nestedArrayMerged: [{ b: 2, c: 3 }, { d: 4 }], // Concatenated
154
+ nestedObjectMerged: { c: { d: 4, e: 5 } }, // Default deep merge
155
+ }
156
+
157
+ const result = mergeWith(merger, obj1, obj2)
158
+ expect(result).toStrictEqual(expectedResult)
159
+ expect(result).toBe(obj1)
160
+ })
161
+
162
+ test("extendWith without a merger should behave like extend", () => {
163
+ const obj1 = { a: 1, b: { c: 2 }, d: [3] }
164
+ const obj2 = { a: 10, b: { d: 20 }, d: [30] }
165
+
166
+ const expected = {
167
+ a: 10,
168
+ b: { c: 2, d: 20 },
169
+ d: [30],
170
+ }
171
+
172
+ const result = extendWith(undefined, obj1, obj2)
173
+ expect(result).toStrictEqual(expected)
174
+ expect(result).not.toBe(obj1)
175
+
176
+ const extendResult = extend(obj1, obj2)
177
+ expect(result).toStrictEqual(extendResult)
178
+ })
179
+
180
+ test("mergeWith without a merger should behave like merge", () => {
181
+ const obj1 = { a: 1, b: { c: 2 }, d: [3] }
182
+ const obj2 = { a: 10, b: { d: 20 }, d: [30] }
183
+
184
+ const expected = {
185
+ a: 10,
186
+ b: { c: 2, d: 20 },
187
+ d: [30],
188
+ }
189
+
190
+ const result = mergeWith(undefined, obj1, obj2)
191
+ expect(result).toStrictEqual(expected)
192
+ expect(result).toBe(obj1)
193
+
194
+ const mergeResult = merge({ a: 1, b: { c: 2 }, d: [3] }, obj2)
195
+ expect(result).toStrictEqual(mergeResult)
196
+ })
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Checks if a value is a function.
3
+ *
4
+ * @param {*} func - The value to check.
5
+ * @returns {boolean} True if the value is a function, false otherwise.
6
+ */
7
+ export function isFunction(func) {
8
+ return typeof func === "function"
9
+ }
@@ -0,0 +1,15 @@
1
+ import { expect, test } from "vitest"
2
+
3
+ import { isFunction } from "./function.js"
4
+
5
+ test("it should correctly identify a function", () => {
6
+ const func = () => {}
7
+
8
+ expect(isFunction(func)).toBe(true)
9
+ })
10
+
11
+ test("it should correctly identify a non-function", () => {
12
+ const str = "hello"
13
+
14
+ expect(isFunction(str)).toBe(false)
15
+ })