-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathsource-configs.js
325 lines (287 loc) · 10.2 KB
/
source-configs.js
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
const Logger = require('roosevelt-logger')
const yargsParser = require('yargs-parser')
let logger
function sourceConfigs (schema, config) {
/**
* when a custom config source doesn't supply an object...
* source the config from the schema instead if it exists
* otherwise ignore it
*/
// ensure config is an object
config = config || {}
// set default source priority when unset
config.sources = config.sources || [
'command line',
'environment variable'
]
// setup the logger
const params = {
params: {
disable: ['SILENT_MODE'] // disable logging during Mocha tests
}
}
logger = new Logger(params)
// disable logging if config turns it off
if (config.logging === false) {
logger.disableLogging()
}
// parse cli args
const commandLineArgs = yargsParser(process.argv.slice(2))
// build the configuration
const configs = parseObject('', schema, commandLineArgs, config.sources)
// run transformation on config if function is in use
if (config.transform && typeof config.transform === 'function') {
sourceConfigs.configs = config.transform(configs, commandLineArgs)
}
const printHelp = function () {
let menu = 'Options:\n'
for (const configName in schema) {
const item = schema[configName]
if (item.commandLineArg !== undefined) {
let line = ' '
if (isStringArray(item.commandLineArg)) {
const args = item.commandLineArg.slice()
args.sort((a, b) => {
return a.length - b.length
})
line += args[0]
for (const arg of args.slice(1)) {
line += ', ' + arg
}
} else {
line += item.commandLineArg
}
if (line.length < 30) {
line += ' '.repeat(30 - line.length)
}
if (item.desc !== undefined) {
line += item.desc
}
if (item.default !== undefined) {
line += ` (default: ${item.default})`
}
menu += line + '\n'
}
}
return menu
}
const safelyPrintSchema = function () {
const postProcessedConfig = {}
for (const key in configs) {
const val = configs[key]
if (schema[key]?.secret) postProcessedConfig[key] = '********'
else postProcessedConfig[key] = val
}
return postProcessedConfig
}
// expose features
sourceConfigs.configs = configs
sourceConfigs.commandLineArgs = commandLineArgs
sourceConfigs.yargsParser = yargsParser
sourceConfigs.printHelp = printHelp
sourceConfigs.safelyPrintSchema = safelyPrintSchema
return sourceConfigs.configs
}
/**
* method to grab items a from configuration object
* @module getFromConfig
*/
function getFromConfig (data, path) {
let pointer = data
const sections = path.split('.')
let i = 0
while (i < sections.length) {
pointer = pointer[sections[i]]
if (pointer === undefined) break
i++
}
return pointer
}
/**
* Recursive function to go through config schema and generate configuration
* @function parseObject
* @param {string} path - current path of the object being parsed delimited by a period
* @param {Object} obj - current level of the config object
* @param {Object} commandLineArgs - parsed commmand line arguments
* @param {Array} sources - list of sources to check from
* @return {Object} generated config object
*/
function parseObject (path, obj, commandLineArgs, sources) {
const config = {}
for (const key in obj) {
const newPath = path === '' ? key : path + '.' + key
// Check if a user-defined function has been implemented before calling init. if not, notify the user on such.
if (obj[key] === 'user defined function' || obj[key] === 'user-defined function') {
logger.error(`Error: Expected user-defined function to be implemented in app level code for schema.${newPath}...`)
logger.error('Setting field to null')
config[key] = null
continue
} else if (typeof obj[key] === 'function') {
config[key] = obj[key](config)
continue
}
// Recurse if the current object is not a primitive (has 'desc', 'envVar', 'default' fields)
if (!isPrimitive(obj[key])) {
config[key] = parseObject(newPath, obj[key], commandLineArgs, sources)
} else {
// Grab the config result from Command Line Args, Environment Variables, or defaults
let configResult = checkConfig(newPath, obj[key], commandLineArgs, sources)
// If value is an enum, make sure it is valid
if (obj[key].values !== undefined) {
configResult = checkEnum(newPath, configResult, obj[key])
}
// Check if it's an array, and if it has strings as values, typecast them
if (isStringArray(configResult)) {
configResult = configResult.map(arrayEntry => typeCastEntry(arrayEntry))
}
// Typecast in case of strings that could be numbers or booleans ('2' -> 2, 'false' -> false)
if ((typeof configResult) === 'string') {
configResult = typeCastEntry(configResult)
}
config[key] = configResult
}
}
return config
}
/**
* Try getting config item from various locations
* @function checkConfig
* @param {string} path - current path of the object being parsed delimited by a period
* @param {Object} configObject - current level of the config object
* @param {Object} commandLineArgs - parsed command line arguments
* @param {Array} sources - list of sources to check from
* @return {*} - the value found for the config item
*/
function checkConfig (path, configObject, commandLineArgs, sources) {
let value
// start looping through sources list
for (const key in sources) {
const source = sources[key]
// handle command line args
if (source === 'command line' || source.name === 'commandLineArg') {
if (commandLineArgs !== undefined && configObject.commandLineArg !== undefined) {
if (isStringArray(configObject.commandLineArg)) {
const parsedArgs = yargsParser(configObject.commandLineArg)
for (const arg in parsedArgs) {
if (arg !== '_') {
if (commandLineArgs[arg] !== undefined) {
value = commandLineArgs[arg]
break
}
}
}
if (value !== undefined) {
break
}
} else {
if (commandLineArgs[configObject.commandLineArg.slice(2)] !== undefined) {
value = commandLineArgs[configObject.commandLineArg.slice(2)]
break
}
}
}
} else if (source === 'environment variable' || source.name === 'envVar') {
// handle environment variables
if (configObject.envVar !== undefined) {
if (isStringArray(configObject.envVar)) {
for (const envVar of configObject.envVar) {
if (process.env[envVar]) {
value = process.env[envVar]
break
}
}
if (value !== undefined) {
break
}
} else {
if (process.env[configObject.envVar]) {
value = process.env[configObject.envVar]
break
}
}
}
} else if (typeof source === 'object') {
// handle custom type
if (getFromConfig(source, path) !== undefined) {
value = getFromConfig(source, path)
break
}
}
}
// if no value was set try to use the default
if (value === undefined && configObject.default !== undefined) {
value = configObject.default
}
// if value is still not set make it the config's name
if (value === undefined) {
value = path
}
// return the value or null
return value
}
/**
* Type cast strings into the correct type (string -> number, boolean)
* @function typeCastEntry
* @param {string} entryString - the string that will be parsed to the correct type
* @return {(string|number|boolean)} - the config entry with the correct type
*/
function typeCastEntry (entryString) {
if (entryString.match(/^\d+$/)) {
// Number
return parseInt(entryString)
} else if (['true', 'false'].includes(entryString.toLowerCase())) {
// Boolean
return entryString.toLowerCase() === 'true'
} else {
// String
return entryString
}
}
/**
* Check if the configResult is valid with the configObject's accepted values
* @function checkEnum
* @param {string} path - current path of the object being parsed delimited by a period
* @param {string} configResult - outputted config string
* @param {Object} configObject - schema object of config primitive
* @return {string} config result after passing it through the pass.
*/
function checkEnum (path, configResult, configObject) {
if (!configObject.values.includes(configResult)) {
if (configObject.default !== undefined) {
logger.warn('Waring: Trying to set config.' + path + ' and found invalid enum value. Setting to default: ' + configObject.default)
logger.warn('Accepted values are: ' + configObject.values.join(', '))
configResult = configObject.default
} else {
logger.error('Error: Trying to set config.' + path + ' and found invalid enum value and no default found. Set to null')
logger.error('Accepted values are: ' + configObject.values.join(', '))
configResult = null
}
}
return configResult
}
/**
* Check if a configObject is a primitive
* All primitives have a .default property so it will fail if that property is undefined
* @function isPrimitive
* @param {Object} configObject - schema object of config primitive
* @return {boolean} - boolean result of if it is a primitive
*/
function isPrimitive (configObject) {
return typeof configObject.description !== 'object' && // If description is not a string it is another configured config item and not a primitive, return false
(Object.keys(configObject).length === 0 ||
configObject.default !== undefined ||
configObject.commandLineArg !== undefined ||
configObject.description !== undefined ||
configObject.values !== undefined ||
configObject.envVar !== undefined)
}
/**
* Check if the configResult is a string array
* @function isStringArray
* @param {*} configResult - outputted config string
* @return {boolean} - boolean result of it is a string array
*/
function isStringArray (configResult) {
return Array.isArray(configResult) && (typeof configResult[0]) === 'string'
}
module.exports = sourceConfigs