configurable 0.1.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -27,12 +27,19 @@ module Configurable
27
27
  # dhash.store # => {:not_delegated => 'value'}
28
28
  # dhash.to_hash # => {:key => 'another', :not_delegated => 'value'}
29
29
  #
30
+ # ==== IndifferentAccess
31
+ #
32
+ # The delegates hash maps keys to Delegate objects. In cases where multiple
33
+ # keys need to map to the same delegate (for example when you want indifferent
34
+ # access for strings and symbols), simply extend the delegate hash so that the
35
+ # AGET ([]) method returns the correct delegate in all cases.
36
+ #
30
37
  class DelegateHash
31
38
 
32
39
  # The bound receiver
33
40
  attr_reader :receiver
34
41
 
35
- # The underlying data store for non-delegate keys
42
+ # The underlying data store
36
43
  attr_reader :store
37
44
 
38
45
  # A hash of (key, Delegate) pairs identifying which
@@ -40,31 +47,32 @@ module Configurable
40
47
  attr_reader :delegates
41
48
 
42
49
  # Initializes a new DelegateHash. Note that initialize simply sets the
43
- # receiver, it does NOT map stored values the same way bind does.
44
- # This allows quick, implicit binding where the bound store is set
45
- # up beforehand.
50
+ # receiver, it does NOT map stored values the same way bind does. This
51
+ # allows quick, implicit binding when the store is set up beforehand.
46
52
  #
47
53
  # For more standard binding use: DelegateHash.new.bind(receiver)
48
54
  def initialize(delegates={}, store={}, receiver=nil)
49
- @receiver = nil
50
55
  @store = store
51
56
  @delegates = delegates
52
57
  @receiver = receiver
53
58
  end
54
59
 
55
- # Binds self to the specified receiver. Mapped keys are
56
- # removed from store and sent to their writer method on
57
- # receiver.
60
+ # Binds self to the specified receiver. Delegate values are removed from
61
+ # store and sent to their writer method on receiver. If the store has no
62
+ # value for a delegate key, the delegate default value will be used.
58
63
  def bind(receiver)
59
64
  raise ArgumentError, "receiver cannot be nil" if receiver == nil
60
- raise ArgumentError, "already bound to: #{@receiver}" if bound? && @receiver != receiver
61
-
62
- store.keys.each do |key|
63
- next unless delegate = delegates[key]
64
- receiver.send(delegate.writer, store.delete(key)) if delegate.writer
65
+
66
+ if bound?
67
+ if @receiver == receiver
68
+ return(self)
69
+ else
70
+ raise ArgumentError, "already bound to: #{@receiver}"
71
+ end
65
72
  end
73
+
66
74
  @receiver = receiver
67
-
75
+ map(store)
68
76
  self
69
77
  end
70
78
 
@@ -73,74 +81,70 @@ module Configurable
73
81
  receiver != nil
74
82
  end
75
83
 
76
- # Unbinds self from the specified receiver. Mapped values
84
+ # Unbinds self from the specified receiver. Delegate values
77
85
  # are stored in store. Returns the unbound receiver.
78
86
  def unbind
79
- delegates.each_pair do |key, delegate|
80
- store[key] = receiver.send(delegate.reader) if delegate.reader
81
- end
82
- current_receiver = receiver
87
+ unmap(store)
83
88
  @receiver = nil
84
-
85
- current_receiver
89
+ self
86
90
  end
87
91
 
88
- # Retrieves the value corresponding to the key. If bound?
89
- # and the key is a delegates key, then the value is
90
- # obtained from the delegate.reader method on the receiver.
92
+ # Retrieves the value corresponding to the key. When bound, delegates with
93
+ # readers pull values from the receiver; otherwise the value in store will
94
+ # be returned. When unbound, if the store has no value for a delegate, the
95
+ # delgate default value will be returned.
91
96
  def [](key)
92
- case
93
- when bound? && delegate = delegates[key]
94
- delegate.reader ? receiver.send(delegate.reader) : store[key]
95
- else store[key]
97
+ return store[key] unless delegate = delegates[key]
98
+
99
+ case
100
+ when bound? && delegate.reader
101
+ receiver.send(delegate.reader)
102
+ when store.has_key?(key)
103
+ store[key]
104
+ else
105
+ store[key] = delegate.default
96
106
  end
97
107
  end
98
108
 
99
- # Associates the value the key. If bound? and the key
100
- # is a delegates key, then the value will be forwarded
101
- # to the delegate.writer method on the receiver.
109
+ # Stores a value for the key. When bound, delegates with writers send the
110
+ # value to the receiver; otherwise values are stored in store.
102
111
  def []=(key, value)
103
- case
104
- when bound? && delegate = delegates[key]
105
- delegate.writer ? receiver.send(delegate.writer, value) : store[key] = value
106
- else store[key] = value
112
+ if bound? && delegate = delegates[key]
113
+ if delegate.writer
114
+ receiver.send(delegate.writer, value)
115
+ return
116
+ end
107
117
  end
118
+
119
+ store[key] = value
108
120
  end
109
-
110
- # True if the key is assigned in self.
111
- def has_key?(key)
112
- (bound? && delegates.has_key?(key)) || store.has_key?(key)
121
+
122
+ # Returns the union of delegate and store keys.
123
+ def keys
124
+ delegates.keys | store.keys
113
125
  end
114
126
 
115
- # Calls block once for each key-value pair stored in self.
116
- def each_pair # :yields: key, value
117
- delegates.each_pair do |key, delegate|
118
- yield(key, receiver.send(delegate.reader)) if delegate.reader
119
- end if bound?
120
-
121
- store.each_pair do |key, value|
122
- yield(key, value)
123
- end
127
+ # True if the key is an assigned delegate or store key.
128
+ def has_key?(key)
129
+ delegates.has_key?(key) || store.has_key?(key)
124
130
  end
125
-
126
- # Updates self to ensure that each delegates key
127
- # has a value in self; the delegate.default value is
128
- # set if a value does not already exist.
129
- #
130
- # Returns self.
131
- def update
132
- delegates.each_pair do |key, delegate|
133
- self[key] ||= delegate.default
131
+
132
+ # Merges another with self.
133
+ def merge!(another)
134
+ if bound?
135
+ (delegates.keys | another.keys).each do |key|
136
+ self[key] = another[key] if another.has_key?(key)
137
+ end
138
+ else
139
+ # optimization for the common case of an
140
+ # unbound merge of another hash
141
+ store.merge!(another.to_hash)
134
142
  end
135
- self
136
143
  end
137
144
 
138
- # Duplicates self, returning an unbound DelegateHash.
139
- def dup
140
- duplicate = super()
141
- duplicate.instance_variable_set(:@receiver, nil)
142
- duplicate.instance_variable_set(:@store, @store.dup)
143
- duplicate
145
+ # Calls block once for each key-value pair stored in self.
146
+ def each_pair # :yields: key, value
147
+ keys.each {|key| yield(key, self[key]) }
144
148
  end
145
149
 
146
150
  # Equal if the to_hash values of self and another are equal.
@@ -148,12 +152,13 @@ module Configurable
148
152
  another.respond_to?(:to_hash) && to_hash == another.to_hash
149
153
  end
150
154
 
151
- # Returns self as a hash.
155
+ # Returns self as a hash. Any DelegateHash values are recursively
156
+ # hashified, to account for nesting.
152
157
  def to_hash
153
- hash = store.dup
154
- delegates.keys.each do |key|
155
- hash[key] = self[key]
156
- end if bound?
158
+ hash = {}
159
+ each_pair do |key, value|
160
+ hash[key] = value.kind_of?(DelegateHash) ? value.to_hash : value
161
+ end
157
162
  hash
158
163
  end
159
164
 
@@ -161,5 +166,43 @@ module Configurable
161
166
  def inspect
162
167
  "#<#{self.class}:#{object_id} to_hash=#{to_hash.inspect}>"
163
168
  end
169
+
170
+ # Ensures duplicates are unbound and store the same values as the original.
171
+ def initialize_copy(orig)
172
+ super
173
+
174
+ @receiver = nil
175
+ @store = @store.dup
176
+ orig.unmap(@store) if orig.bound?
177
+ end
178
+
179
+ protected
180
+
181
+ # helper to map delegate values from source to the receiver
182
+ def map(source) # :nodoc:
183
+ delegates.each_pair do |key, delegate|
184
+ next unless writer = delegate.writer
185
+
186
+ # map the value; if no value is set in the source then use the
187
+ # delegate default. if map_default is false, then simply skip...
188
+ # this ensures each config is initialized to a value when bound
189
+ # UNLESS map_default is set (indicating manual initialization)
190
+ value = case
191
+ when source.has_key?(key) then source.delete(key)
192
+ when delegate[:map_default, true] then delegate.default
193
+ else next
194
+ end
195
+
196
+ receiver.send(writer, value)
197
+ end
198
+ end
199
+
200
+ # helper to unmap delegates from the receiver to a target hash
201
+ def unmap(target) # :nodoc:
202
+ delegates.each_pair do |key, delegate|
203
+ next unless reader = delegate.reader
204
+ target[key] = receiver.send(reader)
205
+ end
206
+ end
164
207
  end
165
208
  end
@@ -1,21 +1,34 @@
1
1
  module Configurable
2
+
3
+ # Implements AGET and ASET methods that symbolize string keys, in effect
4
+ # producing indifferent access. IndifferentAccess is intended to extend
5
+ # a Hash.
6
+ #
7
+ # Note that the indifference produced by this module is very thin indeed.
8
+ # Strings may still be used as keys through store/fetch, and
9
+ # existing string keys are not changed in any way. Nonetheless,
10
+ # these methods are sufficient for Configurable and DelegateHash.
2
11
  module IndifferentAccess
3
-
12
+
13
+ # Symbolizes string keys and calls super.
4
14
  def [](key)
5
15
  super(convert(key))
6
16
  end
7
-
17
+
18
+ # Symbolizes string keys and calls super.
8
19
  def []=(key, value)
9
20
  super(convert(key), value)
10
21
  end
11
-
12
- def key?(key)
13
- super(convert(key))
22
+
23
+ # Ensures duplicates use indifferent access.
24
+ def dup
25
+ super().extend IndifferentAccess
14
26
  end
15
-
27
+
16
28
  private
17
-
18
- def convert(key)
29
+
30
+ # a helper to convert strings to symbols
31
+ def convert(key) # :nodoc:
19
32
  key.kind_of?(String) ? key.to_sym : key
20
33
  end
21
34
  end
@@ -0,0 +1,193 @@
1
+ module Configurable
2
+
3
+ # Utility methods to dump and load configurations, particularly nested
4
+ # configurations.
5
+ module Utils
6
+ module_function
7
+
8
+ default_dump_block = lambda do |key, delegate|
9
+ default = delegate.default(false)
10
+
11
+ # note: this causes order to be lost...
12
+ default = default.to_hash if delegate.is_nest?
13
+ YAML.dump({key => default})[5..-1]
14
+ end
15
+
16
+ # A block performing the default YAML dump.
17
+ DEFAULT_DUMP = default_dump_block
18
+
19
+ default_load_block = lambda do |base, key, value|
20
+ base[key] ||= value
21
+ end
22
+
23
+ # A block performing the default load.
24
+ DEFAULT_LOAD = default_load_block
25
+
26
+ # Dumps delegates to target as yaml. Delegates are output in order, and
27
+ # symbol keys are be stringified if delegates has been extended with
28
+ # IndifferentAccess (this produces a nicer config file).
29
+ #
30
+ # class DumpExample
31
+ # include Configurable
32
+ #
33
+ # config :sym, :value # a symbol config
34
+ # config 'str', 'value' # a string config
35
+ # end
36
+ #
37
+ # Utils.dump(DumpExample.configurations, "\n")
38
+ # # => %q{
39
+ # # sym: :value
40
+ # # str: value
41
+ # # }
42
+ #
43
+ # Dump may be provided with a block to format each (key, delegate) pair;
44
+ # the block results are pushed directly to target, so newlines must be
45
+ # specified manually.
46
+ #
47
+ # Utils.dump(DumpExample.configurations, "\n") do |key, delegate|
48
+ # yaml = YAML.dump({key => delegate.default})[5..-1]
49
+ # "# #{delegate[:desc]}\n#{yaml}\n"
50
+ # end
51
+ # # => %q{
52
+ # # # a symbol config
53
+ # # sym: :value
54
+ # #
55
+ # # # a string config
56
+ # # str: value
57
+ # #
58
+ # # }
59
+ #
60
+ def dump(delegates, target="")
61
+ return dump(delegates, target, &DEFAULT_DUMP) unless block_given?
62
+
63
+ stringify = delegates.kind_of?(IndifferentAccess)
64
+ delegates.each_pair do |key, delegate|
65
+ key = key.to_s if stringify && key.kind_of?(Symbol)
66
+ target << yield(key, delegate)
67
+ end
68
+
69
+ target
70
+ end
71
+
72
+ # Dumps the delegates to the specified file. If recurse is true, nested
73
+ # configurations are each dumped to their own file, based on the nesting
74
+ # key. For instance if you nested a in b:
75
+ #
76
+ # a_configs = {
77
+ # 'key' => Delegate.new(:r, :w, 'a default')}
78
+ # b_configs = {
79
+ # 'key' => Delegate.new(:r, :w, 'b default')}
80
+ # 'a' => Delegate.new(:r, :w, DelegateHash.new(a_configs))}}
81
+ #
82
+ # Utils.dump_file(b_configs, 'b.yml')
83
+ # File.read('b.yml') # => "key: b default"
84
+ # File.read('b/a.yml') # => "key: a default"
85
+ #
86
+ # In this way, each nested config gets it's own file. The load_file method
87
+ # can recursively load configurations from this file structure. When recurse
88
+ # is false, all configs are dumped to a single file.
89
+ #
90
+ # dump_file uses a method that collects all dumps in a preview array before
91
+ # dumping, so that the dump results can be redirected other places than the
92
+ # file system. If preview is set to false, no files will be created. The
93
+ # preview dumps are always returned by dump_file.
94
+ #
95
+ # ==== Note
96
+ # For load_file to correctly load a recursive dump, all delegate hashes
97
+ # must use String keys. Symbol keys are allowed if the delegate hashes use
98
+ # IndifferentAccess; all other keys will not load properly. By default
99
+ # Configurable is set up to satisfy these conditions.
100
+ #
101
+ # 1.8 Bug: Currently dump_file with recurse=false will cause order to be
102
+ # lost in nested configs. See http://bahuvrihi.lighthouseapp.com/projects/21202-configurable/tickets/8
103
+ def dump_file(delegates, path, recurse=false, preview=false, &block)
104
+ return dump_file(delegates, path, recurse, preview, &DEFAULT_DUMP) unless block_given?
105
+
106
+ current = ""
107
+ dumps = [[path, current]]
108
+
109
+ dump(delegates, current) do |key, delegate|
110
+ if recurse && delegate.is_nest?
111
+ nested_delegates = delegate.default(false).delegates
112
+ nested_dumps = dump_file(nested_delegates, recursive_path(key, path), true, true, &block)
113
+
114
+ dumps.concat(nested_dumps)
115
+ ""
116
+ else
117
+ yield(key, delegate)
118
+ end
119
+ end
120
+
121
+ dumps.each do |dump_path, content|
122
+ dir = File.dirname(dump_path)
123
+ Dir.mkdir(dir) unless File.exists?(dir)
124
+ File.open(dump_path, "w") do |io|
125
+ io << content
126
+ end
127
+ end unless preview
128
+
129
+ dumps
130
+ end
131
+
132
+ # Loads the string as YAML.
133
+ def load(str)
134
+ YAML.load(str)
135
+ end
136
+
137
+ # Loads the file contents as YAML. If recurse is true, a hash will be
138
+ # recursively loaded. A block may be provided to set recursively loaded
139
+ # values in the hash loaded from the path.
140
+ def load_file(path, recurse=false, &block)
141
+ return load_file(path, recurse, &DEFAULT_LOAD) if recurse && !block_given?
142
+ base = File.file?(path) ? (YAML.load_file(path) || {}) : {}
143
+
144
+ if recurse
145
+ # determine the files/dirs to load recursively
146
+ # and add them to paths by key (ie the base
147
+ # name of the path, minus any extname)
148
+ paths = {}
149
+ files, dirs = Dir.glob("#{path.chomp(File.extname(path))}/*").partition do |sub_path|
150
+ File.file?(sub_path)
151
+ end
152
+
153
+ # directories are added to paths first so they can be
154
+ # overridden by the files (appropriate since the file
155
+ # will recursively load the directory if it exists)
156
+ dirs.each do |dir|
157
+ paths[File.basename(dir)] = dir
158
+ end
159
+
160
+ # when adding files, check that no two files map to
161
+ # the same key (ex a.yml, a.yaml).
162
+ files.each do |filepath|
163
+ key = File.basename(filepath).chomp(File.extname(filepath))
164
+ if existing = paths[key]
165
+ if File.file?(existing)
166
+ confict = [File.basename(paths[key]), File.basename(filepath)].sort
167
+ raise "multiple files load the same key: #{confict.inspect}"
168
+ end
169
+ end
170
+
171
+ paths[key] = filepath
172
+ end
173
+
174
+ # recursively load each file and reverse merge
175
+ # the result into the base
176
+ paths.each_pair do |key, recursive_path|
177
+ value = load_file(recursive_path, true, &block)
178
+ yield(base, key, value)
179
+ end
180
+ end
181
+
182
+ base
183
+ end
184
+
185
+ # A helper to create and prepare a recursive dump path.
186
+ def recursive_path(key, path)
187
+ ext = File.extname(path)
188
+ dir = path.chomp(ext)
189
+
190
+ "#{File.join(dir, key.to_s)}#{ext}"
191
+ end
192
+ end
193
+ end