1 | 'use strict'
|
2 | const arrayify = require('array-back')
|
3 | const option = require('./option')
|
4 | const Definition = require('./definition')
|
5 | const t = require('typical')
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 | class Definitions extends Array {
|
16 | load (definitions) {
|
17 | this.clear()
|
18 | arrayify(definitions).forEach(def => this.push(new Definition(def)))
|
19 | this.validate()
|
20 | }
|
21 |
|
22 | clear () {
|
23 | this.length = 0
|
24 | }
|
25 |
|
26 | |
27 |
|
28 |
|
29 |
|
30 | validate (argv) {
|
31 | const someHaveNoName = this.some(def => !def.name)
|
32 | if (someHaveNoName) {
|
33 | halt(
|
34 | 'NAME_MISSING',
|
35 | 'Invalid option definitions: the `name` property is required on each definition'
|
36 | )
|
37 | }
|
38 |
|
39 | const someDontHaveFunctionType = this.some(def => def.type && typeof def.type !== 'function')
|
40 | if (someDontHaveFunctionType) {
|
41 | halt(
|
42 | 'INVALID_TYPE',
|
43 | 'Invalid option definitions: the `type` property must be a setter fuction (default: `Boolean`)'
|
44 | )
|
45 | }
|
46 |
|
47 | let invalidOption
|
48 |
|
49 | const numericAlias = this.some(def => {
|
50 | invalidOption = def
|
51 | return t.isDefined(def.alias) && t.isNumber(def.alias)
|
52 | })
|
53 | if (numericAlias) {
|
54 | halt(
|
55 | 'INVALID_ALIAS',
|
56 | 'Invalid option definition: to avoid ambiguity an alias cannot be numeric [--' + invalidOption.name + ' alias is -' + invalidOption.alias + ']'
|
57 | )
|
58 | }
|
59 |
|
60 | const multiCharacterAlias = this.some(def => {
|
61 | invalidOption = def
|
62 | return t.isDefined(def.alias) && def.alias.length !== 1
|
63 | })
|
64 | if (multiCharacterAlias) {
|
65 | halt(
|
66 | 'INVALID_ALIAS',
|
67 | 'Invalid option definition: an alias must be a single character'
|
68 | )
|
69 | }
|
70 |
|
71 | const hypenAlias = this.some(def => {
|
72 | invalidOption = def
|
73 | return def.alias === '-'
|
74 | })
|
75 | if (hypenAlias) {
|
76 | halt(
|
77 | 'INVALID_ALIAS',
|
78 | 'Invalid option definition: an alias cannot be "-"'
|
79 | )
|
80 | }
|
81 |
|
82 | const duplicateName = hasDuplicates(this.map(def => def.name))
|
83 | if (duplicateName) {
|
84 | halt(
|
85 | 'DUPLICATE_NAME',
|
86 | 'Two or more option definitions have the same name'
|
87 | )
|
88 | }
|
89 |
|
90 | const duplicateAlias = hasDuplicates(this.map(def => def.alias))
|
91 | if (duplicateAlias) {
|
92 | halt(
|
93 | 'DUPLICATE_ALIAS',
|
94 | 'Two or more option definitions have the same alias'
|
95 | )
|
96 | }
|
97 |
|
98 | const duplicateDefaultOption = hasDuplicates(this.map(def => def.defaultOption))
|
99 | if (duplicateDefaultOption) {
|
100 | halt(
|
101 | 'DUPLICATE_DEFAULT_OPTION',
|
102 | 'Only one option definition can be the defaultOption'
|
103 | )
|
104 | }
|
105 | }
|
106 |
|
107 | |
108 |
|
109 |
|
110 |
|
111 | get (arg) {
|
112 | return option.short.test(arg)
|
113 | ? this.find(def => def.alias === option.short.name(arg))
|
114 | : this.find(def => def.name === option.long.name(arg))
|
115 | }
|
116 |
|
117 | getDefault () {
|
118 | return this.find(def => def.defaultOption === true)
|
119 | }
|
120 |
|
121 | isGrouped () {
|
122 | return this.some(def => def.group)
|
123 | }
|
124 |
|
125 | whereGrouped () {
|
126 | return this.filter(containsValidGroup)
|
127 | }
|
128 | whereNotGrouped () {
|
129 | return this.filter(def => !containsValidGroup(def))
|
130 | }
|
131 | }
|
132 |
|
133 | function halt (name, message) {
|
134 | const err = new Error(message)
|
135 | err.name = name
|
136 | throw err
|
137 | }
|
138 |
|
139 | function containsValidGroup (def) {
|
140 | return arrayify(def.group).some(group => group)
|
141 | }
|
142 |
|
143 | function hasDuplicates (array) {
|
144 | const items = {}
|
145 | for (let i = 0; i < array.length; i++) {
|
146 | const value = array[i]
|
147 | if (items[value]) {
|
148 | return true
|
149 | } else {
|
150 | if (t.isDefined(value)) items[value] = true
|
151 | }
|
152 | }
|
153 | }
|
154 |
|
155 | module.exports = Definitions
|