Passed
Push — master ( 12429c...102ccc )
by Barry
01:03
created

helpers.js ➔ ... ➔ delKeys   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
c 1
b 0
f 0
nc 3
nop 3
dl 0
loc 9
rs 9.6666
1
/*
2
=====fsnip======
3
4
fsnip is a command line utility to extract and modify json from a file.
5
6
*/
7
const fs = require('fs')
8
const jp = require('jsonpath')
9
const stringify = require('json-stringify-pretty-compact')
10
const chalk = require('chalk')
11
12
export function cli (args) {
13
  if (args.length === 3 && args[2] === '--help') {
14
    console.info('fsnip is a tool for extracting json snippets from json files.\n\n' +
15
                 'Usage:\n' + '' +
16
                 'TODO still \n' +
17
                 '  fsnip FILE [options [arguments]]    process the file and output the result to the console\n' +
18
                 '  FILE         Specifies the file to process.\n' +
19
                 '  --ellipsify  replaces the passed object with ellipses (...)\n' +
20
                 '                 but excludes any keys which follow prepended by ~\n' +
21
                 '                 eg. fsnip myfile.json --ellipsify $..address ~postcode')
22
  } else if (args.length >= 3) {
23
    try {
24
      var txt = fs.readFileSync(args[2]).toString()
25
    } catch (err) {
26
      console.error(chalk.redBright("unable to read file '" + args[2] + "'"))
27
    }
28
    if (typeof txt !== 'undefined') {
29
      console.info(fsnipDo(args.slice(3), txt))
30
    }
31
  } else { // we couldn't recognise the format on the command line
32
    console.error(chalk.redBright('Unrecognised arguments passed to fsnip. See fsnip --help'))
33
  }
34
}
35
36
function fsnipDo (cmdOpts, inputText) {
37
  // does the processing of the fsnip command
38
  // inputText is the text we want to modify
39
  if (cmdOpts === null || cmdOpts.length === 0) { return inputText } // no processing required as no options passed in
40
  var src = { // a temporary structure containing the text we are working on its type eg. 'json' (which is set later)
41
    text: inputText,
42
    type: '',
43
    outputOptions: {},
44
    error: [],
45
    json: null,
46
    plain: null
47
  }
48
49
  // now we are going to parse through the options and arguments to extract individual options together with their arguments
50
  var cmdOpt = '' // current option from the cmdOptsString list
51
  var cmdArgs = [] // array containing any arguments for the cmdOpt
52
  for (var i = 0; i < cmdOpts.length; i++) {
53
    if (cmdOpts[i].substr(0, 2) === '--') { // this is a new option eg. --ellipsify
54
      if (cmdOpt !== '') { runOption(cmdOpt, cmdArgs, src) } // process/run any previous Option we found
55
      cmdOpt = cmdOpts[i] // store the new option we have found
56
      cmdArgs = [] // reset ready for any new arguments
57
    } else {
58
      // this must be an argument for the current option
59
      if (cmdOpt === '') { // error if we don't currently have an option
60
        src.error.push("invalid argument '" + cmdOpts[i] + "' passed without valid option to fsnip")
61
      } else {
62
        cmdArgs.push(cmdOpts[i])
63
      }
64
    }
65
  }
66
  if (cmdOpt !== '') { runOption(cmdOpt, cmdArgs, src) } // process/run the very last Option we found
67
  postProcess(src)
68
  return src.error.length === 0 ? src.text : chalk.redBright(src.error)
69
}
70
71
function runOption (option, args, inpObj) {
72
  // option is a string eg. '-jsonEllipsify'
73
  // arguments is an array of arguments for the option
74
  // inpObj is an object containing the text, type and json object we need to modify
75
  // this function acts as a marsheller to identify options and process them accordingly
76
  let funcs = {
77
    '--json': () => { json(inpObj) },
78
    '--prettify': () => { jsonPrettify(inpObj, args) },
79
    '--ellipsify': () => { jsonEllipsify(inpObj, args) },
80
    '--snip': () => { jsonSnippet(inpObj, args) },
81
    '--delKeys': () => { jsonDelKeys(inpObj, args) },
82
    '--from': () => { textFrom(inpObj, args, false) },
83
    '--start': () => { textFrom(inpObj, args, true) },
84
    '--to': () => { textTo(inpObj, args, false) },
85
    '--finish': () => { textTo(inpObj, args, true) }
86
  }
87
88
  if (funcs[option]) {
89
    funcs[option]()
90
  } else {
91
    inpObj.error.push("invalid option '" + option + "' for fsnip")
92
  }
93
}
94
95
function postProcess (inpObj) {
96
  // does any post process tidying up
97
  if (inpObj.type === 'json') {
98
    // stringify as required
99
    let opts = inpObj.outputOptions
100
    if (opts.maxLength === 'infinity' && opts.margins === false) {
101
      inpObj.text = JSON.stringify(inpObj.json)
102
    } else if (opts.maxLength === 0 && opts.margins === false) {
103
      inpObj.text = JSON.stringify(inpObj.json, null, opts.indent)
104
    } else {
105
      inpObj.text = stringify(inpObj.json, opts)
106
    }
107
    // now replace any placeholders. The placeholders are valid JSON but what we replace them with may not be valid JSON
108
    inpObj.text = inpObj.text.replace(/\[\s*"fsnipPlaceholderArrEllipses"\s*\]/g, '[...]')
109
    inpObj.text = inpObj.text.replace(/\{\s*"fsnipPlaceholderObj"\s*:\s*"Ellipses"\s*\}/g, '{...}') // do this separately to the one below so that if the object is empty it appears all on one line
110
    inpObj.text = inpObj.text.replace(/"fsnipPlaceholderObj"\s*:\s*"Ellipses"/g, '...')
111
    inpObj.text = inpObj.text.replace(/"fsnipPlaceholderStrEllipses"/g, '"..."')
112
  } else if (inpObj.type === 'plain') {
113
    inpObj.text = inpObj.plain.trim()
114
  }
115
}
116
117
export function setInputType (inpObj, newType) { // only exported for testing purposes
118
  if (typeof inpObj.type === 'undefined' || inpObj.type === '') { // type has not previously been set
119
    inpObj.type = newType
120
    if (newType === 'json') {
121
      inpObj.json = JSON.parse(inpObj.text)
122
      jsonPrettify(inpObj) // sets default output options for json
123
      return true
124
    } else if (newType === 'plain') {
125
      inpObj.plain = inpObj.text
126
      return true
127
    } else {
128
      return false
129
    }
130
  } else if (inpObj.type !== newType) { // it's already been set to something else so there's a problem
131
    if (typeof inpObj.error === 'undefined') { inpObj.error = [] }
132
    inpObj.error.push('cannot mix options designed to process different types of file')
133
    return false
134
  } else {
135
    return true
136
  }
137
}
138
139
function buildJsonSearchPath (keyName) {
140
  if (keyName.substr(0, 1) === '$') {
141
    return keyName
142
  } else {
143
    return "$..['" + keyName + "']"
144
  }
145
}
146
147
function removeQuotes (str) {
148
  // if the passed string has matching encapsulating quotes these are removed
149
  if ((str.substr(0, 1) === '\'' && str.substr(-1) === '\'') ||
150
      (str.substr(0, 1) === '"' && str.substr(-1) === '"')) {
151
    return str.substr(1, str.length - 2)
152
  } else {
153
    return str
154
  }
155
}
156
157
// =================json===============================
158
function json (inpObj) {
159
    // cmdArgs is an array of arguments
160
    // json is an object containing the json object we need to modify
161
  setInputType(inpObj, 'json') // all we do is flag our content as being json
162
}
163
164
// =================jsonPrettify=======================
165
function jsonPrettify (inpObj, cmdArgs) {
166
  // cmdArgs is an (optional) array of arguments being indent, maxLength, margins
167
  // they are all passed as strings so need to be converted to numbers where appropriate
168
  // we use - 0 to convert string numbers to numeric and === against itself to check for NaN
169
  if (setInputType(inpObj, 'json')) {
170
    let opts = inpObj.outputOptions
171
    // set defaults
172
    opts.margins = false
173
    opts.maxLength = 45
174
    opts.indent = 2
175
    // overwrite with any values passed in
176
    if (cmdArgs !== undefined) {
177
      if ((cmdArgs[0] - 0) === (cmdArgs[0] - 0)) { opts.indent = (cmdArgs[0] - 0) }
178
      if ((cmdArgs[1] - 0) === (cmdArgs[1] - 0)) { opts.maxLength = (cmdArgs[1] - 0) }
179
      if (cmdArgs[1] === 'infinity') { opts.maxLength = 'infinity' }
180
      opts.margins = (cmdArgs[2] === 'true') // defaults to false if margins anything other than true
181
    }
182
  }
183
}
184
185
// =================ellipsify==========================
186
function jsonEllipsify (inpObj, cmdArgs) {
187
  // cmdArgs is an array of arguments
188
  // json is an object containing the json object we need to modify
189
190
  if (setInputType(inpObj, 'json')) {
191
    // we have two types of argument for Ellipsify, plain and exclude so separate them out
192
    var cmdArgsPlain = []
193
    var cmdArgsExclude = []
194
    for (let i = 0; i < cmdArgs.length; i++) {
195
      if (cmdArgs[i].substr(0, 1) === '~') {
196
        cmdArgsExclude.push(removeQuotes(cmdArgs[i].substr(1)))
197
      } else {
198
        cmdArgsPlain.push(removeQuotes(cmdArgs[i]))
199
      }
200
    }
201
    if (cmdArgsPlain.length === 0) { cmdArgsPlain.push('$') }
202
    for (let i = 0; i < cmdArgsPlain.length; i++) {
203
      minimizeJsonProperty(inpObj.json, cmdArgsPlain[i], cmdArgsExclude)
204
    }
205
  }
206
}
207
208
export function minimizeJsonProperty (json, property, excludes) { // only exported for test purposes
209
  // this function takes a json object as input.and for every occurrence of the given property puts a placeholder
210
  // but only if it is an array or an object.
211
  var arrPlaceholder = ['fsnipPlaceholderArrEllipses'] // a valid json array used as a placeholder to be replaced later with [...] (which is not valid json)
212
  var strPlaceholder = 'fsnipPlaceholderStrEllipses'
213
  var jsonPaths = jp.paths(json, buildJsonSearchPath(property)) // creates an array of all the paths of instances of the the property we want to minimize
214
  for (var i = 0; i < jsonPaths.length; i++) {
215
    let jsonPath = jp.stringify(jsonPaths[i])
216
    switch (jp.value(json, jsonPath).constructor.name) {
217
      case 'Object':
218
        delKeys(json, jsonPath, excludes)
219
        jp.value(json, jsonPath)['fsnipPlaceholderObj'] = 'Ellipses' // add a placeholder for the Ellipses
220
        break
221
      case 'Array':
222
        jp.value(json, jsonPath, arrPlaceholder)
223
        break
224
      case 'String':
225
        jp.value(json, jsonPath, strPlaceholder)
226
        break
227
      default:
228
        // do nothing
229
    }
230
  }
231
232
  function delKeys (json, jsonPath, excludes) {
233
    var keys = Object.keys(jp.value(json, jsonPath))
234
    for (var j = 0; j < keys.length; j++) {
235
      if (excludes.indexOf(keys[j]) === -1) {
236
        // this key is not in the excludes list so we need to delete it
237
        delete jp.value(json, jsonPath)[keys[j]]
238
      }
239
    }
240
  }
241
}
242
243
// ===================snip Function==============================
244
function jsonSnippet (inpObj, cmdArgs) {
245
  // cmdArgs is an array of arguments
246
  // inpObj is an object containing the json object we need to modify
247
  // the format of the call is eg.
248
  // '--snip vessel 2' which would extract the second instance of "vessel" in the json supplied
249
  // with the instance identifier being optional
250
  if (setInputType(inpObj, 'json')) {
251
    var occ = 1
252
    if (cmdArgs.length === 1) {
253
      occ = 1 // by default we snip the first occurrence of this property
254
    } else if (cmdArgs.length === 2) {
255
      if ((cmdArgs[1] - 0) === (cmdArgs[1] - 0)) {
256
        occ = (cmdArgs[1] - 0)
257
        if (occ < 1) {
258
          inpObj.error.push('--snip requires its second argument to be a numeric values of at least 1 being the instance required')
259
          return
260
        }
261
      } else {
262
        inpObj.error.push("--snip requires its second argument to be numeric eg. '--snip vessel 2' with the optional second argument being the instance required")
263
        return
264
      }
265
    } else {
266
      inpObj.error.push("--snip requires 1 or 2 arguments eg. '--snip vessel 2' with the optional second argument being the instance required.")
267
      return
268
    }
269
    var jsonPaths = jp.paths(inpObj.json, buildJsonSearchPath(removeQuotes(cmdArgs[0]))) // creates an array of all the paths to this property
270
    if (jsonPaths.length < occ) {
271
      inpObj.error.push('--snip failed because there were only ' + jsonPaths.length + " occurrences of '" + removeQuotes(cmdArgs[0]) + "' found.")
272
      return
273
    }
274
    inpObj.json = jp.value(inpObj.json, jp.stringify(jsonPaths[occ - 1]))
275
  }
276
}
277
278
// ===================delKeys Function===========================
279
function jsonDelKeys (inpObj, cmdArgs) {
280
  // cmdArgs is an array of arguments
281
  // inpObj is an object containing the json object we need to remove keys from
282
  // the format of the call is eg.
283
  // '-jsonDelKeys vessel gnss' which would delete all instances of "vessel" and "gnss" in the json supplied
284
  if (setInputType(inpObj, 'json')) {
285
    for (var i = 0; i < cmdArgs.length; i++) {
286
      deleteJsonKey(inpObj.json, removeQuotes(cmdArgs[i]))
287
    }
288
  }
289
}
290
291
function deleteJsonKey (json, key) {
292
  // deletes all occurrences of key within json
293
  var jsonPaths = jp.paths(json, buildJsonSearchPath(key)) // creates an array of all the paths of instances of the key we want to delete
294
  var parent
295
  for (var i = 0; i < jsonPaths.length; i++) {
296
    let jsonPath = jp.stringify(jsonPaths[i])
297
    parent = jp.parent(json, jsonPath)
298
    if (Array.isArray(parent)) {
299
      parent.splice(jsonPaths[i][jsonPaths[i].length - 1], 1)
300
    } else {
301
      delete parent[jsonPaths[i][jsonPaths[i].length - 1]]
302
    }
303
  }
304
}
305
306
// ===================textFrom=================================
307
function textFrom (inpObj, cmdArgs, inclusive) {
308
  // cmdArgs is an array of arguments
309
  // inpObj is an object containing the text object we need to snip contents from
310
  // the format of the call is eg.
311
  // '--textFrom "some text" 2 - would start from the second instance of "some text"
312
  if (setInputType(inpObj, 'plain')) {
313
    let x = findLocation(inpObj, cmdArgs, inclusive ? '--start' : '--from')
314
    if (x.found) {
315
      inpObj.plain = inpObj.plain.substr(x.loc + (inclusive === true ? 0 : x.len))
316
    }
317
  }
318
}
319
320
// ===================textTo===================================
321
function textTo (inpObj, cmdArgs, inclusive) {
322
  // cmdArgs is an array of arguments
323
  // inpObj is an object containing the text object we need to snip contents from
324
  // the format of the call is eg.
325
  // '--textTo "some text" 2 - would go up to the second instance of "some text"
326
  if (setInputType(inpObj, 'plain')) {
327
    let x = findLocation(inpObj, cmdArgs, inclusive ? '--finish' : '--to')
328
    if (x.found) {
329
      inpObj.plain = inpObj.plain.substring(0, x.loc + (inclusive === true ? x.len : 0))
330
    }
331
  }
332
}
333
334
function findLocation (inpObj, cmdArgs, errString) {
335
  // find the location of the nth occurrence of the text specified in the command arguments
336
  let occ
337
  if (cmdArgs.length === 1) {
338
    occ = 1 // by default we take from the first occurrence of this text
339
  } else if (cmdArgs.length === 2) {
340
    if ((cmdArgs[1] - 0) === (cmdArgs[1] - 0)) {
341
      occ = (cmdArgs[1] - 0)
342
      if (occ < 1) {
343
        inpObj.error.push(errString + ' requires its second argument to be a numeric value of at least 1 being the instance required')
344
        return {found: false}
345
      }
346
    } else {
347
      inpObj.error.push(errString + " requires its second argument to be numeric eg. '" + errString + " sometext 2' with the optional second argument being the instance required")
348
      return {found: false}
349
    }
350
  } else {
351
    inpObj.error.push(errString + " requires 1 or 2 arguments eg. '" + errString + " sometext' with the optional second argument being the instance required.")
352
    return {found: false}
353
  }
354
  let x = -1
355
  let arg = removeQuotes(cmdArgs[0])
356
  for (let i = 0; i < occ; i++) {
357
    x = inpObj.plain.indexOf(arg, x + 1)
358
  }
359
  if (x === -1) {
360
    inpObj.error.push('unable to find occurrence ' + occ + ' of "' + arg + '"')
361
  }
362
  return {found: (x !== -1), loc: x, len: arg.length}
363
}
364