json-2-csv
Version:
A JSON to CSV and CSV to JSON converter that natively supports sub-documents and auto-generates the CSV heading.
547 lines (545 loc) • 18.5 kB
HTML
<html lang="en">
<head>
<title>Code coverage report for lib/json-2-csv.js</title>
<meta charset="utf-8">
<link rel="stylesheet" href="../prettify.css">
<link rel="stylesheet" href="../base.css">
<style type='text/css'>
div.coverage-summary .sorter {
background-image: url(../sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class="header high">
<h1>Code coverage report for <span class="entity">lib/json-2-csv.js</span></h1>
<h2>
Statements: <span class="metric">95% <small>(57 / 60)</small></span>
Branches: <span class="metric">90% <small>(45 / 50)</small></span>
Functions: <span class="metric">100% <small>(14 / 14)</small></span>
Lines: <span class="metric">96.36% <small>(53 / 55)</small></span>
Ignored: <span class="metric"><span class="ignore-none">none</span></span>
</h2>
<div class="path"><a href="../index.html">All files</a> » <a href="index.html">lib/</a> » json-2-csv.js</div>
</div>
<div class="body">
<pre><table class="coverage">
<tr><td class="line-count">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168</td><td class="line-coverage"><span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-yes">1</span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-yes">1</span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-yes">1</span>
<span class="cline-any cline-yes">106</span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-yes">98</span>
<span class="cline-any cline-yes">276</span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-yes">276</span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-yes">98</span>
<span class="cline-any cline-yes">22</span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-yes">76</span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-yes">76</span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-yes">254</span>
<span class="cline-any cline-yes">254</span>
<span class="cline-any cline-yes">30</span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-yes">76</span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-yes">66</span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-yes">1</span>
<span class="cline-any cline-yes">366</span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-yes">366</span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-yes">910</span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-yes">910</span>
<span class="cline-any cline-yes">90</span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-yes">820</span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-yes">366</span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-yes">1</span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-yes">260</span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-yes">738</span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-yes">1</span>
<span class="cline-any cline-yes">738</span>
<span class="cline-any cline-yes">30</span>
<span class="cline-any cline-yes">708</span>
<span class="cline-any cline-no"> </span>
<span class="cline-any cline-yes">708</span>
<span class="cline-any cline-no"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-yes">708</span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-yes">1</span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-yes">96</span>
<span class="cline-any cline-yes">260</span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-yes">1</span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-yes">149</span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-yes">128</span>
<span class="cline-any cline-yes">128</span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-yes">128</span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-yes">108</span>
<span class="cline-any cline-yes">2</span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-yes">106</span>
<span class="cline-any cline-yes">22</span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-yes">106</span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-yes">96</span>
<span class="cline-any cline-yes">20</span>
<span class="cline-any cline-yes">56</span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-yes">96</span>
<span class="cline-any cline-yes">90</span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-yes">96</span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-yes">10</span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span>
<span class="cline-any cline-neutral"> </span></td><td class="text"><pre class="prettyprint lang-js">'use strict';
var _ = require('underscore'),
constants = require('./constants'),
path = require('doc-path'),
Promise = require('bluebird');
var options = {}; // Initialize the options - this will be populated when the json2csv function is called.
/**
* Retrieve the headings for all documents and return it.
* This checks that all documents have the same schema.
* @param data
* @returns {Promise}
*/
var generateHeading = function(data) {
if (options.KEYS) { return Promise.resolve(options.KEYS); }
var keys = _.map(data, function (document, indx) { // for each key
<span class="missing-if-branch" title="else path not taken" >E</span>if (_.isObject(document)) {
// if the data at the key is a document, then we retrieve the subHeading starting with an empty string heading and the doc
return generateDocumentHeading('', document);
}
});
// Check for a consistent schema that does not require the same order:
// if we only have one document - then there is no possibility of multiple schemas
if (keys && keys.length <= 1) {
return Promise.resolve(_.flatten(keys) || <span class="branch-1 cbranch-no" title="branch not covered" >[])</span>;
}
// else - multiple documents - ensure only one schema (regardless of field ordering)
var firstDocSchema = _.flatten(keys[0]),
schemaDifferences = 0;
_.each(keys, function (keyList) {
// If there is a difference between the schemas, increment the counter of schema inconsistencies
var diff = _.difference(firstDocSchema, _.flatten(keyList));
if (!_.isEqual(diff, [])) {
schemaDifferences++;
}
});
// If there are schema inconsistencies, throw a schema not the same error
if (schemaDifferences) { return Promise.reject(new Error(constants.Errors.json2csv.notSameSchema)); }
return Promise.resolve(_.flatten(keys[0]));
};
/**
* Takes the parent heading and this doc's data and creates the subdocument headings (string)
* @param heading
* @param data
* @returns {Array}
*/
var generateDocumentHeading = function(heading, data) {
var keyName = ''; // temporary variable to aid in determining the heading - used to generate the 'nested' headings
var documentKeys = _.map(_.keys(data), function (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
keyName = heading ? heading + '.' + currentKey : currentKey;
// If we have another nested document, recur on the sub-document to retrieve the full key name
if (_.isObject(data[currentKey]) && !_.isNull(data[currentKey]) && _.isUndefined(data[currentKey].length) && _.keys(data[currentKey]).length) {
return generateDocumentHeading(keyName, data[currentKey]);
}
// Otherwise return this key name since we don't have a sub document
return keyName;
});
return documentKeys; // Return the headings joined by our field delimiter
};
/**
* Convert the given data with the given keys
* @param data
* @param keys
* @returns {Array}
*/
var convertData = function (data, keys) {
// Reduce each key in the data to its CSV value
return _.reduce(keys, function (output, key) {
// Add the CSV representation of the data at the key in the document to the output array
return output.concat(convertField(path.evaluatePath(data, key)));
}, []);
};
/**
* Convert the given value to the CSV representation of the value
* @param value
* @param output
*/
var convertField = function (value) {
if (_.isArray(value)) { // We have an array of values
return options.DELIMITER.WRAP + '[' + value.join(options.DELIMITER.ARRAY) + ']' + options.DELIMITER.WRAP;
} else <span class="missing-if-branch" title="if path not taken" >I</span>if (_.isDate(value)) { // If we have a date
<span class="cstat-no" title="statement not covered" > return options.DELIMITER.WRAP + value.toString() + options.DELIMITER.WRAP;</span>
} else <span class="missing-if-branch" title="if path not taken" >I</span>if (_.isObject(value)) { // If we have an object
<span class="cstat-no" title="statement not covered" > return options.DELIMITER.WRAP + convertData(value, _.keys(value)) + options.DELIMITER.WRAP; </span>// Push the recursively generated CSV
}
return options.DELIMITER.WRAP + (value ? value.toString() : '') + options.DELIMITER.WRAP; // Otherwise push the current value
};
/**
* Generate the CSV representing the given data.
* @param data
* @param headingKeys
* @returns {*}
*/
var generateCsv = function (data, headingKeys) {
// Reduce each JSON document in data to a CSV string and append it to the CSV accumulator
return [headingKeys].concat(_.reduce(data, function (csv, doc) {
return csv += convertData(doc, headingKeys).join(options.DELIMITER.FIELD) + options.EOL;
}, ''));
};
module.exports = {
/**
* Internally exported json2csv function
* Takes options as a document, data as a JSON document array, and a callback that will be used to report the results
* @param opts Object options object
* @param data String csv string
* @param callback Function callback function
*/
json2csv: function (opts, data, callback) {
// If a callback wasn't provided, throw an error
if (!callback) { throw new Error(constants.Errors.callbackRequired); }
// Shouldn't happen, but just in case
<span class="missing-if-branch" title="if path not taken" >I</span>if (!opts) { <span class="cstat-no" title="statement not covered" >return callback(new Error(constants.Errors.optionsRequired)); </span>}
options = opts; // Options were passed, set the global options value
// If we don't receive data, report an error
if (!data) { return callback(new Error(constants.Errors.json2csv.cannotCallJson2CsvOn + data + '.')); }
// If the data was not a single document or an array of documents
if (!_.isObject(data)) {
return callback(new Error(constants.Errors.json2csv.dataNotArrayOfDocuments)); // Report the error back to the caller
}
// Single document, not an array
else if (_.isObject(data) && !data.length) {
data = [data]; // Convert to an array of the given document
}
// Retrieve the heading and then generate the CSV with the keys that are identified
generateHeading(data)
.then(_.partial(generateCsv, data))
.spread(function (csvHeading, csvData) {
// If the fields are supposed to be wrapped... (only perform this if we are actually prepending the header)
if (options.DELIMITER.WRAP && options.PREPEND_HEADER) {
csvHeading = _.map(csvHeading, function(headingKey) {
return options.DELIMITER.WRAP + headingKey + options.DELIMITER.WRAP;
});
}
// If we are prepending the header, then join the csvHeading fields
if (options.PREPEND_HEADER) {
csvHeading = csvHeading.join(options.DELIMITER.FIELD);
}
// If we are prepending the header, then join the header and data by EOL, otherwise just return the data
return callback(null, options.PREPEND_HEADER ? csvHeading + options.EOL + csvData : csvData);
})
.catch(function (err) {
return callback(err);
});
}
};</pre></td></tr>
</table></pre>
</div>
<div class="footer">
<div class="meta">Generated by <a href="http://istanbul-js.org/" target="_blank">istanbul</a> at Sun Apr 26 2015 12:50:58 GMT-0400 (EDT)</div>
</div>
<script src="../prettify.js"></script>
<script>
window.onload = function () {
if (typeof prettyPrint === 'function') {
prettyPrint();
}
};
</script>
<script src="../sorter.js"></script>
</body>
</html>