@ls-stack/utils 3.53.0 → 3.54.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.
@@ -100,6 +100,15 @@ var match = {
100
100
  return ["deepEqual", v];
101
101
  })
102
102
  ]),
103
+ key: {
104
+ any: "$pqkc:any$",
105
+ anyOther: "$pqkc:anyOther$",
106
+ numeric: "$pqkc:numeric$",
107
+ startingWith: (substring) => `$pqkc:startingWith:${substring}$`,
108
+ endingWith: (substring) => `$pqkc:endingWith:${substring}$`,
109
+ containing: (substring) => `$pqkc:contains:${substring}$`,
110
+ matchingRegex: (regex) => `$pqkc:matchesRegex:${regex}$`
111
+ },
103
112
  not: {
104
113
  hasType: {
105
114
  string: createComparison(["not", ["hasType", "string"]]),
@@ -169,12 +178,93 @@ var match = {
169
178
  noExtraKeys: (partialShape) => createComparison(["not", ["withNoExtraKeys", partialShape]]),
170
179
  deepNoExtraKeys: (partialShape) => createComparison(["not", ["withDeepNoExtraKeys", partialShape]]),
171
180
  noExtraDefinedKeys: (partialShape) => createComparison(["not", ["noExtraDefinedKeys", partialShape]]),
172
- deepNoExtraDefinedKeys: (partialShape) => createComparison(["not", ["deepNoExtraDefinedKeys", partialShape]])
181
+ deepNoExtraDefinedKeys: (partialShape) => createComparison(["not", ["deepNoExtraDefinedKeys", partialShape]]),
182
+ key: {
183
+ any: "$pqkc-not:any$",
184
+ anyOther: "$pqkc-not:anyOther$",
185
+ numeric: "$pqkc-not:numeric$",
186
+ startingWith: (substring) => `$pqkc-not:startingWith:${substring}$`,
187
+ endingWith: (substring) => `$pqkc-not:endingWith:${substring}$`,
188
+ containing: (substring) => `$pqkc-not:contains:${substring}$`,
189
+ matchingRegex: (regex) => `$pqkc-not:matchesRegex:${regex}$`
190
+ }
173
191
  }
174
192
  };
175
193
  function isComparison(value) {
176
194
  return value && typeof value === "object" && "~sc" in value;
177
195
  }
196
+ function isKeyMatcher(key) {
197
+ return typeof key === "string" && (key.startsWith("$pqkc:") || key.startsWith("$pqkc-not:"));
198
+ }
199
+ function parseKeyMatcher(key) {
200
+ if (!isKeyMatcher(key)) return null;
201
+ const negated = key.startsWith("$pqkc-not:");
202
+ const prefix = negated ? "$pqkc-not:" : "$pqkc:";
203
+ const suffix = "$";
204
+ if (!key.endsWith(suffix)) return null;
205
+ const content = key.slice(prefix.length, -suffix.length);
206
+ if (content === "any") {
207
+ return { type: "any", negated };
208
+ }
209
+ if (content === "anyOther") {
210
+ return { type: "anyOther", negated };
211
+ }
212
+ if (content === "numeric") {
213
+ return { type: "numeric", negated };
214
+ }
215
+ if (content.startsWith("startingWith:")) {
216
+ return { type: "startingWith", parameter: content.slice("startingWith:".length), negated };
217
+ }
218
+ if (content.startsWith("endingWith:")) {
219
+ return { type: "endingWith", parameter: content.slice("endingWith:".length), negated };
220
+ }
221
+ if (content.startsWith("contains:")) {
222
+ return { type: "contains", parameter: content.slice("contains:".length), negated };
223
+ }
224
+ if (content.startsWith("matchesRegex:")) {
225
+ const regexStr = content.slice("matchesRegex:".length);
226
+ try {
227
+ if (regexStr.startsWith("/") && regexStr.lastIndexOf("/") > 0) {
228
+ const lastSlashIndex = regexStr.lastIndexOf("/");
229
+ const pattern = regexStr.slice(1, lastSlashIndex);
230
+ const flags = regexStr.slice(lastSlashIndex + 1);
231
+ return { type: "matchesRegex", parameter: new RegExp(pattern, flags), negated };
232
+ } else {
233
+ return { type: "matchesRegex", parameter: new RegExp(regexStr), negated };
234
+ }
235
+ } catch {
236
+ return null;
237
+ }
238
+ }
239
+ return null;
240
+ }
241
+ function keyMatchesPattern(key, matcher) {
242
+ let matches = false;
243
+ switch (matcher.type) {
244
+ case "any":
245
+ matches = true;
246
+ break;
247
+ case "anyOther":
248
+ matches = true;
249
+ break;
250
+ case "numeric":
251
+ matches = /^\d+$/.test(key);
252
+ break;
253
+ case "startingWith":
254
+ matches = key.startsWith(matcher.parameter);
255
+ break;
256
+ case "endingWith":
257
+ matches = key.endsWith(matcher.parameter);
258
+ break;
259
+ case "contains":
260
+ matches = key.includes(matcher.parameter);
261
+ break;
262
+ case "matchesRegex":
263
+ matches = matcher.parameter.test(key);
264
+ break;
265
+ }
266
+ return matcher.negated ? !matches : matches;
267
+ }
178
268
  function executeComparison(target, comparison, context) {
179
269
  const [type, value] = comparison;
180
270
  switch (type) {
@@ -871,7 +961,21 @@ function partialEqualInternal(target, sub, context) {
871
961
  return false;
872
962
  }
873
963
  let allMatch = true;
964
+ const regularKeys = [];
965
+ const keyMatchers = [];
874
966
  for (const key of Object.keys(sub)) {
967
+ if (isKeyMatcher(key)) {
968
+ const matcher = parseKeyMatcher(key);
969
+ if (matcher) {
970
+ keyMatchers.push({ key, matcher, value: sub[key] });
971
+ }
972
+ } else {
973
+ regularKeys.push(key);
974
+ }
975
+ }
976
+ const explicitlySpecifiedKeys = /* @__PURE__ */ new Set();
977
+ for (const key of regularKeys) {
978
+ explicitlySpecifiedKeys.add(key);
875
979
  if (isComparison(sub[key]) && sub[key]["~sc"][0] === "keyNotBePresent") {
876
980
  if (has.call(target, key)) {
877
981
  const oldPath2 = context.path;
@@ -915,6 +1019,72 @@ function partialEqualInternal(target, sub, context) {
915
1019
  context.path = oldPath;
916
1020
  if (!result) allMatch = false;
917
1021
  }
1022
+ for (const { matcher, value } of keyMatchers) {
1023
+ let foundMatch = false;
1024
+ const matchedKeys = [];
1025
+ if (matcher.type === "anyOther") {
1026
+ for (const targetKey of Object.keys(target)) {
1027
+ if (!explicitlySpecifiedKeys.has(targetKey)) {
1028
+ const matches = keyMatchesPattern(targetKey, matcher);
1029
+ if (matches) {
1030
+ foundMatch = true;
1031
+ matchedKeys.push(targetKey);
1032
+ const oldPath = context.path;
1033
+ context.path = [...oldPath, targetKey];
1034
+ const result = partialEqualInternal(target[targetKey], value, context);
1035
+ context.path = oldPath;
1036
+ if (!result) allMatch = false;
1037
+ }
1038
+ }
1039
+ }
1040
+ } else if (matcher.type === "any") {
1041
+ let anyKeyMatched = false;
1042
+ for (const targetKey of Object.keys(target)) {
1043
+ if (keyMatchesPattern(targetKey, matcher)) {
1044
+ foundMatch = true;
1045
+ matchedKeys.push(targetKey);
1046
+ explicitlySpecifiedKeys.add(targetKey);
1047
+ const tempContext = {
1048
+ errors: [],
1049
+ path: [...context.path, targetKey]
1050
+ };
1051
+ const result = partialEqualInternal(target[targetKey], value, tempContext);
1052
+ if (result) {
1053
+ anyKeyMatched = true;
1054
+ break;
1055
+ }
1056
+ }
1057
+ }
1058
+ if (foundMatch && !anyKeyMatched) {
1059
+ addError(context, {
1060
+ message: `No keys matching pattern had the expected value`,
1061
+ received: { keysChecked: matchedKeys, availableKeys: Object.keys(target) }
1062
+ });
1063
+ allMatch = false;
1064
+ }
1065
+ } else {
1066
+ for (const targetKey of Object.keys(target)) {
1067
+ if (keyMatchesPattern(targetKey, matcher)) {
1068
+ foundMatch = true;
1069
+ matchedKeys.push(targetKey);
1070
+ explicitlySpecifiedKeys.add(targetKey);
1071
+ const oldPath = context.path;
1072
+ context.path = [...oldPath, targetKey];
1073
+ const result = partialEqualInternal(target[targetKey], value, context);
1074
+ context.path = oldPath;
1075
+ if (!result) allMatch = false;
1076
+ }
1077
+ }
1078
+ }
1079
+ if (!foundMatch) {
1080
+ const patternDescription = matcher.type === "anyOther" ? "anyOther (keys not explicitly specified)" : `${matcher.type}${matcher.parameter ? ` (${matcher.parameter})` : ""}`;
1081
+ addError(context, {
1082
+ message: `No keys found matching pattern: ${patternDescription}`,
1083
+ received: { availableKeys: Object.keys(target), explicitKeys: Array.from(explicitlySpecifiedKeys) }
1084
+ });
1085
+ allMatch = false;
1086
+ }
1087
+ }
918
1088
  return allMatch;
919
1089
  }
920
1090
  if (target !== sub) {
@@ -6,6 +6,16 @@ type ComparisonsType = [type: 'strStartsWith', value: string] | [type: 'strEndsW
6
6
  ] | [type: 'strContains', value: string] | [type: 'strMatchesRegex', value: RegExp] | [type: 'deepEqual', value: any] | [type: 'numIsGreaterThan', value: number] | [type: 'numIsGreaterThanOrEqual', value: number] | [type: 'numIsLessThan', value: number] | [type: 'numIsLessThanOrEqual', value: number] | [type: 'numIsInRange', value: [number, number]] | [type: 'arrayContains', value: any[]] | [type: 'arrayContainsInOrder', value: any[]] | [type: 'arrayStartsWith', value: any[]] | [type: 'arrayEndsWith', value: any[]] | [type: 'arrayLength', value: number] | [type: 'arrayMinLength', value: number] | [type: 'arrayMaxLength', value: number] | [type: 'arrayIncludes', value: any] | [type: 'arrayEvery', value: ComparisonsType] | [type: 'arraySome', value: ComparisonsType] | [type: 'jsonStringHasPartial', value: any] | [type: 'partialEqual', value: any] | [type: 'custom', value: (target: unknown) => boolean | {
7
7
  error: string;
8
8
  }] | [type: 'isInstanceOf', value: new (...args: any[]) => any] | [type: 'keyNotBePresent', value: null] | [type: 'not', value: ComparisonsType] | [type: 'any', value: ComparisonsType[]] | [type: 'all', value: ComparisonsType[]] | [type: 'withNoExtraKeys', partialShape: any] | [type: 'withDeepNoExtraKeys', partialShape: any] | [type: 'noExtraDefinedKeys', partialShape: any] | [type: 'deepNoExtraDefinedKeys', partialShape: any];
9
+ type KeyComparisonPrefix = 'pqkc' | 'pqkc-not';
10
+ type KeyComparison = {
11
+ any: `$${KeyComparisonPrefix}:any$`;
12
+ anyOther: `$${KeyComparisonPrefix}:anyOther$`;
13
+ numeric: `$${KeyComparisonPrefix}:numeric$`;
14
+ startingWith: `$${KeyComparisonPrefix}:startingWith:${string}$`;
15
+ endingWith: `$${KeyComparisonPrefix}:endingWith:${string}$`;
16
+ contains: `$${KeyComparisonPrefix}:contains:${string}$`;
17
+ matchesRegex: `$${KeyComparisonPrefix}:matchesRegex:${string}$`;
18
+ };
9
19
  type Comparison = {
10
20
  '~sc': ComparisonsType;
11
21
  };
@@ -59,6 +69,15 @@ type BaseMatch = {
59
69
  keyNotBePresent: Comparison;
60
70
  any: (...values: any[]) => Comparison;
61
71
  all: (...values: any[]) => Comparison;
72
+ key: {
73
+ any: KeyComparison['any'];
74
+ anyOther: KeyComparison['anyOther'];
75
+ numeric: KeyComparison['numeric'];
76
+ startingWith: (substring: string) => KeyComparison['startingWith'];
77
+ endingWith: (substring: string) => KeyComparison['endingWith'];
78
+ containing: (substring: string) => KeyComparison['contains'];
79
+ matchingRegex: (regex: RegExp) => KeyComparison['matchesRegex'];
80
+ };
62
81
  };
63
82
  type Match = BaseMatch & {
64
83
  not: BaseMatch;
@@ -91,7 +110,10 @@ type PartialError = {
91
110
  * partialEqual([1, 2, 3], match.array.endsWith([2, 3])); // true - suffix matching
92
111
  * partialEqual([1, 2, 3], match.array.length(3)); // true - exact length
93
112
  * partialEqual([1, 2, 3], match.array.includes(2)); // true - includes element
94
- * partialEqual([10, 20, 30], match.array.every(match.num.isGreaterThan(5))); // true - all elements match
113
+ * partialEqual(
114
+ * [10, 20, 30],
115
+ * match.array.every(match.num.isGreaterThan(5)),
116
+ * ); // true - all elements match
95
117
  * partialEqual([1, 10, 3], match.array.some(match.num.isGreaterThan(8))); // true - at least one matches
96
118
  *
97
119
  * // Special comparisons
@@ -6,6 +6,16 @@ type ComparisonsType = [type: 'strStartsWith', value: string] | [type: 'strEndsW
6
6
  ] | [type: 'strContains', value: string] | [type: 'strMatchesRegex', value: RegExp] | [type: 'deepEqual', value: any] | [type: 'numIsGreaterThan', value: number] | [type: 'numIsGreaterThanOrEqual', value: number] | [type: 'numIsLessThan', value: number] | [type: 'numIsLessThanOrEqual', value: number] | [type: 'numIsInRange', value: [number, number]] | [type: 'arrayContains', value: any[]] | [type: 'arrayContainsInOrder', value: any[]] | [type: 'arrayStartsWith', value: any[]] | [type: 'arrayEndsWith', value: any[]] | [type: 'arrayLength', value: number] | [type: 'arrayMinLength', value: number] | [type: 'arrayMaxLength', value: number] | [type: 'arrayIncludes', value: any] | [type: 'arrayEvery', value: ComparisonsType] | [type: 'arraySome', value: ComparisonsType] | [type: 'jsonStringHasPartial', value: any] | [type: 'partialEqual', value: any] | [type: 'custom', value: (target: unknown) => boolean | {
7
7
  error: string;
8
8
  }] | [type: 'isInstanceOf', value: new (...args: any[]) => any] | [type: 'keyNotBePresent', value: null] | [type: 'not', value: ComparisonsType] | [type: 'any', value: ComparisonsType[]] | [type: 'all', value: ComparisonsType[]] | [type: 'withNoExtraKeys', partialShape: any] | [type: 'withDeepNoExtraKeys', partialShape: any] | [type: 'noExtraDefinedKeys', partialShape: any] | [type: 'deepNoExtraDefinedKeys', partialShape: any];
9
+ type KeyComparisonPrefix = 'pqkc' | 'pqkc-not';
10
+ type KeyComparison = {
11
+ any: `$${KeyComparisonPrefix}:any$`;
12
+ anyOther: `$${KeyComparisonPrefix}:anyOther$`;
13
+ numeric: `$${KeyComparisonPrefix}:numeric$`;
14
+ startingWith: `$${KeyComparisonPrefix}:startingWith:${string}$`;
15
+ endingWith: `$${KeyComparisonPrefix}:endingWith:${string}$`;
16
+ contains: `$${KeyComparisonPrefix}:contains:${string}$`;
17
+ matchesRegex: `$${KeyComparisonPrefix}:matchesRegex:${string}$`;
18
+ };
9
19
  type Comparison = {
10
20
  '~sc': ComparisonsType;
11
21
  };
@@ -59,6 +69,15 @@ type BaseMatch = {
59
69
  keyNotBePresent: Comparison;
60
70
  any: (...values: any[]) => Comparison;
61
71
  all: (...values: any[]) => Comparison;
72
+ key: {
73
+ any: KeyComparison['any'];
74
+ anyOther: KeyComparison['anyOther'];
75
+ numeric: KeyComparison['numeric'];
76
+ startingWith: (substring: string) => KeyComparison['startingWith'];
77
+ endingWith: (substring: string) => KeyComparison['endingWith'];
78
+ containing: (substring: string) => KeyComparison['contains'];
79
+ matchingRegex: (regex: RegExp) => KeyComparison['matchesRegex'];
80
+ };
62
81
  };
63
82
  type Match = BaseMatch & {
64
83
  not: BaseMatch;
@@ -91,7 +110,10 @@ type PartialError = {
91
110
  * partialEqual([1, 2, 3], match.array.endsWith([2, 3])); // true - suffix matching
92
111
  * partialEqual([1, 2, 3], match.array.length(3)); // true - exact length
93
112
  * partialEqual([1, 2, 3], match.array.includes(2)); // true - includes element
94
- * partialEqual([10, 20, 30], match.array.every(match.num.isGreaterThan(5))); // true - all elements match
113
+ * partialEqual(
114
+ * [10, 20, 30],
115
+ * match.array.every(match.num.isGreaterThan(5)),
116
+ * ); // true - all elements match
95
117
  * partialEqual([1, 10, 3], match.array.some(match.num.isGreaterThan(8))); // true - at least one matches
96
118
  *
97
119
  * // Special comparisons
@@ -73,6 +73,15 @@ var match = {
73
73
  return ["deepEqual", v];
74
74
  })
75
75
  ]),
76
+ key: {
77
+ any: "$pqkc:any$",
78
+ anyOther: "$pqkc:anyOther$",
79
+ numeric: "$pqkc:numeric$",
80
+ startingWith: (substring) => `$pqkc:startingWith:${substring}$`,
81
+ endingWith: (substring) => `$pqkc:endingWith:${substring}$`,
82
+ containing: (substring) => `$pqkc:contains:${substring}$`,
83
+ matchingRegex: (regex) => `$pqkc:matchesRegex:${regex}$`
84
+ },
76
85
  not: {
77
86
  hasType: {
78
87
  string: createComparison(["not", ["hasType", "string"]]),
@@ -142,12 +151,93 @@ var match = {
142
151
  noExtraKeys: (partialShape) => createComparison(["not", ["withNoExtraKeys", partialShape]]),
143
152
  deepNoExtraKeys: (partialShape) => createComparison(["not", ["withDeepNoExtraKeys", partialShape]]),
144
153
  noExtraDefinedKeys: (partialShape) => createComparison(["not", ["noExtraDefinedKeys", partialShape]]),
145
- deepNoExtraDefinedKeys: (partialShape) => createComparison(["not", ["deepNoExtraDefinedKeys", partialShape]])
154
+ deepNoExtraDefinedKeys: (partialShape) => createComparison(["not", ["deepNoExtraDefinedKeys", partialShape]]),
155
+ key: {
156
+ any: "$pqkc-not:any$",
157
+ anyOther: "$pqkc-not:anyOther$",
158
+ numeric: "$pqkc-not:numeric$",
159
+ startingWith: (substring) => `$pqkc-not:startingWith:${substring}$`,
160
+ endingWith: (substring) => `$pqkc-not:endingWith:${substring}$`,
161
+ containing: (substring) => `$pqkc-not:contains:${substring}$`,
162
+ matchingRegex: (regex) => `$pqkc-not:matchesRegex:${regex}$`
163
+ }
146
164
  }
147
165
  };
148
166
  function isComparison(value) {
149
167
  return value && typeof value === "object" && "~sc" in value;
150
168
  }
169
+ function isKeyMatcher(key) {
170
+ return typeof key === "string" && (key.startsWith("$pqkc:") || key.startsWith("$pqkc-not:"));
171
+ }
172
+ function parseKeyMatcher(key) {
173
+ if (!isKeyMatcher(key)) return null;
174
+ const negated = key.startsWith("$pqkc-not:");
175
+ const prefix = negated ? "$pqkc-not:" : "$pqkc:";
176
+ const suffix = "$";
177
+ if (!key.endsWith(suffix)) return null;
178
+ const content = key.slice(prefix.length, -suffix.length);
179
+ if (content === "any") {
180
+ return { type: "any", negated };
181
+ }
182
+ if (content === "anyOther") {
183
+ return { type: "anyOther", negated };
184
+ }
185
+ if (content === "numeric") {
186
+ return { type: "numeric", negated };
187
+ }
188
+ if (content.startsWith("startingWith:")) {
189
+ return { type: "startingWith", parameter: content.slice("startingWith:".length), negated };
190
+ }
191
+ if (content.startsWith("endingWith:")) {
192
+ return { type: "endingWith", parameter: content.slice("endingWith:".length), negated };
193
+ }
194
+ if (content.startsWith("contains:")) {
195
+ return { type: "contains", parameter: content.slice("contains:".length), negated };
196
+ }
197
+ if (content.startsWith("matchesRegex:")) {
198
+ const regexStr = content.slice("matchesRegex:".length);
199
+ try {
200
+ if (regexStr.startsWith("/") && regexStr.lastIndexOf("/") > 0) {
201
+ const lastSlashIndex = regexStr.lastIndexOf("/");
202
+ const pattern = regexStr.slice(1, lastSlashIndex);
203
+ const flags = regexStr.slice(lastSlashIndex + 1);
204
+ return { type: "matchesRegex", parameter: new RegExp(pattern, flags), negated };
205
+ } else {
206
+ return { type: "matchesRegex", parameter: new RegExp(regexStr), negated };
207
+ }
208
+ } catch {
209
+ return null;
210
+ }
211
+ }
212
+ return null;
213
+ }
214
+ function keyMatchesPattern(key, matcher) {
215
+ let matches = false;
216
+ switch (matcher.type) {
217
+ case "any":
218
+ matches = true;
219
+ break;
220
+ case "anyOther":
221
+ matches = true;
222
+ break;
223
+ case "numeric":
224
+ matches = /^\d+$/.test(key);
225
+ break;
226
+ case "startingWith":
227
+ matches = key.startsWith(matcher.parameter);
228
+ break;
229
+ case "endingWith":
230
+ matches = key.endsWith(matcher.parameter);
231
+ break;
232
+ case "contains":
233
+ matches = key.includes(matcher.parameter);
234
+ break;
235
+ case "matchesRegex":
236
+ matches = matcher.parameter.test(key);
237
+ break;
238
+ }
239
+ return matcher.negated ? !matches : matches;
240
+ }
151
241
  function executeComparison(target, comparison, context) {
152
242
  const [type, value] = comparison;
153
243
  switch (type) {
@@ -844,7 +934,21 @@ function partialEqualInternal(target, sub, context) {
844
934
  return false;
845
935
  }
846
936
  let allMatch = true;
937
+ const regularKeys = [];
938
+ const keyMatchers = [];
847
939
  for (const key of Object.keys(sub)) {
940
+ if (isKeyMatcher(key)) {
941
+ const matcher = parseKeyMatcher(key);
942
+ if (matcher) {
943
+ keyMatchers.push({ key, matcher, value: sub[key] });
944
+ }
945
+ } else {
946
+ regularKeys.push(key);
947
+ }
948
+ }
949
+ const explicitlySpecifiedKeys = /* @__PURE__ */ new Set();
950
+ for (const key of regularKeys) {
951
+ explicitlySpecifiedKeys.add(key);
848
952
  if (isComparison(sub[key]) && sub[key]["~sc"][0] === "keyNotBePresent") {
849
953
  if (has.call(target, key)) {
850
954
  const oldPath2 = context.path;
@@ -888,6 +992,72 @@ function partialEqualInternal(target, sub, context) {
888
992
  context.path = oldPath;
889
993
  if (!result) allMatch = false;
890
994
  }
995
+ for (const { matcher, value } of keyMatchers) {
996
+ let foundMatch = false;
997
+ const matchedKeys = [];
998
+ if (matcher.type === "anyOther") {
999
+ for (const targetKey of Object.keys(target)) {
1000
+ if (!explicitlySpecifiedKeys.has(targetKey)) {
1001
+ const matches = keyMatchesPattern(targetKey, matcher);
1002
+ if (matches) {
1003
+ foundMatch = true;
1004
+ matchedKeys.push(targetKey);
1005
+ const oldPath = context.path;
1006
+ context.path = [...oldPath, targetKey];
1007
+ const result = partialEqualInternal(target[targetKey], value, context);
1008
+ context.path = oldPath;
1009
+ if (!result) allMatch = false;
1010
+ }
1011
+ }
1012
+ }
1013
+ } else if (matcher.type === "any") {
1014
+ let anyKeyMatched = false;
1015
+ for (const targetKey of Object.keys(target)) {
1016
+ if (keyMatchesPattern(targetKey, matcher)) {
1017
+ foundMatch = true;
1018
+ matchedKeys.push(targetKey);
1019
+ explicitlySpecifiedKeys.add(targetKey);
1020
+ const tempContext = {
1021
+ errors: [],
1022
+ path: [...context.path, targetKey]
1023
+ };
1024
+ const result = partialEqualInternal(target[targetKey], value, tempContext);
1025
+ if (result) {
1026
+ anyKeyMatched = true;
1027
+ break;
1028
+ }
1029
+ }
1030
+ }
1031
+ if (foundMatch && !anyKeyMatched) {
1032
+ addError(context, {
1033
+ message: `No keys matching pattern had the expected value`,
1034
+ received: { keysChecked: matchedKeys, availableKeys: Object.keys(target) }
1035
+ });
1036
+ allMatch = false;
1037
+ }
1038
+ } else {
1039
+ for (const targetKey of Object.keys(target)) {
1040
+ if (keyMatchesPattern(targetKey, matcher)) {
1041
+ foundMatch = true;
1042
+ matchedKeys.push(targetKey);
1043
+ explicitlySpecifiedKeys.add(targetKey);
1044
+ const oldPath = context.path;
1045
+ context.path = [...oldPath, targetKey];
1046
+ const result = partialEqualInternal(target[targetKey], value, context);
1047
+ context.path = oldPath;
1048
+ if (!result) allMatch = false;
1049
+ }
1050
+ }
1051
+ }
1052
+ if (!foundMatch) {
1053
+ const patternDescription = matcher.type === "anyOther" ? "anyOther (keys not explicitly specified)" : `${matcher.type}${matcher.parameter ? ` (${matcher.parameter})` : ""}`;
1054
+ addError(context, {
1055
+ message: `No keys found matching pattern: ${patternDescription}`,
1056
+ received: { availableKeys: Object.keys(target), explicitKeys: Array.from(explicitlySpecifiedKeys) }
1057
+ });
1058
+ allMatch = false;
1059
+ }
1060
+ }
891
1061
  return allMatch;
892
1062
  }
893
1063
  if (target !== sub) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ls-stack/utils",
3
3
  "description": "Universal TypeScript utilities for browser and Node.js",
4
- "version": "3.53.0",
4
+ "version": "3.54.0",
5
5
  "license": "MIT",
6
6
  "files": [
7
7
  "dist",