configurable 0.1.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History +9 -0
- data/MIT-LICENSE +1 -1
- data/README +40 -142
- data/lib/cdoc.rb +413 -0
- data/lib/cdoc/cdoc_html_generator.rb +38 -0
- data/lib/cdoc/cdoc_html_template.rb +42 -0
- data/lib/config_parser.rb +302 -52
- data/lib/config_parser/option.rb +70 -21
- data/lib/config_parser/switch.rb +25 -10
- data/lib/config_parser/utils.rb +41 -27
- data/lib/configurable.rb +64 -40
- data/lib/configurable/class_methods.rb +245 -100
- data/lib/configurable/delegate.rb +18 -2
- data/lib/configurable/delegate_hash.rb +112 -69
- data/lib/configurable/indifferent_access.rb +21 -8
- data/lib/configurable/utils.rb +193 -0
- data/lib/configurable/validation.rb +112 -112
- metadata +16 -15
@@ -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
|
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
|
-
#
|
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.
|
56
|
-
#
|
57
|
-
#
|
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
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
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.
|
84
|
+
# Unbinds self from the specified receiver. Delegate values
|
77
85
|
# are stored in store. Returns the unbound receiver.
|
78
86
|
def unbind
|
79
|
-
|
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.
|
89
|
-
#
|
90
|
-
#
|
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
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
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
|
-
#
|
100
|
-
#
|
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
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
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
|
-
#
|
111
|
-
def
|
112
|
-
|
121
|
+
|
122
|
+
# Returns the union of delegate and store keys.
|
123
|
+
def keys
|
124
|
+
delegates.keys | store.keys
|
113
125
|
end
|
114
126
|
|
115
|
-
#
|
116
|
-
def
|
117
|
-
delegates.
|
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
|
-
#
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
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
|
-
#
|
139
|
-
def
|
140
|
-
|
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 =
|
154
|
-
|
155
|
-
hash[key] =
|
156
|
-
end
|
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
|
-
|
13
|
-
|
22
|
+
|
23
|
+
# Ensures duplicates use indifferent access.
|
24
|
+
def dup
|
25
|
+
super().extend IndifferentAccess
|
14
26
|
end
|
15
|
-
|
27
|
+
|
16
28
|
private
|
17
|
-
|
18
|
-
|
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
|