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
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'opto/extensions/snake_case'
|
2
|
+
require 'opto/extensions/hash_string_or_symbol_key'
|
3
|
+
|
4
|
+
if RUBY_VERSION < '2.1'
|
5
|
+
using Opto::Extension::SnakeCase
|
6
|
+
using Opto::Extension::HashStringOrSymbolKey
|
7
|
+
end
|
8
|
+
|
9
|
+
module Opto
|
10
|
+
# Base for resolvers.
|
11
|
+
#
|
12
|
+
# Resolvers are scripts that can retrieve or generate a value for an option.
|
13
|
+
# Such resolvers are for example Env, which can try to find the value for
|
14
|
+
# the option from an environment variable. An example of generators is
|
15
|
+
# RandomString, which can generate random strings of defined length.
|
16
|
+
class Resolver
|
17
|
+
|
18
|
+
using Opto::Extension::SnakeCase unless RUBY_VERSION < '2.1'
|
19
|
+
|
20
|
+
attr_accessor :hint
|
21
|
+
attr_accessor :option
|
22
|
+
|
23
|
+
class << self
|
24
|
+
# Find a resolver using an origin_name definition, such as :env or :file
|
25
|
+
# @param [Symbol, String] origin
|
26
|
+
def for(origin)
|
27
|
+
raise NameError, "Unknown resolver: #{origin}" unless resolvers[origin]
|
28
|
+
resolvers[origin]
|
29
|
+
end
|
30
|
+
|
31
|
+
def inherited(where)
|
32
|
+
resolvers[where.origin] = where
|
33
|
+
end
|
34
|
+
|
35
|
+
def resolvers
|
36
|
+
@resolvers ||= {}
|
37
|
+
end
|
38
|
+
|
39
|
+
def origin
|
40
|
+
name.to_s.split('::').last.snakecase.to_sym
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Initialize an instance of a resolver.
|
45
|
+
# @param hint A "hint" for the resolver, for example. the environment variable name or a set of rules for generators.
|
46
|
+
# @param [Opto::Option] option The option parent of this resolver instance
|
47
|
+
# @return [Opto::Resolver]
|
48
|
+
def initialize(hint = nil, option = nil)
|
49
|
+
@hint = hint
|
50
|
+
@option = option
|
51
|
+
end
|
52
|
+
|
53
|
+
# This is a "base" class, you're supposed to inherit from this in your resolver and define a #resolve method.
|
54
|
+
def resolve
|
55
|
+
raise RuntimeError, "#{self.class}.resolve not defined"
|
56
|
+
end
|
57
|
+
|
58
|
+
# The origin "tag" of this resolver, for example: 'random_string' or 'env'
|
59
|
+
def origin
|
60
|
+
self.class.origin
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
Dir[File.expand_path('../resolvers/*.rb', __FILE__)].each {|file| require file}
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Opto
|
2
|
+
module Resolvers
|
3
|
+
# Find a value using Environment.
|
4
|
+
#
|
5
|
+
# Hint should be a name of environment variable, such as 'HOME'
|
6
|
+
#
|
7
|
+
# Numbers will be converted to fixnums, "true" and "false" will be converted to booleans.
|
8
|
+
class Env < Opto::Resolver
|
9
|
+
|
10
|
+
def resolve
|
11
|
+
raise ArgumentError, "Environment variable name not set" if hint.nil?
|
12
|
+
val = ENV[hint.to_s]
|
13
|
+
return nil if val.nil?
|
14
|
+
case val
|
15
|
+
when /\A\d+\z/ then val.to_i
|
16
|
+
when /\Atrue\z/ then true
|
17
|
+
when /\Afalse\z/ then false
|
18
|
+
when /\A(?:null|nil)\z/ then nil
|
19
|
+
else val
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Opto
|
2
|
+
module Resolvers
|
3
|
+
# Read the value from a file, path defined in hint.
|
4
|
+
class File < Opto::Resolver
|
5
|
+
|
6
|
+
def ignore_errors?
|
7
|
+
return false unless hint.kind_of?(Hash) && (hint['ignore_errors'] || hint[:ignore_errors])
|
8
|
+
end
|
9
|
+
|
10
|
+
def file_path
|
11
|
+
if hint.kind_of?(String)
|
12
|
+
hint
|
13
|
+
elsif hint.kind_of?(Hash) && (hint['path'] || hint[:path])
|
14
|
+
hint['path'] || hint[:path]
|
15
|
+
else
|
16
|
+
raise ArgumentError, "File path not set"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def resolve
|
21
|
+
if ignore_errors?
|
22
|
+
file_path
|
23
|
+
::File.read(file_path) rescue nil
|
24
|
+
else
|
25
|
+
::File.read(file_path)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'opto/extensions/snake_case'
|
2
|
+
require 'opto/extensions/hash_string_or_symbol_key'
|
3
|
+
|
4
|
+
if RUBY_VERSION < '2.1'
|
5
|
+
using Opto::Extension::SnakeCase
|
6
|
+
using Opto::Extension::HashStringOrSymbolKey
|
7
|
+
end
|
8
|
+
|
9
|
+
module Opto
|
10
|
+
module Resolvers
|
11
|
+
# Geneerate a new random number. Requires :min and :max in hint to define range.
|
12
|
+
class RandomNumber < Opto::Resolver
|
13
|
+
|
14
|
+
using Opto::Extension::HashStringOrSymbolKey unless RUBY_VERSION < '2.1'
|
15
|
+
|
16
|
+
def resolve
|
17
|
+
raise ArgumentError, "Range not set" if hint.nil?
|
18
|
+
|
19
|
+
unless hint.kind_of?(Hash)
|
20
|
+
raise TypeError, "Range invalid, define min: and max: using hash syntax"
|
21
|
+
end
|
22
|
+
|
23
|
+
unless hint[:min]
|
24
|
+
raise ArgumentError, "Range definition missing :min"
|
25
|
+
end
|
26
|
+
|
27
|
+
unless hint[:max]
|
28
|
+
raise ArgumentError, "Range definition missing :max"
|
29
|
+
end
|
30
|
+
|
31
|
+
rand(hint[:min]..hint[:max])
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'opto/extensions/snake_case'
|
2
|
+
require 'opto/extensions/hash_string_or_symbol_key'
|
3
|
+
|
4
|
+
if RUBY_VERSION < '2.1'
|
5
|
+
using Opto::Extension::SnakeCase
|
6
|
+
using Opto::Extension::HashStringOrSymbolKey
|
7
|
+
end
|
8
|
+
|
9
|
+
module Opto
|
10
|
+
module Resolvers
|
11
|
+
# Generates a random string.
|
12
|
+
#
|
13
|
+
# Requires at least :length.
|
14
|
+
# Also accepts :charset which can be one of:
|
15
|
+
# - numbers (0-9),
|
16
|
+
# - letters (a-z + A-Z),
|
17
|
+
# - downcase (a-z),
|
18
|
+
# - upcase (A-Z),
|
19
|
+
# - alphanumeric (0-9 + a-z + A-Z),
|
20
|
+
# - hex (0-9 + a-f),
|
21
|
+
# - hex_upcase (0-9 + A-F),
|
22
|
+
# - base64 (base64 charset (length has to be divisible by four when using base64)),
|
23
|
+
#- ascii_printable (all printable ascii chars)
|
24
|
+
# - or a set of characters, for example:
|
25
|
+
# { length: 8, charset: '01' } Will generate something like: 01001100
|
26
|
+
class RandomString < Opto::Resolver
|
27
|
+
|
28
|
+
using Opto::Extension::HashStringOrSymbolKey unless RUBY_VERSION < '2.1'
|
29
|
+
|
30
|
+
def charset(name)
|
31
|
+
case name.to_s
|
32
|
+
when 'numbers'
|
33
|
+
(0..9).map(&:to_s)
|
34
|
+
when /\A\d+\-\d+\z/, /\A[a-z]\-[a-z]\z/
|
35
|
+
from, to = name.split('-')
|
36
|
+
(from..to).map(&:to_s)
|
37
|
+
when 'letters'
|
38
|
+
charset('upcase') + charset('downcase')
|
39
|
+
when 'downcase'
|
40
|
+
('a'..'z').to_a
|
41
|
+
when 'upcase'
|
42
|
+
('A'..'Z').to_a
|
43
|
+
when 'alphanumeric'
|
44
|
+
charset('letters') + charset('numbers')
|
45
|
+
when 'hex'
|
46
|
+
charset('numbers') + ('a'..'f').to_a
|
47
|
+
when 'hex_upcase'
|
48
|
+
charset('numbers') + ('A'..'F').to_a
|
49
|
+
when 'base64'
|
50
|
+
charset('alphanumeric') + ['+', '/']
|
51
|
+
when 'ascii_printable'
|
52
|
+
(33..126).map {|ord| ord.chr}
|
53
|
+
else
|
54
|
+
name.to_s.split('')
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def resolve
|
59
|
+
if hint.kind_of?(Hash)
|
60
|
+
if hint[:length].nil?
|
61
|
+
raise ArgumentError, "Invalid settings for random string. Required: length, optional: charset. Charsets : numbers, letters, alphanumeric, hex, base64, ascii_printable and X-Y range."
|
62
|
+
end
|
63
|
+
elsif (hint.kind_of?(String) && hint.to_i > 0) || hint.kind_of?(Fixnum)
|
64
|
+
self.hint = { length: hint.to_i }
|
65
|
+
else
|
66
|
+
raise ArgumentError, "Missing settings for random string."
|
67
|
+
end
|
68
|
+
|
69
|
+
if hint[:charset].to_s == 'base64' && hint[:length] % 4 != 0
|
70
|
+
raise ArgumentError, "Length must be divisible by 4 when using base64"
|
71
|
+
end
|
72
|
+
|
73
|
+
chars = charset(hint[:charset] || 'alphanumeric')
|
74
|
+
(1..hint[:length].to_i).each_with_object('') do |_, str|
|
75
|
+
str << chars.sample
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
|
84
|
+
|
data/lib/opto/setter.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'opto/extensions/snake_case'
|
2
|
+
require 'opto/extensions/hash_string_or_symbol_key'
|
3
|
+
|
4
|
+
if RUBY_VERSION < '2.1'
|
5
|
+
using Opto::Extension::SnakeCase
|
6
|
+
using Opto::Extension::HashStringOrSymbolKey
|
7
|
+
end
|
8
|
+
|
9
|
+
module Opto
|
10
|
+
# Base for Setters.
|
11
|
+
#
|
12
|
+
# Resolvers are scripts that can retrieve or generate a value for an option.
|
13
|
+
# Such resolvers are for example Env, which can try to find the value for
|
14
|
+
# the option from an environment variable. An example of generators is
|
15
|
+
# RandomString, which can generate random strings of defined length.
|
16
|
+
class Setter
|
17
|
+
|
18
|
+
using Opto::Extension::SnakeCase unless RUBY_VERSION < '2.1'
|
19
|
+
|
20
|
+
attr_accessor :hint
|
21
|
+
attr_accessor :option
|
22
|
+
|
23
|
+
class << self
|
24
|
+
# Find a setter using a target_name definition, such as :env or :file
|
25
|
+
# @param [Symbol, String] target
|
26
|
+
def for(target)
|
27
|
+
raise NameError, "Unknown setter: #{target}" unless targets[target]
|
28
|
+
targets[target]
|
29
|
+
end
|
30
|
+
|
31
|
+
def inherited(where)
|
32
|
+
targets[where.target] = where
|
33
|
+
end
|
34
|
+
|
35
|
+
def targets
|
36
|
+
@targets ||= {}
|
37
|
+
end
|
38
|
+
|
39
|
+
def target
|
40
|
+
name.to_s.split('::').last.snakecase.to_sym
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Initialize an instance of a setter.
|
45
|
+
# @param hint A "hint" for the setter, for example. the environment variable name
|
46
|
+
# @param [Opto::Option] option The option parent of this resolver instance
|
47
|
+
# @return [Opto::Resolver]
|
48
|
+
def initialize(hint = nil, option = nil)
|
49
|
+
@hint = hint
|
50
|
+
@option = option
|
51
|
+
end
|
52
|
+
|
53
|
+
# This is a "base" class, you're supposed to inherit from this in your setter and define a #set method.
|
54
|
+
def set(value)
|
55
|
+
raise RuntimeError, "#{self.class}.set not defined"
|
56
|
+
end
|
57
|
+
|
58
|
+
# The target "tag" of this resolver, for example: 'file' or 'env'
|
59
|
+
def target
|
60
|
+
self.class.target
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
Dir[File.expand_path('../setters/*.rb', __FILE__)].each {|file| require file}
|
66
|
+
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'opto/extensions/snake_case'
|
2
|
+
require 'opto/extensions/hash_string_or_symbol_key'
|
3
|
+
|
4
|
+
if RUBY_VERSION < '2.1'
|
5
|
+
using Opto::Extension::SnakeCase
|
6
|
+
using Opto::Extension::HashStringOrSymbolKey
|
7
|
+
end
|
8
|
+
|
9
|
+
module Opto
|
10
|
+
module Setters
|
11
|
+
# Set a value to environment.
|
12
|
+
#
|
13
|
+
# Hint should be a name of environment variable, such as 'HOME'
|
14
|
+
#
|
15
|
+
# Everything will be converted to strings unless hint is a hash with :options. (also include :name in that case)
|
16
|
+
class Env < Opto::Setter
|
17
|
+
|
18
|
+
using Opto::Extension::HashStringOrSymbolKey unless RUBY_VERSION < '2.1'
|
19
|
+
|
20
|
+
attr_accessor :env_name, :dont_stringify
|
21
|
+
|
22
|
+
def normalize_hint
|
23
|
+
raise ArgumentError, "Environment variable name not set" if hint.nil?
|
24
|
+
if hint.kind_of?(Hash)
|
25
|
+
raise ArgumentError, "Environment variable name not set" unless hint[:name]
|
26
|
+
@env_name = hint[:name].to_s
|
27
|
+
else
|
28
|
+
@env_name = hint.to_s
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def set(value)
|
33
|
+
normalize_hint
|
34
|
+
ENV[env_name] = value.to_s
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
|
data/lib/opto/type.rb
ADDED
@@ -0,0 +1,148 @@
|
|
1
|
+
require 'opto/extensions/snake_case'
|
2
|
+
require 'opto/extensions/hash_string_or_symbol_key'
|
3
|
+
|
4
|
+
if RUBY_VERSION < '2.1'
|
5
|
+
using Opto::Extension::SnakeCase
|
6
|
+
using Opto::Extension::HashStringOrSymbolKey
|
7
|
+
end
|
8
|
+
|
9
|
+
module Opto
|
10
|
+
# Defines a type handler. Used as a base from which to inherit in the type handlers.
|
11
|
+
class Type
|
12
|
+
GLOBAL_OPTIONS = {
|
13
|
+
required: true
|
14
|
+
}
|
15
|
+
|
16
|
+
attr_accessor :options
|
17
|
+
|
18
|
+
unless RUBY_VERSION < '2.1'
|
19
|
+
using Opto::Extension::SnakeCase
|
20
|
+
using Opto::Extension::HashStringOrSymbolKey
|
21
|
+
end
|
22
|
+
|
23
|
+
class << self
|
24
|
+
def inherited(where)
|
25
|
+
types[where.type] = where
|
26
|
+
end
|
27
|
+
|
28
|
+
def types
|
29
|
+
@types ||= {}
|
30
|
+
end
|
31
|
+
|
32
|
+
def type
|
33
|
+
name.to_s.split('::').last.snakecase.to_sym
|
34
|
+
end
|
35
|
+
|
36
|
+
# Find a type handler for :type_name, for example: Opto::Type.for(:string)
|
37
|
+
# @param [String,Symbol] type_name
|
38
|
+
def for(type_name)
|
39
|
+
raise NameError, "No handler for type #{type_name}" unless types[type_name]
|
40
|
+
types[type_name]
|
41
|
+
end
|
42
|
+
|
43
|
+
def validators
|
44
|
+
@validators ||= []
|
45
|
+
end
|
46
|
+
|
47
|
+
# Define a validator:
|
48
|
+
# @example
|
49
|
+
# class Foo < Opto::Type
|
50
|
+
# validator :is_foo do |value|
|
51
|
+
# unless value == 'foo'
|
52
|
+
# "Foo is not foo."
|
53
|
+
# end
|
54
|
+
# end
|
55
|
+
# end
|
56
|
+
def validator(name, &block)
|
57
|
+
raise TypeError, "Block required" unless block_given?
|
58
|
+
define_method("validate_#{name}", &block)
|
59
|
+
validators << "validate_#{name}".to_sym
|
60
|
+
# RUBY_VERSION >= 2.1 would allow validators << define_method("validate_#{name}", block)
|
61
|
+
end
|
62
|
+
|
63
|
+
def sanitizers
|
64
|
+
@sanitizers ||= []
|
65
|
+
end
|
66
|
+
|
67
|
+
# Define a sanitizer. Can be used to for example convert strings to integers
|
68
|
+
# or to remove whitespace, etc.
|
69
|
+
#
|
70
|
+
# @example
|
71
|
+
# class Foo < Opto::Type
|
72
|
+
# sanitizer :add_suffix |value|
|
73
|
+
# value.to_s + "-1"
|
74
|
+
# end
|
75
|
+
# end
|
76
|
+
def sanitizer(name, &block)
|
77
|
+
raise TypeError, "Block required" unless block_given?
|
78
|
+
define_method("sanitize_#{name}", &block)
|
79
|
+
sanitizers << "sanitize_#{name}".to_sym
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# The default :in validator, returns an error unless the
|
84
|
+
# value is not one of listed in the :in definition of the option,
|
85
|
+
# @example
|
86
|
+
# Opto::Option.new(name: 'foo', type: 'string', in: ['dog', 'cat']) (only "dog" or "cat" allowed as value)
|
87
|
+
validator :in do |value|
|
88
|
+
return true unless options[:in]
|
89
|
+
options[:in].each do |val|
|
90
|
+
return true if value === val
|
91
|
+
end
|
92
|
+
"Value #{value} not in #{options[:in].join(', ')}"
|
93
|
+
end
|
94
|
+
|
95
|
+
def initialize(options = {})
|
96
|
+
@options = Type::GLOBAL_OPTIONS.merge(self.class.const_defined?(:OPTIONS) ? self.class.const_get(:OPTIONS) : {}).merge(options)
|
97
|
+
end
|
98
|
+
|
99
|
+
def type
|
100
|
+
self.class.type
|
101
|
+
end
|
102
|
+
|
103
|
+
def required?
|
104
|
+
!!options[:required]
|
105
|
+
end
|
106
|
+
|
107
|
+
def sanitize(value)
|
108
|
+
new_value = value
|
109
|
+
self.class.sanitizers.each do |sanitizer|
|
110
|
+
begin
|
111
|
+
new_value = self.send(sanitizer, new_value)
|
112
|
+
rescue StandardError => ex
|
113
|
+
raise ex, "Sanitizer #{sanitizer} : #{ex.message}"
|
114
|
+
end
|
115
|
+
end
|
116
|
+
new_value
|
117
|
+
end
|
118
|
+
|
119
|
+
def errors
|
120
|
+
@errors ||= {}
|
121
|
+
end
|
122
|
+
|
123
|
+
def valid?(value)
|
124
|
+
validate(value)
|
125
|
+
errors.empty?
|
126
|
+
end
|
127
|
+
|
128
|
+
def validate(value)
|
129
|
+
errors.clear
|
130
|
+
if value.nil?
|
131
|
+
errors[:presence] = "Required value missing" if required?
|
132
|
+
else
|
133
|
+
(Type.validators + self.class.validators).each do |validator|
|
134
|
+
begin
|
135
|
+
result = self.send(validator, value)
|
136
|
+
rescue StandardError => ex
|
137
|
+
raise ex, "Validator #{validator} : #{ex.message}"
|
138
|
+
end
|
139
|
+
unless result.kind_of?(NilClass) || result.kind_of?(TrueClass)
|
140
|
+
errors[validator] = result
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
Dir[File.expand_path('../types/*.rb', __FILE__)].each {|file| require file}
|