Passed
Push — master ( 8553c8...d02e9f )
by Barry
01:02
created

helpers.js ➔ ... ➔ ???   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 1
rs 10
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
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
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
  let funcs = {
80
    '--json': () => { json(inpObj, args) },
81
    '--prettify': () => { jsonPrettify(inpObj, args) },
82
    '--ellipsify': () => { jsonEllipsify(inpObj, args) },
83
    '--snip': () => { jsonSnippet(inpObj, args) },
84
    '--delKeys': () => { jsonDelKeys(inpObj, args) },
85
    '--from': () => { textFrom(inpObj, args, false) },
86
    '--start': () => { textFrom(inpObj, args, true) },
87
    '--to': () => { textTo(inpObj, args, false) },
88
    '--finish': () => { textTo(inpObj, args, true) }
89
  }
90
91
  if (funcs[option]) {
92
    funcs[option]()
93
  } else {
94
    inpObj.error.push("invalid option '" + option + "' for fsnip")
95
  }
96
}
97
98
function postProcess (inpObj) {
99
  // does any post process tidying up
100
  if (inpObj.type === 'json') {
101
    // stringify as required
102
    let opts = inpObj.outputOptions
103
    if (opts.maxLength === 'infinity' && opts.margins === false) {
104
      inpObj.text = JSON.stringify(inpObj.json)
105
    } else if (opts.maxLength === 0 && opts.margins === false) {
106
      inpObj.text = JSON.stringify(inpObj.json, null, opts.indent)
107
    } else {
108
      inpObj.text = stringify(inpObj.json, opts)
109
    }
110
    // now replace any placeholders. The placeholders are valid JSON but what we replace them with may not be valid JSON
111
    inpObj.text = inpObj.text.replace(/\[\s*"fsnipPlaceholderArrEllipses"\s*\]/g, '[...]')
112
    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
113
    inpObj.text = inpObj.text.replace(/"fsnipPlaceholderObj"\s*:\s*"Ellipses"/g, '...')
114
    inpObj.text = inpObj.text.replace(/"fsnipPlaceholderStrEllipses"/g, '"..."')
115
  } else if (inpObj.type === 'plain') {
116
    inpObj.text = inpObj.plain.trim()
117
  }
118
}
119
120
export function setInputType (inpObj, newType) { // only exported for testing purposes
121
  if (typeof inpObj.type === 'undefined' || inpObj.type === '') { // type has not previously been set
122
    inpObj.type = newType
123
    if (newType === 'json') {
124
      inpObj.json = JSON.parse(inpObj.text)
125
      jsonPrettify(inpObj) // sets default output options for json
126
      return true
127
    } else if (newType === 'plain') {
128
      inpObj.plain = inpObj.text
129
      return true
130
    } else {
131
      return false
132
    }
133
  } else if (inpObj.type !== newType) { // it's already been set to something else so there's a problem
134
    if (typeof inpObj.error === 'undefined') { inpObj.error = [] }
135
    inpObj.error.push('cannot mix options designed to process different types of file')
136
    return false
137
  } else {
138
    return true
139
  }
140
}
141
142
function buildJsonSearchPath (keyName) {
143
  if (keyName.substr(0, 1) === '$') {
144
    return keyName
145
  } else {
146
    return "$..['" + keyName + "']"
147
  }
148
}
149
150
function removeQuotes (str) {
151
  // if the passed string has matching encapsulating quotes these are removed
152
  if ((str.substr(0, 1) === '\'' && str.substr(-1) === '\'') ||
153
      (str.substr(0, 1) === '"' && str.substr(-1) === '"')) {
154
    return str.substr(1, str.length - 2)
155
  } else {
156
    return str
157
  }
158
}
159
160
// =================json===============================
161
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...
162
    // cmdArgs is an array of arguments
163
    // json is an object containing the json object we need to modify
164
  setInputType(inpObj, 'json') // all we do is flag our content as being json
165
}
166
167
// =================jsonPrettify=======================
168
function jsonPrettify (inpObj, cmdArgs) {
169
  // cmdArgs is an (optional) array of arguments being indent, maxLength, margins
170
  // they are all passed as strings so need to be converted to numbers where appropriate
171
  // we use - 0 to convert string numbers to numeric and === against itself to check for NaN
172
  if (setInputType(inpObj, 'json')) {
173
    let opts = inpObj.outputOptions
174
    // set defaults
175
    opts.margins = false
176
    opts.maxLength = 45
177
    opts.indent = 2
178
    // overwrite with any values passed in
179
    if (cmdArgs !== undefined) {
180
      if ((cmdArgs[0] - 0) === (cmdArgs[0] - 0)) { opts.indent = (cmdArgs[0] - 0) }
181
      if ((cmdArgs[1] - 0) === (cmdArgs[1] - 0)) { opts.maxLength = (cmdArgs[1] - 0) }
182
      if (cmdArgs[1] === 'infinity') { opts.maxLength = 'infinity' }
183
      opts.margins = (cmdArgs[2] === 'true') // defaults to false if margins anything other than true
184
    }
185
  }
186
}
187
188
// =================ellipsify==========================
189
function jsonEllipsify (inpObj, cmdArgs) {
190
  // cmdArgs is an array of arguments
191
  // json is an object containing the json object we need to modify
192
193
  if (setInputType(inpObj, 'json')) {
194
    // we have two types of argument for Ellipsify, plain and exclude so separate them out
195
    var cmdArgsPlain = []
196
    var cmdArgsExclude = []
197
    for (let i = 0; i < cmdArgs.length; i++) {
198
      if (cmdArgs[i].substr(0, 1) === '~') {
199
        cmdArgsExclude.push(removeQuotes(cmdArgs[i].substr(1)))
200
      } else {
201
        cmdArgsPlain.push(removeQuotes(cmdArgs[i]))
202
      }
203
    }
204
    if (cmdArgsPlain.length === 0) { cmdArgsPlain.push('$') }
205
    for (let i = 0; i < cmdArgsPlain.length; i++) {
206
      minimizeJsonProperty(inpObj.json, cmdArgsPlain[i], cmdArgsExclude)
207
    }
208
  }
209
}
210
211
export function minimizeJsonProperty (json, property, excludes) { // only exported for test purposes
212
  // this function takes a json object as input.and for every occurrence of the given property puts a placeholder
213
  // but only if it is an array or an object.
214
  var arrPlaceholder = ['fsnipPlaceholderArrEllipses'] // a valid json array used as a placeholder to be replaced later with [...] (which is not valid json)
215
  var strPlaceholder = 'fsnipPlaceholderStrEllipses'
216
  var jsonPaths = jp.paths(json, buildJsonSearchPath(property)) // creates an array of all the paths of instances of the the property we want to minimize
217
  for (var i = 0; i < jsonPaths.length; i++) {
218
    let jsonPath = jp.stringify(jsonPaths[i])
219
    switch (jp.value(json, jsonPath).constructor.name) {
220
      case 'Object':
221
        var keys = Object.keys(jp.value(json, jsonPath))
222
        for (var j = 0; j < keys.length; j++) {
223
          if (excludes.indexOf(keys[j]) === -1) {
224
            // this key is not in the excludes list so we need to delete it
225
            delete jp.value(json, jsonPath)[keys[j]]
226
          }
227
        }
228
        jp.value(json, jsonPath)['fsnipPlaceholderObj'] = 'Ellipses' // add a placeholder for the Ellipses
229
        break
230
      case 'Array':
231
        jp.value(json, jsonPath, arrPlaceholder)
232
        break
233
      case 'String':
234
        jp.value(json, jsonPath, strPlaceholder)
235
        break
236
      default:
237
        // do nothing
238
    }
239
  }
240
}
241
242
// ===================snip Function==============================
243
function jsonSnippet (inpObj, cmdArgs) {
244
  // cmdArgs is an array of arguments
245
  // inpObj is an object containing the json object we need to modify
246
  // the format of the call is eg.
247
  // '--snip vessel 2' which would extract the second instance of "vessel" in the json supplied
248
  // with the instance identifier being optional
249
  if (setInputType(inpObj, 'json')) {
250
    var occ = 1
251
    if (cmdArgs.length === 1) {
252
      occ = 1 // by default we snip the first occurrence of this property
253
    } else if (cmdArgs.length === 2) {
254
      if ((cmdArgs[1] - 0) === (cmdArgs[1] - 0)) {
255
        occ = (cmdArgs[1] - 0)
256
        if (occ < 1) {
257
          inpObj.error.push('--snip requires its second argument to be a numeric values of at least 1 being the instance required')
258
          return
259
        }
260
      } else {
261
        inpObj.error.push("--snip requires its second argument to be numeric eg. '--snip vessel 2' with the optional second argument being the instance required")
262
        return
263
      }
264
    } else {
265
      inpObj.error.push("--snip requires 1 or 2 arguments eg. '--snip vessel 2' with the optional second argument being the instance required.")
266
      return
267
    }
268
    var jsonPaths = jp.paths(inpObj.json, buildJsonSearchPath(removeQuotes(cmdArgs[0]))) // creates an array of all the paths to this property
269
    if (jsonPaths.length < occ) {
270
      inpObj.error.push('--snip failed because there were only ' + jsonPaths.length + " occurrences of '" + removeQuotes(cmdArgs[0]) + "' found.")
271
      return
272
    }
273
    inpObj.json = jp.value(inpObj.json, jp.stringify(jsonPaths[occ - 1]))
274
  }
275
}
276
277
// ===================delKeys Function===========================
278
function jsonDelKeys (inpObj, cmdArgs) {
279
  // cmdArgs is an array of arguments
280
  // inpObj is an object containing the json object we need to remove keys from
281
  // the format of the call is eg.
282
  // '-jsonDelKeys vessel gnss' which would delete all instances of "vessel" and "gnss" in the json supplied
283
  if (setInputType(inpObj, 'json')) {
284
    for (var i = 0; i < cmdArgs.length; i++) {
285
      deleteJsonKey(inpObj.json, removeQuotes(cmdArgs[i]))
286
    }
287
  }
288
}
289
290
function deleteJsonKey (json, key) {
291
  // deletes all occurrences of key within json
292
  var jsonPaths = jp.paths(json, buildJsonSearchPath(key)) // creates an array of all the paths of instances of the key we want to delete
293
  var parent
294
  for (var i = 0; i < jsonPaths.length; i++) {
295
    let jsonPath = jp.stringify(jsonPaths[i])
296
    parent = jp.parent(json, jsonPath)
297
    if (Array.isArray(parent)) {
298
      parent.splice(jsonPaths[i][jsonPaths[i].length - 1], 1)
299
    } else {
300
      delete parent[jsonPaths[i][jsonPaths[i].length - 1]]
301
    }
302
  }
303
}
304
305
// ===================textFrom=================================
306
function textFrom (inpObj, cmdArgs, inclusive) {
307
  // cmdArgs is an array of arguments
308
  // inpObj is an object containing the text object we need to snip contents from
309
  // the format of the call is eg.
310
  // '--textFrom "some text" 2 - would start from the second instance of "some text"
311
  if (setInputType(inpObj, 'plain')) {
312
    let x = findLocation(inpObj, cmdArgs, inclusive ? '--start' : '--from')
313
    if (x.found) {
314
      inpObj.plain = inpObj.plain.substr(x.loc + (inclusive === true ? 0 : x.len))
315
    }
316
  }
317
}
318
319
// ===================textTo===================================
320
function textTo (inpObj, cmdArgs, inclusive) {
321
  // cmdArgs is an array of arguments
322
  // inpObj is an object containing the text object we need to snip contents from
323
  // the format of the call is eg.
324
  // '--textTo "some text" 2 - would go up to the second instance of "some text"
325
  if (setInputType(inpObj, 'plain')) {
326
    let x = findLocation(inpObj, cmdArgs, inclusive ? '--finish' : '--to')
327
    if (x.found) {
328
      inpObj.plain = inpObj.plain.substring(0, x.loc + (inclusive === true ? x.len : 0))
329
    }
330
  }
331
}
332
333
function findLocation (inpObj, cmdArgs, errString) {
334
  // find the location of the nth occurrence of the text specified in the command arguments
335
  let occ
336
  if (cmdArgs.length === 1) {
337
    occ = 1 // by default we take from the first occurrence of this text
338
  } else if (cmdArgs.length === 2) {
339
    if ((cmdArgs[1] - 0) === (cmdArgs[1] - 0)) {
340
      occ = (cmdArgs[1] - 0)
341
      if (occ < 1) {
342
        inpObj.error.push(errString + ' requires its second argument to be a numeric value of at least 1 being the instance required')
343
        return {found: false}
344
      }
345
    } else {
346
      inpObj.error.push(errString + " requires its second argument to be numeric eg. '" + errString + " sometext 2' with the optional second argument being the instance required")
347
      return {found: false}
348
    }
349
  } else {
350
    inpObj.error.push(errString + " requires 1 or 2 arguments eg. '" + errString + " sometext' with the optional second argument being the instance required.")
351
    return {found: false}
352
  }
353
  let x = -1
354
  let arg = removeQuotes(cmdArgs[0])
355
  for (let i = 0; i < occ; i++) {
356
    x = inpObj.plain.indexOf(arg, x + 1)
357
  }
358
  if (x === -1) {
359
    inpObj.error.push('unable to find occurrence ' + occ + ' of "' + arg + '"')
360
  }
361
  return {found: (x !== -1), loc: x, len: arg.length}
362
}
363