Projekt

Obecné

Profil

Stáhnout (9.85 KB) Statistiky
| Větev: | Revize:
1 3a515b92 cagy
'use strict'
2
const argsert = require('./argsert')
3
const objFilter = require('./obj-filter')
4
const specialKeys = ['$0', '--', '_']
5
6
// validation-type-stuff, missing params,
7
// bad implications, custom checks.
8
module.exports = function validation (yargs, usage, y18n) {
9
  const __ = y18n.__
10
  const __n = y18n.__n
11
  const self = {}
12
13
  // validate appropriate # of non-option
14
  // arguments were provided, i.e., '_'.
15
  self.nonOptionCount = function nonOptionCount (argv) {
16
    const demandedCommands = yargs.getDemandedCommands()
17
    // don't count currently executing commands
18
    const _s = argv._.length - yargs.getContext().commands.length
19
20
    if (demandedCommands._ && (_s < demandedCommands._.min || _s > demandedCommands._.max)) {
21
      if (_s < demandedCommands._.min) {
22
        if (demandedCommands._.minMsg !== undefined) {
23
          usage.fail(
24
            // replace $0 with observed, $1 with expected.
25
            demandedCommands._.minMsg ? demandedCommands._.minMsg.replace(/\$0/g, _s).replace(/\$1/, demandedCommands._.min) : null
26
          )
27
        } else {
28
          usage.fail(
29
            __('Not enough non-option arguments: got %s, need at least %s', _s, demandedCommands._.min)
30
          )
31
        }
32
      } else if (_s > demandedCommands._.max) {
33
        if (demandedCommands._.maxMsg !== undefined) {
34
          usage.fail(
35
            // replace $0 with observed, $1 with expected.
36
            demandedCommands._.maxMsg ? demandedCommands._.maxMsg.replace(/\$0/g, _s).replace(/\$1/, demandedCommands._.max) : null
37
          )
38
        } else {
39
          usage.fail(
40
            __('Too many non-option arguments: got %s, maximum of %s', _s, demandedCommands._.max)
41
          )
42
        }
43
      }
44
    }
45
  }
46
47
  // validate the appropriate # of <required>
48
  // positional arguments were provided:
49
  self.positionalCount = function positionalCount (required, observed) {
50
    if (observed < required) {
51
      usage.fail(
52
        __('Not enough non-option arguments: got %s, need at least %s', observed, required)
53
      )
54
    }
55
  }
56
57
  // make sure all the required arguments are present.
58
  self.requiredArguments = function requiredArguments (argv) {
59
    const demandedOptions = yargs.getDemandedOptions()
60
    let missing = null
61
62
    Object.keys(demandedOptions).forEach((key) => {
63
      if (!argv.hasOwnProperty(key) || typeof argv[key] === 'undefined') {
64
        missing = missing || {}
65
        missing[key] = demandedOptions[key]
66
      }
67
    })
68
69
    if (missing) {
70
      const customMsgs = []
71
      Object.keys(missing).forEach((key) => {
72
        const msg = missing[key]
73
        if (msg && customMsgs.indexOf(msg) < 0) {
74
          customMsgs.push(msg)
75
        }
76
      })
77
78
      const customMsg = customMsgs.length ? `\n${customMsgs.join('\n')}` : ''
79
80
      usage.fail(__n(
81
        'Missing required argument: %s',
82
        'Missing required arguments: %s',
83
        Object.keys(missing).length,
84
        Object.keys(missing).join(', ') + customMsg
85
      ))
86
    }
87
  }
88
89
  // check for unknown arguments (strict-mode).
90
  self.unknownArguments = function unknownArguments (argv, aliases, positionalMap) {
91
    const commandKeys = yargs.getCommandInstance().getCommands()
92
    const unknown = []
93
    const currentContext = yargs.getContext()
94
95
    Object.keys(argv).forEach((key) => {
96
      if (specialKeys.indexOf(key) === -1 &&
97
        !positionalMap.hasOwnProperty(key) &&
98
        !yargs._getParseContext().hasOwnProperty(key) &&
99
        !aliases.hasOwnProperty(key)
100
      ) {
101
        unknown.push(key)
102
      }
103
    })
104
105
    if (commandKeys.length > 0) {
106
      argv._.slice(currentContext.commands.length).forEach((key) => {
107
        if (commandKeys.indexOf(key) === -1) {
108
          unknown.push(key)
109
        }
110
      })
111
    }
112
113
    if (unknown.length > 0) {
114
      usage.fail(__n(
115
        'Unknown argument: %s',
116
        'Unknown arguments: %s',
117
        unknown.length,
118
        unknown.join(', ')
119
      ))
120
    }
121
  }
122
123
  // validate arguments limited to enumerated choices
124
  self.limitedChoices = function limitedChoices (argv) {
125
    const options = yargs.getOptions()
126
    const invalid = {}
127
128
    if (!Object.keys(options.choices).length) return
129
130
    Object.keys(argv).forEach((key) => {
131
      if (specialKeys.indexOf(key) === -1 &&
132
        options.choices.hasOwnProperty(key)) {
133
        [].concat(argv[key]).forEach((value) => {
134
          // TODO case-insensitive configurability
135
          if (options.choices[key].indexOf(value) === -1 &&
136
              value !== undefined) {
137
            invalid[key] = (invalid[key] || []).concat(value)
138
          }
139
        })
140
      }
141
    })
142
143
    const invalidKeys = Object.keys(invalid)
144
145
    if (!invalidKeys.length) return
146
147
    let msg = __('Invalid values:')
148
    invalidKeys.forEach((key) => {
149
      msg += `\n  ${__(
150
        'Argument: %s, Given: %s, Choices: %s',
151
        key,
152
        usage.stringifiedValues(invalid[key]),
153
        usage.stringifiedValues(options.choices[key])
154
      )}`
155
    })
156
    usage.fail(msg)
157
  }
158
159
  // custom checks, added using the `check` option on yargs.
160
  let checks = []
161
  self.check = function check (f, global) {
162
    checks.push({
163
      func: f,
164
      global
165
    })
166
  }
167
168
  self.customChecks = function customChecks (argv, aliases) {
169
    for (let i = 0, f; (f = checks[i]) !== undefined; i++) {
170
      const func = f.func
171
      let result = null
172
      try {
173
        result = func(argv, aliases)
174
      } catch (err) {
175
        usage.fail(err.message ? err.message : err, err)
176
        continue
177
      }
178
179
      if (!result) {
180
        usage.fail(__('Argument check failed: %s', func.toString()))
181
      } else if (typeof result === 'string' || result instanceof Error) {
182
        usage.fail(result.toString(), result)
183
      }
184
    }
185
  }
186
187
  // check implications, argument foo implies => argument bar.
188
  let implied = {}
189
  self.implies = function implies (key, value) {
190
    argsert('<string|object> [array|number|string]', [key, value], arguments.length)
191
192
    if (typeof key === 'object') {
193
      Object.keys(key).forEach((k) => {
194
        self.implies(k, key[k])
195
      })
196
    } else {
197
      yargs.global(key)
198
      if (!implied[key]) {
199
        implied[key] = []
200
      }
201
      if (Array.isArray(value)) {
202
        value.forEach((i) => self.implies(key, i))
203
      } else {
204
        implied[key].push(value)
205
      }
206
    }
207
  }
208
  self.getImplied = function getImplied () {
209
    return implied
210
  }
211
212
  self.implications = function implications (argv) {
213
    const implyFail = []
214
215
    Object.keys(implied).forEach((key) => {
216
      const origKey = key
217
      ;(implied[key] || []).forEach((value) => {
218
        let num
219
        let key = origKey
220
        const origValue = value
221
222
        // convert string '1' to number 1
223
        num = Number(key)
224
        key = isNaN(num) ? key : num
225
226
        if (typeof key === 'number') {
227
          // check length of argv._
228
          key = argv._.length >= key
229
        } else if (key.match(/^--no-.+/)) {
230
          // check if key doesn't exist
231
          key = key.match(/^--no-(.+)/)[1]
232
          key = !argv[key]
233
        } else {
234
          // check if key exists
235
          key = argv[key]
236
        }
237
238
        num = Number(value)
239
        value = isNaN(num) ? value : num
240
241
        if (typeof value === 'number') {
242
          value = argv._.length >= value
243
        } else if (value.match(/^--no-.+/)) {
244
          value = value.match(/^--no-(.+)/)[1]
245
          value = !argv[value]
246
        } else {
247
          value = argv[value]
248
        }
249
        if (key && !value) {
250
          implyFail.push(` ${origKey} -> ${origValue}`)
251
        }
252
      })
253
    })
254
255
    if (implyFail.length) {
256
      let msg = `${__('Implications failed:')}\n`
257
258
      implyFail.forEach((value) => {
259
        msg += (value)
260
      })
261
262
      usage.fail(msg)
263
    }
264
  }
265
266
  let conflicting = {}
267
  self.conflicts = function conflicts (key, value) {
268
    argsert('<string|object> [array|string]', [key, value], arguments.length)
269
270
    if (typeof key === 'object') {
271
      Object.keys(key).forEach((k) => {
272
        self.conflicts(k, key[k])
273
      })
274
    } else {
275
      yargs.global(key)
276
      if (!conflicting[key]) {
277
        conflicting[key] = []
278
      }
279
      if (Array.isArray(value)) {
280
        value.forEach((i) => self.conflicts(key, i))
281
      } else {
282
        conflicting[key].push(value)
283
      }
284
    }
285
  }
286
  self.getConflicting = () => conflicting
287
288
  self.conflicting = function conflictingFn (argv) {
289
    Object.keys(argv).forEach((key) => {
290
      if (conflicting[key]) {
291
        conflicting[key].forEach((value) => {
292
          // we default keys to 'undefined' that have been configured, we should not
293
          // apply conflicting check unless they are a value other than 'undefined'.
294
          if (value && argv[key] !== undefined && argv[value] !== undefined) {
295
            usage.fail(__('Arguments %s and %s are mutually exclusive', key, value))
296
          }
297
        })
298
      }
299
    })
300
  }
301
302
  self.recommendCommands = function recommendCommands (cmd, potentialCommands) {
303
    const distance = require('./levenshtein')
304
    const threshold = 3 // if it takes more than three edits, let's move on.
305
    potentialCommands = potentialCommands.sort((a, b) => b.length - a.length)
306
307
    let recommended = null
308
    let bestDistance = Infinity
309
    for (let i = 0, candidate; (candidate = potentialCommands[i]) !== undefined; i++) {
310
      const d = distance(cmd, candidate)
311
      if (d <= threshold && d < bestDistance) {
312
        bestDistance = d
313
        recommended = candidate
314
      }
315
    }
316
    if (recommended) usage.fail(__('Did you mean %s?', recommended))
317
  }
318
319
  self.reset = function reset (localLookup) {
320
    implied = objFilter(implied, (k, v) => !localLookup[k])
321
    conflicting = objFilter(conflicting, (k, v) => !localLookup[k])
322
    checks = checks.filter(c => c.global)
323
    return self
324
  }
325
326
  let frozen
327
  self.freeze = function freeze () {
328
    frozen = {}
329
    frozen.implied = implied
330
    frozen.checks = checks
331
    frozen.conflicting = conflicting
332
  }
333
  self.unfreeze = function unfreeze () {
334
    implied = frozen.implied
335
    checks = frozen.checks
336
    conflicting = frozen.conflicting
337
    frozen = undefined
338
  }
339
340
  return self
341
}