11'use strict' ;
22
33var _ = require ( 'underscore' ) ,
4+ path = require ( 'doc-path' ) ,
45 constants = require ( './constants' ) ;
56
67var options = { } ; // Initialize the options - this will be populated when the csv2json function is called.
78
8- // Generate the JSON heading from the CSV
9+ /**
10+ * Generate the JSON heading from the CSV
11+ * @param lines
12+ * @param callback
13+ * @returns {* }
14+ */
915var retrieveHeading = function ( lines , callback ) {
10- if ( ! lines . length ) { // If there are no lines passed in, then throw an error
16+ // If there are no lines passed in, return an error
17+ if ( ! lines . length ) {
1118 return callback ( new Error ( constants . Errors . csv2json . noDataRetrieveHeading ) ) ; // Pass an error back to the user
1219 }
13- var heading = lines [ 0 ] . split ( options . DELIMITER . FIELD ) ; // Grab the top line (header line) and split by the field delimiter
14- heading = _ . map ( heading , function ( headerKey , index ) {
15- return {
16- value : headerKey ,
17- index : index
18- }
19- } ) ;
20- return heading ;
21- } ;
2220
23- // Add a nested key and its value in the given document
24- var addNestedKey = function ( key , value , doc ) {
25- var subDocumentRoot = doc , // This is the document that we will be using to add the nested keys to.
26- trackerDocument = subDocumentRoot , // This is the document that will use to iterate through the subDocument, starting at the root
27- nestedKeys = key . split ( '.' ) , // Array of all keys and sub keys for the document
28- finalKey = nestedKeys . pop ( ) ; // Retrieve the last sub key.
29- _ . each ( nestedKeys , function ( nestedKey ) {
30- if ( keyExists ( nestedKey , trackerDocument ) ) { // This nestedKey already exists, use an existing doc
31- trackerDocument = trackerDocument [ nestedKey ] ; // Update the trackerDocument to use the existing document
32- } else {
33- trackerDocument [ nestedKey ] = { } ; // Add document at the current subKey
34- trackerDocument = trackerDocument [ nestedKey ] ; // Update trackerDocument to be the added doc for the subKey
35- }
36- } ) ;
37- trackerDocument [ finalKey ] = value ; // Set the final layer key to the value
38- return subDocumentRoot ; // Return the document with the nested document structure setup
39- } ;
40-
41- // Helper function to check if the given key already exists in the given document
42- var keyExists = function ( key , doc ) {
43- return ( ! _ . isUndefined ( doc [ key ] ) ) ; // If the key doesn't exist, then the type is 'undefined'
21+ // Generate and return the heading keys
22+ return _ . map ( lines [ 0 ] . split ( options . DELIMITER . FIELD ) ,
23+ function ( headerKey , index ) {
24+ return {
25+ value : headerKey ,
26+ index : index
27+ } ;
28+ } ) ;
4429} ;
4530
31+ /**
32+ * Does the given value represent an array?
33+ * @param value
34+ * @returns {boolean }
35+ */
4636var isArrayRepresentation = function ( value ) {
47- return ( value && value . indexOf ( '[' ) === 0 && value . lastIndexOf ( ']' ) === value . length - 1 ) ;
37+ // Verify that there is a value and it starts with '[' and ends with ']'
38+ return ( value && / ^ \[ .* \] $ / . test ( value ) ) ;
4839} ;
4940
50- var convertArrayRepresentation = function ( val ) {
51- val = _ . filter ( val . substring ( 1 , val . length - 1 ) . split ( options . DELIMITER . ARRAY ) , function ( value ) {
41+ /**
42+ * Converts the value from a CSV 'array'
43+ * @param val
44+ * @returns {Array }
45+ */
46+ var convertArrayRepresentation = function ( arrayRepresentation ) {
47+ // Remove the '[' and ']' characters
48+ arrayRepresentation = arrayRepresentation . replace ( / ( \[ | \] ) / g, '' ) ;
49+
50+ // Split the arrayRepresentation into an array by the array delimiter
51+ arrayRepresentation = arrayRepresentation . split ( options . DELIMITER . ARRAY ) ;
52+
53+ // Filter out non-empty strings
54+ return _ . filter ( arrayRepresentation , function ( value ) {
5255 return value ;
5356 } ) ;
54- _ . each ( val , function ( value , indx ) {
55- if ( isArrayRepresentation ( value ) ) {
56- val [ indx ] = convertArrayRepresentation ( value ) ;
57- }
58- } ) ;
59- return val ;
6057} ;
6158
62- // Create a JSON document with the given keys (designated by the CSV header) and the values (from the given line)
63- var createDoc = function ( keys , line ) {
64- if ( line == '' ) { return false ; } // If we have an empty line, then return false so we can remove all blank lines (falsy values)
65- var doc = { } , // JSON document to start with and manipulate
66- val , // Temporary variable to set the current key's value to
67- line = line . trim ( ) . split ( options . DELIMITER . FIELD ) ; // Split the line using the given field delimiter after trimming whitespace
68- _ . each ( keys , function ( key , indx ) {
69- val = line [ key . index ] === '' ? null : line [ key . index ] ;
59+ /**
60+ * Create a JSON document with the given keys (designated by the CSV header)
61+ * and the values (from the given line)
62+ * @param keys String[]
63+ * @param line String
64+ * @returns {Object } created json document
65+ */
66+ var createDocument = function ( keys , line ) {
67+ var line = line . split ( options . DELIMITER . FIELD ) , // Split the line using the given field delimiter after trimming whitespace
68+ val ; // Temporary variable to set the current key's value to
69+
70+ // Reduce the keys into a JSON document representing the given line
71+ return _ . reduce ( keys , function ( document , key ) {
72+ // If there is a value at the key's index in the line, set the value; otherwise null
73+ val = line [ key . index ] ? line [ key . index ] : null ;
74+
75+ // If the value is an array representation, convert it
7076 if ( isArrayRepresentation ( val ) ) {
7177 val = convertArrayRepresentation ( val ) ;
7278 }
73- if ( key . value . indexOf ( '.' ) ) { // If key has '.' representing nested document
74- doc = addNestedKey ( key . value , val , doc ) ; // Update the document to add the nested key structure
75- } else { // Else we just have a straight key:value mapping
76- doc [ key ] = val ; // Set the value at the current key
77- }
78- } ) ;
79- return doc ; // Return the created document
79+ // Otherwise add the key and value to the document
80+ return path . setPath ( document , key . value , val ) ;
81+ } , { } ) ;
8082} ;
8183
82- // Main wrapper function to convert the CSV to the JSON document array
84+ /**
85+ * Main helper function to convert the CSV to the JSON document array
86+ * @param lines String[]
87+ * @param callback Function callback function
88+ * @returns {Array }
89+ */
8390var convertCSV = function ( lines , callback ) {
8491 var generatedHeaders = retrieveHeading ( lines , callback ) , // Retrieve the headings from the CSV, unless the user specified the keys
85- jsonDocs = [ ] , // Create an array that we can add the generated documents to
92+ nonHeaderLines = lines . splice ( 1 ) , // All lines except for the header line
93+ // If the user provided keys, filter the generated keys to just the user provided keys so we also have the key index
8694 headers = options . KEYS ? _ . filter ( generatedHeaders , function ( headerKey ) {
8795 return _ . contains ( options . KEYS , headerKey . value ) ;
8896 } ) : generatedHeaders ;
89- lines = lines . splice ( 1 ) ; // Grab all lines except for the header
90- _ . each ( lines , function ( line ) { // For each line, create the document and add it to the array of documents
91- jsonDocs . push ( createDoc ( headers , line ) ) ;
92- } ) ;
93- return _ . filter ( jsonDocs , function ( doc ) { return doc !== false ; } ) ; // Return all non 'falsey' values to filter blank lines
97+
98+ return _ . reduce ( nonHeaderLines , function ( documentArray , line ) { // For each line, create the document and add it to the array of documents
99+ if ( ! line ) { return documentArray ; } // skip over empty lines
100+ var generatedDocument = createDocument ( headers , line . trim ( ) ) ;
101+ return documentArray . concat ( generatedDocument ) ;
102+ } , [ ] ) ;
94103} ;
95104
96105module . exports = {
97-
98- // Function to export internally
99- // Takes options as a document, data as a CSV string, and a callback that will be used to report the results
106+
107+ /**
108+ * Internally exported csv2json function
109+ * Takes options as a document, data as a CSV string, and a callback that will be used to report the results
110+ * @param opts Object options object
111+ * @param data String csv string
112+ * @param callback Function callback function
113+ */
100114 csv2json : function ( opts , data , callback ) {
101- if ( ! callback ) { throw new Error ( constants . Errors . callbackRequired ) ; } // If a callback wasn't provided, throw an error
102- if ( ! opts ) { return callback ( new Error ( constants . Errors . optionsRequired ) ) ; return null ; } // Shouldn't happen, but just in case
103- else { options = opts ; } // Options were passed, set the global options value
104- if ( ! data ) { return callback ( new Error ( constants . Errors . csv2json . cannotCallCsv2JsonOn + data + '.' ) ) ; return null ; } // If we don't receive data, report an error
105- if ( ! _ . isString ( data ) ) { // The data is not a string
115+ // If a callback wasn't provided, throw an error
116+ if ( ! callback ) { throw new Error ( constants . Errors . callbackRequired ) ; }
117+
118+ // Shouldn't happen, but just in case
119+ if ( ! opts ) { return callback ( new Error ( constants . Errors . optionsRequired ) ) ; return null ; }
120+ options = opts ; // Options were passed, set the global options value
121+
122+ // If we don't receive data, report an error
123+ if ( ! data ) { return callback ( new Error ( constants . Errors . csv2json . cannotCallCsv2JsonOn + data + '.' ) ) ; return null ; }
124+
125+ // The data provided is not a string
126+ if ( ! _ . isString ( data ) ) {
106127 return callback ( new Error ( constants . Errors . csv2json . csvNotString ) ) ; // Report an error back to the caller
107128 }
108- var lines = data . split ( options . EOL ) ; // Split the CSV into lines using the specified EOL option
109- var json = convertCSV ( lines , callback ) ; // Retrieve the JSON document array
110- callback ( null , json ) ; // Send the data back to the caller
129+
130+ // Split the CSV into lines using the specified EOL option
131+ var lines = data . split ( options . EOL ) ,
132+ json = convertCSV ( lines , callback ) ; // Retrieve the JSON document array
133+ return callback ( null , json ) ; // Send the data back to the caller
111134 }
112135
113136} ;
0 commit comments