configurable 0.5.0 → 0.6.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,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