mod-json v0.1.0
mod-json
mod-json lets you modularize your JSON files with $include and $extend directives. mod-json is a docmodel
component which has been split out into its own module. There are synchronous and asynchronous loaders available (the latter returning bluebird promises), all underlying utilities are publicly exposed, and typings are bundled with the module.
Install
npm install mod-json
Usage
In the following example, we have 2 JSON files (one.json
, and two.json
). one.json
contains a key that's value will be the contents of two.json
. Additionally, there is an object which builds on ($extends) another object.
one.json
{
"a": 1,
"b": {"$include": "two.json"},
"c": {
"$extend": "b",
"e" : 4
}
}
two.json
{
"d": 2,
"e": 3
}
Module Usage
// Require module. TS: import * as json from 'mod-json'
const json = require('mod-json');
// Asynchronous [promise]
json.load('/path/to/one.json')
.then(object => {
// ...
})
.catch(err => {
// ...
})
// Synchronous
const object = json.load_sync('/path/to/one.json')
The result of object
in both cases will be:
{
"a": 1,
"b": {
"d": 2,
"e": 3
},
"c": {
"d": 2,
"e": 4
}
}
JSON $include
An object where the only key is $include
and the value is a path to the JSON file you would like to include.
.json
at the end of paths is optional. Both of the following includes are identical:
{
"a": {"$include": "some_other_file.json"},
"b": {"$include": "some_other_file"}
}
$includes can be nested to any level, and are processed depth-first. Please note that there is currently no explicit protection against a file including itself infinitely. This will be, em, include this is the next version.
File paths are resolved relative to an include directory. In default configuration, this will be the directory of the initial file you pass to a load()
or load_sync()
call. See API descriptors, below, for more details.
$include directives are resolved before $extend directives.
JSON $extend
An object where one of its keys is $extend
and the value is the key of an object you would like to inherit properties from. The extended object can add new keys, or override values inherited from the base:
{
"a": {
"b": 1,
"c": 2
},
"b": {
"$extend": "a",
"c" : 3,
"d" : 4
}
}
$extends can be nested to any level and are processed depth-first. If the requested base cannot be found, the extended object will be preserved with its remaining values. An object cannot extend itself.
$extend directives are resolved after $include directives.
API
load()
load(path: string, options?: {cwd?: string, ignore_bad_files: boolean = false}): promise<any>
- Load a JSON file and process/resolve all $include and $extend directives within.
- Returns a promise for the resolved object.
- Throws if any file cannot be opened or parsed. To override this behavior and silently ignore bad files, set the options
ignore_bad_files
value totrue
. In this case, any $includes which cannot be resolved will be set tonull
.
Please note that file paths are resolved relative to an include directory, not relative to the containing JSON file. If the cwd
option is set, this will be used as the include directory, and paths will be resolve relative to it. If cwd
is not set, it will be inferred from the path you pass to load()
, i.e. all files will be loaded relative to the initial path's directory. In both cases, all files (including the initial file passed to load()
) are included relative to the cwd
location (whether it is explicitly set or inferred).
const json = require('mod-json');
const path = require('path');
const file = path.resolve(__dirname, './includes-directory/some_file.json');
json.load(file)
.then(object => {
// ...
})
.catch(err => {
// ...
});
// Alternative for the same result:
json.load('some_file.json', {cwd: path.resolve(__dirname, './includes-directory/')})
.then(object => {
// ...
})
.catch(err => {
// ...
});
// Silently ignore bad files:
json.load(file, {ignore_bad_files: true})
.then(object => {
// object will be null if root file could not
// be opened / parsed.
});
load_sync()
load_sync(path: string, options?: {cwd?: string}): any
- Load a JSON file and process/resolve all $include and $extend directives within.
- Returns the resolved object.
- If any file cannot be opened / parsed, will return
null
.
Please note that file paths are resolved relative to an include directory, not relative to the containing JSON file. If the cwd
option is set, this will be used as the include directory, and paths will be resolve relative to it. If cwd
is not set, it will be inferred from the path you pass to load_sync()
, i.e. all files will be loaded relative to the initial path's directory. In both cases, all files (including the initial file passed to load_sync()
) are included relative to the cwd
location (whether it is explicitly set or inferred).
const json = require('mod-json');
const path = require('path');
const file = path.resolve(__dirname, './includes-directory/some_file.json');
const object = json.load_sync(file);
// Alternative for the same result:
// const object = json.load_sync('some_file.json', {cwd: path.resolve(__dirname, './includes-directory/')});
Other Functions
While mod-json is mainly intended as a single function (in sync and async variants) module, all of its underlying functions are also publicly exposed. As such, these are also described here.
resolve_directive_include()
resolve_directive_include(object: any, options: {cwd: string, ignore_bad_files: boolean}): promise<any>
- Underlying functionality for processing asynchronous includes in an object.
- There is no option to specify a file path to load an initial / root file--this function only works on existing objects. However, you can pass an on-the-fly object literal which $includes the desired initial file. This is how
load()
makes use of this function. E.g.:
const json = require('mod-json');
const path = require('path');
const cwd = path.resolve(__dirname, './includes-directory/');
json.resolve_directive_include({$include: 'some_file.json'}, {cwd: cwd, ignore_bad_files: false})
.then(object => {
// etc.
})
Note that you must explicitly pass option values to this function. If an object containing an $include directive has any siblings, these will be removed (the object is replaced with the contents of the included file).
resolve_directive_include_sync()
resolve_directive_include_sync(object: any, options: {cwd: string}): any
- Underlying functionality for processing synchronous includes in an object.
- There is no option to specify a file path to load an initial / root file--this function only works on existing objects. However, you can pass an on-the-fly object literal which $includes the desired initial file. This is how
load_sync()
makes use of this function. E.g.:
const json = require('mod-json');
const path = require('path');
const cwd = path.resolve(__dirname, './includes-directory/');
const object = json.resolve_directive_include_sync({$include: 'some_file.json'}, {cwd: cwd});
Note that you must explicitly pass option values to this function. If an object containing an $include directive has any siblings, these will be removed (the object is replaced with the contents of the included file).
resolve_directive_extend()
resolve_directive_extend(object: any, original?: any): any
- Underlying functionality to process extends. This is always a synchronous operation.
- The optional
original
parameter is used by the function to track the original object for key searches, as the function is recursively called. However, you could pass another object here to extend values from it, rather than the object which contains the $extend directives. - Uses
utils.deep_find()
to search for the base object--this is a depth-first search.
const json = require('mod-json');
let an_object = {
a {
b: 1
},
b {
c: 2
},
d: {
$extend: b
}
}
// d will extend a.b (first dept-first occurrence of 'b')
let resolved = json.resolve_directive_extend(an_object);
utils.is_object()
is_object(value: any): boolean
- Returns false for strings, numbers, arrays, null, NaN, undefined, etc. True for all other types.
const json = require('mod-json');
json.utils.is_object('foo'); // false
json.utils.is_object(null); // false
json.utils.is_object(undefined); // false
json.utils.is_object(42); // false
json.utils.is_object([1, 2, 3]); // false
json.utils.is_object({}); // true
json.utils.is_object(new Object()); // true
json.utils.is_object({foo: 'bar'}); // true
utils.merge()
merge(...objects: any[]): any
- Merge 1 (copy) to n objects into a new object. Values from right-most objects will always take precedence.
- Copies are semi-shallow.
merge()
will dig as deep as it can, but only simple values (strings, numbers, booleans etc.) are copied; all other values will reference their original objects.
const json = require('mod-json');
// {a: 4, b: 2, c: {d: 3, e: 5}}
let merged = json.utils.merge(
{a: 1, b: 2, c: {d: 3}},
{a: 4, c: {e: 5}}
);
utils.objects_are_equal()
objects_are_equal(a: any, b: any): boolean
const json = require('mod-json');
// True
json.utils.objects_are_equal(
{a: 1, b: {c: 2}},
{a: 1, b: {c: 2}}
);
- Performs a recursive deep-equality check of the values in 2 objects.
- Internal items are checked with strict equality operators (
===
) - Will also return true for: simple values which return true for a strict equality check, the same object as both
a
andb
utils.deep_find()
deep_find(object: any, key: any): any
- Performs a depth-first search in
object
for the givenkey
. - Returns the value of the first key match, or
null
if the key is not found. - Keys can be of any type which is comparable with strict equality operators (
===
).
const json = require('mod-json');
let object_to_search = {
a: {
b: 1
},
b: 2,
c: {
b: 3
}
};
json.utils.deep_find(object_to_search, 'b'); // 1
json.utils.deep_find(object_to_search[c], 'b'); // 3
json.utils.deep_find(object_to_search, 'e'); // null
Test
npm test
Typings
TypeScript definition files are bundled with the module and will be available out-of-type box (assuming TypeScript 2 or newer).
Contribute
Contributions in the form of code are most welcome through https://bitbucket.org/ShaneGavin/mod-json
License
Copyright (c) 2017, Shane Gavin (nodehead.com)
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
7 years ago