configurable 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,97 @@
1
+ module Configurable
2
+
3
+ # Configs are used by ConfigHash to determine how to delegate read/write
4
+ # operations to a receiver. Configs also track metadata related to their
5
+ # presentation in various contexts.
6
+ class Config
7
+ class << self
8
+
9
+ # Determines if the value is duplicable. Non-duplicable values
10
+ # include nil, true, false, Symbol, Numeric, Method, Module, and
11
+ # any object that does not respond to dup.
12
+ def duplicable_value?(value)
13
+ case value
14
+ when nil, true, false, Symbol, Numeric, Method, Module then false
15
+ else value.respond_to?(:dup)
16
+ end
17
+ end
18
+ end
19
+
20
+ # The reader method called on a receiver during get
21
+ attr_reader :reader
22
+
23
+ # The writer method called on a receiver during set
24
+ attr_reader :writer
25
+
26
+ # An hash of metadata for self, often used to indicate how a config is
27
+ # presented in different contexts (ex on the command line, in a web form,
28
+ # or a desktop app).
29
+ attr_reader :attributes
30
+
31
+ # Initializes a new Config.
32
+ def initialize(reader, writer="#{reader}=", default=nil, attributes={}, init=true, dup=nil)
33
+ self.reader = reader
34
+ self.writer = writer
35
+ @default = default
36
+ @attributes = attributes
37
+ @init = init
38
+ @dup = dup.nil? ? Config.duplicable_value?(default) : dup
39
+ end
40
+
41
+ # Returns the default value. If duplicate is specified and the default
42
+ # may be duplicated (see Config.duplicable_value?) then a duplicate
43
+ # of the default is returned.
44
+ def default(duplicate=true)
45
+ duplicate && @dup ? @default.dup : @default
46
+ end
47
+
48
+ # Returns the value for the specified attribute, or default if the
49
+ # attribute is unspecified.
50
+ def [](key, default=nil)
51
+ attributes.has_key?(key) ? attributes[key] : default
52
+ end
53
+
54
+ # Calls reader on the receiver and returns the result.
55
+ def get(receiver)
56
+ receiver.send(reader)
57
+ end
58
+
59
+ # Calls writer on the receiver with the value.
60
+ def set(receiver, value)
61
+ receiver.send(writer, value)
62
+ end
63
+
64
+ # Sets the default value on the receiver. Normally this method is only
65
+ # called during Configurable#initialize_config, and only then when init?
66
+ # returns true.
67
+ def init(receiver)
68
+ receiver.send(writer, default)
69
+ end
70
+
71
+ # Returns true or false as specified in new. True indicates that this
72
+ # delegate is allowed to initialize values on the receiver during
73
+ # Configurable#initialize_config.
74
+ def init?
75
+ @init
76
+ end
77
+
78
+ # Returns an inspection string.
79
+ def inspect
80
+ "#<#{self.class}:#{object_id} reader=#{reader} writer=#{writer} default=#{default.inspect} >"
81
+ end
82
+
83
+ protected
84
+
85
+ # Sets the reader for self, assuring the reader is not nil.
86
+ def reader=(value) # :nodoc:
87
+ raise ArgumentError, "reader may not be nil" if value.nil?
88
+ @reader = value.to_sym
89
+ end
90
+
91
+ # Sets the writer for self, assuring the writer is not nil.
92
+ def writer=(value) # :nodoc:
93
+ raise ArgumentError, "writer may not be nil" if value.nil?
94
+ @writer = value.to_sym
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,198 @@
1
+ require 'configurable/nest_config'
2
+
3
+ module Configurable
4
+
5
+ # ConfigHash acts like a hash that maps get and set operations as specified
6
+ # in a Configurable's class configurations.
7
+ #
8
+ # class Sample
9
+ # include Configurable
10
+ # config :key
11
+ # end
12
+ #
13
+ # sample = Sample.new
14
+ # sample.config.class # => ConfigHash
15
+ #
16
+ # sample.key = 'value'
17
+ # sample.config[:key] # => 'value'
18
+ #
19
+ # sample.config[:key] = 'another'
20
+ # sample.key # => 'another'
21
+ #
22
+ # Non-configuration keys are sent to an underlying data store:
23
+ #
24
+ # sample.config[:not_delegated] = 'value'
25
+ # sample.config[:not_delegated] # => 'value'
26
+ #
27
+ # sample.config.store # => {:not_delegated => 'value'}
28
+ # sample.config.to_hash # => {:key => 'another', :not_delegated => 'value'}
29
+ #
30
+ # ==== IndifferentAccess
31
+ #
32
+ # A ConfigHash uses the receiver class configurations to determine when and
33
+ # how to map get/set operations. In cases where multiple keys need to map
34
+ # in the same way (for example when you want indifferent access for strings
35
+ # and symbols), simply extend the class configurations so that the AGET ([])
36
+ # method returns the correct Config in all cases.
37
+ #
38
+ # ==== Inconsistency
39
+ #
40
+ # ConfigHashes can fall into an inconsistent state if you manually add values
41
+ # to store that would normally be mapped to the receiver. This is both easy
42
+ # to avoid and easy to repair.
43
+ #
44
+ # To avoid inconsistency, don't manually add values to the store and set
45
+ # import_store to true during initialization. To repair inconsistency,
46
+ # import the current store to self.
47
+ #
48
+ # config_hash = Sample.new.config
49
+ # config_hash[:key] = 'a'
50
+ # config_hash.store[:key] = 'b'
51
+ #
52
+ # config_hash[:key] # => 'a'
53
+ # config_hash.to_hash # => {:key => 'b'}
54
+ # config_hash.inconsistent? # => true
55
+ #
56
+ # config_hash.import(config_hash.store)
57
+ #
58
+ # config_hash[:key] # => 'b'
59
+ # config_hash.to_hash # => {:key => 'b'}
60
+ # config_hash.inconsistent? # => false
61
+ #
62
+ class ConfigHash
63
+
64
+ # The bound receiver
65
+ attr_reader :receiver
66
+
67
+ # The underlying data store; setting values in store directly
68
+ # can result in an inconsistent state. Use []= instead.
69
+ attr_reader :store
70
+
71
+ # Initializes a new ConfigHash. Initialize normally imports values from
72
+ # store to ensure it doesn't contain entries that could be stored on the
73
+ # receiver.
74
+ #
75
+ # Setting import_store to false allows quick initialization but can result
76
+ # in an inconsistent state.
77
+ def initialize(receiver, store={}, import_store=true)
78
+ @receiver = receiver
79
+ @store = store
80
+
81
+ import(store) if import_store
82
+ end
83
+
84
+ # Returns receiver.class.configurations.
85
+ def configs
86
+ receiver.class.configurations
87
+ end
88
+
89
+ # Imports stored values that can be mapped to the receiver. The values
90
+ # are removed from store in the process. Returns self.
91
+ #
92
+ # Primarily used to create a consistent state for self (see above).
93
+ def import(store)
94
+ configs = self.configs # cache as an optimization
95
+ store.keys.each do |key|
96
+ next unless config = configs[key]
97
+ config.set(receiver, store.delete(key))
98
+ end
99
+
100
+ self
101
+ end
102
+
103
+ # Returns true if the store has entries that can be stored on the
104
+ # receiver.
105
+ def inconsistent?
106
+ configs = self.configs # cache as an optimization
107
+ store.keys.any? {|key| configs[key] }
108
+ end
109
+
110
+ # Retrieves the value for the key, either from the receiver or the store.
111
+ def [](key)
112
+ if config = configs[key]
113
+ config.get(receiver)
114
+ else
115
+ store[key]
116
+ end
117
+ end
118
+
119
+ # Stores a value for the key, either on the receiver or in the store.
120
+ def []=(key, value)
121
+ if config = configs[key]
122
+ config.set(receiver, value)
123
+ else
124
+ store[key] = value
125
+ end
126
+ end
127
+
128
+ # Returns the union of configs and store keys.
129
+ def keys
130
+ configs.keys | store.keys
131
+ end
132
+
133
+ # True if the key is a key in configs or store.
134
+ def has_key?(key)
135
+ configs[key] != nil || store.has_key?(key)
136
+ end
137
+
138
+ # Merges another with self.
139
+ def merge!(another)
140
+ # cache configs and inline set as a significant optimization
141
+ configs = self.configs
142
+ (configs.keys | another.keys).each do |key|
143
+ next unless another.has_key?(key)
144
+
145
+ value = another[key]
146
+ if config = configs[key]
147
+ config.set(receiver, value)
148
+ else
149
+ store[key] = value
150
+ end
151
+ end
152
+ end
153
+
154
+ # Calls block once for each key-value pair stored in self.
155
+ def each_pair # :yields: key, value
156
+ configs.each_pair do |key, config|
157
+ yield(key, config.get(receiver))
158
+ end
159
+
160
+ store.each_pair do |key, value|
161
+ yield(key, value)
162
+ end
163
+ end
164
+
165
+ # Equal if the to_hash values of self and another are equal.
166
+ def ==(another)
167
+ another.respond_to?(:to_hash) && to_hash == another.to_hash
168
+ end
169
+
170
+ # Returns self as a hash. Any ConfigHash values are recursively
171
+ # hashified, to account for nesting.
172
+ def to_hash(scrub=false, &block)
173
+ hash = {}
174
+ each_pair do |key, value|
175
+ if value.kind_of?(ConfigHash)
176
+ value = value.to_hash(scrub, &block)
177
+ end
178
+
179
+ if scrub
180
+ config = configs[key]
181
+ next if config && config.default == value
182
+ end
183
+
184
+ if block_given?
185
+ yield(hash, key, value)
186
+ else
187
+ hash[key] = value
188
+ end
189
+ end
190
+ hash
191
+ end
192
+
193
+ # Returns an inspection string.
194
+ def inspect
195
+ "#<#{self.class}:#{object_id} to_hash=#{to_hash.inspect}>"
196
+ end
197
+ end
198
+ end
@@ -7,7 +7,7 @@ module Configurable
7
7
  # Note that the indifference produced by this module is very thin indeed.
8
8
  # Strings may still be used as keys through store/fetch, and
9
9
  # existing string keys are not changed in any way. Nonetheless,
10
- # these methods are sufficient for Configurable and DelegateHash.
10
+ # these methods are sufficient for Configurable and ConfigHash.
11
11
  module IndifferentAccess
12
12
 
13
13
  # Symbolizes string keys and calls super.
@@ -1,27 +1,17 @@
1
+ require 'lazydoc'
1
2
  require 'configurable/class_methods'
2
3
 
3
4
  module Configurable
4
5
  module ModuleMethods
6
+ module_function
5
7
 
6
8
  # Extends including classes with Configurable::ClassMethods
7
- def included(mod)
8
- mod.extend ClassMethods
9
- mod.extend ModuleMethods unless mod.kind_of?(Class)
10
-
11
- unless mod.instance_variable_defined?(:@source_file)
12
- caller[1] =~ Lazydoc::CALLER_REGEXP
13
- mod.instance_variable_set(:@source_file, File.expand_path($1))
14
- end
15
-
16
- unless mod.instance_variable_defined?(:@configurations)
17
- mod.send(:initialize_configurations).extend(IndifferentAccess)
18
- end
19
-
20
- # add module configurations
21
- configurations.each_pair do |key, config|
22
- mod.configurations[key] = config.dup
23
- end unless self == Configurable
9
+ def included(base)
10
+ base.extend ClassMethods
11
+ base.extend Lazydoc::Attributes
12
+ base.extend ModuleMethods unless base.kind_of?(Class)
24
13
 
14
+ ClassMethods.initialize(base)
25
15
  super
26
16
  end
27
17
  end
@@ -0,0 +1,78 @@
1
+ require 'configurable/config'
2
+
3
+ module Configurable
4
+
5
+ # NestConfigs are used to nest configurable classes.
6
+ class NestConfig < Config
7
+
8
+ # The nested configurable class
9
+ attr_reader :nest_class
10
+
11
+ # Initializes a new NestConfig
12
+ def initialize(nest_class, reader, writer="#{reader}=", attributes={}, init=true)
13
+ self.nest_class = nest_class
14
+ self.reader = reader
15
+ self.writer = writer
16
+ @attributes = attributes
17
+ @init = init
18
+ end
19
+
20
+ # Returns a hash of the default configuration values for nest_class.
21
+ def default
22
+ default = {}
23
+ nest_class.configurations.each_pair do |key, delegate|
24
+ default[key] = delegate.default
25
+ end
26
+ default
27
+ end
28
+
29
+ # Calls the reader on the reciever to retreive an instance of the
30
+ # nest_class and returns it's config. Returns nil if the reader
31
+ # returns nil.
32
+ def get(receiver)
33
+ if instance = receiver.send(reader)
34
+ instance.config
35
+ else
36
+ nil
37
+ end
38
+ end
39
+
40
+ # Calls the reader on the reciever to retrieve an instance of the
41
+ # nest_class, and reconfigures it with value. The instance will
42
+ # be initialized by init if necessary.
43
+ #
44
+ # If value is an instance of the nest_class, then it will be set
45
+ # by calling writer.
46
+ def set(receiver, value)
47
+ if value.kind_of?(nest_class)
48
+ receiver.send(writer, value)
49
+ else
50
+ configurable = receiver.send(reader) || init(receiver)
51
+ configurable.reconfigure(value)
52
+ end
53
+ end
54
+
55
+ # Initializes an instance of nest_class and sets it on the receiver. The
56
+ # instance is initialized by calling nest_class.new with no arguments.
57
+ def init(receiver)
58
+ receiver.send(writer, nest_class.new)
59
+ end
60
+
61
+ # Returns an inspection string.
62
+ def inspect
63
+ "#<#{self.class}:#{object_id} reader=#{reader} writer=#{writer} nest_class=#{nest_class.inspect} >"
64
+ end
65
+
66
+ protected
67
+
68
+ # sets nest_class, checking that the nested class
69
+ # is both a Class and Configurable
70
+ def nest_class=(nest_class) # :nodoc:
71
+ unless nest_class.kind_of?(Class) && nest_class.ancestors.include?(Configurable)
72
+ raise ArgumentError, "not a Configurable class: #{nest_class}"
73
+ end
74
+
75
+ @nest_class = nest_class
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,85 @@
1
+ module Configurable
2
+
3
+ # Beginning with ruby 1.9, Hash tracks the order of insertion and methods
4
+ # like each_pair return pairs in order. Configurable leverages this feature
5
+ # to keep configurations in order for the command line documentation produced
6
+ # by ConfigParser.
7
+ #
8
+ # Pre-1.9 ruby implementations require a patched Hash that tracks insertion
9
+ # order. This very thin subclass of hash does that for ASET insertions and
10
+ # each_pair. OrderedHashPatches are used as the configurations object in
11
+ # Configurable classes for pre-1.9 ruby implementations and for nothing else.
12
+ #
13
+ # OrderedHashPatch is only loaded for pre-1.9 ruby implementations.
14
+ class OrderedHashPatch < Hash
15
+ def initialize
16
+ super
17
+ @insertion_order = []
18
+ end
19
+
20
+ # ASET insertion, tracking insertion order.
21
+ def []=(key, value)
22
+ @insertion_order << key unless @insertion_order.include?(key)
23
+ super
24
+ end
25
+
26
+ # Keys, sorted into insertion order
27
+ def keys
28
+ super.sort_by do |key|
29
+ @insertion_order.index(key) || length
30
+ end
31
+ end
32
+
33
+ # Yields each key-value pair to the block in insertion order.
34
+ def each_pair
35
+ keys.each do |key|
36
+ yield(key, fetch(key))
37
+ end
38
+ end
39
+
40
+ # Merges another into self in a way that preserves insertion order.
41
+ def merge!(another)
42
+ another.each_pair do |key, value|
43
+ self[key] = value
44
+ end
45
+ end
46
+
47
+ # Ensures the insertion order of duplicates is separate from parents.
48
+ def initialize_copy(orig)
49
+ super
50
+ @insertion_order = orig.instance_variable_get(:@insertion_order).dup
51
+ end
52
+
53
+ # Overridden to load an array of [key, value] pairs in order (see to_yaml).
54
+ # The default behavior for loading from a hash of key-value pairs is
55
+ # preserved, but the insertion order will not be preserved.
56
+ def yaml_initialize( tag, val )
57
+ @insertion_order ||= []
58
+
59
+ if Array === val
60
+ val.each do |k, v|
61
+ self[k] = v
62
+ end
63
+ else
64
+ super
65
+ end
66
+ end
67
+
68
+ # Overridden to preserve insertion order by serializing self as an array
69
+ # of [key, value] pairs.
70
+ def to_yaml( opts = {} )
71
+ YAML::quick_emit( object_id, opts ) do |out|
72
+ out.seq( taguri, to_yaml_style ) do |seq|
73
+ each_pair do |key, value|
74
+ seq.add( [key, value] )
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+
81
+ module ClassMethods
82
+ remove_const(:CONFIGURATIONS_CLASS)
83
+ CONFIGURATIONS_CLASS = OrderedHashPatch
84
+ end
85
+ end