@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.
- package/dist/partialEqual.cjs +171 -1
- package/dist/partialEqual.d.cts +23 -1
- package/dist/partialEqual.d.ts +23 -1
- package/dist/partialEqual.js +171 -1
- package/package.json +1 -1
package/dist/partialEqual.cjs
CHANGED
|
@@ -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) {
|
package/dist/partialEqual.d.cts
CHANGED
|
@@ -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(
|
|
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
|
package/dist/partialEqual.d.ts
CHANGED
|
@@ -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(
|
|
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
|
package/dist/partialEqual.js
CHANGED
|
@@ -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) {
|