-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathutils.js
More file actions
361 lines (300 loc) · 11.1 KB
/
Copy pathutils.js
File metadata and controls
361 lines (300 loc) · 11.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
// utils.js
/**
* Standard result object for this project.
*
*/
/**
* Provide a consistent return format for all functions.
*
* @typedef {Object} Result
* @property {boolean} ok - True on success, false on failure.
* @property {string} message - Description of the outcome.
* @property {string} functionName - Description of the outcome.
* @property {*} data - Payload data (or null on failure).
*/
const theResults = (pass = false, message, functionName, data = null) => {
Logger.log(`${functionName}. ${message}. Is data null? = ${data === null}`);
return ({
ok: pass,
message: message,
data: data
});
}
/**
* This is the list of validators to run on the input data.
* The order of the validators matters!
* See the comments in each validator function for more details.
* Don't change the order of the validators or make edits without understanding
* the implications.
*/
const getValidators = () => [
checkIsAttributeUnique,
checkIsAttributeKnownKey,
checkIsAttributeValueDefined,
assignValuesToHash,
];
/**
* Validates that a value is a valid non-empty string.
*
* Validation rules:
* 1. Must not be null or undefined
* 2. Must be able to be cast as a string
* 3. Must have length > 0 after trimming whitespace
*
* @param {*} value - The value to validate
* @param {string} paramName - The parameter name (for error messages)
* @returns {Result}
* ok: true if valid, false if invalid
* message: describes the validation outcome
* data: the trimmed string value if valid, null if invalid
*/
const validateNonEmptyString = (value, paramName = 'parameter') => {
const functionName = 'validateNonEmptyString';
// Check for null or undefined
if (value === null || value === undefined) {
return theResults(false, `${paramName} must not be null or undefined`, functionName);
}
// Attempt to cast as string
let stringValue;
try {
stringValue = String(value);
} catch (error) {
return theResults(false, `${paramName} cannot be cast as a string: ${error.message}`, functionName);
}
// Trim and check for empty string
const trimmedValue = stringValue.trim();
if (trimmedValue.length === 0) {
return theResults(false, `${paramName} must not be empty after trimming whitespace`, functionName);
}
return theResults(true, `${paramName} is valid`, functionName, trimmedValue);
};
/**
*
* Generates a timestamp string for the current date and time in
* YYYYMMDDHHMMSS format (e.g., 20250930172629).
* * @returns {string} The formatted timestamp string.
*
* @returns {string}
*/
const getTimestampString = () => {
const pad = num => (num < 10 ? '0' : '') + num;
const now = new Date();
return [
now.getFullYear(),
pad(now.getMonth() + 1),
pad(now.getDate()),
pad(now.getHours()),
pad(now.getMinutes()),
pad(now.getSeconds())
].join('');
};
/**
* Processes an array of tuples through a pipeline of validator functions.
* Each validator runs once and receives the full array of tuples and the
* current state of the hash.
*
* Pre-conditions:
* - tuples is an array of [attributeString, valueString] pairs
* - hash has predefined keys with undefined values
* - validators is an array of functions with signature:
* (tuples: Array<Array<string>>, hash: Object) => { ok, message, data: updatedHash }
*
* @param {Array<Array<string>>} tuples - Array of [attributeString, valueString] pairs
* @param {Object} hash - Predefined keys with undefined values
* @param {Array<Function>} validators - Array of validator functions
* @returns {Result}
* data = final state of hash after all validators processed
*/
const populateInputValues = (tuples, hash, validators) => {
const functionName = `populateInputValues`;
Logger.log(`${functionName}. Started.`);
// --- Guard: validate inputs ---
if (!tuples || !Array.isArray(tuples)) {
return theResults(false, 'tuples must be an array', functionName);
}
if (!hash || typeof hash !== 'object') {
return theResults(false, 'hash must be an object', functionName);
}
if (!validators || !Array.isArray(validators)) {
return theResults(false, 'validators must be an array', functionName);
}
// --- Start with a clean copy of the incoming hash ---
let currentHash = { ...hash };
// --- Loop: each validator runs once against the full tuples array ---
Logger.log(`>>populateInputValues. Start Validator Loop`);
for (const validator of validators) {
Logger.log(`>> Running validator: ${validator.name}`);
const result = validator(tuples, currentHash);
if (!result.ok) {
return theResults(false, result.message, functionName);
}
Logger.log(`>> ${validator.name} result: ${result.ok} - ${result.message}`);
// Carry the updated hash forward to the next validator
currentHash = { ...result.data };
}
Logger.log(`>>populateInputValues. Completed Validator Loop`);
// print out the required data keys along with what
// is in the actual config data
Object.keys(hash).forEach(function(key) {
const value = currentHash[key];
Logger.log(`populateInputValues. Final. >${key}< = >${value}<`);
});
// --- All validators passed ---
return theResults(true, 'Completed.', functionName, currentHash);
};
/**
* Determines whether a tuple should be skipped entirely.
* Called before the validator pipeline — not part of the validators array.
*
* Skip conditions:
* 1. attributeName is empty or blank
* 2. attributeName is the word 'comment' (case insensitive)
*
* @param {Array<string>} tuple - [attributeName, value]
* @param {Object} hash
* @returns {{ ok: boolean, message: string, data: Object|null }}
* ok: true = yes, skip this tuple
* ok: false = no, continue to validator pipeline
*/
/**
*
* @param tuple
* @returns {Result}
*/
const okToSkip = tuple => {
const functionName = 'okToSkip';
// Logger.log(`${functionName}. Started.`);
const trimmed = tuple[0].trim().toLowerCase();
if (trimmed === '') {
return theResults(true, 'empty attribute - skip', functionName);
}
if (trimmed === 'comment') {
return theResults(true, 'comment - skip', functionName);
}
return theResults(false, 'not a skip condition - continue processing', functionName);
};
/**
* Fails if any attribute name appears more than once in the tuples array.
* Ignores comment and empty rows.
* This must run after checkIsAttributeKnownKey
*
* @param {Array<Array<string>>} tuples
* @param {Object} hash
* @returns {Result}
*/
const checkIsAttributeUnique = (tuples, hash) => {
const functionName = 'checkIsAttributeUnique';
Logger.log(`${functionName}. Started.`);
const seen = {};
const duplicates = [];
for (const tuple of tuples) {
const attributeName = tuple[0].trim().toLowerCase();
// Skip empty and comment rows
if (okToSkip(tuple).ok) continue;
if (seen[attributeName]) {
if (!duplicates.includes(attributeName)) {
duplicates.push(attributeName);
}
} else {
seen[attributeName] = true;
}
}
if (duplicates.length > 0) {
return theResults(false, `Duplicate attribute(s) found: ${duplicates.join(', ')}`, functionName);
}
return theResults(true, 'No duplicates found', functionName, {...hash});
};
/**
* Fails if any attribute name in the tuples array is not a predefined key in the hash.
* Unknown keys are not valid input.
* Ignores comment and empty rows.
* This must be the first validator that is run
*
* @param {Array<Array<string>>} tuples - Array of [attributeName, value] pairs
* @param {Object} hash
* @returns {Result}
* data = current hash unchanged
*/
const checkIsAttributeKnownKey = (tuples, hash) => {
const functionName = 'checkIsAttributeKnownKey';
Logger.log(`${functionName}. Started.`);
const unknownKeys = [];
const hashByHashKey = getConfigHash();
for (const key of Object.keys(hashByHashKey)) {
hashByHashKey[key] = key;
//Logger.log(`checkIsAttributeKnownKey. key = ${key}, hashByHashKey[key] = ${hashByHashKey[key]}`);
}
for (const key of Object.keys(hashByHashKey)) {
Logger.log(`checkIsAttributeKnownKey. key = ${key}, hashByHashKey[key] = ${hashByHashKey[key]}`);
}
for (const tuple of tuples) {
// Skip empty and comment rows
if (okToSkip(tuple).ok) continue;
const attributeName = tuple[0];
Logger.log(`checkIsAttributeKnownKey. attributeName = ${attributeName}, hashByHashKey[attributeName] = ${hashByHashKey[attributeName]}, ${attributeName === hashByHashKey[attributeName] ? '✅' : '❌'}`);
if (hashByHashKey[attributeName] !== attributeName) {
unknownKeys.push(attributeName);
}
}
if (unknownKeys.length > 0) return theResults(false, `Unknown attribute(s) found: ${unknownKeys.join(', ')}`, functionName);
return theResults(true, 'All attribute names are known keys', functionName, {...hash});
};
/**
* Fails if any tuple value in the tuples array is missing, not a string, or blank.
* Ignores comment and empty rows.
* Must run after validator: checkIsAttributeKnownKey
*
* @param {Array<Array<string>>} tuples - Array of [attributeName, value] pairs
* @param {Object} hash
* @returns {Result}
* data = current hash unchanged
*/
const checkIsAttributeValueDefined = (tuples, hash) => {
const functionName = 'checkIsAttributeValueDefined';
Logger.log(`${functionName}. Started.`);
const invalidValues = [];
for (const tuple of tuples) {
// Skip empty and comment rows
if (okToSkip(tuple).ok) continue;
const attributeName = tuple[0].trim() ;
const value = tuple[1].trim();
if (value === '') {
invalidValues.push(`[${attributeName}] value is blank`);
continue;
}
if (value === undefined || value === null) {
invalidValues.push(`[${attributeName}] value is undefined or null`);
continue;
}
if (typeof value !== 'string') {
invalidValues.push(`[${attributeName}] value is not a string: ${value}`);
continue; //unnecessary as the last statement in a loop
}
}
if (invalidValues.length > 0) return theResults(false, `Invalid value(s) found: ${invalidValues.join(', ')}`, functionName);
return theResults(true, 'All attribute values are valid', functionName, {...hash});
};
/**
* Assigns values from the tuples array to the correct keys in the hash.
* This should always be the last validator in the pipeline.
* Ignores comment and empty rows.
*
* @param {Array<Array<string>>} tuples - Array of [attributeName, value] pairs
* @param {Object} hash
* @returns {Result}
* data = updated copy of hash with all values assigned
*/
const assignValuesToHash = (tuples, hash) => {
const functionName = 'assignValuesToHash';
Logger.log(`${functionName}. Started.`);
const updatedHash = { ...hash };
for (const tuple of tuples) {
// Skip empty and comment rows
if (okToSkip(tuple).ok) continue;
const attributeName = tuple[0];
const value = tuple[1];
updatedHash[attributeName] = value;
Logger.log(`assignValuesToHash updatedHash[${attributeName}]=${value}`);
}
return theResults(true, 'All values assigned to hash', functionName, updatedHash);
};