rabbitt-configurator 1.2.4

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