opto 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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}