Passed
Push — master ( e53e62...8553c8 )
by Barry
01:08
created

helpers.js ➔ textTo   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

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