From 5ced5d293445a7fcbec52ef1654edf1c6ecf35ec Mon Sep 17 00:00:00 2001 From: Alex Indigo Date: Sun, 11 Oct 2015 16:33:36 -0700 Subject: [PATCH 1/4] Added support for dotted keys. --- lib/index.js | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/lib/index.js b/lib/index.js index db30c4c..857a7d0 100644 --- a/lib/index.js +++ b/lib/index.js @@ -74,13 +74,36 @@ File.prototype.set = function(key, value) { // Has a callback, but is NOT async File.prototype._resolve = function(key, callback) { - var current = this.data; - var keys = key.split('.'); - key = keys.pop(); + var current, parseable, keys, lastKey; + + // init top node + this.data = this.data || {}; + + // get slider reference + current = this.data; + + // separate for traversable and non-traversable parts + parseable = key.split(/\[(.*)\]/, 2); + + // get traversable keys + keys = parseable[0].split('.'); + + // last key or non-traversable part + lastKey = parseable[1] || keys.pop(); + + // traverse keys.forEach(function(key) { + + // if key doesn't exist (or not referencing object), create it as empty object + if (typeof current[key] != 'object') { + current[key] = {}; + } + + // go deeper current = current[key]; }); - return callback(current, key, current[key]); + + return callback(current, lastKey, current[lastKey]); }; // ------------------------------------------------------------------ @@ -101,4 +124,3 @@ function determineWhitespace(contents) { } } } - From 617330cc9fad5b45bfadb6b1665b14ffcd30aab1 Mon Sep 17 00:00:00 2001 From: Alex Indigo Date: Tue, 13 Oct 2015 19:03:47 -0700 Subject: [PATCH 2/4] Migrated from json-file. --- .editorconfig | 10 ++++ .eslintrc | 22 ++++++++ .gitignore | 3 ++ .npmignore | 4 ++ .travis.yml | 6 +++ index.js | 128 +++++++++++++++++++++++++++++++++++++++++++++ lib/index.js | 126 -------------------------------------------- package.json | 27 ++++++---- readme.md | 11 ++-- test.js | 44 ++++++++++++++++ test/expected.json | 13 +++++ test/input.json | 10 ++++ test/tmp/.keep | 0 13 files changed, 264 insertions(+), 140 deletions(-) create mode 100644 .editorconfig create mode 100644 .eslintrc create mode 100644 .gitignore create mode 100644 .npmignore create mode 100644 .travis.yml create mode 100644 index.js delete mode 100644 lib/index.js create mode 100644 test.js create mode 100644 test/expected.json create mode 100644 test/input.json create mode 100644 test/tmp/.keep diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..0f09989 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +# editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..e26ff63 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,22 @@ +{ + "rules": { + "indent": [2, 2], + "quotes": [2, "single"], + "linebreak-style": [2, "unix"], + "semi": [2, "always"], + "curly": [2, "multi-line"], + "handle-callback-err": [2, "^err"], + "strict": 0, + "eqeqeq": 0, + "eol-last": 0, + "dot-notation": 0, + "no-mixed-requires": 0, + "no-multi-spaces": 0, + "no-use-before-define": 0, + "no-underscore-dangle": 0 + }, + "env": { + "node": true, + "browser": true + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9376d71 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +test/tmp/*.json +*.log diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..94845ae --- /dev/null +++ b/.npmignore @@ -0,0 +1,4 @@ +node_modules/ +test/ +test.js +*.log diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..9ccb608 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,6 @@ +sudo: false +language: node_js +node_js: + - "0.10" + - "0.12" + - "iojs" diff --git a/index.js b/index.js new file mode 100644 index 0000000..fb98527 --- /dev/null +++ b/index.js @@ -0,0 +1,128 @@ +var fs = require('fs'); +var path = require('path'); +var findWhitespace = /^(\s+)/; + + + +var File = exports.File = function(filePath) { + this.path = path.normalize(filePath); +}; + +exports.read = function(filePath, callback) { + var file = new File(filePath); + if (callback) { + file.read(callback); + } else { + file.readSync(); + } + return file; +}; + +File.prototype.data = undefined; +File.prototype.indent = null; + +// ------------------------------------------------------------------ +// File I/O + +File.prototype.read = function(callback) { + fs.readFile(this.path, 'utf8', this._afterRead.bind(this, callback)); +}; + +File.prototype._afterRead = function(callback, err, json) { + if (err) { + return callback(err); + } + this._processJson(json); + callback(); +}; + +File.prototype.readSync = function(callback) { + this._processJson( + fs.readFileSync(this.path, 'utf8') + ); +}; + +File.prototype._processJson = function(json) { + this.data = JSON.parse(json); + this.indent = determineWhitespace(json); +}; + +File.prototype.write = function(callback, replacer, space) { + var space = space || this.indent, + json = JSON.stringify(this.data, replacer, space); + fs.writeFile(this.path, json, callback); +}; + +File.prototype.writeSync = function(replacer, space) { + var space = space || this.indent, + json = JSON.stringify(this.data, replacer, space); + fs.writeFileSync(this.path, json); +}; + +// ------------------------------------------------------------------ +// Property editing + +File.prototype.get = function(key) { + return this._resolve(key, function(scope, key, value) { + return value; + }); +}; + +File.prototype.set = function(key, value) { + this._resolve(key, function(scope, key) { + scope[key] = value; + }); + return this; +}; + +// Has a callback, but is NOT async +File.prototype._resolve = function(key, callback) { + var current, parseable, keys, lastKey; + + // init top node + this.data = this.data || {}; + + // get slider reference + current = this.data; + + // separate for traversable and non-traversable parts + parseable = key.split(/\[(.*)\]/, 2); + + // get traversable keys + keys = parseable[0] ? parseable[0].split('.') : []; + + // last key or non-traversable part + lastKey = parseable[1] || keys.pop(); + + // traverse + keys.forEach(function(key) { + + // if key doesn't exist (or not referencing object), create it as empty object + if (typeof current[key] != 'object') { + current[key] = {}; + } + + // go deeper + current = current[key]; + }); + + return callback(current, lastKey, current[lastKey]); +}; + +// ------------------------------------------------------------------ + +function determineWhitespace(contents) { + var whitespace = 0; + contents = contents.split('\n'); + for (var i = 0, c = contents.length; i < c; i++) { + var match = findWhitespace.exec(contents); + if (match && typeof match[1] === 'string') { + if (match[1][0] === '\t') { + whitespace = '\t'; + break; + } else if (match[1].length < whitespace || ! whitespace) { + whitespace = match[1].length; + } + } + } +} diff --git a/lib/index.js b/lib/index.js deleted file mode 100644 index 857a7d0..0000000 --- a/lib/index.js +++ /dev/null @@ -1,126 +0,0 @@ - -var fs = require('fs'); -var path = require('path'); - -var File = exports.File = function(filePath) { - this.indent = null; - this.data = void(0); - this.path = path.normalize(filePath); -}; - -exports.read = function(filePath, callback) { - var file = new File(filePath); - if (callback) { - file.read(callback); - } else { - file.readSync(); - } - return file; -}; - -// ------------------------------------------------------------------ -// File I/O - -File.prototype.read = function(callback) { - fs.readFile(this.path, 'utf8', this._afterRead.bind(this, callback)); -}; - -File.prototype._afterRead = function(callback, err, json) { - if (err) { - return callback(err); - } - this._processJson(json); - callback(); -}; - -File.prototype.readSync = function(callback) { - this._processJson( - fs.readFileSync(this.path, 'utf8') - ); -}; - -File.prototype._processJson = function(json) { - this.data = JSON.parse(json); - this.indent = determineWhitespace(json); -}; - -File.prototype.write = function(callback, replacer, space) { - var space = space || this.indent, - json = JSON.stringify(this.data, replacer, space); - fs.writeFile(this.path, json, callback); -}; - -File.prototype.writeSync = function(replacer, space) { - var space = space || this.indent, - json = JSON.stringify(this.data, replacer, space); - fs.writeFileSync(this.path, json); -}; - -// ------------------------------------------------------------------ -// Property editing - -File.prototype.get = function(key) { - return this._resolve(key, function(scope, key, value) { - return value; - }); -}; - -File.prototype.set = function(key, value) { - this._resolve(key, function(scope, key) { - scope[key] = value; - }); - return this; -}; - -// Has a callback, but is NOT async -File.prototype._resolve = function(key, callback) { - var current, parseable, keys, lastKey; - - // init top node - this.data = this.data || {}; - - // get slider reference - current = this.data; - - // separate for traversable and non-traversable parts - parseable = key.split(/\[(.*)\]/, 2); - - // get traversable keys - keys = parseable[0].split('.'); - - // last key or non-traversable part - lastKey = parseable[1] || keys.pop(); - - // traverse - keys.forEach(function(key) { - - // if key doesn't exist (or not referencing object), create it as empty object - if (typeof current[key] != 'object') { - current[key] = {}; - } - - // go deeper - current = current[key]; - }); - - return callback(current, lastKey, current[lastKey]); -}; - -// ------------------------------------------------------------------ - -var findWhitespace = /^(\s+)/; -function determineWhitespace(contents) { - var whitespace = 0; - contents = contents.split('\n'); - for (var i = 0, c = contents.length; i < c; i++) { - var match = findWhitespace.exec(contents); - if (match && typeof match[1] === 'string') { - if (match[1][0] === '\t') { - whitespace = '\t'; - break; - } else if (match[1].length < whitespace || ! whitespace) { - whitespace = match[1].length; - } - } - } -} diff --git a/package.json b/package.json index 7bf6217..c0f4820 100644 --- a/package.json +++ b/package.json @@ -1,20 +1,29 @@ { - "name": "json-file", - "version": "0.1.0", - "description": "A module for modifiying JSON files", - "main": "lib/index.js", + "name": "jsonfile2", + "version": "2.0.0", + "description": "A module for modifying JSON files (extended from npm.org/json-file)", + "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "node test.js", + "lint": "eslint -c .eslintrc *.js" }, + "pre-commit": [ + "lint", + "test" + ], "repository": { "type": "git", - "url": "git://github.com/UmbraEngineering/json-file" + "url": "git://github.com/alexindigo/jsonfile2" }, "keywords": [ "json", "file", - "i/o" + "keys" ], - "author": "James Brumond", - "license": "MIT" + "author": "Alex Indigo ", + "license": "MIT", + "devDependencies": { + "eslint": "^1.6.0", + "pre-commit": "^1.1.1" + } } diff --git a/readme.md b/readme.md index c2582b2..0239ca1 100644 --- a/readme.md +++ b/readme.md @@ -1,17 +1,17 @@ -# json-file +# jsonfile2 -A Node.js module for reading/modifying/writing JSON files. +A Node.js module for reading/modifying/writing JSON files. Based on (json-file)[http://www.npmjs.com/package/json-file]. ## Install ```bash -$ npm install json-file +$ npm install --save jsonfile2 ``` ## Usage ```javascript -var json = require('json-file'); +var json = require('jsonfile2'); // Load a JSON file var file = json.read('./package.json'); @@ -99,6 +99,7 @@ Get a value from the JSON data. ```javascript file.get('foo'); // === file.data['foo'] file.get('foo.bar.baz'); // === file.data['foo']['bar']['baz'] +file.get('foo[bar.baz]'); // === file.data['foo']['bar.baz'] ``` #### File::set ( Mixed key, Mixed value ) @@ -108,6 +109,7 @@ Set a value in the JSON data. ```javascript file.set('foo', 'bar'); file.set('a.b.c', 'baz'); +file.set('a[b.c]', 'baz'); ``` The `set` method returns the file object itself, so this method can be chained. @@ -117,4 +119,3 @@ file.set('a', 'foo') .set('b', 'bar') .set('c', 'baz'); ``` - diff --git a/test.js b/test.js new file mode 100644 index 0000000..a2bf9a3 --- /dev/null +++ b/test.js @@ -0,0 +1,44 @@ +var json = require('./') + , assert = require('assert') + , inputFile = './test/input.json' + , outputFile = './test/tmp/output.json' + , expected = require('./test/expected.json') + , input = require(inputFile) + , inFile = new json.File(inputFile) + , outFile = new json.File(outputFile) + , doneInput = false + , doneOutput = false + ; + +// Input tests +inFile.read(function() +{ + assert.deepEqual(input.foo, inFile.get('foo')); + assert.deepEqual(input.foo.bar.baz, inFile.get('foo.bar.baz')); + assert.deepEqual(input.foo['bar.baz'], inFile.get('foo[bar.baz]')); + doneInput = true; +}); + +// Output tests +outFile.set('foo', 'bar'); +outFile.set('a.b.c', 'baz'); +outFile.set('a[b.c]', 'baz'); + +outFile.set('x', 'foo for x') + .set('y', 'bar for y') + .set('z', 'baz for z') + .set('[x.y.z]', 'boom for [x.y.z]') + ; + +outFile.write(function() +{ + // done writing, compare + assert.deepEqual(expected, require(outputFile)); + doneOutput = true; +}); + +process.on('exit', function() +{ + assert.ok(doneInput, 'input tests has not passed'); + assert.ok(doneOutput, 'output tests has not passed'); +}); diff --git a/test/expected.json b/test/expected.json new file mode 100644 index 0000000..c45d05c --- /dev/null +++ b/test/expected.json @@ -0,0 +1,13 @@ +{ + "foo": "bar", + "a": { + "b": { + "c": "baz" + }, + "b.c": "baz" + }, + "x": "foo for x", + "y": "bar for y", + "z": "baz for z", + "x.y.z": "boom for [x.y.z]" +} diff --git a/test/input.json b/test/input.json new file mode 100644 index 0000000..ba06874 --- /dev/null +++ b/test/input.json @@ -0,0 +1,10 @@ +{ + "foo": + { + "bar": + { + "baz": "value of foo.bar.baz" + }, + "bar.baz": "value of foo.['bar.baz']" + } +} diff --git a/test/tmp/.keep b/test/tmp/.keep new file mode 100644 index 0000000..e69de29 From 7746283e832ed0b548ccc98df52d6c48d54ca58f Mon Sep 17 00:00:00 2001 From: Alex Indigo Date: Tue, 13 Oct 2015 19:10:28 -0700 Subject: [PATCH 3/4] Fixed README. Added travis badge. --- package.json | 2 +- readme.md | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index c0f4820..cc67c59 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsonfile2", - "version": "2.0.0", + "version": "2.0.1", "description": "A module for modifying JSON files (extended from npm.org/json-file)", "main": "index.js", "scripts": { diff --git a/readme.md b/readme.md index 0239ca1..838c514 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,8 @@ # jsonfile2 -A Node.js module for reading/modifying/writing JSON files. Based on (json-file)[http://www.npmjs.com/package/json-file]. +A Node.js module for reading/modifying/writing JSON files. Based on [json-file](http://www.npmjs.com/package/json-file]). + +[![Build Status](https://travis-ci.org/alexindigo/jsonfile2.svg)](https://travis-ci.org/alexindigo/jsonfile2) ## Install From 6d916be8e2d0c29fdbe2899db8c164052ce32fbb Mon Sep 17 00:00:00 2001 From: Alex Indigo Date: Tue, 13 Oct 2015 21:24:47 -0700 Subject: [PATCH 4/4] Added update and del methods. --- index.js | 51 +++++++++++++++++++++++++++++++++++++++++---- package.json | 4 ++-- readme.md | 58 +++++++++++++++++++++++++++++++++++++++++++--------- test.js | 54 ++++++++++++++++++++++++++++++++++++++++++++---- 4 files changed, 147 insertions(+), 20 deletions(-) diff --git a/index.js b/index.js index fb98527..465a377 100644 --- a/index.js +++ b/index.js @@ -1,9 +1,7 @@ -var fs = require('fs'); +var fs = require('fs'); var path = require('path'); var findWhitespace = /^(\s+)/; - - var File = exports.File = function(filePath) { this.path = path.normalize(filePath); }; @@ -24,6 +22,38 @@ File.prototype.indent = null; // ------------------------------------------------------------------ // File I/O +/** + * Combines read/write method and creates new file + * if one doesn't exist + * + * @param {function} callback - invoked when file's been read into object + */ +File.prototype.update = function(callback) { + this.read(function(err, json) { + // ignore nonexistent file error + if (err && err.code != 'ENOENT') { + return callback.call(this, err); + } + + // invoke callback within file instance + // with `write` function as `save` callback + callback.call(this, null, this.write.bind(this)); + }.bind(this)); +}; + +File.prototype.updateSync = function(callback) { + var content; + + try { + content = fs.readFileSync(this.path, 'utf8'); + } catch (e) { + if (e.code != 'ENOENT') throw e; + content = '{}'; + } + + this._processJson(content); +}; + File.prototype.read = function(callback) { fs.readFile(this.path, 'utf8', this._afterRead.bind(this, callback)); }; @@ -36,7 +66,7 @@ File.prototype._afterRead = function(callback, err, json) { callback(); }; -File.prototype.readSync = function(callback) { +File.prototype.readSync = function() { this._processJson( fs.readFileSync(this.path, 'utf8') ); @@ -75,6 +105,19 @@ File.prototype.set = function(key, value) { return this; }; +/** + * Remove key from the object + * + * @param {mixed} key - key to delete + * @returns {File} returns itself for chaining + */ +File.prototype.del = function(key) { + this._resolve(key, function(scope, key) { + delete scope[key]; + }); + return this; +}; + // Has a callback, but is NOT async File.prototype._resolve = function(key, callback) { var current, parseable, keys, lastKey; diff --git a/package.json b/package.json index cc67c59..f38f056 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "jsonfile2", - "version": "2.0.1", + "version": "2.1.0", "description": "A module for modifying JSON files (extended from npm.org/json-file)", "main": "index.js", "scripts": { - "test": "node test.js", + "test": "cp test/input.json test/tmp/update.json && node test.js", "lint": "eslint -c .eslintrc *.js" }, "pre-commit": [ diff --git a/readme.md b/readme.md index 838c514..b665428 100644 --- a/readme.md +++ b/readme.md @@ -46,13 +46,13 @@ file.readSync(); // Likewise, this... var file = json.read('/a/b/c', function() { - // ... + // ... }); // Is equivilent to this... var file = new json.File('/a/b/c'); file.read(function() { - // ... + // ... }); ``` #### json.File ( String filePath ) @@ -68,10 +68,8 @@ var file = new json.File('/path/to/file.json'); Reads the JSON file and parses the contents. ```javascript -file.read(function() { - // - // Now you can do things like use the .get() and .set() methods - // +file.read(function(err) { + // Now you can do things like use the .get() and .set() methods }); ``` #### File::readSync ( void ) @@ -83,10 +81,8 @@ Reads the JSON file and parses the contents synchronously. Write the new contents back to the file. ```javascript -file.write(function() { - // - // Your JSON file has been updated - // +file.write(function(err) { + // Your JSON file has been updated }); ``` @@ -94,6 +90,38 @@ file.write(function() { Write the new contents back to the file synchronously. +#### File::update ( Function callback ) + +Reads the JSON file and parses the contents, +will not error if file doesn't exist. +In that case file will be created upon save. +All other errors will propagate. + +*Note: `callback` function will be called within context of the jsonFile object.* + +```javascript +file.update(function(err, save) { + // Now you can do things like use the .get() and .set() methods + this.get('a.b'); + this.set('[c.d]', 25); + save(); +}); +``` +#### File::updateSync ( void ) + +Reads the JSON file and parses the contents synchronously, +same as async sibling doesn't throw on non-existent file. +(Will throw on all other errors). + +```javascript +file.updateSync(); +// Now you can do things like use the .get() and .set() methods +file.get('a.b'); +file.set('[c.d]', 25); + +file.writeSync(); +``` + #### File::get ( Mixed key ) Get a value from the JSON data. @@ -104,6 +132,16 @@ file.get('foo.bar.baz'); // === file.data['foo']['bar']['baz'] file.get('foo[bar.baz]'); // === file.data['foo']['bar.baz'] ``` +#### File::del ( Mixed key ) + +Delete a key from the JSON data. + +```javascript +file.del('foo'); // file.data['foo'] branch will be removed +file.del('foo.bar.baz'); // file.data['foo']['bar']['baz'] node will be removed +file.del('foo[bar.baz]'); // file.data['foo']['bar.baz'] node will be removed +``` + #### File::set ( Mixed key, Mixed value ) Set a value in the JSON data. diff --git a/test.js b/test.js index a2bf9a3..73073d5 100644 --- a/test.js +++ b/test.js @@ -2,17 +2,21 @@ var json = require('./') , assert = require('assert') , inputFile = './test/input.json' , outputFile = './test/tmp/output.json' + , updateFile = './test/tmp/update.json' , expected = require('./test/expected.json') , input = require(inputFile) , inFile = new json.File(inputFile) , outFile = new json.File(outputFile) + , upFile = new json.File(updateFile) , doneInput = false , doneOutput = false + , doneUpdate = false ; // Input tests -inFile.read(function() +inFile.read(function(err) { + assert.ifError(err); assert.deepEqual(input.foo, inFile.get('foo')); assert.deepEqual(input.foo.bar.baz, inFile.get('foo.bar.baz')); assert.deepEqual(input.foo['bar.baz'], inFile.get('foo[bar.baz]')); @@ -30,15 +34,57 @@ outFile.set('x', 'foo for x') .set('[x.y.z]', 'boom for [x.y.z]') ; -outFile.write(function() +outFile.write(function(err) { + assert.ifError(err); // done writing, compare assert.deepEqual(expected, require(outputFile)); doneOutput = true; }); +// Update tests +upFile.update(function(err, save) +{ + assert.ifError(err); + + assert.deepEqual(input.foo, this.get('foo')); + assert.deepEqual(input.foo.bar.baz, this.get('foo.bar.baz')); + assert.deepEqual(input.foo['bar.baz'], this.get('foo[bar.baz]')); + + // remove existing keys + this.del('foo.bar.baz'); + assert.deepEqual(input.foo['bar.baz'], this.get('foo[bar.baz]')); + + this.del('foo[bar.baz]'); + assert.deepEqual({bar:{}}, this.get('foo')); + + this.del('foo'); + + // add new items + this.set('foo', 'bar'); + this.set('a.b.c', 'baz'); + this.set('a[b.c]', 'baz'); + + this.set('x', 'foo for x') + .set('y', 'bar for y') + .set('z', 'baz for z') + .set('[x.y.z]', 'boom for [x.y.z]') + ; + + // save the changes + save(function(err) + { + assert.ifError(err); + // compare saved + assert.deepEqual(expected, require(updateFile)); + doneUpdate = true; + }); +}); + +// almost done process.on('exit', function() { - assert.ok(doneInput, 'input tests has not passed'); - assert.ok(doneOutput, 'output tests has not passed'); + assert.ok(doneInput, 'input tests have not passed'); + assert.ok(doneOutput, 'output tests have not passed'); + assert.ok(doneUpdate, 'update tests have not passed'); });