@carecard/validate 3.1.17 → 3.1.18
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.
|
@@ -18,7 +18,53 @@ const MAX_NESTING_DEPTH = 5;
|
|
|
18
18
|
const MAX_KEYS_PER_CALL = 5000;
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
21
|
+
* Returns true if the segment contains a mix of snake_case (underscore) and
|
|
22
|
+
* camelCase (uppercase letter), e.g. `my_mixName`. Such names are ambiguous
|
|
23
|
+
* and are not supported.
|
|
24
|
+
* @param {string} segment
|
|
25
|
+
* @returns {boolean}
|
|
26
|
+
*/
|
|
27
|
+
function isMixedCaseSegment(segment) {
|
|
28
|
+
return /_/.test(segment) && /[A-Z]/.test(segment);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Converts a snake_case identifier to camelCase. Leaves identifiers without
|
|
33
|
+
* underscores unchanged.
|
|
34
|
+
* @param {string} s
|
|
35
|
+
* @returns {string}
|
|
36
|
+
*/
|
|
37
|
+
function snakeToCamel(s) {
|
|
38
|
+
return s.replace(/_([a-zA-Z0-9])/g, (_, c) => c.toUpperCase());
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Converts a camelCase identifier to snake_case. Leaves identifiers without
|
|
43
|
+
* uppercase letters unchanged.
|
|
44
|
+
* @param {string} s
|
|
45
|
+
* @returns {string}
|
|
46
|
+
*/
|
|
47
|
+
function camelToSnake(s) {
|
|
48
|
+
return s.replace(/[A-Z]/g, c => `_${c.toLowerCase()}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Returns the alternate-case form of the segment:
|
|
53
|
+
* - snake_case -> camelCase
|
|
54
|
+
* - camelCase -> snake_case
|
|
55
|
+
* - otherwise (no `_` and no uppercase) the segment itself.
|
|
56
|
+
* @param {string} segment
|
|
57
|
+
* @returns {string}
|
|
58
|
+
*/
|
|
59
|
+
function alternateCase(segment) {
|
|
60
|
+
if (segment.indexOf('_') !== -1) return snakeToCamel(segment);
|
|
61
|
+
if (/[A-Z]/.test(segment)) return camelToSnake(segment);
|
|
62
|
+
return segment;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Splits a dot-notation property path into its segments and validates depth
|
|
67
|
+
* and per-segment casing (mixed snake/camel segments are rejected).
|
|
22
68
|
* @param {string} path
|
|
23
69
|
* @returns {string[]}
|
|
24
70
|
*/
|
|
@@ -29,6 +75,13 @@ function splitPath(path) {
|
|
|
29
75
|
userMessage: `Property path "${path}" exceeds maximum nesting depth of ${MAX_NESTING_DEPTH}`,
|
|
30
76
|
});
|
|
31
77
|
}
|
|
78
|
+
for (const seg of segments) {
|
|
79
|
+
if (isMixedCaseSegment(seg)) {
|
|
80
|
+
throwBadInputError({
|
|
81
|
+
userMessage: `Property path "${path}" has a segment "${seg}" mixing snake_case and camelCase`,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
32
85
|
return segments;
|
|
33
86
|
}
|
|
34
87
|
|
|
@@ -42,16 +95,27 @@ function splitPath(path) {
|
|
|
42
95
|
* @returns {{ found: boolean, value: any }}
|
|
43
96
|
*/
|
|
44
97
|
function readLeaf(obj, segments) {
|
|
98
|
+
// Resolve a segment against the current node, trying its as-written form
|
|
99
|
+
// first and then its alternate snake/camel form. Returns the actual key
|
|
100
|
+
// present in the node, or undefined if neither form exists.
|
|
101
|
+
function resolveKey(node, seg) {
|
|
102
|
+
if (Object.prototype.hasOwnProperty.call(node, seg)) return seg;
|
|
103
|
+
const alt = alternateCase(seg);
|
|
104
|
+
if (alt !== seg && Object.prototype.hasOwnProperty.call(node, alt)) return alt;
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
45
108
|
let current = obj;
|
|
46
109
|
for (let i = 0; i < segments.length - 1; i++) {
|
|
47
110
|
if (current === null || typeof current !== 'object') return { found: false, value: undefined };
|
|
48
|
-
|
|
49
|
-
|
|
111
|
+
const key = resolveKey(current, segments[i]);
|
|
112
|
+
if (key === undefined) return { found: false, value: undefined };
|
|
113
|
+
current = current[key];
|
|
50
114
|
}
|
|
51
115
|
if (current === null || typeof current !== 'object') return { found: false, value: undefined };
|
|
52
|
-
const
|
|
53
|
-
if (
|
|
54
|
-
return { found: true, value: current[
|
|
116
|
+
const leafKey = resolveKey(current, segments[segments.length - 1]);
|
|
117
|
+
if (leafKey === undefined) return { found: false, value: undefined };
|
|
118
|
+
return { found: true, value: current[leafKey] };
|
|
55
119
|
}
|
|
56
120
|
|
|
57
121
|
/**
|
|
@@ -146,7 +210,7 @@ function validateWhitelistProperties(
|
|
|
146
210
|
});
|
|
147
211
|
}
|
|
148
212
|
|
|
149
|
-
const requiredPaths = requiredProperties.map(p => ({ raw: p, segments: splitPath(p) }));
|
|
213
|
+
const requiredPaths = (requiredProperties || []).map(p => ({ raw: p, segments: splitPath(p) }));
|
|
150
214
|
const optionalPaths = optionalProperties.map(p => ({ raw: p, segments: splitPath(p) }));
|
|
151
215
|
|
|
152
216
|
let validatedObject = {};
|