rabbitt-configurator 1.2.4

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,281 @@
1
+ =begin
2
+ Copyright (C) 2013 Carl P. Corliss
3
+
4
+ This program is free software; you can redistribute it and/or modify
5
+ it under the terms of the GNU General Public License as published by
6
+ the Free Software Foundation; either version 2 of the License, or
7
+ (at your option) any later version.
8
+
9
+ This program is distributed in the hope that it will be useful,
10
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ GNU General Public License for more details.
13
+
14
+ You should have received a copy of the GNU General Public License along
15
+ with this program; if not, write to the Free Software Foundation, Inc.,
16
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
+ =end
18
+
19
+ require 'pathname'
20
+
21
+ module Configurator
22
+ class Option
23
+ attr_accessor :name, :parent
24
+ attr_reader :type, :default, :caster, :validations, :required
25
+ private :validations, :required
26
+
27
+ UNDEFINED_OPTION = :__undefined__
28
+
29
+ def initialize(name, parent, options={})
30
+ @name = name.to_sym
31
+ @value = nil
32
+ @parent = parent
33
+ @guarding = false
34
+
35
+ @default = (options.delete(:default) || UNDEFINED_OPTION).freeze
36
+ @type = (type = options.delete(:type)).nil? ? compute_type(@default) : type
37
+ @caster = (cast = options.delete(:cast)).nil? ? Cast::Director[@type] : Cast::Director[cast]
38
+
39
+ @required = determine_if_required?(options)
40
+ @validations = gather_validations(options)
41
+
42
+ if options.count > 0
43
+ warn "#{path_name}: encountered unknown options: #{options.inspect}"
44
+ end
45
+ rescue StandardError => e
46
+ raise OptionInvalid.new("Failed to add option #{parent.path_name}.#{name}: #{e.class.name}: #{e.message}") { |ve|
47
+ ve.set_backtrace(e.backtrace)
48
+ }
49
+ end
50
+
51
+ def compute_type(type)
52
+ case type
53
+ when UNDEFINED_OPTION then :any
54
+ when OptionValue then type.type
55
+ when Bignum, Fixnum then :integer
56
+ when Float then :float
57
+ when Symbol then :symbol
58
+ when FalseClass, TrueClass, /(true|false|yes|no|enabled?|disabled?|on|off)/i then :boolean
59
+ when String then :string
60
+ when Pathname then :path
61
+ when URI then :uri
62
+ when Hash then :hash
63
+ when Array then
64
+ type.size <= 0 ? :array : [compute_type(type.first)]
65
+ when Proc then
66
+ with_loop_guard do
67
+ compute_type(type.call)
68
+ end rescue :any
69
+ else :any
70
+ end
71
+ end
72
+
73
+ def value=(v)
74
+ return nil unless validate(v)
75
+ @value = v
76
+ end
77
+
78
+ def value
79
+ return nil if @value.nil? && @default == UNDEFINED_OPTION
80
+
81
+ value = (@value || @default)
82
+
83
+ begin
84
+ with_loop_guard do
85
+ if value.respond_to? :call
86
+ unless value.arity == 0
87
+ raise OptionInvalidCallableDefault, "#{path_name}: callable defaults must not accept any arguments"
88
+ end
89
+ value = value.call
90
+ end
91
+ end
92
+ rescue OptionLoopError
93
+ raise # bubble up
94
+ rescue NoMethodError => e
95
+ method = e.message.match(/undefined method .([^']+)'.+/)[1]
96
+ raise OptionInvalidCallableDefault, "#{path_name}: bad method/option name #{method.inspect} in callable default."
97
+ rescue StandardError => e
98
+ excp = OptionInvalidCallableDefault.new "#{path_name}: error executing callable default: #{e.class.name}: #{e.message}"
99
+ excp.set_backtrace(e.backtrace)
100
+ raise excp
101
+ end
102
+
103
+ @caster.convert(value)
104
+ end
105
+
106
+ def include?(data)
107
+ value.respond_to?(:include?) ? value.include?(data) : false
108
+ end
109
+
110
+ def empty?; value.nil? || value.empty?; end
111
+ def valid?; validate(value); end
112
+ def required?; !!@required; end
113
+ def optional?; !required?; end
114
+ def path_name; [ parent.path_name, name ].join('.'); end
115
+ def deprecated?; false; end
116
+ def renamed?; false; end
117
+
118
+ private
119
+
120
+ def determine_if_required?(options)
121
+ if options.key?(:required) && options.key?(:optional)
122
+ unless !!options[:required] != !!options[:optional]
123
+ raise OptionInvalidArgument, "#{path_name}: can't be both required and optional at the same time!"
124
+ else
125
+ options.delete(:optional)
126
+ !!options.delete(:required)
127
+ end
128
+ elsif options.key?(:required)
129
+ !!options.delete(:required)
130
+ elsif options.key?(:optional)
131
+ not !!options.delete(:optional)
132
+ else
133
+ # if there's no default, require option
134
+ default == :__undefined__ ? true : false
135
+ end
136
+ end
137
+
138
+ def gather_validations(options)
139
+ # XXX: create Validation classes (Expection would derive from Validation) and
140
+ # move all validation logic into those classes
141
+ [].tap { |validations|
142
+ validation = (v = options.delete(:validate)).nil? ? true : v
143
+ validate_msg = options.delete(:validate_message)
144
+
145
+ expectations = options.delete(:expect)
146
+ expect_msg = options.delete(:expect_messgae)
147
+
148
+ type_validator = options.delete(:type_validator)
149
+ type_validator_msg = options.delete(:type_validation_message)
150
+
151
+ if !validation
152
+ if expectations
153
+ raise OptionInvalidArgument, "#{path_name}: can't disable validations and set an expectation at the same time!"
154
+ elsif type_validator
155
+ raise OptionInvalidArgument, "#{path_name}: can't disable validations and assign a type validator at the same time!"
156
+ end
157
+ end
158
+
159
+ return [] unless validation
160
+
161
+ if type_validator
162
+ validations << lambda { |_value|
163
+ unless type_validator.call(_value)
164
+ if type_validator_msg
165
+ raise ValidationError, "#{path_name}: #{_value.inspect} fails to validate as custom type: #{type_validator_msg}"
166
+ else
167
+ raise ValidationError, "#{path_name}: #{_value.inspect} fails to validate as custom type."
168
+ end
169
+ end
170
+ true
171
+ }
172
+ else
173
+ validations << lambda { |_value|
174
+ unless validate_type(_value)
175
+ raise ValidationError, "#{path_name}: #{_value.inspect} fails to validate as #{type.inspect}"
176
+ end
177
+ true
178
+ }
179
+ end
180
+
181
+ validations << lambda { |_value|
182
+ unless validation.call(_value)
183
+ if validate_msg
184
+ raise ValidationError, "#{path_name}: #{_value.inspect} fails custom validation rule: #{validate_msg}"
185
+ else
186
+ raise ValidationError, "#{path_name}: #{_value.inspect} fails custom validation rule"
187
+ end
188
+ end
189
+ true
190
+ } if validation.respond_to?(:call)
191
+
192
+ unless expectations.nil?
193
+ if expectations.respond_to? :call
194
+ validations << lambda { |_value|
195
+ unless expectations.call(_value)
196
+ if expect_msg
197
+ raise ValidationError, "#{path_name}: #{_value.inspect} fails custom expectation: #{expect_msg}"
198
+ else
199
+ raise ValidationError, "#{path_name}: #{_value.inspect} fails custom expectation"
200
+ end
201
+ end
202
+ true
203
+ }
204
+ else
205
+ validations << lambda { |_value|
206
+ unless expectations.include?(_value)
207
+ raise ValidationError, "#{path_name}: Failed expectation: #{_value.inspect} not in list: #{expectations.collect(&:inspect).join(', ')}"
208
+ end
209
+ true
210
+ }
211
+ end
212
+ end
213
+ }
214
+ end
215
+
216
+ def validate(_value)
217
+ return true if type == :any && validations.empty?
218
+
219
+ begin
220
+ # try on just the raw value first
221
+ validations.all? { |validation| validation.call(_value.freeze) }
222
+ rescue StandardError => initial_exception
223
+ begin
224
+ # now try on the converted value
225
+ cast_value = @caster.convert(_value)
226
+ validations.all? { |validation| validation.call(cast_value) }
227
+ rescue ValidationError => e
228
+ raise ValidationError.new(e.message).tap {|ve| ve.set_backtrace(initial_exception.backtrace) }
229
+ rescue CastError
230
+ raise initial_exception
231
+ end
232
+ end
233
+ end
234
+
235
+ def validate_type(_value, validation_type = nil)
236
+ validation_type ||= type
237
+
238
+ case validation_type
239
+ when :any; true
240
+ when Array then
241
+ return _value.is_a?(Array) if validation_type.empty?
242
+ [*_value].flatten.all? { |v|
243
+ validate_type(v, validation_type.first)
244
+ }
245
+ when :scalar then
246
+ validate_type(_value, :integer) || validate_type(_value, :float) ||
247
+ validate_type(_value, :symbol) || validate_type(_value, :string) ||
248
+ validate_type(_value, :boolean)
249
+ when :boolean then
250
+ _value.is_a?(FalseClass) || _value.is_a?(TrueClass)
251
+ when :float then
252
+ ((Float(_value) rescue nil) == _value.to_f)
253
+ when :integer then
254
+ ((Float(_value).to_i rescue nil) == _value.to_i)
255
+ when :path then
256
+ _value.is_a?(Pathname)
257
+ when :array then _value.is_a?(Array)
258
+ when :hash then _value.is_a?(Hash)
259
+ when :string then _value.is_a? String
260
+ when :symbol then _value.is_a? Symbol
261
+ when :uri then !!(URI.parse(_value) rescue false)
262
+ else
263
+ warn "unable to validate - no handler for type: #{type.inspect}"
264
+ true # assume valid
265
+ end
266
+ end
267
+
268
+ def with_loop_guard(&block)
269
+ begin
270
+ raise OptionLoopError if @guarding
271
+ @guarding = true
272
+ yield
273
+ rescue OptionLoopError => error
274
+ raise error.tap { |e| e.stack << path_name }
275
+ ensure
276
+ @guarding = false
277
+ end
278
+ end
279
+
280
+ end
281
+ end
@@ -0,0 +1,254 @@
1
+ =begin
2
+ Copyright (C) 2013 Carl P. Corliss
3
+
4
+ This program is free software; you can redistribute it and/or modify
5
+ it under the terms of the GNU General Public License as published by
6
+ the Free Software Foundation; either version 2 of the License, or
7
+ (at your option) any later version.
8
+
9
+ This program is distributed in the hope that it will be useful,
10
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ GNU General Public License for more details.
13
+
14
+ You should have received a copy of the GNU General Public License along
15
+ with this program; if not, write to the Free Software Foundation, Inc.,
16
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
+ =end
18
+
19
+ module Configurator
20
+ class Section
21
+ attr_accessor :name, :parent
22
+ attr_reader :table
23
+
24
+ def initialize(name, parent = nil, options = {})
25
+ @table = {}
26
+ @name = name
27
+ @parent = parent
28
+
29
+ load options
30
+ end
31
+
32
+ def type; :section; end
33
+ def deprecated?; false; end
34
+ def renamed?; false; end
35
+
36
+ def root; parent.nil? ? self : parent.root; end
37
+ def path_name; parent.nil? ? name : [ parent.path_name, name ].join('.'); end
38
+ def required?; options.any? { |k,o| o.required? }; end
39
+ def optional?; !required?; end
40
+
41
+ def include?(option_name)
42
+ @table.key? option_name.to_sym
43
+ end
44
+
45
+ def [](option_name)
46
+ @table[option_name.to_sym]
47
+ end
48
+
49
+ def []=(option_name, value)
50
+ @table[option_name.to_sym].value = value
51
+ end
52
+
53
+ def each(&block)
54
+ @table.each &block
55
+ end
56
+
57
+ def inject(*args, &block)
58
+ @table.inject(*args, &block)
59
+ end
60
+
61
+ def to_h
62
+ inject({}) { |hash,(_name,_option)|
63
+ hash.tap {|h|
64
+ unless _option.deprecated? || _option.renamed?
65
+ h[_name] = _option.to_h rescue _option.value
66
+ end
67
+ }
68
+ }
69
+ end
70
+
71
+ def config() self; end
72
+ alias :value :config
73
+
74
+ def load(data)
75
+ unless data.is_a? Hash
76
+ warn "#{path_name}: invalid load data for section (#{data.inspect}) - skipping..."
77
+ else
78
+ data.each { |key,value|
79
+ if not @table.key? key.to_sym
80
+ warn "#{path_name}: unable to load data for unknown key #{key.inspect} -> #{value.inspect}"
81
+ next
82
+ end
83
+ @table[key.to_sym].value = value
84
+ }
85
+ end
86
+ end
87
+ alias :value= :load
88
+
89
+ def requirements_fullfilled?
90
+ @table.collect { |k,v|
91
+ if v.respond_to? :requirements_fullfilled?
92
+ v.requirements_fullfilled?
93
+ else
94
+ next true unless v.required? && v.value.nil? && !(v.deprecated? rescue false)
95
+ warn "#{v.path_name}: option required but nil value."
96
+ false
97
+ end
98
+ }
99
+ end
100
+
101
+ def option(option_name, *args)
102
+ _options = args.last.is_a?(Hash) ? args.pop : {}
103
+ _options.merge!(:type => args.first)
104
+
105
+ option_name = option_name.to_sym
106
+ deprecated = _options.delete(:deprecated)
107
+
108
+ option = Option.new(option_name, self, _options)
109
+
110
+ if deprecated
111
+ option = DelegatedOption::Deprecated.new(
112
+ option.name, option.parent, option, end_of_life
113
+ )
114
+ end
115
+
116
+ add_option option_name, option
117
+ end
118
+
119
+ def options(*names)
120
+ names.each do |option_name|
121
+ option option_name
122
+ end
123
+ end
124
+
125
+ def section(option_name, options = {}, &block)
126
+ option_name = option_name.to_sym
127
+ deprecated = options.delete(:deprecated)
128
+
129
+ section = Section.new(option_name, self).tap { |s|
130
+ s.instance_eval(&block) if block_given?
131
+ }
132
+
133
+ if deprecated
134
+ section = DelegatedOption::Deprecated.new(
135
+ section.name, section.parent, section, end_of_life
136
+ )
137
+ end
138
+
139
+ add_option(option_name, section)
140
+ end
141
+
142
+ def alias!(orig_path, new_path)
143
+ orig_path = "root.#{orig_path}" unless orig_path.include? 'root.'
144
+ new_path = "root.#{new_path}" unless new_path.include? 'root.'
145
+
146
+ unless _option = root.get_path(orig_path)
147
+ raise DeprecateFailed, "Unable to alias #{new_path} to #{orig_path} - option does not appear to be defined."
148
+ end
149
+
150
+ _option = root.get_path(orig_path)
151
+ _parent, _name = new_path.option_path_split
152
+
153
+ _parent = root.get_path(_parent)
154
+ new_option = AliasedOption.new(_name, _parent, _option)
155
+ _parent.add_option(_name, new_option)
156
+ end
157
+ alias :aliased! :alias!
158
+
159
+ def deprecate!(option_paths, end_of_life = nil)
160
+ [*option_paths].collect {|option_path|
161
+ option_path = "root.#{option_path}" unless option_path.include? 'root.'
162
+
163
+ unless _option = root.get_path(option_path)
164
+ raise DeprecateFailed, "Unable to deprecated #{option_path} - option does not appear to be defined."
165
+ end
166
+
167
+ _option = DeprecatedOption.new(_option.name, _option.parent, _option, end_of_life)
168
+ _option.parent.replace_option(_option.name, _option)
169
+ }
170
+ end
171
+ alias :deprecated! :deprecate!
172
+
173
+ # like alias but with reversed arguments and a warning on assignment
174
+ # note: new path must already exist. old_path is created as an alias
175
+ # to new_path.
176
+ def rename!(old_path, target_path)
177
+ old_path = "root.#{old_path}" unless old_path.include? 'root.'
178
+ target_path = "root.#{target_path}" unless target_path.include? 'root.'
179
+
180
+ unless _option = root.get_path(target_path)
181
+ raise OptionNotExist, "option #{target_path} does not exist - target path must exist for rename."
182
+ end
183
+
184
+ _parent, _name = old_path.option_path_split
185
+ _section = root.get_path(_parent)
186
+
187
+ renamed_option = RenamedOption.new(_name, _section, _option)
188
+
189
+ begin
190
+ _section.add_option(_name, renamed_option)
191
+ rescue OptionExists => e
192
+ raise RenameFailed, "Unable to rename #{old_path} -> #{target_path}"
193
+ end
194
+ end
195
+ alias :renamed! :rename!
196
+ alias :moved! :rename!
197
+
198
+ def add_option(option_name, object)
199
+ option_name = option_name.to_sym
200
+ if @table.key? option_name
201
+ raise OptionExists, "Option #{path_name}.#{option_name} already exists"
202
+ end
203
+
204
+ @table[option_name] = object.tap {
205
+ self.class.class_eval(<<-EOF, __FILE__, __LINE__ + 1)
206
+ def #{option_name}()
207
+ OptionValue.new(@table[#{option_name.inspect}])
208
+ end
209
+
210
+ def #{option_name}=(_value)
211
+ @table[#{option_name.inspect}].value = _value
212
+ end
213
+ EOF
214
+ }
215
+ end
216
+
217
+ def replace_option(name, new_option)
218
+ name = name.to_sym
219
+ unless @table.include? name
220
+ raise OptionNotExist, "#{path_name}.#{name} doesn't exist"
221
+ end
222
+ @table[name] = new_option
223
+ end
224
+
225
+ def get_path(path)
226
+ begin
227
+ # remove the root - we start there anyway
228
+ path.gsub!(/^root\.?/, '')
229
+ current_path = [:root]
230
+
231
+ path.split('.').collect(&:to_sym).inject(root) do |option, path_component|
232
+ current_path << path_component
233
+ unless option.include? path_component
234
+ raise InvalidOptionPath, "#{current_path.join('.')}: doesn't exist in the current configuration."
235
+ else
236
+ option = option[path_component]
237
+ end
238
+ end
239
+ rescue StandardError => e
240
+ warn "#{e.class.name}: #{e.message}\n\t#{e.backtrace.join("\n\t")}"
241
+ end
242
+ end
243
+
244
+ alias :original_respond_to? :respond_to?
245
+ def respond_to?(method, include_private = false)
246
+ @table.key?(method) || original_respond_to?(method, include_private)
247
+ end
248
+
249
+ def method_missing(method, *args, &block)
250
+ return super unless respond_to?(method)
251
+ self[method] if include?(method)
252
+ end
253
+ end
254
+ end
@@ -0,0 +1,18 @@
1
+ =begin
2
+ Copyright (C) 2013 Carl P. Corliss
3
+
4
+ This program is free software; you can redistribute it and/or modify
5
+ it under the terms of the GNU General Public License as published by
6
+ the Free Software Foundation; either version 2 of the License, or
7
+ (at your option) any later version.
8
+
9
+ This program is distributed in the hope that it will be useful,
10
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ GNU General Public License for more details.
13
+
14
+ You should have received a copy of the GNU General Public License along
15
+ with this program; if not, write to the Free Software Foundation, Inc.,
16
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
+ =end
18
+
@@ -0,0 +1,21 @@
1
+ =begin
2
+ Copyright (C) 2013 Carl P. Corliss
3
+
4
+ This program is free software; you can redistribute it and/or modify
5
+ it under the terms of the GNU General Public License as published by
6
+ the Free Software Foundation; either version 2 of the License, or
7
+ (at your option) any later version.
8
+
9
+ This program is distributed in the hope that it will be useful,
10
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ GNU General Public License for more details.
13
+
14
+ You should have received a copy of the GNU General Public License along
15
+ with this program; if not, write to the Free Software Foundation, Inc.,
16
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
+ =end
18
+
19
+ module Configurator
20
+ VERSION = "1.2.4"
21
+ end
@@ -0,0 +1,30 @@
1
+ =begin
2
+ Copyright (C) 2013 Carl P. Corliss
3
+
4
+ This program is free software; you can redistribute it and/or modify
5
+ it under the terms of the GNU General Public License as published by
6
+ the Free Software Foundation; either version 2 of the License, or
7
+ (at your option) any later version.
8
+
9
+ This program is distributed in the hope that it will be useful,
10
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ GNU General Public License for more details.
13
+
14
+ You should have received a copy of the GNU General Public License along
15
+ with this program; if not, write to the Free Software Foundation, Inc.,
16
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
+ =end
18
+
19
+ require 'configurator/errors'
20
+ require 'configurator/dsl'
21
+ require 'configurator/extensions'
22
+ require 'configurator/delegated'
23
+
24
+ module Configurator
25
+ autoload :Loader, 'configurator/loader'
26
+ autoload :Section, 'configurator/section'
27
+ autoload :Option, 'configurator/option'
28
+ autoload :Cast, 'configurator/cast'
29
+ autoload :VERSION, 'configurator/version'
30
+ end