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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
// use crate::config::Config;
use crate::error::Error;
use csv::StringRecord;
use regex::Regex;
use serde_json::{Map, Value};

/// Reads the issues template .csv file and converts it to a JSON object
/// ```
/// use jiragen::csv_to_json;
/// use csv::StringRecord;
/// use serde_json::json;
///
/// // string conversion
/// let headers = vec!["summary"];
/// let records = vec![StringRecord::from(vec!["A Test Summary"])];
/// assert_eq!(
///   csv_to_json(headers, records).unwrap(),
///   vec![json!({ "summary": "A Test Summary" })]
/// );
///
/// // string array conversion
/// let headers = vec!["labels[]"];
/// let records = vec![StringRecord::from(vec!["a-label"])];
/// assert_eq!(
///   csv_to_json(headers, records).unwrap(),
///   vec![json!({ "labels": ["a-label"] })]
/// );
///
/// // single array, multiple string values
/// let headers = vec!["labels[]", "labels[]"];
/// let records = vec![StringRecord::from(vec!["a-label", "b-label"])];
/// assert_eq!(
///   csv_to_json(headers, records).unwrap(),
///   vec![json!({ "labels": ["a-label", "b-label"] })]
/// );
///
/// // object property
/// let headers = vec!["issuetype.id"];
/// let records = vec![StringRecord::from(vec!["12345"])];
/// assert_eq!(
///   csv_to_json(headers, records).unwrap(),
///   vec![json!({ "issuetype": {"id": "12345"} })]
/// );
///
/// // an array of object
/// let headers = vec!["components[].id"];
/// let records = vec![StringRecord::from(vec!["A Component"])];
/// assert_eq!(
///   csv_to_json(headers, records).unwrap(),
///   vec![json!({ "components": [{"id": "A Component"}] })]
/// );
///
/// // an object with an array of objects
/// let headers = vec!["watcher.watchers[].accountId"];
/// let records = vec![StringRecord::from(vec!["abcc281-qk3j8d8fj"])];
/// assert_eq!(
///   csv_to_json(headers, records).unwrap(),
///   vec![json!({
///     "watcher": {
///       "watchers": [
///         {"accountId": "abcc281-qk3j8d8fj"}
///       ]
///     }
///   })]
/// );
///
/// // single object, multiple properties
/// let headers = vec![
///   "timetracking.originalEstimate",
///   "timetracking.remainingEstimate",
/// ];
/// let records = vec![StringRecord::from(vec!["10", "5"])];
/// assert_eq!(
///   csv_to_json(headers, records).unwrap(),
///   vec![json!({ "timetracking": { "originalEstimate": "10", "remainingEstimate": "5" } })]
/// );
///
/// // an array with mutiple objects (each with 1 item)
/// let headers = vec!["fixVersions[].id", "fixVersions[].id"];
/// let records = vec![StringRecord::from(vec!["10000", "10001"])];
/// assert_eq!(
///   csv_to_json(headers, records).unwrap(),
///   vec![json!({ "fixVersions": [ {"id": "10000"}, {"id": "10001"} ] })]
/// );
/// ```
pub fn csv_to_json(headers: Vec<&str>, records: Vec<StringRecord>) -> Result<Vec<Value>, Error> {
  let arr: Vec<Value> = records
    .iter()
    .map(|record| {
      let mut fields = Map::new();

      for (i, id) in headers.iter().enumerate() {
        let record_field_value: &str = match record.get(i) {
          Some(value) => value,
          None => "",
        };

        let (json_key, json_val) = csv_value_to_json(id, record_field_value);

        match fields.contains_key(&json_key) {
          true => {
            fields = merge_json(fields, json_key, json_val);
          }
          false => {
            fields.insert(json_key, json_val);
          }
        };
      }

      Value::Object(fields)
    })
    .collect();

  Ok(arr)
}

// Converts the supplied header column string (which describes the json schema) and value into a JSON object.
fn csv_value_to_json(header: &str, value: &str) -> (String, Value) {
  lazy_static! {
    static ref FIND_NESTING_DELIMITERS: Regex = Regex::new(r"\[\]|\.").unwrap();
  }

  let val: Value = match FIND_NESTING_DELIMITERS.find(header) {
    Some(matching_delimiter) => {
      // get the string following the "[]" delimiter. If empty string, then we know that the value is an array of strings
      let rest_header: &str = &header[matching_delimiter.end()..];

      match matching_delimiter.as_str() {
        // is array
        "[]" => {
          let nested_val = match rest_header {
            // an array of strings
            "" => Value::Array(vec![Value::String(value.to_string())]),
            // an array of object|arrays
            _ => {
              let (_, recursive_val) = csv_value_to_json(rest_header, value);
              Value::Array(vec![recursive_val])
            }
          };

          nested_val
        }

        // is object
        "." => {
          let nested_val = match rest_header {
            // This should never happen. There should always be a non-empty string following the `.`
            "" => panic!("There should always be text following the period (.)"),
            // an object of strings|objects|arrays
            _ => {
              let (recursive_key, recursive_val) = csv_value_to_json(rest_header, value);
              let mut fields = Map::new();
              fields.insert(recursive_key, recursive_val);
              Value::Object(fields)
            }
          };

          nested_val
        }
        _ => panic!("csv_value_to_json() matched a character that should not have been matched."),
      }
    }
    None => Value::String(value.to_string()),
  };

  let root_header_name: Vec<&str> = FIND_NESTING_DELIMITERS.split(header).collect();

  (root_header_name[0].to_string(), val)
}

// Merges json_val into fields, where fields already has a value for json_key.
// If json_val is an object, then the key:val is appended to the existing object.
// If json_val is an array, then each item in the array is appended to the existing array.
fn merge_json(
  mut fields: Map<String, Value>,
  json_key: String,
  json_val: Value,
) -> Map<String, Value> {
  let existing_val = fields.get_mut(&json_key).unwrap();
  match existing_val {
    Value::Object(existing_map) => {
      // append key:vals to the existing object
      if let Value::Object(json_val_map) = json_val {
        for (key, val) in json_val_map.into_iter() {
          existing_map.insert(key, val);
        }
      }
    }

    Value::Array(existing_vec) => {
      if let Value::Array(json_val_vec) = json_val {
        for val in json_val_vec.into_iter() {
          existing_vec.push(val);
        }
      }
    }

    _ => {
      // assumed to be a string
      fields.insert(json_key, json_val);
    }
  };

  fields
}