Skip to content

Commit

Permalink
Add escapeNestedDots option for preserving key structure. (#17)
Browse files Browse the repository at this point in the history
This module did not previously offer a way to encode any nested '.'
characters in a key such that it could be easily identified for use by
the end user or other modules. As a result, any keys which had nested
'.' characters appeared as though they were simply another nested object
layer, but this could cause issues if trying to reconstruct the object.
This commit adds the escapeNestedDots option which allows the user to
optionally specify that this information should be encoded for later use
by the consuming software.

Related to mrodrig/json-2-csv#184
  • Loading branch information
mrodrig committed May 26, 2021
1 parent 6e0bb48 commit 88ef7e7
Show file tree
Hide file tree
Showing 6 changed files with 175 additions and 35 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ env:
- CC_TEST_REPORTER_ID=6c017ab8f2c6f9c4283e1d07e82e8f408fe53bfe1b4a0442c607a8e489b21789
language: node_js
node_js:
- "16"
- "15"
- "14"
- "12"
- "10"
sudo: false
before_script:
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
Expand Down
42 changes: 36 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,19 @@ included in the returned key path list?
```
- ignoreEmptyArraysWhenExpanding = `false` results in: `['features.name', 'rebates']`
- ignoreEmptyArraysWhenExpanding = `true` results in: `['features.name']`
- escapeNestedDots - `Boolean` (Default: `false`) - Should `.` characters that appear in keys be escaped with a preceding `\` character.
- Example:
```json
{
"a.a": "1",
"a.b": {
"c": "2",
"c.d": "3"
}
}
```
- escapeNestedDots = `false` results in: `['a.a', 'a.b.c', 'a.b.c.d']`
- escapeNestedDots = `true` results in: `['a\\.a', 'a\\.b.c', 'a\\.b.c\\.d']`

Returns: `Array[String]`

Expand Down Expand Up @@ -129,8 +142,23 @@ included in the returned key path list?
{ "features": [] }
]
```
- ignoreEmptyArraysWhenExpanding = `false` results in: `['features.name', 'features']`
- ignoreEmptyArraysWhenExpanding = `true` results in: `['features.name']`
- ignoreEmptyArraysWhenExpanding = `false` results in: `[ ['features.name', 'features'] ]`
- ignoreEmptyArraysWhenExpanding = `true` results in: `[ ['features.name'] ]`
- escapeNestedDots - `Boolean` (Default: `false`) - Should `.` characters that appear in keys be escaped with a preceding `\` character.
- Example:
```json
[
{
"a.a": "1",
"a.b": {
"c": "2",
"c.d": "3"
}
}
]
```
- escapeNestedDots = `false` results in: `[ ['a.a', 'a.b.c', 'a.b.c.d'] ]`
- escapeNestedDots = `true` results in: `[ ['a\\.a', 'a\\.b.c', 'a\\.b.c\\.d'] ]`

Returns: `Array[Array[String]]`

Expand All @@ -142,6 +170,8 @@ This module integrates really nicely with the
[`doc-path`](https://github.com/mrodrig/doc-path) module, which allows
the programmatic getting and setting of key paths produced by this module.

Additionally, `doc-path@>=3` works with the keys returned when the `escapeNestedDots` option is specified.

Here's an example of how this works:

```javascript
Expand Down Expand Up @@ -181,8 +211,8 @@ $ npm run coverage

Current Coverage is:
```
Statements : 100% ( 46/46 )
Branches : 100% ( 30/30 )
Functions : 100% ( 9/9 )
Lines : 100% ( 45/45 )
Statements : 100% ( 45/45 )
Branches : 100% ( 32/32 )
Functions : 100% ( 18/18 )
Lines : 100% ( 44/44 )
```
12 changes: 10 additions & 2 deletions lib/deeks.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ function deepKeysFromList(list, options) {
function generateDeepKeysList(heading, data, options) {
let keys = Object.keys(data).map((currentKey) => {
// If the given heading is empty, then we set the heading to be the subKey, otherwise set it as a nested heading w/ a dot
let keyName = buildKeyName(heading, currentKey);
let keyName = buildKeyName(heading, escapeNestedDotsIfSpecified(currentKey, options));

// If we have another nested document, recur on the sub-document to retrieve the full key name
if (isDocumentToRecurOn(data[currentKey])) {
Expand Down Expand Up @@ -78,13 +78,20 @@ function processArrayKeys(subArray, currentKeyPath, options) {
if (isEmptyArray(schemaKeys)) {
return [currentKeyPath];
}
return schemaKeys.map((subKey) => buildKeyName(currentKeyPath, subKey));
return schemaKeys.map((subKey) => buildKeyName(currentKeyPath, escapeNestedDotsIfSpecified(subKey, options)));
});

return utils.unique(utils.flatten(subArrayKeys));
}
}

function escapeNestedDotsIfSpecified(key, options) {
if (options.escapeNestedDots) {
return key.replace('.', '\\.');
}
return key;
}

/**
* Function used to generate the key path
* @param upperKeyName String accumulated key path
Expand Down Expand Up @@ -129,6 +136,7 @@ function mergeOptions(options) {
return {
expandArrayObjects: false,
ignoreEmptyArraysWhenExpanding: false,
escapeNestedDots: false,
...options || {}
};
}
25 changes: 0 additions & 25 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,7 @@

module.exports = {
// underscore replacements:
isString,
isNull,
isError,
isDate,
isFunction,
isUndefined,
isObject,
unique,
flatten
Expand All @@ -17,34 +12,14 @@ module.exports = {
* Helper functions which were created to remove underscorejs from this package.
*/

function isString(value) {
return typeof value === 'string';
}

function isObject(value) {
return typeof value === 'object';
}

function isFunction(value) {
return typeof value === 'function';
}

function isNull(value) {
return value === null;
}

function isDate(value) {
return value instanceof Date;
}

function isUndefined(value) {
return typeof value === 'undefined';
}

function isError(value) {
return Object.prototype.toString.call(value) === '[object Error]';
}

function unique(array) {
return [...new Set(array)];
}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,6 @@
"should": "13.2.3"
},
"engines": {
"node": ">= 10"
"node": ">= 12"
}
}
127 changes: 127 additions & 0 deletions test/tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,24 @@ describe('deeks Module', () => {
.and.have.lengthOf(4);
done();
});

it('should not escape nested dots in key values when option not specified', (done) => {
let testObj = {
'a.a': '2',
'a.b': {
c: '3',
'c.d': '4'
}
},
keys = deeks.deepKeys(testObj);

keys.should.be.an.instanceOf(Array)
.and.containEql('a.a')
.and.containEql('a.b.c')
.and.containEql('a.b.c.d')
.and.have.lengthOf(3);
done();
});
});

describe('Custom Options', () => {
Expand Down Expand Up @@ -310,6 +328,53 @@ describe('deeks Module', () => {
.and.have.lengthOf(4);
done();
});

it('[escapeNestedDots] should not escape nested dots in key values when option not specified', (done) => {
let testObj = {
'a.a': '2',
'a.b': {
c: '3',
'c.d': '4'
}
},
keys = deeks.deepKeys(testObj, {escapeNestedDots: true});

keys.should.be.an.instanceOf(Array)
.and.containEql('a\\.a')
.and.containEql('a\\.b.c')
.and.containEql('a\\.b.c\\.d')
.and.have.lengthOf(3);
done();
});

it('[expandArrayObjects, ignoreEmptyArraysWhenExpanding, escapeNestedDots] should retrieve the keys for objects containing an array of non-objects', (done) => {
let testObj = {
make: 'Nissan',
model: 'GT-R',
'model.trim': 'NISMO',
'features.exterior': ['Insane horsepower', 'Fast acceleration'],
'oem.options': {
'cost.total': '3200',
'cost.minusRebates': '1295'
}
},
options = {
expandArrayObjects: true,
ignoreEmptyArraysWhenExpanding: true,
escapeNestedDots: true
},
keys = deeks.deepKeys(testObj, options);

keys.should.be.an.instanceOf(Array)
.and.containEql('make')
.and.containEql('model')
.and.containEql('model\\.trim')
.and.containEql('features\\.exterior')
.and.containEql('oem\\.options.cost\\.total')
.and.containEql('oem\\.options.cost\\.minusRebates')
.and.have.lengthOf(6);
done();
});
});
});

Expand Down Expand Up @@ -498,6 +563,24 @@ describe('deeks Module', () => {
.and.have.lengthOf(1);
done();
});

it('should not escape nested dots in key values when option not specified', (done) => {
let testList = [
{
'a.a': '2',
'a.b': {
c: '3',
'c.d': '4'
}
}
],
keys = deeks.deepKeysFromList(testList);

keys.should.be.an.instanceOf(Array)
.and.containEql(['a.a', 'a.b.c', 'a.b.c.d'])
.and.have.lengthOf(1);
done();
});
});

describe('Custom Options', () => {
Expand Down Expand Up @@ -664,6 +747,50 @@ describe('deeks Module', () => {
.and.have.lengthOf(1);
done();
});

it('[escapeNestedDots] should not escape nested dots in key values when option not specified', (done) => {
let testList = [
{
'a.a': '2',
'a.b': {
c: '3',
'c.d': '4'
}
}
],
keys = deeks.deepKeysFromList(testList, {escapeNestedDots: true});

keys.should.be.an.instanceOf(Array)
.and.containEql(['a\\.a', 'a\\.b.c', 'a\\.b.c\\.d'])
.and.have.lengthOf(1);
done();
});

it('[expandArrayObjects, ignoreEmptyArraysWhenExpanding, escapeNestedDots] should retrieve keys for an array of one object with an array of non-objects', (done) => {
let testList = [
{
make: 'Nissan',
model: 'GT-R',
'model.trim': 'NISMO',
'features.exterior': ['Insane horsepower', 'Fast acceleration'],
'oem.options': {
'cost.total': '3200',
'cost.minusRebates': '1295'
}
}
],
options = {
expandArrayObjects: true,
ignoreEmptyArraysWhenExpanding: true,
escapeNestedDots: true
},
keys = deeks.deepKeysFromList(testList, options);

keys.should.be.an.instanceOf(Array)
.and.containEql(['make', 'model', 'model\\.trim', 'features\\.exterior', 'oem\\.options.cost\\.total', 'oem\\.options.cost\\.minusRebates'])
.and.have.lengthOf(1);
done();
});
});
});
});

0 comments on commit 88ef7e7

Please sign in to comment.