opto 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rspec +2 -0
- data/.travis.yml +18 -0
- data/Gemfile +9 -0
- data/LICENSE.txt +21 -0
- data/README.md +407 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/opto.rb +51 -0
- data/lib/opto/extensions/hash_string_or_symbol_key.rb +18 -0
- data/lib/opto/extensions/snake_case.rb +22 -0
- data/lib/opto/group.rb +112 -0
- data/lib/opto/option.rb +298 -0
- data/lib/opto/resolver.rb +65 -0
- data/lib/opto/resolvers/default.rb +10 -0
- data/lib/opto/resolvers/environment_variable.rb +25 -0
- data/lib/opto/resolvers/file_content.rb +32 -0
- data/lib/opto/resolvers/random_number.rb +35 -0
- data/lib/opto/resolvers/random_string.rb +84 -0
- data/lib/opto/resolvers/random_uuid.rb +15 -0
- data/lib/opto/setter.rb +66 -0
- data/lib/opto/setters/environment_variable.rb +40 -0
- data/lib/opto/type.rb +148 -0
- data/lib/opto/types/boolean.rb +57 -0
- data/lib/opto/types/enum.rb +107 -0
- data/lib/opto/types/integer.rb +47 -0
- data/lib/opto/types/string.rb +71 -0
- data/lib/opto/types/uri.rb +36 -0
- data/lib/opto/version.rb +3 -0
- data/opto.gemspec +24 -0
- metadata +119 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 01abb09f30890132075384eb8ff969899b0fb168
|
4
|
+
data.tar.gz: 51d608217ef7b48968d66a75ae2e8515c38c4d9d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 66121b1275518c5023ee8e2a4633e7835f3cff613c6173bc18debe8a1b29905f0913f13a4b1579e83d46f8fd4f049ebb17a2936497660c3fda1d1c2471446a07
|
7
|
+
data.tar.gz: 89ecd251166c29beeae9bcb96962d7d873215d9f55225e7c6813050fa1b772b400497977289594549a182e51e0098e498887e0a8bd38c0a0b17e0c80c2d66494
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
language: ruby
|
2
|
+
rvm:
|
3
|
+
- 2.0.0-p648
|
4
|
+
- 2.1.8
|
5
|
+
- 2.2.3
|
6
|
+
- 2.3.1
|
7
|
+
env:
|
8
|
+
- secure: "bCu1bVzr6SP+gAbMo88GvcHHbhO3LcnJtWp+YuSZ5qW4EHhdBw+DOQeOlzoKHtYhNhptmb/65pZZctRTE6jOOSZzpOUHaeqaqHZpELldkCMu7rE8bXuCV4OfhG0CKWvURS0x5p5F5onEx1a1sjiu+MVEUqktAPDAcdTBNBW1irRwvnSifgjcZRLzfm1aRs1fqzBJpHaAXyD++2GQaWyXDtQsEsc/Eyhmnkac5Y2cTeQiYlDluqBeB885q9K16ruLp2NWx/rnR8RGMP1LSnrye/GEo2mRWOWy1MyIEwNDcVaA6MVDY2GBIQUoXg4PBrZAleVIsO3LzwR4oorFCaSgWxzqYI67g5Kb2zn8fW9Yu5lDcFEJKIHQAwBrlq3n3Mi5it/IxGQDWXQ/2fjBxCwuilvI0WTzOH0m4g8Uf6jHaQLFC6ahdWDjfs6aCzdPpbJls26/r39NIB5k/6ZO29NZkxGBsQ7E7m4wurr7a9ksLODgPOyPiB8/4Txu35Llp+IuJpbshrlkRIMwEIFVWiLi6MZhRBkxaRzlkJQ0bS3RD9W4z9UJyoBujmgXMrv0eHNTGCOC+/4tvrA+pXzJwmFQJA6TnenoDl9hc6o03gyfH/YvmMgrtJLbJ8rGoLRaY8HsQwHWgimpCl1o043kO8/UkhhS7J4wyQXYwqGnA2PC/j0="
|
9
|
+
before_install: gem install bundler -v 1.12.5
|
10
|
+
cache: bundler
|
11
|
+
script: bundle install && bundle exec rspec spec/
|
12
|
+
deploy:
|
13
|
+
provider: rubygems
|
14
|
+
api_key: $GEM_TOKEN
|
15
|
+
gem: opto
|
16
|
+
on:
|
17
|
+
tags: true
|
18
|
+
rvm: 2.3.1
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 Kimmo Lehto
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,407 @@
|
|
1
|
+
# Opto
|
2
|
+
|
3
|
+
[](https://travis-ci.org/kontena/opto)
|
4
|
+
|
5
|
+
An option parser, built for generating options from YAML based [Kontena](https://github.com/kontena/kontena/) stack
|
6
|
+
definition files, but can be just as well used for other things, such as an API input validator.
|
7
|
+
|
8
|
+
The values for options can be resolved from files, env-variables, custom interactive prompts, random generators, etc.
|
9
|
+
|
10
|
+
The option type handlers can perform validations, such as defining a range or length requirements.
|
11
|
+
|
12
|
+
Transformations can be performed on values, such as upcasing strings or removing white space.
|
13
|
+
|
14
|
+
Options can have simple conditionals for determining if it needs to be processed or not, an option for defining a database
|
15
|
+
password can be processed only if a database has been selected.
|
16
|
+
|
17
|
+
Finally the value for the option can be placed to some destination, such as an environment variable or sent to a command.
|
18
|
+
|
19
|
+
## Installation
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
# gem install opto
|
23
|
+
|
24
|
+
require 'opto'
|
25
|
+
```
|
26
|
+
|
27
|
+
## YAML definition examples:
|
28
|
+
|
29
|
+
```yaml
|
30
|
+
# Enum type
|
31
|
+
remote_driver:
|
32
|
+
type: "enum"
|
33
|
+
required: true
|
34
|
+
label: "Remote Driver"
|
35
|
+
description: "Remote Git and Auth scheme"
|
36
|
+
options:
|
37
|
+
- github
|
38
|
+
- bitbucket
|
39
|
+
- gitlab
|
40
|
+
- gogs
|
41
|
+
```
|
42
|
+
|
43
|
+
```yaml
|
44
|
+
# String validation and transformation
|
45
|
+
foo_username:
|
46
|
+
type: string
|
47
|
+
required: true
|
48
|
+
min_length: 1
|
49
|
+
max_length: 30
|
50
|
+
strip: true # remove leading / trailing whitespace
|
51
|
+
upcase: true # make UPCASE
|
52
|
+
env: FOO_USER # read value from ENV variable FOO_USER
|
53
|
+
```
|
54
|
+
|
55
|
+
```yaml
|
56
|
+
# Enum with prettier item descriptions
|
57
|
+
name: foo_os
|
58
|
+
type: enum
|
59
|
+
can_be_other: true # otherwise value has to be one of the options to be valid.
|
60
|
+
options:
|
61
|
+
- value: coreos
|
62
|
+
label: CoreOS
|
63
|
+
description: CoreOS Stable
|
64
|
+
- value: ubuntu
|
65
|
+
label: Ubuntu
|
66
|
+
description: Ubuntu Bubuntu
|
67
|
+
```
|
68
|
+
|
69
|
+
```yaml
|
70
|
+
# Integer with default value and allowed range
|
71
|
+
foo_instances:
|
72
|
+
type: integer
|
73
|
+
default: 1
|
74
|
+
min: 1
|
75
|
+
max: 30
|
76
|
+
```
|
77
|
+
|
78
|
+
```yaml
|
79
|
+
# Uri validator
|
80
|
+
host_url:
|
81
|
+
type: uri
|
82
|
+
schemes:
|
83
|
+
- file # only allow file:/// uris
|
84
|
+
```
|
85
|
+
|
86
|
+
## Resolvers
|
87
|
+
|
88
|
+
Simple so far. Now let's mix in "resolvers" which can fetch the value from a number of sources or even generate new data:
|
89
|
+
|
90
|
+
```yaml
|
91
|
+
# Generate random strings
|
92
|
+
vault_iv:
|
93
|
+
type: string
|
94
|
+
from:
|
95
|
+
random_string:
|
96
|
+
length: 64
|
97
|
+
charset: ascii_printable # Other charsets include hex, hex_upcase, alphanumeric, etc.
|
98
|
+
```
|
99
|
+
|
100
|
+
```yaml
|
101
|
+
# Try to get value from multiple sources
|
102
|
+
aws_secret:
|
103
|
+
type: string
|
104
|
+
strip: true # removes any leading / trailing whitespace from a string
|
105
|
+
upcase: true # turns the string to upcase
|
106
|
+
from:
|
107
|
+
env: 'FOOFOO'
|
108
|
+
file: /tmp/aws_secret.txt # if env is not set, try to read it from this file, raises if not readable
|
109
|
+
|
110
|
+
aws_secret:
|
111
|
+
type: string
|
112
|
+
strip: true # removes any leading / trailing whitespace from a string
|
113
|
+
upcase: true # turns the string to upcase
|
114
|
+
from:
|
115
|
+
env: FOOFOO
|
116
|
+
file: # if env is not set, try to read it from this file, returns nil if not readable
|
117
|
+
path: /tmp/aws_secret.txt
|
118
|
+
ignore_errors: true
|
119
|
+
random_string: 30 # not there either, generate a random string.
|
120
|
+
```
|
121
|
+
|
122
|
+
## Setters
|
123
|
+
|
124
|
+
Ok, so what to do with the values? There's setters for that.
|
125
|
+
|
126
|
+
```yaml
|
127
|
+
aws_secret:
|
128
|
+
type: string
|
129
|
+
from:
|
130
|
+
env: AWS_TOKEN
|
131
|
+
to:
|
132
|
+
env: AWS_SECRET_TOKEN # once a valid value is set, set it to this variable.
|
133
|
+
|
134
|
+
# There aren't any more setters right now, but one could imagine setters such as
|
135
|
+
# output to a file, interpolate into a file, run a command, etc.
|
136
|
+
```
|
137
|
+
|
138
|
+
## Conditionals
|
139
|
+
|
140
|
+
There's also rudimentary conditional support:
|
141
|
+
|
142
|
+
```yaml
|
143
|
+
- name: foo
|
144
|
+
type: string
|
145
|
+
value: 'hello'
|
146
|
+
- name: bar
|
147
|
+
type: integer
|
148
|
+
only_if: # only process if 'foo' has the value 'hello'
|
149
|
+
- foo: hello
|
150
|
+
```
|
151
|
+
|
152
|
+
```ruby
|
153
|
+
group.option('bar').skip?
|
154
|
+
=> false
|
155
|
+
group.option('foo').value = 'world'
|
156
|
+
group.option('bar').skip?
|
157
|
+
=> true
|
158
|
+
```
|
159
|
+
|
160
|
+
```yaml
|
161
|
+
- name: bar
|
162
|
+
type: integer
|
163
|
+
skip_if: # same but reverse, do not process if 'foo' has value 'hello'
|
164
|
+
- foo: 'hello'
|
165
|
+
```
|
166
|
+
|
167
|
+
```ruby
|
168
|
+
group.option('foo').value = 'world'
|
169
|
+
group.option('bar').skip?
|
170
|
+
=> false
|
171
|
+
group.option('foo').value = 'hello'
|
172
|
+
group.option('bar').skip?
|
173
|
+
=> true
|
174
|
+
```
|
175
|
+
|
176
|
+
```yaml
|
177
|
+
# These work too:
|
178
|
+
|
179
|
+
- name: bar
|
180
|
+
type: integer
|
181
|
+
skip_if:
|
182
|
+
- foo: hello # AND
|
183
|
+
- baz: world
|
184
|
+
|
185
|
+
- name: bar
|
186
|
+
type: integer
|
187
|
+
only_if: foo # process if foo is not null, false or 'false'
|
188
|
+
|
189
|
+
- name: bar
|
190
|
+
type: integer
|
191
|
+
only_if:
|
192
|
+
- foo # foo is not null
|
193
|
+
- baz # AND baz is not null
|
194
|
+
```
|
195
|
+
|
196
|
+
Pretty nifty, right?
|
197
|
+
|
198
|
+
## Examples
|
199
|
+
|
200
|
+
```ruby
|
201
|
+
# Read definitions from 'options' key inside a YAML:
|
202
|
+
Opto.load('/tmp/stack.yml', :options)
|
203
|
+
|
204
|
+
# Read definitions from root of YAML
|
205
|
+
Opto.load('/tmp/stack.yml')
|
206
|
+
|
207
|
+
# Create an option group:
|
208
|
+
Opto.new( [ {name: 'foo', type: :string} ] )
|
209
|
+
# or
|
210
|
+
group = Opto::Group.new
|
211
|
+
group.build_option(name: 'foo', type: :string, value: "hello")
|
212
|
+
group.build_option(name: 'bar', type: :string, required: true)
|
213
|
+
group.first
|
214
|
+
=> #<Opto::Option:xxx>
|
215
|
+
group.size
|
216
|
+
=> 2
|
217
|
+
group.each { .. }
|
218
|
+
group.errors
|
219
|
+
=> { 'bar' => { :presence => "Required value missing" } }
|
220
|
+
group.options_with_errors.each { ... }
|
221
|
+
group.valid?
|
222
|
+
=> false
|
223
|
+
```
|
224
|
+
|
225
|
+
## Creating a custom resolver
|
226
|
+
|
227
|
+
Want to prompt for values? Try something like this:
|
228
|
+
|
229
|
+
```ruby
|
230
|
+
# gem install tty-prompt
|
231
|
+
require 'tty-prompt'
|
232
|
+
class Prompter < Opto::Resolver
|
233
|
+
def resolve
|
234
|
+
# option = accessor to the option currently being resolved
|
235
|
+
# option.handler = accessor to the type handler
|
236
|
+
# hint = resolver options, for example the env variable name for env resolver, not used here.
|
237
|
+
return nil if option.skip?
|
238
|
+
if option.type == :enum
|
239
|
+
TTY::Prompt.new.select("Select #{option.label}") do |menu|
|
240
|
+
option.handler.options[:options].each do |opt| # quite ugly way to access the option's value list definition
|
241
|
+
menu.choice opt[:label], opt[:value]
|
242
|
+
end
|
243
|
+
end
|
244
|
+
else
|
245
|
+
TTY::Prompt.new.ask("Enter value for #{option.label}")
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
# And the option:
|
251
|
+
- name: foo
|
252
|
+
type: enum
|
253
|
+
options:
|
254
|
+
- foo: Foo
|
255
|
+
- bar: Bar
|
256
|
+
from: prompter
|
257
|
+
```
|
258
|
+
|
259
|
+
## Subclassing a predefined type handler, setter, etc
|
260
|
+
|
261
|
+
```ruby
|
262
|
+
class VersionNumber < Opto::Types::String
|
263
|
+
Opto::Type.inherited(self) # need to call Opto::Type.inherited for registering the handler for now.
|
264
|
+
|
265
|
+
OPTIONS = Opto::Types::String::OPTIONS.merge(
|
266
|
+
min_version: nil,
|
267
|
+
max_version: nil
|
268
|
+
)
|
269
|
+
|
270
|
+
validate :min_version do |value|
|
271
|
+
if options[:min_version] && value < options[:min_version]
|
272
|
+
"Minimum version required: #{options[:min_version]}"
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
validate :max_version do |value|
|
277
|
+
if options[:max_version] && value > options[:max_version]
|
278
|
+
"Maximum version: #{options[:max_version]}, yours is #{value}"
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
sanitize :remove_build_info do |value|
|
283
|
+
value.split('+').first
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
# And to use:
|
288
|
+
> opt = Opto::Option.new(type: :version_number, name: 'foo', minimum_version: '1.0.0')
|
289
|
+
> opt.value = '0.1.0'
|
290
|
+
> opt.valid?
|
291
|
+
=> false
|
292
|
+
> opt.errors
|
293
|
+
=> { :validate_min_version => "Minimum version required: 1.0.0" }
|
294
|
+
```
|
295
|
+
|
296
|
+
## Default types
|
297
|
+
|
298
|
+
Global validations:
|
299
|
+
|
300
|
+
```yaml
|
301
|
+
in: # only allow one of the following values
|
302
|
+
- a
|
303
|
+
- b
|
304
|
+
- c
|
305
|
+
```
|
306
|
+
|
307
|
+
### boolean
|
308
|
+
|
309
|
+
```ruby
|
310
|
+
{
|
311
|
+
truthy: ['true', 'yes', '1', 'on', 'enabled', 'enable'], # These strings will be turned into true
|
312
|
+
nil_is: false, # If the value is null, set to false
|
313
|
+
blank_is: false, # If the value is a blank string, set to false
|
314
|
+
false: 'false', # When outputting, emit this value when value is false
|
315
|
+
true: 'true', # When outputting, emit this value when value is true
|
316
|
+
as: 'string' # Output a string, can be 'boolean' or 'integer'
|
317
|
+
}
|
318
|
+
```
|
319
|
+
|
320
|
+
### enum
|
321
|
+
|
322
|
+
```ruby
|
323
|
+
{
|
324
|
+
options: [], # List of the possible option values
|
325
|
+
can_be_other: false # Or allow values outside the option list
|
326
|
+
}
|
327
|
+
```
|
328
|
+
|
329
|
+
### integer
|
330
|
+
```ruby
|
331
|
+
{
|
332
|
+
min: 0, # minimum value, can be negative
|
333
|
+
max: nil, # maximum value
|
334
|
+
nil_is_zero: false # null value will be turned into zero
|
335
|
+
}
|
336
|
+
```
|
337
|
+
|
338
|
+
### string
|
339
|
+
```ruby
|
340
|
+
{
|
341
|
+
min_length: nil, # minimum length
|
342
|
+
max_length: nil, # maximum length
|
343
|
+
empty_is_nil: true, # if string contains whitespace only, make value null
|
344
|
+
encode_64: false, # encode content to base64
|
345
|
+
decode_64: false, # decode content from base64
|
346
|
+
upcase: false, # convert to UPPERCASE
|
347
|
+
downcase: false, # convert to lowercase
|
348
|
+
strip: false, # remove leading/trailing whitespace,
|
349
|
+
chomp: false, # remove trailing linefeed
|
350
|
+
capitalize: false # convert to Capital case.
|
351
|
+
}
|
352
|
+
```
|
353
|
+
|
354
|
+
### uri
|
355
|
+
```ruby
|
356
|
+
{
|
357
|
+
schemes: [ 'http', 'https' ] # only http and https urls are considered valid
|
358
|
+
}
|
359
|
+
```
|
360
|
+
|
361
|
+
## Default resolvers
|
362
|
+
Hint is the value that gets passed to the resolver when doing for example: `env: FOO` (FOO is the hint)
|
363
|
+
|
364
|
+
### env
|
365
|
+
Hint is the environment variable name to read from. Defaults to the option's name.
|
366
|
+
|
367
|
+
### file
|
368
|
+
Hint can be a string containing a path to the file, or a hash that defines `path: 'file_path', ignore_errors: true`
|
369
|
+
|
370
|
+
### random_number
|
371
|
+
Hint must be a hash containing `min: minimum_number, max: maximum_number`
|
372
|
+
|
373
|
+
### random_string
|
374
|
+
Hint can be a string/number that defines minimum length. Default charset is 'alphanumeric'
|
375
|
+
Hint can also be a hash that defines `length: length_of_generated_string, charset: 'charset_name'`
|
376
|
+
|
377
|
+
Defined charsets:
|
378
|
+
* numbers (0-9)
|
379
|
+
* letters (a-z + A-Z)
|
380
|
+
* downcase (a-z)
|
381
|
+
* upcase (A-Z)
|
382
|
+
* alphanumeric (0-9 + a-z + A-Z)
|
383
|
+
* hex (0-9 + a-f)
|
384
|
+
* hex_upcase (0-9 + A-F)
|
385
|
+
* base64 (base64 charset (length has to be divisible by four when using base64))
|
386
|
+
* ascii_printable (all printable ascii chars)
|
387
|
+
* or a set of characters, for example: { length: 8, charset: '01' } Will generate something like: 01001100
|
388
|
+
|
389
|
+
### random_uuid
|
390
|
+
Ignores hint completely.
|
391
|
+
|
392
|
+
Output is a 'random' UUID generated by `SecureRandom.uuid`, such as `78b6decf-e312-45a1-ac8c-d562270036ba`
|
393
|
+
|
394
|
+
## Default setters
|
395
|
+
|
396
|
+
### env
|
397
|
+
Works exactly the same as env resolver, except in reverse.
|
398
|
+
|
399
|
+
## Contributing
|
400
|
+
|
401
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/kontena/opto. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
402
|
+
|
403
|
+
|
404
|
+
## License
|
405
|
+
|
406
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
407
|
+
|