Skip to content

Commit be3eaad

Browse files
committed
feat: uses templates for output strings, major rewrites
1 parent f1b7aaa commit be3eaad

File tree

12 files changed

+385
-327
lines changed

12 files changed

+385
-327
lines changed

README.md

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
- [Usage](#usage)
1717
- [Configuring](#configuring)
1818
- [Options](#options)
19+
- [Custom Templates](#custom-templates)
1920
- [Support for Dates](#support-for-dates)
2021
- [Prefiltering](#prefiltering)
2122
- [Contributors](#contributors)
@@ -54,17 +55,50 @@ console.log(humanObjectDiff.renderName());
5455

5556
### Options
5657

57-
`human-object-diff` supports a variety of options to allow you to take control over the output of your object diff. Future versions will allow users to fully customize sentence structure.
58-
59-
| Option | type | Default | Description |
60-
| ------------ | ----------- | -------------------- | ------------------------------------------------------------------------------------------------ |
61-
| objectName | String | 'Obj' | This is the object name when presented in the path. ie... "Obj.foo" ignored if hidePath is true |
62-
| prefilter | Array\|Func | | see [prefiltering](#prefiltering) |
63-
| dateFormat | String | 'MM/dd/yyyy hh:mm a' | dateFns format string see [below](#support-for-dates) |
64-
| futureTense | Bool | 'past' | If set to true, sentences will output "will be" changed instead of "was changed" |
65-
| hidePath | Bool | false | If set to true, path..ie "(Obj.foo)".. is suppressed making the output less technical |
66-
| techTerms | Bool | true | False causes paths to be hidden, "Array" will be changed to list and "index" changed to position |
67-
| ignoreArrays | Bool | false | If array differences aren't needed. Set to true and skip processing |
58+
`human-object-diff` supports a variety of options to allow you to take control over the output of your object diff.
59+
60+
| Option | type | Default | Description |
61+
| ------------ | ----------- | ---------------------------------- | ----------------------------------------------------------------------------------------------- |
62+
| objectName | String | 'Obj' | This is the object name when presented in the path. ie... "Obj.foo" ignored if hidePath is true |
63+
| prefilter | Array\|Func | | see [prefiltering](#prefiltering) |
64+
| dateFormat | String | 'MM/dd/yyyy hh:mm a' | dateFns format string see [below](#support-for-dates) |
65+
| ignoreArrays | Bool | false | If array differences aren't needed. Set to true and skip processing |
66+
| templates | Object | see [templates](#custom-templates) | Completely customize the output. |
67+
68+
### Custom Templates
69+
70+
`human-object-dff` let's you fully customize your sentences by allowing you to pass custom sentence templates.
71+
72+
The default template looks like the following:
73+
74+
```js
75+
const templates = {
76+
N: '"FIELD", with a value of "NEWVALUE" (at DOTPATH) was added',
77+
D: '"FIELD", with a value of "OLDVALUE" (at DOTPATH) was removed',
78+
E:
79+
'"FIELD", with a value of "OLDVALUE" (at DOTPATH) was changed to "NEWVALUE"',
80+
I:
81+
'Array "FIELD" (at DOTPATH), had a value of "NEWVALUE" inserted at index INDEX',
82+
R:
83+
'Array "FIELD" (at DOTPATH), had a value of "OLDVALUE" removed at index INDEX',
84+
AE:
85+
'Array "FIELD" (at DOTPATH), had a value of "OLDVALUE" changed to "NEWVALUE" at index INDEX',
86+
NS: '"FIELD" (at DOTPATH) was added',
87+
DS: '"FIELD" (at DOTPATH) was removed',
88+
ES: '"FIELD" (at DOTPATH) was changed',
89+
IS: 'Array "FIELD" (at DOTPATH), had a value inserted at index INDEX',
90+
RS: 'Array "FIELD" (at DOTPATH), had a value removed at index INDEX',
91+
AES: 'Array "FIELD" (at DOTPATH), had a value changed at index INDEX'
92+
};
93+
```
94+
95+
Where N is a new key, D is a deleted key, E is an edited key, I is an inserted array value, R is a removed array value, and AE is an edited array property.
96+
97+
We also expose a sensitiveFields array option which will cause a path to use the S option template.
98+
99+
You can define each sentence in templates to be whatever you'd like them to be and you can use the following codes that will be replaced by their diff values in the final output.
100+
101+
The available values you can plug in to your sentences are `FIELD`, `DOTPATH`,`NEWVALUE`,`OLDVALUE`, `INDEX`, `POSITION`. Position is just index+1. Be aware that not all sentence types will have values for each token. For instance non array changes will not have a position or an index.
68102

69103
### Support for Dates
70104

index.js

Lines changed: 1 addition & 168 deletions
Original file line numberDiff line numberDiff line change
@@ -1,168 +1 @@
1-
const deepdiff = require('deep-diff');
2-
const format = require('date-fns/format');
3-
const humanizeStr = require('humanize-string');
4-
const titleize = require('titleize');
5-
const processArray = require('./src/process-array');
6-
7-
/**
8-
* For non-array values
9-
* (array values are special and need
10-
* different handling to detect correctly.)
11-
*
12-
* 1) property name string
13-
* which is the first non array-index path string eg... 'name'
14-
* if it is an array it will say "Array 'name'"
15-
*
16-
* 2) property value string
17-
* is written as "with a value of 'value'"
18-
*
19-
* 3) list path (optional)
20-
* path in dot notation. eg...'(at Obj,a.b[1].c)' where dots
21-
* indicate object addresses and brackets indicate array indices
22-
*
23-
* 4) verb
24-
* the verb tells what type of change occurred
25-
* enum [changed, added, removed]
26-
* can select past tense or present tense
27-
* ie...'was changed' or 'will be changed'
28-
* for added and removed plain values this will end the string
29-
*
30-
* 4b) If the verb was changed - we need to show the change
31-
* ie... was changed to x
32-
*/
33-
const saveForArrayPreProcessing = diff =>
34-
diff.kind === 'A' || typeof diff.path[diff.path.length - 1] === 'number';
35-
36-
function humanReadableDiff(lhs, rhs, config = {}) {
37-
const objectName = config.objectName || 'Obj';
38-
const dontHumanizePropertyNames = config.dontHumanizePropertyNames || false;
39-
const tense = config.tense || 'past';
40-
const techTerms = config.techTerms !== false;
41-
const dateFormat = config.dateFormat || 'MM/dd/yyyy hh:mm a';
42-
const hidePath = config.hidePath ? techTerms : false;
43-
const ignoreArrays = config.ignoreArrays || false;
44-
const arrayMem = [];
45-
46-
const terms = {
47-
array: techTerms ? 'Array' : 'List',
48-
index: techTerms ? 'index' : 'position'
49-
};
50-
51-
let prefilter;
52-
if (Array.isArray(config.prefilter))
53-
prefilter = (path, key) =>
54-
path.length === 0 && config.prefilter.includes(key);
55-
else if (typeof config.prefilter === 'function') prefilter = config.prefilter;
56-
57-
function humanize(prop) {
58-
return dontHumanizePropertyNames ? prop : titleize(humanizeStr(prop));
59-
}
60-
61-
function getPropertyString(diff) {
62-
let propertyIndex = diff.path.length - 1;
63-
while (typeof diff.path[propertyIndex] !== 'string') propertyIndex -= 1;
64-
const property = diff.path[propertyIndex];
65-
if (diff.dotPath) return `${terms.array} "${humanize(property)}"`;
66-
return `"${humanize(property)}"`;
67-
}
68-
69-
function formatPropertyValue(val) {
70-
if (typeof val === 'string') return `"${val}"`;
71-
if (typeof val === 'number' || typeof val === 'boolean')
72-
return `"${String(val)}"`;
73-
if (val instanceof Date) return `"${format(val, dateFormat)}"`;
74-
return JSON.stringify(val);
75-
}
76-
77-
function getPropertyValueString(diff) {
78-
let formatted = '';
79-
if (diff.kind === 'N') formatted = formatPropertyValue(diff.rhs);
80-
if (diff.kind === 'D' || diff.kind === 'E')
81-
formatted = formatPropertyValue(diff.lhs);
82-
if (diff.val) {
83-
formatted = formatPropertyValue(diff.val);
84-
return ` had a value of ${formatted}`;
85-
}
86-
87-
return ` with a value of ${formatted}`;
88-
}
89-
90-
function getPathString(diff) {
91-
if (hidePath) return '';
92-
93-
if (diff.dotPath) {
94-
diff.path = diff.dotPath.split('.');
95-
return `(at ${objectName}.${diff.dotPath})`;
96-
}
97-
98-
const path = diff.path.reduce(
99-
(acc, val, i) =>
100-
typeof val === 'string'
101-
? typeof diff.path[i + 1] === 'string'
102-
? acc.concat(`${String(val)}.`)
103-
: acc.concat(String(val))
104-
: typeof diff.path[i + 1] === 'string'
105-
? acc.concat(`[${String(val)}].`)
106-
: acc.concat(`[${String(val)}]`),
107-
''
108-
);
109-
return `(at ${objectName}.${path})`;
110-
}
111-
112-
function getVerbString(diff) {
113-
const verb = {
114-
N: 'added',
115-
D: 'removed',
116-
E: 'changed',
117-
I: 'inserted',
118-
R: 'removed'
119-
}[diff.kind];
120-
121-
const preVerb = tense === 'past' ? 'was' : 'will be';
122-
123-
if (['I', 'R'].includes(diff.kind)) return `${verb}`;
124-
125-
return `${preVerb} ${verb}${
126-
verb === 'changed' ? ` to ${formatPropertyValue(diff.rhs)}` : ''
127-
}`;
128-
}
129-
130-
function reducer(acc, diff) {
131-
// don't process array diffs
132-
// until they have been pre-processed
133-
if (!ignoreArrays && saveForArrayPreProcessing(diff)) {
134-
arrayMem.push(diff);
135-
return acc;
136-
}
137-
138-
const property = getPropertyString(diff);
139-
const propertyValue = getPropertyValueString(diff);
140-
const path = getPathString(diff);
141-
const verb = getVerbString(diff);
142-
143-
let diffString = '';
144-
145-
if (diff.dotPath) {
146-
diffString = `${property} ${path},${propertyValue} ${verb} at ${
147-
terms.index
148-
} ${techTerms ? diff.index : diff.index + 1}`;
149-
} else if (path)
150-
diffString = `${property},${propertyValue} ${path} ${verb}`;
151-
else diffString = `${property},${propertyValue} ${verb}`;
152-
153-
return acc.concat(diffString);
154-
}
155-
156-
function humanReadable(lhs, rhs) {
157-
const differences = deepdiff(lhs, rhs, prefilter);
158-
if (!differences) return [];
159-
const changes = differences.reduce(reducer, []);
160-
const arrDiffs = processArray(arrayMem, lhs, rhs);
161-
const changeStrings = changes.concat(arrDiffs.reduce(reducer, []));
162-
return changeStrings;
163-
}
164-
165-
return humanReadable(lhs, rhs);
166-
}
167-
168-
module.exports = humanReadableDiff;
1+
module.exports = require('./src/diff');

src/diff.js

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
const deepdiff = require('deep-diff');
2+
const format = require('date-fns/format');
3+
const humanizeStr = require('humanize-string');
4+
const titleize = require('titleize');
5+
const processArray = require('./process-array');
6+
const strFormatter = require('./format');
7+
8+
const saveForArrayPreProcessing = diff =>
9+
diff.kind === 'A' || typeof diff.path[diff.path.length - 1] === 'number';
10+
11+
function humanReadableDiff(lhs, rhs, config = {}) {
12+
const objectName = config.objectName || 'Obj';
13+
const dontHumanizePropertyNames = config.dontHumanizePropertyNames || false;
14+
const dateFormat = config.dateFormat || 'MM/dd/yyyy hh:mm a';
15+
const ignoreArrays = config.ignoreArrays || false;
16+
const sensitivePaths = config.sensitivePaths || [];
17+
const arrayMem = [];
18+
19+
const templates = {
20+
N: '"FIELD", with a value of "NEWVALUE" (at DOTPATH) was added',
21+
D: '"FIELD", with a value of "OLDVALUE" (at DOTPATH) was removed',
22+
E:
23+
'"FIELD", with a value of "OLDVALUE" (at DOTPATH) was changed to "NEWVALUE"',
24+
I:
25+
'Array "FIELD" (at DOTPATH), had a value of "NEWVALUE" inserted at index INDEX',
26+
R:
27+
'Array "FIELD" (at DOTPATH), had a value of "OLDVALUE" removed at index INDEX',
28+
AE:
29+
'Array "FIELD" (at DOTPATH), had a value of "OLDVALUE" changed to "NEWVALUE" at index INDEX',
30+
NS: '"FIELD" (at DOTPATH) was added',
31+
DS: '"FIELD" (at DOTPATH) was removed',
32+
ES: '"FIELD" (at DOTPATH) was changed',
33+
IS: 'Array "FIELD" (at DOTPATH), had a value inserted at index INDEX',
34+
RS: 'Array "FIELD" (at DOTPATH), had a value removed at index INDEX',
35+
AES: 'Array "FIELD" (at DOTPATH), had a value changed at index INDEX',
36+
...config.templates
37+
};
38+
39+
let prefilter;
40+
if (Array.isArray(config.prefilter))
41+
prefilter = (path, key) =>
42+
path.length === 0 && config.prefilter.includes(key);
43+
else if (typeof config.prefilter === 'function') prefilter = config.prefilter;
44+
45+
function humanize(prop) {
46+
return dontHumanizePropertyNames ? prop : titleize(humanizeStr(prop));
47+
}
48+
49+
function formatPropertyValue(val) {
50+
if (typeof val === 'string') return `"${val}"`;
51+
if (typeof val === 'number' || typeof val === 'boolean') return String(val);
52+
if (val instanceof Date) return `${format(val, dateFormat)}`;
53+
return JSON.stringify(val);
54+
}
55+
56+
function getField(diff) {
57+
let propertyIndex = diff.path.length - 1;
58+
while (typeof diff.path[propertyIndex] !== 'string') propertyIndex -= 1;
59+
const property = diff.path[propertyIndex];
60+
return humanize(property);
61+
}
62+
63+
function getOldVal(diff) {
64+
let formatted = '';
65+
// if (diff.kind === 'N') formatted = formatPropertyValue(diff.rhs);
66+
if (diff.lhs) formatted = formatPropertyValue(diff.lhs);
67+
else if (diff.val) {
68+
formatted = formatPropertyValue(diff.val);
69+
}
70+
71+
return formatted.replace(/"/g, '');
72+
}
73+
74+
function getDotPath(diff) {
75+
if (diff.dotPath) {
76+
diff.path = diff.dotPath.split('.');
77+
return `${objectName}.${diff.dotPath}`;
78+
}
79+
80+
const path = diff.path.reduce(
81+
(acc, val, i) =>
82+
typeof val === 'string'
83+
? typeof diff.path[i + 1] === 'string'
84+
? acc.concat(`${String(val)}.`)
85+
: acc.concat(String(val))
86+
: typeof diff.path[i + 1] === 'string'
87+
? acc.concat(`[${String(val)}].`)
88+
: acc.concat(`[${String(val)}]`),
89+
''
90+
);
91+
92+
return `${objectName}.${path}`;
93+
}
94+
95+
function getNewVal(diff) {
96+
let formatted;
97+
if (diff.val) formatted = formatPropertyValue(diff.val);
98+
else if (diff.rhs) formatted = formatPropertyValue(diff.rhs);
99+
else formatted = '';
100+
return formatted.replace(/"/g, '');
101+
}
102+
103+
function reducer(acc, diff) {
104+
// don't process array diffs
105+
// until they have been pre-processed
106+
if (saveForArrayPreProcessing(diff)) {
107+
if (!ignoreArrays) arrayMem.push(diff);
108+
return acc;
109+
}
110+
111+
const DOTPATH = getDotPath(diff);
112+
113+
let str = '';
114+
if (sensitivePaths.includes(DOTPATH.replace(`${objectName}.`, '')))
115+
str = templates[`${diff.kind}S`];
116+
else str = templates[diff.kind];
117+
118+
const diffString = strFormatter(str, {
119+
FIELD: getField(diff),
120+
DOTPATH: getDotPath(diff),
121+
OLDVALUE: getOldVal(diff),
122+
NEWVALUE: getNewVal(diff),
123+
INDEX: diff.index,
124+
POSITION: diff.index - 1
125+
});
126+
127+
return acc.concat(diffString);
128+
}
129+
130+
function humanReadable(lhs, rhs) {
131+
const differences = deepdiff(lhs, rhs, prefilter);
132+
if (!differences) return [];
133+
const changes = differences.reduce(reducer, []);
134+
const arrDiffs = processArray(arrayMem, lhs, rhs);
135+
const changeStrings = changes.concat(arrDiffs.reduce(reducer, []));
136+
return changeStrings;
137+
}
138+
139+
return humanReadable(lhs, rhs);
140+
}
141+
142+
module.exports = humanReadableDiff;

0 commit comments

Comments
 (0)