configurable 0.5.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History +19 -0
- data/MIT-LICENSE +17 -15
- data/README +112 -40
- data/lib/config_parser.rb +159 -96
- data/lib/config_parser/option.rb +10 -3
- data/lib/config_parser/switch.rb +1 -1
- data/lib/config_parser/utils.rb +52 -18
- data/lib/configurable.rb +128 -36
- data/lib/configurable/class_methods.rb +171 -192
- data/lib/configurable/config.rb +97 -0
- data/lib/configurable/config_hash.rb +198 -0
- data/lib/configurable/indifferent_access.rb +1 -1
- data/lib/configurable/module_methods.rb +7 -17
- data/lib/configurable/nest_config.rb +78 -0
- data/lib/configurable/ordered_hash_patch.rb +85 -0
- data/lib/configurable/utils.rb +25 -32
- data/lib/configurable/validation.rb +69 -31
- data/lib/configurable/version.rb +7 -0
- metadata +13 -8
- data/lib/configurable/delegate.rb +0 -103
- data/lib/configurable/delegate_hash.rb +0 -226
@@ -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
|
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(
|
8
|
-
|
9
|
-
|
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
|