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.
- 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
|