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..465a377 --- /dev/null +++ b/index.js @@ -0,0 +1,171 @@ +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 + +/** + * 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)); +}; + +File.prototype._afterRead = function(callback, err, json) { + if (err) { + return callback(err); + } + this._processJson(json); + callback(); +}; + +File.prototype.readSync = function() { + 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; +}; + +/** + * 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; + + // 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 db30c4c..0000000 --- a/lib/index.js +++ /dev/null @@ -1,104 +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 = this.data; - var keys = key.split('.'); - key = keys.pop(); - keys.forEach(function(key) { - current = current[key]; - }); - return callback(current, key, current[key]); -}; - -// ------------------------------------------------------------------ - -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..f38f056 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.1.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": "cp test/input.json test/tmp/update.json && 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..b665428 100644 --- a/readme.md +++ b/readme.md @@ -1,17 +1,19 @@ -# 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]). + +[![Build Status](https://travis-ci.org/alexindigo/jsonfile2.svg)](https://travis-ci.org/alexindigo/jsonfile2) ## 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'); @@ -44,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 ) @@ -66,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 ) @@ -81,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 }); ``` @@ -92,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. @@ -99,6 +129,17 @@ 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::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 ) @@ -108,6 +149,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 +159,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..73073d5 --- /dev/null +++ b/test.js @@ -0,0 +1,90 @@ +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(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]')); + 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(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 have not passed'); + assert.ok(doneOutput, 'output tests have not passed'); + assert.ok(doneUpdate, 'update tests have 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