├── .gitignore ├── README.md ├── index.js ├── out.gif ├── package.json └── test ├── config-template.test.js └── helper.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # config-template 2 | Create server specific configuration files from a template via the command line. 3 | 4 | ![image](/out.gif) 5 | 6 | ### Install 7 | 8 | `npm install --save config-template` 9 | 10 | ### Use case 11 | 12 | * Deploying your app to a new server via an automated script 13 | * Installing your app on your dev machine for the first time 14 | * Using a task runner (eg. gulp) to check if the config file exists and if not, prompting the user to create it before booting up the app. 15 | 16 | Imagine you have a configuration file on your dev machine that looks like this: 17 | 18 | `config.js` 19 | 20 | ```js 21 | module.exports = { 22 | environment: 'development', 23 | port: 3000, 24 | devMode: true, 25 | database: { 26 | connection: { 27 | user: 'alarner', 28 | password: 'not a real password', 29 | database: 'test' 30 | } 31 | } 32 | }; 33 | ``` 34 | 35 | Whenever you want to deploy this app or set up a new dev, test or staging instance you'll want to tweak those values and set up a new config file. You'll also want to document all of the settings that are available and describe what they do. For example your config.template.js file might look like this: 36 | 37 | `config.template.js` 38 | 39 | ```js 40 | module.exports = { 41 | environment: '[string] The environment to run under.', 42 | port: '[number] The web server port.', 43 | debug: '[boolean] Show debug messages or not.', 44 | database: { 45 | connection: { 46 | user: '[string] Database user.', 47 | password: '[string] Database password.', 48 | database: '[string] Database name.' 49 | } 50 | } 51 | }; 52 | ``` 53 | 54 | Config loader can read a template file like the one above and provide a command line interface for creating that config file for the first time. 55 | 56 | ```js 57 | var configTemplate = require('config-template'); 58 | var tpl = require('./config.template.js'); 59 | 60 | configTemplate(tpl).then(function(config) { 61 | console.log(JSON.stringify(config)); 62 | }) 63 | 64 | /* 65 | 66 | { 67 | environment: 'development', 68 | port: 3000, 69 | devMode: true, 70 | database: { 71 | connection: { 72 | user: 'alarner', 73 | password: 'not a real password', 74 | database: 'test' 75 | } 76 | } 77 | } 78 | 79 | */ 80 | ``` 81 | 82 | ![image](/out.gif) 83 | 84 | ## Supported data types 85 | 86 | * string 87 | * number 88 | * boolean 89 | * json 90 | 91 | ## Options 92 | 93 | An options object can be passed as the second argument to config loader. Config loader understand 94 | the following options... 95 | 96 | * inputSource - defaults to stdin. Mostly used for testing, but you can supply a different input source if you choose. 97 | * values - defaults to an empty object. This option allows you to specify any values that you'd like to be pre-filled in. 98 | * appendExtraData - defaults to true. If this is set to true and options.values has properties that don'e match up with one of your template properties, config loader will automatically add that property to the template. If it is set to false then extraneous properties in options.values will be ignored. 99 | 100 | ## Features 101 | 102 | * Displays your customized description (from your template) of the config property when that property is selected. 103 | * Basic data validation 104 | * Ability to ignore / remove properties from the config object 105 | * Set empty strings 106 | * Color coding 107 | 108 | ## Todo 109 | 110 | * horizontal scrolling -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*eslint strict:0 */ 2 | 'use strict'; 3 | let _ = require('lodash'); 4 | let term = require('terminal-kit').terminal; 5 | let keypress = require('keypress'); 6 | let spawn = require('child_process').spawn; 7 | let validator = require('validator'); 8 | 9 | const TAB = ' '; 10 | const helpRegex = /^\[(string|number|boolean|json)\].*$/i; 11 | 12 | /* 13 | * Takes a config template as well as extra data that should be added to the template. Returns a new 14 | * template with the extra data included. 15 | */ 16 | function appendExtraData(tmpl, extra) { 17 | const returnObject = {}; 18 | if (!extra) extra = {}; 19 | for (const key in tmpl) { 20 | if (tmpl.hasOwnProperty(key)) { 21 | if (_.isObject(tmpl[key])) { 22 | returnObject[key] = appendExtraData(tmpl[key], extra[key]); 23 | } else { 24 | returnObject[key] = tmpl[key]; 25 | } 26 | } 27 | } 28 | for (const key in extra) { 29 | if (extra.hasOwnProperty(key)) { 30 | if (_.isObject(extra[key])) { 31 | if (!tmpl[key]) tmpl[key] = {}; 32 | if (!tmpl[key].toString().match(/^\[json\].*/)) 33 | returnObject[key] = appendExtraData(tmpl[key], extra[key]); 34 | } else { 35 | if (!tmpl.hasOwnProperty(key)) returnObject[key] = buildTemplateStringFromValue(extra[key]); 36 | } 37 | } 38 | } 39 | return returnObject; 40 | } 41 | 42 | function setDefaultValues(parsedTmpl, values) { 43 | for (let i=0; i { 163 | keypress(process.stdin); 164 | // c.pipe(process.stdout); 165 | 166 | readStream.resume(); 167 | readStream.setEncoding('utf8'); 168 | readStream.setRawMode(true); 169 | state.width = process.stdout.columns; 170 | state.height = process.stdout.rows; 171 | 172 | readStream.on('keypress', onKeyPress); 173 | goToLine(state.currentLineIndex); 174 | 175 | setTimeout(render, 200); 176 | 177 | function onKeyPress(text, key) { 178 | // {"name":"c","ctrl":true,"meta":false,"shift":false,"sequence":"\u0003"} 179 | let charCode = text ? text.charCodeAt(0) : null; 180 | let line = lines[state.currentLineIndex]; 181 | if(key && key.ctrl) { 182 | if(key.name === 'c') { 183 | exit((err, result) => { 184 | if(result) { 185 | reject('Edit session canceled by user.'); 186 | } 187 | }); 188 | } 189 | if(key.name === 's') { 190 | exit((err, result) => { 191 | if(result) { 192 | resolve(buildObject()); 193 | } 194 | }); 195 | } 196 | else if(key.name === 'e' && line.type === 'string') { 197 | line.empty = true; 198 | line.deleted = false; 199 | line.value = ''; 200 | state.cursor.x = background[line.backgroundLineNum].length + 2; 201 | render(); 202 | } 203 | else if(key.name === 'r') { 204 | line.deleted = true; 205 | line.empty = false; 206 | line.value = ''; 207 | state.cursor.x = background[line.backgroundLineNum].length + 1; 208 | render(); 209 | } 210 | } 211 | else if(key && key.name === 'return') { 212 | goToLine(state.currentLineIndex+1); 213 | } 214 | else if(key && key.name === 'down') { 215 | goToLine(state.currentLineIndex+1); 216 | } 217 | else if(key && key.name === 'up') { 218 | goToLine(state.currentLineIndex-1); 219 | } 220 | else if(key && key.name === 'left') { 221 | let offset = line.type === 'string' && !line.deleted ? 1 : 0; 222 | if(state.cursor.x-1 > background[line.backgroundLineNum].length + offset) { 223 | state.cursor.x--; 224 | render(); 225 | } 226 | } 227 | else if(key && key.name === 'right') { 228 | let offset = line.type === 'string' && !line.deleted ? 1 : 0; 229 | if(state.cursor.x+1 <= background[line.backgroundLineNum].length + line.value.length + offset + 1) { 230 | state.cursor.x++; 231 | render(); 232 | } 233 | } 234 | else if(key && key.name === 'backspace') { 235 | let offset = line.type === 'string' && !line.deleted ? 1 : 0; 236 | if(state.cursor.x-1 > background[line.backgroundLineNum].length + offset) { 237 | del(); 238 | } 239 | 240 | } 241 | else if(charCode >= 32 && charCode <= 126) { 242 | insert(text); 243 | } 244 | } 245 | 246 | function splice(text, idx, rem, str) { 247 | return text.slice(0, idx) + str + text.slice(idx + Math.abs(rem)); 248 | }; 249 | 250 | function insert(char) { 251 | let line = lines[state.currentLineIndex]; 252 | let pos = state.cursor.x - background[line.backgroundLineNum].length; 253 | state.cursor.x++; 254 | 255 | // Deal with quotation marks 256 | if(line.type === 'string' && !line.empty) { 257 | pos--; 258 | if(!line.value){ 259 | state.cursor.x++; 260 | } 261 | } 262 | line.deleted = false; 263 | line.empty = false; 264 | line.value = splice(line.value, pos-1, 0, char); 265 | render(); 266 | } 267 | 268 | function del() { 269 | let line = lines[state.currentLineIndex]; 270 | let pos = state.cursor.x - background[line.backgroundLineNum].length - 2; 271 | state.cursor.x--; 272 | 273 | // Deal with quotation marks 274 | if(line.value && line.type === 'string' && !line.empty) { 275 | pos -= 1; 276 | } 277 | 278 | line.value = splice(line.value, pos, 1, ''); 279 | 280 | // Deal with removing marks 281 | if(!line.value && line.type === 'string' && !line.empty) { 282 | state.cursor.x--; 283 | } 284 | 285 | render(); 286 | } 287 | 288 | function checkError(line) { 289 | if(!line.value) { 290 | return ''; 291 | } 292 | switch(line.type) { 293 | case 'number': 294 | return validator.isDecimal(line.value) ? '' : 'Invalid number'; 295 | case 'boolean': 296 | line.value = line.value.toLowerCase(); 297 | return validator.isIn(line.value, ['true', 'false']) ? '' : 'Invalid boolean'; 298 | case 'json': 299 | return validator.isJSON(line.value) ? '' : 'Invalid JSON. Use double quotes on keys and values.'; 300 | } 301 | return ''; 302 | } 303 | 304 | function goToLine(num) { 305 | // Validate data 306 | let line = lines[state.currentLineIndex]; 307 | state.error = checkError(line); 308 | if(state.error) { 309 | return render(); 310 | } 311 | 312 | // Move line 313 | let updatedNum = num; 314 | if(num < 0) { 315 | state.top = 0; 316 | updatedNum = 0; 317 | } 318 | else if(num >= lines.length) { 319 | updatedNum = state.currentLineIndex = lines.length-1; 320 | } 321 | let x = background[lines[updatedNum].backgroundLineNum].length + lines[updatedNum].value.length + 1; 322 | let y = lines[updatedNum].backgroundLineNum + state.headerHeight - state.top + 1; 323 | 324 | if(y < state.top - 1) { 325 | state.top += y - state.top + 1; 326 | } 327 | else if(num >= lines.length) { 328 | state.top = Math.max(0, background.length - state.height + state.headerHeight + state.footerHeight); 329 | } 330 | else if(y > state.height - state.headerHeight) { 331 | state.top += y - state.height + state.headerHeight; 332 | } 333 | 334 | y = lines[updatedNum].backgroundLineNum + state.headerHeight - state.top + 1; 335 | 336 | state.cursor.x = x; 337 | state.cursor.y = y; 338 | state.currentLineIndex = updatedNum; 339 | 340 | render(); 341 | } 342 | 343 | function buildObject() { 344 | let obj = {}; 345 | term.eraseDisplay(); 346 | 347 | lines.forEach((line) => { 348 | if(line.deleted) { 349 | return; 350 | } 351 | let target = obj; 352 | for(let i=0; i { 432 | if(line.backgroundLineNum < state.top || line.backgroundLineNum >= endLine) { 433 | return; 434 | } 435 | let y = line.backgroundLineNum - state.top + state.headerHeight + 1; 436 | let x = background[line.backgroundLineNum].length + 1; 437 | term.moveTo(x, y); 438 | if(line.value) { 439 | switch(line.type) { 440 | case 'string': 441 | term.yellow('"'+line.value+'"'); 442 | break; 443 | case 'number': 444 | term.cyan(line.value); 445 | break; 446 | case 'boolean': 447 | term.magenta(line.value); 448 | break; 449 | case 'json': 450 | term.colorRgb(150, 200, 255, line.value); 451 | break; 452 | default: 453 | term(line.value); 454 | break; 455 | } 456 | 457 | } 458 | else if(line.empty) { 459 | term.yellow('""'); 460 | } 461 | else if(line.deleted) { 462 | term.italic.colorRgb(50, 50, 50, '- removed -'); 463 | } 464 | else { 465 | term.colorRgb(50, 50, 50, '______'); 466 | } 467 | term(','); 468 | }); 469 | } 470 | }); 471 | 472 | } 473 | 474 | module.exports = function(configTemplate, options) { 475 | if (!options) options = {}; 476 | if (!options.inputSource) options.inputSource = process.stdin; 477 | if (!options.values) options.values = {}; 478 | if (options.appendExtraData==null) options.appendExtraData = true; 479 | if (options.appendExtraData) { 480 | configTemplate = appendExtraData(configTemplate, options.values); 481 | } 482 | let parsedTmpl = interpretTmpl(configTemplate); 483 | setDefaultValues(parsedTmpl, options.values); 484 | let prev = {}; 485 | let background = []; 486 | 487 | parsedTmpl.lines.forEach((line) => { 488 | background = background 489 | .concat(getFiller(prev.path || [], line.path)) 490 | .concat(getLine(line)); 491 | line.backgroundLineNum = background.length-1; 492 | prev = line; 493 | }); 494 | background = background 495 | .concat(getFiller(prev.path, [])); 496 | background[background.length-1] = background[background.length-1].substr(0, background[background.length-1].length-1); 497 | 498 | return editor(background, parsedTmpl.lines, options.inputSource); 499 | }; 500 | 501 | module.exports.interpretTmpl = interpretTmpl; 502 | module.exports.appendExtraData = appendExtraData; 503 | module.exports.setDefaultValues = setDefaultValues; 504 | module.exports.buildTemplateStringFromValue = buildTemplateStringFromValue; 505 | -------------------------------------------------------------------------------- /out.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alarner/config-template/ec93a96634fe411ef901eea7a95bebf4f992e30a/out.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "config-template", 3 | "version": "1.1.0", 4 | "description": "A command line utility to traverse a JSON formatted template configuration file and produce a new configuration object", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha ./test/*.test.js" 8 | }, 9 | "author": "Aaron Larner", 10 | "license": "ISC", 11 | "dependencies": { 12 | "keypress": "^0.2.1", 13 | "lodash": "^4.17.2", 14 | "terminal-kit": "^0.25.3", 15 | "validator": "^6.1.0" 16 | }, 17 | "devDependencies": { 18 | "chai": "^3.5.0", 19 | "mocha": "^3.2.0" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/alarner/config-template.git" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/config-template.test.js: -------------------------------------------------------------------------------- 1 | const should = require('chai').should(); 2 | const expect = require('chai').expect; 3 | const helper = require('./helper'); 4 | const configTemplate = require('..'); 5 | 6 | describe('Template Editor', function(){ 7 | 8 | it('edits from template', function(done){ 9 | var fakeSTDIN = new helper.FakeSTDIN(); 10 | configTemplate({ // Template 11 | environment: '[string] The environment to run under.', 12 | port: '[number] The web server port.', 13 | debug: '[boolean] Show debug messages or not.', 14 | database: { 15 | connection: { 16 | user: '[string] Database user.', 17 | password: '[string] Database password.', 18 | database: '[string] Database name.' 19 | } 20 | } 21 | }, { // Options 22 | inputSource: fakeSTDIN 23 | }).then(function(config) { 24 | config.should.be.deep.equal({ 25 | environment: 'Testing', 26 | port: 123, 27 | debug: true, 28 | database: { 29 | connection: { 30 | user: 'someone', 31 | password: 'mysecret', 32 | database: 'that-database' 33 | } 34 | } 35 | }); 36 | done(); 37 | }).catch(done); 38 | // Simulate use input: 39 | fakeSTDIN.write('Testing'); 40 | fakeSTDIN.keypress({name:'down'}); 41 | fakeSTDIN.write('123'); 42 | fakeSTDIN.keypress({name:'down'}); 43 | fakeSTDIN.write('true'); 44 | fakeSTDIN.keypress({name:'down'}); 45 | fakeSTDIN.write('someone'); 46 | fakeSTDIN.keypress({name:'down'}); 47 | fakeSTDIN.write('mysecret'); 48 | fakeSTDIN.keypress({name:'down'}); 49 | fakeSTDIN.write('that-database'); 50 | fakeSTDIN.keypress({name:'s', ctrl:true}); 51 | }); 52 | 53 | it('edits from template with default values', function(done){ 54 | var fakeSTDIN = new helper.FakeSTDIN(); 55 | configTemplate({ // Template 56 | aa: '[string] Some text.', 57 | bb: '[number] Some number.', 58 | cc: '[string] Other Text.', 59 | }, { // Options 60 | inputSource: fakeSTDIN, 61 | values: { 62 | aa: 'Banana', 63 | bb: '12', 64 | cc: 'Dog' 65 | } 66 | }).then(function(config) { 67 | config.should.be.deep.equal({ 68 | aa: 'Banana and Cucumber', 69 | bb: 1234, 70 | cc: 'Cat' 71 | }); 72 | done(); 73 | }).catch(done); 74 | // Simulate use input: 75 | fakeSTDIN.keypress({name:'right'}); 76 | fakeSTDIN.write(' and Cucumber'); 77 | fakeSTDIN.keypress({name:'down'}); 78 | fakeSTDIN.write('34'); 79 | fakeSTDIN.keypress({name:'down'}); 80 | fakeSTDIN.keypress({name:'right'}); 81 | fakeSTDIN.keypress({name:'backspace'}); 82 | fakeSTDIN.keypress({name:'backspace'}); 83 | fakeSTDIN.keypress({name:'backspace'}); 84 | fakeSTDIN.write('Cat'); 85 | fakeSTDIN.keypress({name:'s', ctrl:true}); 86 | }); 87 | 88 | it('appends extra data from values by default', function(done){ 89 | var fakeSTDIN = new helper.FakeSTDIN(); 90 | configTemplate({ // Template 91 | aa: '[string] Some text.' 92 | }, { // Options 93 | inputSource: fakeSTDIN, 94 | values: { 95 | aa: 'Banana', 96 | bb: 12345 97 | } 98 | }).then(function(config) { 99 | config.should.be.deep.equal({ 100 | aa: 'Banana', 101 | bb: 12345 102 | }); 103 | done(); 104 | }).catch(done); 105 | // Simulate use input: 106 | fakeSTDIN.keypress({name:'s', ctrl:true}); 107 | }); 108 | 109 | it('does not append extra data if appendExtraData==false', function(done){ 110 | var fakeSTDIN = new helper.FakeSTDIN(); 111 | configTemplate({ // Template 112 | aa: '[string] Some text.' 113 | }, { // Options 114 | inputSource: fakeSTDIN, 115 | appendExtraData: false, 116 | values: { 117 | aa: 'Banana', 118 | bb: 12345 119 | } 120 | }).then(function(config) { 121 | config.should.be.deep.equal({ 122 | aa: 'Banana' 123 | }); 124 | done(); 125 | }).catch(done); 126 | // Simulate use input: 127 | fakeSTDIN.keypress({name:'s', ctrl:true}); 128 | }); 129 | 130 | }); 131 | 132 | describe('Editor Helpers', function(){ 133 | 134 | describe('buildTemplateStringFromValue', function() { 135 | it('should work with strings', function() { 136 | expect(configTemplate.buildTemplateStringFromValue('this is a test')) 137 | .to.equal('[string] Defaults to "this is a test"'); 138 | }); 139 | it('should work with numbers', function() { 140 | expect(configTemplate.buildTemplateStringFromValue(7)) 141 | .to.equal('[number] Defaults to 7'); 142 | }); 143 | it('should work with booleans', function() { 144 | expect(configTemplate.buildTemplateStringFromValue(true)) 145 | .to.equal('[boolean] Defaults to true'); 146 | }); 147 | it('should work with objects', function() { 148 | expect(configTemplate.buildTemplateStringFromValue({foo: 'bar'})) 149 | .to.equal('[json] Defaults to {"foo":\"bar\"}'); 150 | }); 151 | }); 152 | 153 | describe('appendExtraData', function(){ 154 | 155 | it ('add data fields to tmpl', function(){ 156 | const tmpl = configTemplate.appendExtraData({ 157 | env: '[string] The environment.', 158 | port: '[number] to open', 159 | db: { 160 | user: '[string] DB user.' 161 | } 162 | }, { 163 | env: 'Testing', 164 | debug: true, 165 | db: { 166 | user: 'someone', 167 | host: 'example.com' 168 | } 169 | }); 170 | expect(tmpl).to.be.deep.equal({ 171 | env: '[string] The environment.', 172 | port: '[number] to open', 173 | debug: '[boolean] Defaults to true', 174 | db: { 175 | user: '[string] DB user.', 176 | host: '[string] Defaults to "example.com"' 177 | } 178 | }); 179 | }); 180 | 181 | it ('add nestled data fields to tmpl', function(){ 182 | const tmpl = configTemplate.appendExtraData({ 183 | aa: '[string] something...', 184 | }, { 185 | bb: 'otherthing', 186 | xx: { cc: 'morething' } 187 | }); 188 | expect(tmpl).to.deep.equal({ 189 | aa: '[string] something...', 190 | bb: '[string] Defaults to "otherthing"', 191 | xx: { cc: '[string] Defaults to "morething"' } 192 | }); 193 | }); 194 | 195 | it ('do not add data fields in a parent declared as `[json]`', function(){ 196 | const tmpl = configTemplate.appendExtraData({ 197 | aa: '[string] something...', 198 | xx: '[json] anything...' 199 | }, { 200 | bb: 'otherthing', 201 | xx: { cc: 'morething' } 202 | }); 203 | expect(tmpl).to.be.deep.equal({ 204 | aa: '[string] something...', 205 | bb: '[string] Defaults to "otherthing"', 206 | xx: '[json] anything...' 207 | }); 208 | }); 209 | 210 | it('nested json field', function() { 211 | var tmpl = configTemplate.appendExtraData({ 212 | aa: '[string] something...', 213 | xx: { 214 | test: '[string] foo', 215 | json: '[json] anything...' 216 | } 217 | }, 218 | { 219 | bb: 'otherthing', 220 | xx: { test: 'morething', json: { a: 'b' } } 221 | } 222 | ); 223 | tmpl.should.be.deep.equal({ 224 | aa: '[string] something...', 225 | bb: '[string] Defaults to "otherthing"', 226 | xx: { 227 | test: '[string] foo', 228 | json: '[json] anything...' 229 | } 230 | }); 231 | }); 232 | 233 | }); 234 | 235 | describe('interpretTmpl', function(){ 236 | 237 | it ('recognize template structure', function(){ 238 | const parsedTmpl = configTemplate.interpretTmpl({ 239 | env: '[string] The environment.', 240 | port: '[number] to open', 241 | db: { 242 | user: '[string] DB user.', 243 | passwd: '[string] DB password.' 244 | } 245 | }); 246 | expect(parsedTmpl).to.be.deep.equal({ 247 | lines: [ 248 | { 249 | type: 'string', deleted: false, empty: false, line: 1, value: '', 250 | help: '[string] The environment.', 251 | path: ['env'] 252 | }, { 253 | type: 'number', deleted: false, empty: false, line: 2, value: '', 254 | help: '[number] to open', 255 | path: ['port'] 256 | }, { 257 | type: 'string', deleted: false, empty: false, line: 3, value: '', 258 | help: '[string] DB user.', 259 | path: ['db', 'user'] 260 | }, { 261 | type: 'string', deleted: false, empty: false, line: 4, value: '', 262 | help: '[string] DB password.', 263 | path: ['db', 'passwd'] 264 | } 265 | ], 266 | counter: 5 267 | }); 268 | }); 269 | 270 | it('throws error if template format is incorrect', function() { 271 | expect(() => { 272 | configTemplate.interpretTmpl({ 273 | env: 'The environment.', 274 | port: 123, 275 | ok: true 276 | }) 277 | }).to.throw('The environment. does not adhere to template syntax: [string|number|boolean|json] help message'); 278 | }); 279 | 280 | }); 281 | 282 | describe('setDefaultValues', function() { 283 | 284 | it('Add values to parsed template', function(){ 285 | const parsedTmpl = configTemplate.interpretTmpl({ 286 | env: '[string] The environment.', 287 | port: '[number] to open', 288 | db: { 289 | user: '[string] DB user.' 290 | } 291 | }); 292 | configTemplate.setDefaultValues(parsedTmpl, { 293 | env: 'Testing', 294 | port: 1234, 295 | db: { 296 | user: 'someone' 297 | } 298 | }); 299 | expect(parsedTmpl).to.be.deep.equal({ 300 | lines: [ 301 | { 302 | type: 'string', deleted: false, empty: false, line: 1, 303 | value: 'Testing', 304 | help: '[string] The environment.', 305 | path: ['env'] 306 | }, { 307 | type: 'number', deleted: false, empty: false, line: 2, 308 | value: '1234', 309 | help: '[number] to open', 310 | path: ['port'] 311 | }, { 312 | type: 'string', deleted: false, empty: false, line: 3, 313 | value: 'someone', 314 | help: '[string] DB user.', 315 | path: ['db', 'user'] 316 | } 317 | ], 318 | counter: 4 319 | }); 320 | }); 321 | 322 | it('ignore missing values', function(){ 323 | var parsedTmpl = configTemplate.interpretTmpl({ 324 | aa: '[string] something...', 325 | bb: '[number] otherthing...' 326 | }); 327 | configTemplate.setDefaultValues(parsedTmpl, { 328 | aa: 'some value' 329 | }); 330 | parsedTmpl.should.be.deep.equal({ 331 | lines: [ 332 | { 333 | type: 'string', deleted: false, empty: false, line: 1, 334 | value: 'some value', 335 | help: '[string] something...', 336 | path: ['aa'] 337 | }, { 338 | type: 'number', deleted: false, empty: false, line: 2, 339 | value: '', 340 | help: '[number] otherthing...', 341 | path: ['bb'] 342 | } 343 | ], 344 | counter: 3 345 | }); 346 | }); 347 | 348 | it('ignore extra values', function(){ 349 | var parsedTmpl = configTemplate.interpretTmpl({ 350 | aa: '[string] something...', 351 | bb: '[number] otherthing...' 352 | }); 353 | configTemplate.setDefaultValues(parsedTmpl, { 354 | aa: 'some value', 355 | cc: 'other value' 356 | }); 357 | parsedTmpl.should.be.deep.equal({ 358 | lines: [ 359 | { 360 | type: 'string', deleted: false, empty: false, line: 1, 361 | value: 'some value', 362 | help: '[string] something...', 363 | path: ['aa'] 364 | }, { 365 | type: 'number', deleted: false, empty: false, line: 2, 366 | value: '', 367 | help: '[number] otherthing...', 368 | path: ['bb'] 369 | } 370 | ], 371 | counter: 3 372 | }); 373 | }); 374 | 375 | }); 376 | 377 | }); 378 | -------------------------------------------------------------------------------- /test/helper.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('events'); 2 | 3 | var sleep = exports.sleep = function sleep(ms) { 4 | var sleepInit = Date.now(); 5 | while( sleepInit+ms > Date.now() ) { /* sleep */ } 6 | } 7 | 8 | var FakeSTDIN = exports.FakeSTDIN = function FakeSTDIN() { } 9 | FakeSTDIN.prototype = new EventEmitter(); 10 | FakeSTDIN.prototype.pause = function(){ }; 11 | FakeSTDIN.prototype.resume = function(){ }; 12 | FakeSTDIN.prototype.setRawMode = function(){ }; 13 | FakeSTDIN.prototype.setEncoding = function(){ }; 14 | FakeSTDIN.prototype.write = function(data) { 15 | for (var char,i=0; char=data[i]; i++) { 16 | this.keypress({name:char}); 17 | } 18 | }; 19 | FakeSTDIN.prototype.keypress = function(data) { 20 | sleep(33); // because we need to see that working. 21 | this.emit('keypress', data.name, data); 22 | }; 23 | FakeSTDIN.prototype._read = function() { 24 | var tmp = this.data; 25 | this.data = ''; 26 | return tmp; 27 | }; 28 | --------------------------------------------------------------------------------