configorama 0.9.0 → 0.9.5

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "configorama",
3
- "version": "0.9.0",
3
+ "version": "0.9.5",
4
4
  "description": "Variable support for configuration files",
5
5
  "main": "src/index.js",
6
6
  "types": "index.d.ts",
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
- const exists = dotProp.get(fullObject, varString)
339
- // console.log('fallThroughSelfMatcher exists', exists)
340
- if (!exists) {
341
- // @ Todo make recursive
342
- const deepProperties = [valueObject.path[0]].concat(varString)
343
- const dotPropPath = deepProperties.join('.')
344
- const deeperExists = dotProp.get(fullObject, dotPropPath)
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
- return fullObject[startOf[0]]
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
  /*
@@ -1251,8 +1255,8 @@ class Configorama {
1251
1255
  Config error:\n
1252
1256
  Path "${configValuePath}" resolved to "undefined".\n
1253
1257
  Verify the ${varDisplay} in config at "${configValuePath}".\n
1254
- ${leaf ? `See:\n ${leaf.originalValuePath}: ${leaf.originalSource} ` : ''}
1255
- ${leaf && leaf.isFileRef ? `\n The error could be deeper in the referenced file at ${configValuePath.replace(leaf.originalValuePath, '').replace(/^\./, '')} key.\n` : ''}`
1258
+ ${leaf ? `See:\n ${configValuePath}: ${leaf.originalSource} ` : ''}
1259
+ ${leaf && leaf.isFileRef ? `\n The error could be deeper in the referenced file at ${configValuePath.replace(leaf.originalValuePath || configValuePath, '').replace(/^\./, '')} key.\n` : ''}`
1256
1260
  throw new Error(errorMessage)
1257
1261
  }
1258
1262
  if (typeof rawValue === 'string') {
@@ -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
- const dotPropPath = deepProperties.length > 1 ? deepProperties.join('.') : deepProperties[0]
3268
- const exists = dotProp.get(valueToPopulate, dotPropPath)
3269
- // console.log('self exists', exists)
3270
- if (!exists) {
3271
- // @ Todo make recursive
3272
- deepProperties = [data.path[0]].concat(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
- argsToPass = formatFunctionArgs(splitter)
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
@@ -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.replace(nested, `"${nested}"`)
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.replace(nested, `"${nested}"`)
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
  }
@@ -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
- const returnValueFunction = moduleName ? jsFile[moduleName] : jsFile
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('ascii')
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()
@@ -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 (current[key] === undefined) {
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
- return { [functionName]: isString(data) ? split(data, '.', 2) : data };
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
  },