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.
@@ -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,10 @@
1
+ module Opto
2
+ module Resolvers
3
+ # Resolve a value through the default value defined during option initialization
4
+ class Default < Opto::Resolver
5
+ def resolve
6
+ hint.default
7
+ end
8
+ end
9
+ end
10
+ end
@@ -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
+
@@ -0,0 +1,15 @@
1
+ require 'securerandom'
2
+
3
+ module Opto
4
+ module Resolvers
5
+ # Generates a UUID, such as "b379de07-3324-44b1-a5f3-8617ed1b41ea"
6
+ class RandomUuid < Opto::Resolver
7
+ def resolve
8
+ SecureRandom.uuid
9
+ end
10
+ end
11
+ end
12
+ end
13
+
14
+
15
+
@@ -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
+
@@ -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}