Passed
Push — master ( a9c254...e53e62 )
by Barry
57s
created

helpers.js ➔ setInputType   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
c 1
b 0
f 0
nc 6
nop 2
dl 0
loc 21
rs 7.551
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.log(fsnipDo(args.slice(3), txt))
0 ignored issues
show
Debugging Code introduced by
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
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. -jsonEllipsify
54
      if (cmdOpt !== '') runOption(cmdOpt, cmdArgs, src) // process/run any previous Option we found
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
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
      }
62
      cmdArgs.push(cmdOpts[i])
63
    }
64
  }
65
  if (cmdOpt !== '') runOption(cmdOpt, cmdArgs, src) // process/run the very last Option we found
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
66
  postProcess(src)
67
  if (src.error.length === 0) {
68
    return src.text
69
  } else {
70
    return chalk.redBright(src.error)
71
  }
72
}
73
74
function runOption (option, args, inpObj) {
75
  // option is a string eg. '-jsonEllipsify'
76
  // arguments is an array of arguments for the option
77
  // inpObj is an object containing the text, type and json object we need to modify
78
  // this function acts as a marsheller to identify options and process them accordingly
79
  if (option === '--json') {
80
    json(inpObj, args)
81
  } else if (option === '--prettify') {
82
    jsonPrettify(inpObj, args)
83
  } else if (option === '--ellipsify') {
84
    jsonEllipsify(inpObj, args)
85
  } else if (option === '--snip') {
86
    jsonSnippet(inpObj, args)
87
  } else if (option === '--delKeys') {
88
    jsonDelKeys(inpObj, args)
89
  } else if (option === '--from') {
90
    textFrom(inpObj, args, false)
91
  } else if (option === '--start') {
92
    textFrom(inpObj, args, true)
93
  } else if (option === '--to') {
94
    textTo(inpObj, args, false)
95
  } else if (option === '--finish') {
96
    textTo(inpObj, args, true)
97
  } else {
98
    inpObj.error.push("invalid option '" + option + "' for fsnip")
99
  }
100
}
101
102
function postProcess (inpObj) {
103
  // does any post process tidying up
104
  if (inpObj.type === 'json') {
105
    // stringify as required
106
    let opts = inpObj.outputOptions
107
    if (opts.maxLength === 'infinity' && opts.margins === false) {
108
      inpObj.text = JSON.stringify(inpObj.json)
109
    } else if (opts.maxLength === 0 && opts.margins === false) {
110
      inpObj.text = JSON.stringify(inpObj.json, null, opts.indent)
111
    } else {
112
      inpObj.text = stringify(inpObj.json, opts)
113
    }
114
    // now replace any placeholders. The placeholders are valid JSON but what we replace them with may not be valid JSON
115
    inpObj.text = inpObj.text.replace(/\[\s*"fsnipPlaceholderArrEllipses"\s*\]/g, '[...]')
116
    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
117
    inpObj.text = inpObj.text.replace(/"fsnipPlaceholderObj"\s*:\s*"Ellipses"/g, '...')
118
    inpObj.text = inpObj.text.replace(/"fsnipPlaceholderStrEllipses"/g, '"..."')
119
  } else if (inpObj.type === 'plain') {
120
    inpObj.text = inpObj.plain.trim()
121
  }
122
}
123
124
export function setInputType (inpObj, newType) { // only exported for testing purposes
125
  if (typeof inpObj.type === 'undefined' || inpObj.type === '') { // type has not previously been set
126
    inpObj.type = newType
127
    if (newType === 'json') {
128
      inpObj.json = JSON.parse(inpObj.text)
129
      jsonPrettify(inpObj) // sets default output options for json
130
      return true
131
    } else if (newType === 'plain') {
132
      inpObj.plain = inpObj.text
133
      return true
134
    } else {
135
      return false
136
    }
137
  } else if (inpObj.type !== newType) { // it's already been set to something else so there's a problem
138
    if (typeof inpObj.error === 'undefined') { inpObj.error = [] }
139
    inpObj.error.push('cannot mix options designed to process different types of file')
140
    return false
141
  } else {
142
    return true
143
  }
144
}
145
146
function buildJsonSearchPath (keyName) {
147
  if (keyName.substr(0, 1) === '$') {
148
    return keyName
149
  } else {
150
    return "$..['" + keyName + "']"
151
  }
152
}
153
154
function removeQuotes (str) {
155
  // if the passed string has matching encapsulating quotes these are removed
156
  if ((str.substr(0, 1) === '\'' && str.substr(-1) === '\'') ||
157
      (str.substr(0, 1) === '"' && str.substr(-1) === '"')) {
158
    return str.substr(1, str.length - 2)
159
  } else {
160
    return str
161
  }
162
}
163
164
// =================json===============================
165
function json (inpObj, cmdArgs) {
0 ignored issues
show
Unused Code introduced by
The parameter cmdArgs is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
166
    // cmdArgs is an array of arguments
167
    // json is an object containing the json object we need to modify
168
  setInputType(inpObj, 'json') // all we do is flag our content as being json
169
}
170
171
// =================jsonPrettify=======================
172
function jsonPrettify (inpObj, cmdArgs) {
173
  // cmdArgs is an (optional) array of arguments being indent, maxLength, margins
174
  // they are all passed as strings so need to be converted to numbers where appropriate
175
  // we use - 0 to convert string numbers to numeric and === against itself to check for NaN
176
  if (setInputType(inpObj, 'json')) {
177
    let opts = inpObj.outputOptions
178
    // set defaults
179
    opts.margins = false
180
    opts.maxLength = 45
181
    opts.indent = 2
182
    // overwrite with any values passed in
183
    if (cmdArgs !== undefined) {
184
      if ((cmdArgs[0] - 0) === (cmdArgs[0] - 0)) { opts.indent = (cmdArgs[0] - 0) }
185
      if ((cmdArgs[1] - 0) === (cmdArgs[1] - 0)) { opts.maxLength = (cmdArgs[1] - 0) }
186
      if (cmdArgs[1] === 'infinity') { opts.maxLength = 'infinity' }
187
      opts.margins = (cmdArgs[2] === 'true') // defaults to false if margins anything other than true
188
    }
189
  }
190
}
191
192
// =================ellipsify==========================
193
function jsonEllipsify (inpObj, cmdArgs) {
194
  // cmdArgs is an array of arguments
195
  // json is an object containing the json object we need to modify
196
197
  if (setInputType(inpObj, 'json')) {
198
    // we have two types of argument for Ellipsify, plain and exclude so separate them out
199
    var cmdArgsPlain = []
200
    var cmdArgsExclude = []
201
    for (let i = 0; i < cmdArgs.length; i++) {
202
      if (cmdArgs[i].substr(0, 1) === '~') {
203
        cmdArgsExclude.push(removeQuotes(cmdArgs[i].substr(1)))
204
      } else {
205
        cmdArgsPlain.push(removeQuotes(cmdArgs[i]))
206
      }
207
    }
208
    if (cmdArgsPlain.length === 0) { cmdArgsPlain.push('$') }
209
    for (let i = 0; i < cmdArgsPlain.length; i++) {
210
      minimizeJsonProperty(inpObj.json, cmdArgsPlain[i], cmdArgsExclude)
211
    }
212
  }
213
}
214
215
export function minimizeJsonProperty (json, property, excludes) { // only exported for test purposes
216
  // this function takes a json object as input.and for every occurrence of the given property puts a placeholder
217
  // but only if it is an array or an object.
218
  var arrPlaceholder = ['fsnipPlaceholderArrEllipses'] // a valid json array used as a placeholder to be replaced later with [...] (which is not valid json)
219
  var strPlaceholder = 'fsnipPlaceholderStrEllipses'
220
  var jsonPaths = jp.paths(json, buildJsonSearchPath(property)) // creates an array of all the paths of instances of the the property we want to minimize
221
  for (var i = 0; i < jsonPaths.length; i++) {
222
    let jsonPath = jp.stringify(jsonPaths[i])
223
    switch (jp.value(json, jsonPath).constructor.name) {
224
      case 'Object':
225
        var keys = Object.keys(jp.value(json, jsonPath))
226
        for (var j = 0; j < keys.length; j++) {
227
          if (excludes.indexOf(keys[j]) === -1) {
228
            // this key is not in the excludes list so we need to delete it
229
            delete jp.value(json, jsonPath)[keys[j]]
230
          }
231
        }
232
        jp.value(json, jsonPath)['fsnipPlaceholderObj'] = 'Ellipses' // add a placeholder for the Ellipses
233
        break
234
      case 'Array':
235
        jp.value(json, jsonPath, arrPlaceholder)
236
        break
237
      case 'String':
238
        jp.value(json, jsonPath, strPlaceholder)
239
        break
240
      default:
241
        // do nothing
242
    }
243
  }
244
}
245
246
// ===================snip Function==============================
247
function jsonSnippet (inpObj, cmdArgs) {
248
  // cmdArgs is an array of arguments
249
  // inpObj is an object containing the json object we need to modify
250
  // the format of the call is eg.
251
  // '--snip vessel 2' which would extract the second instance of "vessel" in the json supplied
252
  // with the instance identifier being optional
253
  if (setInputType(inpObj, 'json')) {
254
    var occ = 1
1 ignored issue
show
Unused Code introduced by
The assignment to variable occ seems to be never used. Consider removing it.
Loading history...
255
    if (cmdArgs.length === 1) {
256
      occ = 1 // by default we snip the first occurrence of this property
257
    } else if (cmdArgs.length === 2) {
258
      if ((cmdArgs[1] - 0) === (cmdArgs[1] - 0)) {
259
        occ = (cmdArgs[1] - 0)
260
        if (occ < 1) {
261
          inpObj.error.push('--snip requires its second argument to be a numeric values of at least 1 being the instance required')
262
          return
263
        }
264
      } else {
265
        inpObj.error.push("--snip requires its second argument to be numeric eg. '--snip vessel 2' with the optional second argument being the instance required")
266
        return
267
      }
268
    } else {
269
      inpObj.error.push("--snip requires 1 or 2 arguments eg. '--snip vessel 2' with the optional second argument being the instance required.")
270
      return
271
    }
272
    var jsonPaths = jp.paths(inpObj.json, buildJsonSearchPath(removeQuotes(cmdArgs[0]))) // creates an array of all the paths to this property
273
    if (jsonPaths.length < occ) {
274
      inpObj.error.push('--snip failed because there were only ' + jsonPaths.length + " occurrences of '" + removeQuotes(cmdArgs[0]) + "' found.")
275
      return
276
    }
277
    inpObj.json = jp.value(inpObj.json, jp.stringify(jsonPaths[occ - 1]))
278
  }
279
}
280
281
// ===================delKeys Function===========================
282
function jsonDelKeys (inpObj, cmdArgs) {
283
  // cmdArgs is an array of arguments
284
  // inpObj is an object containing the json object we need to remove keys from
285
  // the format of the call is eg.
286
  // '-jsonDelKeys vessel gnss' which would delete all instances of "vessel" and "gnss" in the json supplied
287
  if (setInputType(inpObj, 'json')) {
288
    for (var i = 0; i < cmdArgs.length; i++) {
289
      deleteJsonKey(inpObj.json, removeQuotes(cmdArgs[i]))
290
    }
291
  }
292
}
293
294
function deleteJsonKey (json, key) {
295
  // deletes all occurrences of key within json
296
  var jsonPaths = jp.paths(json, buildJsonSearchPath(key)) // creates an array of all the paths of instances of the key we want to delete
297
  var parent
298
  for (var i = 0; i < jsonPaths.length; i++) {
299
    let jsonPath = jp.stringify(jsonPaths[i])
300
    parent = jp.parent(json, jsonPath)
301
    if (Array.isArray(parent)) {
302
      parent.splice(jsonPaths[i][jsonPaths[i].length - 1], 1)
303
    } else {
304
      delete parent[jsonPaths[i][jsonPaths[i].length - 1]]
305
    }
306
  }
307
}
308
309
// ===================textFrom=================================
310 View Code Duplication
function textFrom (inpObj, cmdArgs, inclusive) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
311
  // cmdArgs is an array of arguments
312
  // inpObj is an object containing the text object we need to snip contents from
313
  // the format of the call is eg.
314
  // '--textFrom "some text" 2 - would start from the second instance of "some text"
315
  if (setInputType(inpObj, 'plain')) {
316
    let occ
317
    if (cmdArgs.length === 1) {
318
      occ = 1 // by default we take from the first occurrence of this text
319
    } else if (cmdArgs.length === 2) {
320
      if ((cmdArgs[1] - 0) === (cmdArgs[1] - 0)) {
321
        occ = (cmdArgs[1] - 0)
322
        if (occ < 1) {
323
          inpObj.error.push('--from and --start require their second argument to be a numeric values of at least 1 being the instance required')
324
          return
325
        }
326
      } else {
327
        inpObj.error.push("--from and --start require their second argument to be numeric eg. '--from \"some text\" 2' with the optional second argument being the instance required")
328
        return
329
      }
330
    } else {
331
      inpObj.error.push("--from and --start require 1 or 2 arguments eg. '--from \"some text\"' with the optional second argument being the instance required.")
332
      return
333
    }
334
    let x = -1
335
    let arg = removeQuotes(cmdArgs[0])
336
    for (let i = 0; i < occ; i++) {
337
      x = inpObj.plain.indexOf(arg, x + 1)
338
    }
339
    if (x === -1) {
340
      inpObj.error.push('unable to find occurrence ' + occ + ' of "' + arg + '"')
341
      return
342
    }
343
    if (inclusive === true) {
344
      inpObj.plain = inpObj.plain.substr(x)
345
    } else {
346
      inpObj.plain = inpObj.plain.substr(x + arg.length)
347
    }
348
  }
349
}
350
351
// ===================textFrom=================================
352 View Code Duplication
function textTo (inpObj, cmdArgs, inclusive) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
353
  // cmdArgs is an array of arguments
354
  // inpObj is an object containing the text object we need to snip contents from
355
  // the format of the call is eg.
356
  // '--textTo "some text" 2 - would go up to the second instance of "some text"
357
  if (setInputType(inpObj, 'plain')) {
358
    let occ
359
    if (cmdArgs.length === 1) {
360
      occ = 1 // by default we take from the first occurrence of this text
361
    } else if (cmdArgs.length === 2) {
362
      if ((cmdArgs[1] - 0) === (cmdArgs[1] - 0)) {
363
        occ = (cmdArgs[1] - 0)
364
        if (occ < 1) {
365
          inpObj.error.push('--to and --finish require their second argument to be a numeric values of at least 1 being the instance required')
366
          return
367
        }
368
      } else {
369
        inpObj.error.push("--to and --finish require their second argument to be numeric eg. '--from \"some text\" 2' with the optional second argument being the instance required")
370
        return
371
      }
372
    } else {
373
      inpObj.error.push("--to and --finish require 1 or 2 arguments eg. '--from \"some text\"' with the optional second argument being the instance required.")
374
      return
375
    }
376
    let x = -1
377
    let arg = removeQuotes(cmdArgs[0])
378
    for (let i = 0; i < occ; i++) {
379
      x = inpObj.plain.indexOf(arg, x + 1)
380
    }
381
    if (x === -1) {
382
      inpObj.error.push('unable to find occurrence ' + occ + ' of "' + arg + '"')
383
      return
384
    }
385
    if (inclusive === true) {
386
      inpObj.plain = inpObj.plain.substring(0, x + arg.length)
387
    } else {
388
      inpObj.plain = inpObj.plain.substring(0, x)
389
    }
390
  }
391
}
392