json-2-csv
Version:
A JSON to CSV and CSV to JSON converter that natively supports sub-documents and auto-generates the CSV heading.
112 lines (100 loc) • 5.84 kB
JavaScript
;
var _ = require('underscore');
var options = {}; // Initialize the options - this will be populated when the csv2json function is called.
// Generate the JSON heading from the CSV
var retrieveHeading = function (lines, callback) {
if (!lines.length) { // If there are no lines passed in, then throw an error
return callback(new Error("No data provided to retrieve heading.")); // Pass an error back to the user
}
var heading = lines[0].split(options.DELIMITER.FIELD); // Grab the top line (header line) and split by the field delimiter
heading = _.map(heading, function (headerKey, index) {
return {
value: headerKey,
index: index
}
});
return heading;
};
// Add a nested key and its value in the given document
var addNestedKey = function (key, value, doc) {
var subDocumentRoot = doc, // This is the document that we will be using to add the nested keys to.
trackerDocument = subDocumentRoot, // This is the document that will use to iterate through the subDocument, starting at the root
nestedKeys = key.split('.'), // Array of all keys and sub keys for the document
finalKey = nestedKeys.pop(); // Retrieve the last sub key.
_.each(nestedKeys, function (nestedKey) {
if (keyExists(nestedKey, trackerDocument)) { // This nestedKey already exists, use an existing doc
trackerDocument = trackerDocument[nestedKey]; // Update the trackerDocument to use the existing document
} else {
trackerDocument[nestedKey] = {}; // Add document at the current subKey
trackerDocument = trackerDocument[nestedKey]; // Update trackerDocument to be the added doc for the subKey
}
});
trackerDocument[finalKey] = value; // Set the final layer key to the value
return subDocumentRoot; // Return the document with the nested document structure setup
};
// Helper function to check if the given key already exists in the given document
var keyExists = function (key, doc) {
return (!_.isUndefined(doc[key])); // If the key doesn't exist, then the type is 'undefined'
};
var isArrayRepresentation = function (value) {
return (value && value.indexOf('[') === 0 && value.lastIndexOf(']') === value.length-1);
};
var convertArrayRepresentation = function (val) {
val = _.filter(val.substring(1, val.length-1).split(options.DELIMITER.ARRAY), function (value) {
return value;
});
_.each(val, function (value, indx) {
if (isArrayRepresentation(value)) {
val[indx] = convertArrayRepresentation(value);
}
});
return val;
};
// Create a JSON document with the given keys (designated by the CSV header) and the values (from the given line)
var createDoc = function (keys, line) {
if (line == '') { return false; } // If we have an empty line, then return false so we can remove all blank lines (falsy values)
var doc = {}, // JSON document to start with and manipulate
val, // Temporary variable to set the current key's value to
line = line.trim().split(options.DELIMITER.FIELD); // Split the line using the given field delimiter after trimming whitespace
_.each(keys, function (key, indx) {
val = line[key.index] === '' ? null : line[key.index];
if (isArrayRepresentation(val)) {
val = convertArrayRepresentation(val);
}
if (key.value.indexOf('.')) { // If key has '.' representing nested document
doc = addNestedKey(key.value, val, doc); // Update the document to add the nested key structure
} else { // Else we just have a straight key:value mapping
doc[key] = val; // Set the value at the current key
}
});
return doc; // Return the created document
};
// Main wrapper function to convert the CSV to the JSON document array
var convertCSV = function (lines, callback) {
var generatedHeaders = retrieveHeading(lines, callback), // Retrieve the headings from the CSV, unless the user specified the keys
jsonDocs = [], // Create an array that we can add the generated documents to
headers = options.KEYS ? _.filter(generatedHeaders, function (headerKey) {
return _.contains(options.KEYS, headerKey.value);
}) : generatedHeaders;
lines = lines.splice(1); // Grab all lines except for the header
_.each(lines, function (line) { // For each line, create the document and add it to the array of documents
jsonDocs.push(createDoc(headers, line));
});
return _.filter(jsonDocs, function (doc) { return doc !== false; }); // Return all non 'falsey' values to filter blank lines
};
module.exports = {
// Function to export internally
// Takes options as a document, data as a CSV string, and a callback that will be used to report the results
csv2json: function (opts, data, callback) {
if (!callback) { throw new Error('A callback is required!'); } // If a callback wasn't provided, throw an error
if (!opts) { callback(new Error('Options were not passed and are required.')); return null; } // Shouldn't happen, but just in case
else { options = opts; } // Options were passed, set the global options value
if (!data) { callback(new Error('Cannot call csv2json on ' + data + '.')); return null; } // If we don't receive data, report an error
if (!_.isString(data)) { // The data is not a string
callback(new Error("CSV is not a string.")); // Report an error back to the caller
}
var lines = data.split(options.EOL); // Split the CSV into lines using the specified EOL option
var json = convertCSV(lines, callback); // Retrieve the JSON document array
callback(null, json); // Send the data back to the caller
}
};