configorama 0.9.0 → 0.9.3
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 +1 -1
- package/src/main.js +31 -20
- package/src/parsers/yaml.js +4 -4
- package/src/parsers/yaml.test.js +52 -0
- package/src/resolvers/valueFromFile.js +15 -2
- package/src/utils/encoders/unknown-values.js +1 -1
- package/src/utils/encoders/unknown-values.test.js +146 -0
- package/src/utils/lodash.js +5 -4
- package/src/utils/lodash.test.js +172 -0
- package/src/utils/parsing/cloudformationSchema.js +24 -2
- package/src/utils/parsing/cloudformationSchema.test.js +236 -0
- package/src/utils/parsing/mergeByKeys.js +9 -8
- package/src/utils/parsing/mergeByKeys.test.js +189 -0
- package/src/utils/parsing/parse.js +5 -2
- package/src/utils/paths/getFullFilePath.js +2 -2
- package/src/utils/paths/getFullFilePath.test.js +152 -0
- package/src/utils/regex/index.js +65 -1
- package/src/utils/regex/index.test.js +195 -0
- package/src/utils/strings/formatFunctionArgs.js +4 -0
- package/src/utils/strings/splitCsv.js +46 -19
- package/types/src/main.d.ts.map +1 -1
- package/types/src/resolvers/valueFromFile.d.ts.map +1 -1
- package/types/src/utils/lodash.d.ts.map +1 -1
- package/types/src/utils/parsing/mergeByKeys.d.ts.map +1 -1
- package/types/src/utils/parsing/parse.d.ts.map +1 -1
- package/types/src/utils/regex/index.d.ts +15 -1
- package/types/src/utils/regex/index.d.ts.map +1 -1
- package/types/src/utils/strings/formatFunctionArgs.d.ts.map +1 -1
- package/types/src/utils/strings/splitCsv.d.ts +1 -1
- package/types/src/utils/strings/splitCsv.d.ts.map +1 -1
package/package.json
CHANGED
package/src/main.js
CHANGED
|
@@ -335,21 +335,25 @@ class Configorama {
|
|
|
335
335
|
/** */
|
|
336
336
|
/* its file ref so we need to shift lookup for self in nested files */
|
|
337
337
|
if (valueObject.isFileRef) {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
// console.log('fallThroughSelfMatcher deeper', deeperExists)
|
|
346
|
-
return deeperExists
|
|
338
|
+
// First check if property exists in the nested file's context (preferred)
|
|
339
|
+
const nestedPath = [valueObject.path[0]].concat(varString)
|
|
340
|
+
const nestedDotPath = nestedPath.join('.')
|
|
341
|
+
if (dotProp.has(fullObject, nestedDotPath)) {
|
|
342
|
+
// Property exists in nested context - return true to indicate match
|
|
343
|
+
// (actual value resolution happens in resolver, not here)
|
|
344
|
+
return true
|
|
347
345
|
}
|
|
346
|
+
// Fall back to top-level lookup
|
|
347
|
+
if (dotProp.has(fullObject, varString)) {
|
|
348
|
+
return true
|
|
349
|
+
}
|
|
350
|
+
return false
|
|
348
351
|
}
|
|
349
352
|
// console.log('fallthrough fullObject', fullObject)
|
|
350
353
|
/* is simple ${whatever} reference in same file */
|
|
351
354
|
const startOf = varString.split('.')
|
|
352
|
-
|
|
355
|
+
// Use has() to properly check existence for falsy values
|
|
356
|
+
return dotProp.has(fullObject, startOf[0])
|
|
353
357
|
},
|
|
354
358
|
resolver: (varString, options, config, pathValue) => {
|
|
355
359
|
/*
|
|
@@ -2893,7 +2897,7 @@ Missing Value ${missingValue} - ${matchedString}
|
|
|
2893
2897
|
// Find the most recent call for this variableString
|
|
2894
2898
|
for (let i = this.resolutionTracking[pathKey].calls.length - 1; i >= 0; i--) {
|
|
2895
2899
|
if (this.resolutionTracking[pathKey].calls[i].variableString === variableString) {
|
|
2896
|
-
const v = (typeof val === 'object' && val.__internal_only_flag) ? val.value : val
|
|
2900
|
+
const v = (val && typeof val === 'object' && val.__internal_only_flag) ? val.value : val
|
|
2897
2901
|
this.resolutionTracking[pathKey].calls[i].resolvedValue = v
|
|
2898
2902
|
this.resolutionTracking[pathKey].calls[i].resolverType = resolverType
|
|
2899
2903
|
break
|
|
@@ -3264,14 +3268,14 @@ Missing Value ${missingValue} - ${matchedString}
|
|
|
3264
3268
|
|
|
3265
3269
|
/* its file ref so we need to shift lookup for self in nested files */
|
|
3266
3270
|
if (data.isFileRef) {
|
|
3267
|
-
|
|
3268
|
-
const
|
|
3269
|
-
|
|
3270
|
-
if (
|
|
3271
|
-
//
|
|
3272
|
-
deepProperties =
|
|
3273
|
-
// console.log('self fixed deepProperties', deepProperties)
|
|
3271
|
+
// First check if property exists in the nested file's context (preferred for file refs)
|
|
3272
|
+
const nestedPath = [data.path[0]].concat(deepProperties)
|
|
3273
|
+
const nestedDotPath = nestedPath.join('.')
|
|
3274
|
+
if (dotProp.has(valueToPopulate, nestedDotPath)) {
|
|
3275
|
+
// Property exists in nested context, prefer it over top-level
|
|
3276
|
+
deepProperties = nestedPath
|
|
3274
3277
|
}
|
|
3278
|
+
// Otherwise, keep deepProperties as-is to try top-level lookup
|
|
3275
3279
|
}
|
|
3276
3280
|
|
|
3277
3281
|
return this.getDeeperValue(deepProperties, valueToPopulate).then((res) => {
|
|
@@ -3447,7 +3451,7 @@ Missing Value ${missingValue} - ${matchedString}
|
|
|
3447
3451
|
})
|
|
3448
3452
|
}
|
|
3449
3453
|
runFunction(variableString) {
|
|
3450
|
-
console.log('runFunction', variableString)
|
|
3454
|
+
// console.log('runFunction', variableString)
|
|
3451
3455
|
/* If json object value return it */
|
|
3452
3456
|
if (variableString.match(/^\s*{/) && variableString.match(/}\s*$/)) {
|
|
3453
3457
|
return variableString
|
|
@@ -3473,7 +3477,14 @@ Missing Value ${missingValue} - ${matchedString}
|
|
|
3473
3477
|
// TODO fix how commas + spaces are ned
|
|
3474
3478
|
const splitter = splitCsv(rawArgs, ', ')
|
|
3475
3479
|
// console.log('splitter', splitter)
|
|
3476
|
-
|
|
3480
|
+
// Recursively evaluate any nested function calls in arguments
|
|
3481
|
+
const evaluatedArgs = splitter.map((arg) => {
|
|
3482
|
+
if (typeof arg === 'string' && funcRegex.test(arg)) {
|
|
3483
|
+
return this.runFunction(arg)
|
|
3484
|
+
}
|
|
3485
|
+
return arg
|
|
3486
|
+
})
|
|
3487
|
+
argsToPass = formatFunctionArgs(evaluatedArgs)
|
|
3477
3488
|
}
|
|
3478
3489
|
// console.log('argsToPass runFunction', argsToPass)
|
|
3479
3490
|
// TODO check for camelCase version. | toUpperCase messes with function name
|
package/src/parsers/yaml.js
CHANGED
|
@@ -132,8 +132,8 @@ function preProcess(ymlStr = '') {
|
|
|
132
132
|
if (txt.indexOf(`'${nested}'`) > -1) {
|
|
133
133
|
return
|
|
134
134
|
}
|
|
135
|
-
/* Replace variable wrapped in quotes */
|
|
136
|
-
fixedText = fixedText.
|
|
135
|
+
/* Replace ALL occurrences of variable wrapped in quotes */
|
|
136
|
+
fixedText = fixedText.replaceAll(nested, `"${nested}"`)
|
|
137
137
|
})
|
|
138
138
|
ymlStr = ymlStr.replace(txt, fixedText)
|
|
139
139
|
}
|
|
@@ -171,8 +171,8 @@ function preProcess(ymlStr = '') {
|
|
|
171
171
|
if (txt.indexOf(`'${nested}'`) > -1) {
|
|
172
172
|
return
|
|
173
173
|
}
|
|
174
|
-
/* Replace variable wrapped in quotes */
|
|
175
|
-
fixedText = fixedText.
|
|
174
|
+
/* Replace ALL occurrences of variable wrapped in quotes */
|
|
175
|
+
fixedText = fixedText.replaceAll(nested, `"${nested}"`)
|
|
176
176
|
})
|
|
177
177
|
ymlStr = ymlStr.replace(txt, fixedText)
|
|
178
178
|
}
|
package/src/parsers/yaml.test.js
CHANGED
|
@@ -166,4 +166,56 @@ test('preProcess - should handle empty input', () => {
|
|
|
166
166
|
assert.equal(result, '')
|
|
167
167
|
})
|
|
168
168
|
|
|
169
|
+
// ==========================================
|
|
170
|
+
// Duplicate variable tests - Bug: String.replace() only replaces first occurrence
|
|
171
|
+
// ==========================================
|
|
172
|
+
|
|
173
|
+
test('preProcess - should wrap ALL duplicate variables in quotes within same array', () => {
|
|
174
|
+
const input = `
|
|
175
|
+
items: [\${var:foo}, \${var:foo}, \${var:foo}]
|
|
176
|
+
`
|
|
177
|
+
const result = preProcess(input)
|
|
178
|
+
|
|
179
|
+
// All three occurrences should be wrapped
|
|
180
|
+
const wrappedCount = (result.match(/"\$\{var:foo\}"/g) || []).length
|
|
181
|
+
|
|
182
|
+
assert.is(wrappedCount, 3, `Expected 3 wrapped occurrences, got ${wrappedCount}. Output: ${result}`)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
test('preProcess - should wrap ALL duplicate variables in quotes within same object', () => {
|
|
186
|
+
const input = `
|
|
187
|
+
config: {stage: \${env:STAGE}, region: \${env:STAGE}}
|
|
188
|
+
`
|
|
189
|
+
const result = preProcess(input)
|
|
190
|
+
|
|
191
|
+
// Both occurrences should be wrapped
|
|
192
|
+
const wrappedCount = (result.match(/"\$\{env:STAGE\}"/g) || []).length
|
|
193
|
+
|
|
194
|
+
assert.is(wrappedCount, 2, `Expected 2 wrapped occurrences, got ${wrappedCount}. Output: ${result}`)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
test('preProcess - should wrap duplicate variables mixed with unique variables in array', () => {
|
|
198
|
+
const input = `
|
|
199
|
+
mixed: [\${env:FOO}, \${env:BAR}, \${env:FOO}]
|
|
200
|
+
`
|
|
201
|
+
const result = preProcess(input)
|
|
202
|
+
|
|
203
|
+
const fooCount = (result.match(/"\$\{env:FOO\}"/g) || []).length
|
|
204
|
+
const barCount = (result.match(/"\$\{env:BAR\}"/g) || []).length
|
|
205
|
+
|
|
206
|
+
assert.is(fooCount, 2, `Expected 2 wrapped FOO occurrences, got ${fooCount}. Output: ${result}`)
|
|
207
|
+
assert.is(barCount, 1, `Expected 1 wrapped BAR occurrence, got ${barCount}. Output: ${result}`)
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
test('preProcess - should wrap duplicate variables in nested array structure', () => {
|
|
211
|
+
const input = `
|
|
212
|
+
nested: [[\${opt:stage}, \${opt:stage}], [\${opt:stage}]]
|
|
213
|
+
`
|
|
214
|
+
const result = preProcess(input)
|
|
215
|
+
|
|
216
|
+
const wrappedCount = (result.match(/"\$\{opt:stage\}"/g) || []).length
|
|
217
|
+
|
|
218
|
+
assert.is(wrappedCount, 3, `Expected 3 wrapped occurrences, got ${wrappedCount}. Output: ${result}`)
|
|
219
|
+
})
|
|
220
|
+
|
|
169
221
|
test.run()
|
|
@@ -253,7 +253,19 @@ ${JSON.stringify(options.context, null, 2)}`,
|
|
|
253
253
|
if (fileExtension === 'js' || fileExtension === 'cjs') {
|
|
254
254
|
const jsFile = require(fullFilePath)
|
|
255
255
|
const { moduleName } = parseModuleReference(variableString, matchedFileString)
|
|
256
|
-
|
|
256
|
+
// For default export functions with :property syntax, keep the function and use deep properties
|
|
257
|
+
// For named exports (non-function module), look up the named export
|
|
258
|
+
let returnValueFunction = jsFile
|
|
259
|
+
let includeFirstProperty = false
|
|
260
|
+
|
|
261
|
+
if (moduleName && typeof jsFile === 'function') {
|
|
262
|
+
// Default export function with property access - include first property in path
|
|
263
|
+
returnValueFunction = jsFile
|
|
264
|
+
includeFirstProperty = true
|
|
265
|
+
} else if (moduleName) {
|
|
266
|
+
// Named export - look it up directly
|
|
267
|
+
returnValueFunction = jsFile[moduleName]
|
|
268
|
+
}
|
|
257
269
|
|
|
258
270
|
return processExecutableFile({
|
|
259
271
|
fileModule: jsFile,
|
|
@@ -264,7 +276,8 @@ ${JSON.stringify(options.context, null, 2)}`,
|
|
|
264
276
|
matchedFileString,
|
|
265
277
|
relativePath,
|
|
266
278
|
fileType: 'javascript',
|
|
267
|
-
getDeeperValue: ctx.getDeeperValue
|
|
279
|
+
getDeeperValue: ctx.getDeeperValue,
|
|
280
|
+
includeFirstProperty
|
|
268
281
|
})
|
|
269
282
|
}
|
|
270
283
|
|
|
@@ -20,7 +20,7 @@ function decodeUnknown(rawValue) {
|
|
|
20
20
|
let val = rawValue.replace(PASSTHROUGH_PATTERN, '')
|
|
21
21
|
if (x.length) {
|
|
22
22
|
x.forEach(({ match, value }) => {
|
|
23
|
-
const decodedValue = Buffer.from(value, 'base64').toString('
|
|
23
|
+
const decodedValue = Buffer.from(value, 'base64').toString('utf8')
|
|
24
24
|
val = val.replace(match, decodedValue)
|
|
25
25
|
})
|
|
26
26
|
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for unknown-values.js - encoding/decoding passthrough variables
|
|
3
|
+
*
|
|
4
|
+
* These functions handle variables that cannot be resolved by Configorama
|
|
5
|
+
* but need to be passed through to external systems (like Serverless Dashboard).
|
|
6
|
+
*/
|
|
7
|
+
const { test } = require('uvu')
|
|
8
|
+
const assert = require('uvu/assert')
|
|
9
|
+
const {
|
|
10
|
+
encodeUnknown,
|
|
11
|
+
decodeUnknown,
|
|
12
|
+
findUnknownValues,
|
|
13
|
+
hasEncodedUnknown,
|
|
14
|
+
PASSTHROUGH_PATTERN
|
|
15
|
+
} = require('./unknown-values')
|
|
16
|
+
|
|
17
|
+
// ==========================================
|
|
18
|
+
// encodeUnknown tests
|
|
19
|
+
// ==========================================
|
|
20
|
+
|
|
21
|
+
test('encodeUnknown - encodes string with passthrough wrapper', () => {
|
|
22
|
+
const result = encodeUnknown('${param:test}')
|
|
23
|
+
assert.ok(result.startsWith('>passthrough[_['))
|
|
24
|
+
assert.ok(result.endsWith(']_]'))
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('encodeUnknown - produces valid base64', () => {
|
|
28
|
+
const result = encodeUnknown('${param:test}')
|
|
29
|
+
const base64Part = result.replace('>passthrough[_[', '').replace(']_]', '')
|
|
30
|
+
// Should not throw
|
|
31
|
+
const decoded = Buffer.from(base64Part, 'base64').toString('utf8')
|
|
32
|
+
assert.is(decoded, '${param:test}')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
// ==========================================
|
|
36
|
+
// decodeUnknown tests - basic functionality
|
|
37
|
+
// ==========================================
|
|
38
|
+
|
|
39
|
+
test('decodeUnknown - decodes encoded value back to original', () => {
|
|
40
|
+
const input = '${param:simpleValue}'
|
|
41
|
+
const encoded = encodeUnknown(input)
|
|
42
|
+
const decoded = decodeUnknown(encoded)
|
|
43
|
+
assert.is(decoded, input)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('decodeUnknown - handles multiple encoded values', () => {
|
|
47
|
+
const input1 = '${param:first}'
|
|
48
|
+
const input2 = '${param:second}'
|
|
49
|
+
const combined = `prefix ${encodeUnknown(input1)} middle ${encodeUnknown(input2)} suffix`
|
|
50
|
+
const decoded = decodeUnknown(combined)
|
|
51
|
+
assert.is(decoded, `prefix ${input1} middle ${input2} suffix`)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('decodeUnknown - returns original string if no encoded values', () => {
|
|
55
|
+
const input = 'no encoded values here'
|
|
56
|
+
const result = decodeUnknown(input)
|
|
57
|
+
assert.is(result, input)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
// ==========================================
|
|
61
|
+
// decodeUnknown tests - UTF-8 support (bug fix)
|
|
62
|
+
// ==========================================
|
|
63
|
+
|
|
64
|
+
test('encodeUnknown/decodeUnknown - handles UTF-8 accented characters', () => {
|
|
65
|
+
const input = '${param:café}'
|
|
66
|
+
const encoded = encodeUnknown(input)
|
|
67
|
+
const decoded = decodeUnknown(encoded)
|
|
68
|
+
assert.is(decoded, input, 'UTF-8 accented characters should decode correctly')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('encodeUnknown/decodeUnknown - handles UTF-8 emoji', () => {
|
|
72
|
+
const input = '${param:celebration-🎉}'
|
|
73
|
+
const encoded = encodeUnknown(input)
|
|
74
|
+
const decoded = decodeUnknown(encoded)
|
|
75
|
+
assert.is(decoded, input, 'UTF-8 emoji should decode correctly')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('encodeUnknown/decodeUnknown - handles multi-byte UTF-8 characters (Japanese)', () => {
|
|
79
|
+
const input = '${param:日本語}'
|
|
80
|
+
const encoded = encodeUnknown(input)
|
|
81
|
+
const decoded = decodeUnknown(encoded)
|
|
82
|
+
assert.is(decoded, input, 'Multi-byte UTF-8 characters should decode correctly')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('encodeUnknown/decodeUnknown - handles Chinese characters', () => {
|
|
86
|
+
const input = '${param:中文测试}'
|
|
87
|
+
const encoded = encodeUnknown(input)
|
|
88
|
+
const decoded = decodeUnknown(encoded)
|
|
89
|
+
assert.is(decoded, input, 'Chinese characters should decode correctly')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('encodeUnknown/decodeUnknown - handles mixed ASCII and UTF-8', () => {
|
|
93
|
+
const input = '${param:hello-世界-🌍}'
|
|
94
|
+
const encoded = encodeUnknown(input)
|
|
95
|
+
const decoded = decodeUnknown(encoded)
|
|
96
|
+
assert.is(decoded, input, 'Mixed ASCII and UTF-8 should decode correctly')
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('encodeUnknown/decodeUnknown - handles special UTF-8 punctuation', () => {
|
|
100
|
+
const input = '${param:price-€100-£50}'
|
|
101
|
+
const encoded = encodeUnknown(input)
|
|
102
|
+
const decoded = decodeUnknown(encoded)
|
|
103
|
+
assert.is(decoded, input, 'UTF-8 currency symbols should decode correctly')
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
// ==========================================
|
|
107
|
+
// findUnknownValues tests
|
|
108
|
+
// ==========================================
|
|
109
|
+
|
|
110
|
+
test('findUnknownValues - finds encoded values in text', () => {
|
|
111
|
+
const encoded = encodeUnknown('${param:test}')
|
|
112
|
+
const results = findUnknownValues(encoded)
|
|
113
|
+
assert.is(results.length, 1)
|
|
114
|
+
assert.ok(results[0].match.startsWith('[_['))
|
|
115
|
+
assert.ok(results[0].value.length > 0)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
test('findUnknownValues - returns empty array for text without encoded values', () => {
|
|
119
|
+
const results = findUnknownValues('no encoded values')
|
|
120
|
+
assert.is(results.length, 0)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('findUnknownValues - finds multiple encoded values', () => {
|
|
124
|
+
const text = `${encodeUnknown('${a}')} and ${encodeUnknown('${b}')}`
|
|
125
|
+
const results = findUnknownValues(text)
|
|
126
|
+
assert.is(results.length, 2)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
// ==========================================
|
|
130
|
+
// hasEncodedUnknown tests
|
|
131
|
+
// ==========================================
|
|
132
|
+
|
|
133
|
+
test('hasEncodedUnknown - returns true for encoded values', () => {
|
|
134
|
+
const encoded = encodeUnknown('${param:test}')
|
|
135
|
+
assert.is(hasEncodedUnknown(encoded), true)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test('hasEncodedUnknown - returns false for plain text', () => {
|
|
139
|
+
assert.is(hasEncodedUnknown('plain text'), false)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test('hasEncodedUnknown - returns false for regular variables', () => {
|
|
143
|
+
assert.is(hasEncodedUnknown('${param:test}'), false)
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
test.run()
|
package/src/utils/lodash.js
CHANGED
|
@@ -32,14 +32,15 @@ function set(object, path, value) {
|
|
|
32
32
|
const lastIndex = keys.length - 1;
|
|
33
33
|
|
|
34
34
|
for (let i = 0; i < lastIndex; i++) {
|
|
35
|
-
const key = keys[i]
|
|
35
|
+
const key = keys[i]
|
|
36
36
|
|
|
37
|
-
if (
|
|
37
|
+
// Check if value is undefined, null, or not an object (primitives can't have properties)
|
|
38
|
+
if (current[key] == null || typeof current[key] !== 'object') {
|
|
38
39
|
// Create appropriate container based on next key type
|
|
39
|
-
current[key] = Number.isInteger(keys[i + 1]) && keys[i + 1] >= 0 ? [] : {}
|
|
40
|
+
current[key] = Number.isInteger(keys[i + 1]) && keys[i + 1] >= 0 ? [] : {}
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
current = current[key]
|
|
43
|
+
current = current[key]
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
current[keys[lastIndex]] = value;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for custom lodash utility implementations
|
|
3
|
+
*/
|
|
4
|
+
const { test } = require('uvu')
|
|
5
|
+
const assert = require('uvu/assert')
|
|
6
|
+
const { set, trim } = require('./lodash')
|
|
7
|
+
|
|
8
|
+
// ==========================================
|
|
9
|
+
// set() - basic functionality
|
|
10
|
+
// ==========================================
|
|
11
|
+
|
|
12
|
+
test('set - sets value at simple path', () => {
|
|
13
|
+
const obj = {}
|
|
14
|
+
set(obj, 'a', 'value')
|
|
15
|
+
assert.is(obj.a, 'value')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('set - sets value at nested path', () => {
|
|
19
|
+
const obj = {}
|
|
20
|
+
set(obj, 'a.b.c', 'value')
|
|
21
|
+
assert.is(obj.a.b.c, 'value')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('set - creates intermediate objects', () => {
|
|
25
|
+
const obj = {}
|
|
26
|
+
set(obj, 'a.b.c', 'value')
|
|
27
|
+
assert.type(obj.a, 'object')
|
|
28
|
+
assert.type(obj.a.b, 'object')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('set - handles array path', () => {
|
|
32
|
+
const obj = {}
|
|
33
|
+
set(obj, ['a', 'b', 'c'], 'value')
|
|
34
|
+
assert.is(obj.a.b.c, 'value')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('set - creates arrays for numeric keys', () => {
|
|
38
|
+
const obj = {}
|
|
39
|
+
set(obj, 'items.0.name', 'first')
|
|
40
|
+
assert.ok(Array.isArray(obj.items))
|
|
41
|
+
assert.is(obj.items[0].name, 'first')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('set - overwrites existing value', () => {
|
|
45
|
+
const obj = { a: { b: 'old' } }
|
|
46
|
+
set(obj, 'a.b', 'new')
|
|
47
|
+
assert.is(obj.a.b, 'new')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('set - returns the object', () => {
|
|
51
|
+
const obj = {}
|
|
52
|
+
const result = set(obj, 'a', 'value')
|
|
53
|
+
assert.is(result, obj)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('set - returns object unchanged if object is null', () => {
|
|
57
|
+
const result = set(null, 'a.b', 'value')
|
|
58
|
+
assert.is(result, null)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('set - returns object unchanged if object is primitive', () => {
|
|
62
|
+
const result = set('string', 'a.b', 'value')
|
|
63
|
+
assert.is(result, 'string')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
// ==========================================
|
|
67
|
+
// set() - null intermediate values (bug fix)
|
|
68
|
+
// ==========================================
|
|
69
|
+
|
|
70
|
+
test('set - overwrites null intermediate value with object', () => {
|
|
71
|
+
const obj = { a: null }
|
|
72
|
+
set(obj, 'a.b.c', 'value')
|
|
73
|
+
assert.type(obj.a, 'object')
|
|
74
|
+
assert.is(obj.a.b.c, 'value')
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test('set - overwrites null at deeper level', () => {
|
|
78
|
+
const obj = { a: { b: null } }
|
|
79
|
+
set(obj, 'a.b.c', 'value')
|
|
80
|
+
assert.type(obj.a.b, 'object')
|
|
81
|
+
assert.is(obj.a.b.c, 'value')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
// ==========================================
|
|
85
|
+
// set() - primitive intermediate values (bug fix)
|
|
86
|
+
// ==========================================
|
|
87
|
+
|
|
88
|
+
test('set - overwrites string intermediate value', () => {
|
|
89
|
+
const obj = { a: 'string' }
|
|
90
|
+
set(obj, 'a.b.c', 'value')
|
|
91
|
+
assert.type(obj.a, 'object')
|
|
92
|
+
assert.is(obj.a.b.c, 'value')
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('set - overwrites number intermediate value', () => {
|
|
96
|
+
const obj = { a: 42 }
|
|
97
|
+
set(obj, 'a.b.c', 'value')
|
|
98
|
+
assert.type(obj.a, 'object')
|
|
99
|
+
assert.is(obj.a.b.c, 'value')
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test('set - overwrites boolean intermediate value', () => {
|
|
103
|
+
const obj = { a: true }
|
|
104
|
+
set(obj, 'a.b.c', 'value')
|
|
105
|
+
assert.type(obj.a, 'object')
|
|
106
|
+
assert.is(obj.a.b.c, 'value')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('set - overwrites false intermediate value', () => {
|
|
110
|
+
const obj = { a: false }
|
|
111
|
+
set(obj, 'a.b.c', 'value')
|
|
112
|
+
assert.type(obj.a, 'object')
|
|
113
|
+
assert.is(obj.a.b.c, 'value')
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('set - overwrites zero intermediate value', () => {
|
|
117
|
+
const obj = { a: 0 }
|
|
118
|
+
set(obj, 'a.b.c', 'value')
|
|
119
|
+
assert.type(obj.a, 'object')
|
|
120
|
+
assert.is(obj.a.b.c, 'value')
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('set - overwrites empty string intermediate value', () => {
|
|
124
|
+
const obj = { a: '' }
|
|
125
|
+
set(obj, 'a.b.c', 'value')
|
|
126
|
+
assert.type(obj.a, 'object')
|
|
127
|
+
assert.is(obj.a.b.c, 'value')
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
// ==========================================
|
|
131
|
+
// set() - array handling with null/primitives
|
|
132
|
+
// ==========================================
|
|
133
|
+
|
|
134
|
+
test('set - creates array when overwriting null for numeric path', () => {
|
|
135
|
+
const obj = { items: null }
|
|
136
|
+
set(obj, 'items.0.name', 'value')
|
|
137
|
+
assert.ok(Array.isArray(obj.items))
|
|
138
|
+
assert.is(obj.items[0].name, 'value')
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
test('set - creates array when overwriting primitive for numeric path', () => {
|
|
142
|
+
const obj = { items: 'not an array' }
|
|
143
|
+
set(obj, 'items.0.name', 'value')
|
|
144
|
+
assert.ok(Array.isArray(obj.items))
|
|
145
|
+
assert.is(obj.items[0].name, 'value')
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
// ==========================================
|
|
149
|
+
// trim() - basic functionality
|
|
150
|
+
// ==========================================
|
|
151
|
+
|
|
152
|
+
test('trim - removes whitespace from both ends', () => {
|
|
153
|
+
assert.is(trim(' hello '), 'hello')
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
test('trim - handles null', () => {
|
|
157
|
+
assert.is(trim(null), '')
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
test('trim - handles undefined', () => {
|
|
161
|
+
assert.is(trim(undefined), '')
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
test('trim - removes custom characters', () => {
|
|
165
|
+
assert.is(trim('---hello---', '-'), 'hello')
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
test('trim - handles string with no trim needed', () => {
|
|
169
|
+
assert.is(trim('hello'), 'hello')
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
test.run()
|
|
@@ -6,6 +6,7 @@ const flatten = require('lodash.flatten');
|
|
|
6
6
|
const map = require('lodash.map');
|
|
7
7
|
|
|
8
8
|
const functionNames = [
|
|
9
|
+
// Standard intrinsic functions
|
|
9
10
|
'And',
|
|
10
11
|
'Base64',
|
|
11
12
|
'Cidr',
|
|
@@ -23,6 +24,16 @@ const functionNames = [
|
|
|
23
24
|
'Select',
|
|
24
25
|
'Split',
|
|
25
26
|
'Sub',
|
|
27
|
+
'Transform',
|
|
28
|
+
// AWS::LanguageExtensions functions
|
|
29
|
+
'ForEach',
|
|
30
|
+
'Length',
|
|
31
|
+
'ToJsonString',
|
|
32
|
+
// Rule-specific functions (valid in Rules section)
|
|
33
|
+
'EachMemberEquals',
|
|
34
|
+
'EachMemberIn',
|
|
35
|
+
'ValueOf',
|
|
36
|
+
'ValueOfAll',
|
|
26
37
|
];
|
|
27
38
|
|
|
28
39
|
const yamlType = (name, kind) => {
|
|
@@ -31,8 +42,19 @@ const yamlType = (name, kind) => {
|
|
|
31
42
|
kind,
|
|
32
43
|
construct: data => {
|
|
33
44
|
if (name === 'GetAtt') {
|
|
34
|
-
// special GetAtt dot syntax
|
|
35
|
-
|
|
45
|
+
// special GetAtt dot syntax - split only at FIRST dot
|
|
46
|
+
// Attribute names can contain dots (e.g., Endpoint.Address, CertificateDetails.CAIdentifier)
|
|
47
|
+
if (isString(data)) {
|
|
48
|
+
const dotIndex = data.indexOf('.');
|
|
49
|
+
if (dotIndex === -1) {
|
|
50
|
+
return { [functionName]: [data] };
|
|
51
|
+
}
|
|
52
|
+
return { [functionName]: [
|
|
53
|
+
data.substring(0, dotIndex), // Resource name (before first dot)
|
|
54
|
+
data.substring(dotIndex + 1) // Attribute name (after first dot, may contain dots)
|
|
55
|
+
]};
|
|
56
|
+
}
|
|
57
|
+
return { [functionName]: data };
|
|
36
58
|
}
|
|
37
59
|
return { [functionName]: data };
|
|
38
60
|
},
|