configurability 1.0.0 → 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/ChangeLog +40 -1
- data/README.md +126 -2
- data/Rakefile +7 -3
- data/lib/configurability/behavior.rb +26 -0
- data/lib/configurability/config.rb +561 -0
- data/lib/configurability.rb +5 -2
- data/rake/documentation.rb +25 -0
- data/rake/hg.rb +8 -1
- data/spec/configurability/config_spec.rb +311 -0
- metadata +13 -4
data/ChangeLog
CHANGED
@@ -1,4 +1,43 @@
|
|
1
|
-
|
1
|
+
17[tip] 4bff6d5f7b6e 2010-08-08 10:26 -0700 ged
|
2
|
+
Added tag 1.0.1 for changeset e3605ccbe057
|
3
|
+
|
4
|
+
16[1.0.1] e3605ccbe057 2010-08-08 10:26 -0700 ged
|
5
|
+
Added signature for changeset 41bc1de0bf36
|
6
|
+
|
7
|
+
15 41bc1de0bf36 2010-08-04 18:51 -0700 ged
|
8
|
+
More Configurability::Config work
|
9
|
+
|
10
|
+
14 44db952eb824 2010-08-04 18:23 -0700 ged
|
11
|
+
More work on Configurability::Config
|
12
|
+
|
13
|
+
13 2323bf260e7c 2010-08-04 16:28 -0700 ged
|
14
|
+
Finishing up Configurability::Config.
|
15
|
+
|
16
|
+
12 9e0bab83afd5 2010-08-04 10:18 -0700 ged
|
17
|
+
Build system updates.
|
18
|
+
|
19
|
+
11 cba63cef2f7f 2010-08-04 10:17 -0700 ged
|
20
|
+
Add an rspec shared behavior for testing classes with Configurability
|
21
|
+
|
22
|
+
10 4ffdde3f7a2f 2010-07-16 16:55 -0700 ged
|
23
|
+
Adding a Configurability::Config class for YAML config loading.
|
24
|
+
|
25
|
+
9 d225cef4269e 2010-07-16 16:54 -0700 ged
|
26
|
+
Updating README, project metadata, adding .irbrc
|
27
|
+
|
28
|
+
8 9da882b82786 2010-07-16 16:53 -0700 ged
|
29
|
+
Version bump; debugging log
|
30
|
+
|
31
|
+
7 ecf5d4565338 2010-07-12 15:52 -0700 ged
|
32
|
+
Added tag 1.0.0 for changeset 74b5dd9a89c9
|
33
|
+
|
34
|
+
6[1.0.0] 74b5dd9a89c9 2010-07-12 15:52 -0700 ged
|
35
|
+
Added signature for changeset e0fef8dabba4
|
36
|
+
|
37
|
+
5 e0fef8dabba4 2010-07-12 15:51 -0700 ged
|
38
|
+
README formatting, YARD tag fix.
|
39
|
+
|
40
|
+
4 4d7044641f87 2010-07-12 13:38 -0700 ged
|
2
41
|
Documentation, added a test for classname normalization.
|
3
42
|
|
4
43
|
3 0b395dbf657f 2010-07-12 08:13 -0700 ged
|
data/README.md
CHANGED
@@ -10,6 +10,7 @@ configuration is split up and sent to the objects that will use it.
|
|
10
10
|
To add configurability to a class, just require the library and extend
|
11
11
|
the class:
|
12
12
|
|
13
|
+
require 'configurability'
|
13
14
|
class User
|
14
15
|
extend Configurability
|
15
16
|
end
|
@@ -35,7 +36,12 @@ _section name_ as a `Symbol` or a `String`:
|
|
35
36
|
|
36
37
|
The section name is based on an object's _config key_, which is the name of
|
37
38
|
the object that is being extended with all non-word characters converted into
|
38
|
-
underscores (`_`) by default.
|
39
|
+
underscores (`_`) by default. It will also have any leading Ruby-style
|
40
|
+
namespaces stripped, e.g.,
|
41
|
+
|
42
|
+
MyClass -> :myclass
|
43
|
+
Acme::User -> :user
|
44
|
+
"J. Random Hacker" -> :j_random_hacker
|
39
45
|
|
40
46
|
If the object responds to the `#name` method, then the return value of that
|
41
47
|
method is used to derive the name. If it doesn't have a `#name` method, the
|
@@ -82,13 +88,131 @@ a `#configure` method that takes the config section as an argument:
|
|
82
88
|
class WebServer
|
83
89
|
extend Configurability
|
84
90
|
|
91
|
+
config_key :webserver
|
92
|
+
|
85
93
|
def self::configure( configsection )
|
86
94
|
@default_bind_addr = configsection[:host]
|
87
95
|
@default_port = configsection[:port]
|
88
96
|
end
|
89
97
|
end
|
90
98
|
|
91
|
-
If you still want the
|
99
|
+
If you still want the `@config` variable to be set, just `super` from your implementation; don't if you don't want it to be set.
|
100
|
+
|
101
|
+
|
102
|
+
## Configuration Objects
|
103
|
+
|
104
|
+
Configurability also includes `Configurability::Config`, a fairly simple
|
105
|
+
configuration object class that can be used to load a YAML configuration file,
|
106
|
+
and then present both a Hash-like and a Struct-like interface for reading
|
107
|
+
configuration sections and values; it's meant to be used in tandem with Configurability, but it's also useful on its own.
|
108
|
+
|
109
|
+
Here's a quick example to demonstrate some of its features. Suppose you have a
|
110
|
+
config file that looks like this:
|
111
|
+
|
112
|
+
---
|
113
|
+
database:
|
114
|
+
development:
|
115
|
+
adapter: sqlite3
|
116
|
+
database: db/dev.db
|
117
|
+
pool: 5
|
118
|
+
timeout: 5000
|
119
|
+
testing:
|
120
|
+
adapter: sqlite3
|
121
|
+
database: db/testing.db
|
122
|
+
pool: 2
|
123
|
+
timeout: 5000
|
124
|
+
production:
|
125
|
+
adapter: postgres
|
126
|
+
database: fixedassets
|
127
|
+
pool: 25
|
128
|
+
timeout: 50
|
129
|
+
ldap:
|
130
|
+
uri: ldap://ldap.acme.com/dc=acme,dc=com
|
131
|
+
bind_dn: cn=web,dc=acme,dc=com
|
132
|
+
bind_pass: Mut@ge.Mix@ge
|
133
|
+
branding:
|
134
|
+
header: "#333"
|
135
|
+
title: "#dedede"
|
136
|
+
anchor: "#9fc8d4"
|
137
|
+
|
138
|
+
You can load this config like so:
|
139
|
+
|
140
|
+
require 'configurability/config'
|
141
|
+
config = Configurability::Config.load( 'examples/config.yml' )
|
142
|
+
# => #<Configurability::Config:0x1018a7c7016 loaded from
|
143
|
+
examples/config.yml; 3 sections: database, ldap, branding>
|
144
|
+
|
145
|
+
And then access it using struct-like methods:
|
146
|
+
|
147
|
+
config.database
|
148
|
+
# => #<Configurability::Config::Struct:101806fb816
|
149
|
+
{:development=>{:adapter=>"sqlite3", :database=>"db/dev.db", :pool=>5,
|
150
|
+
:timeout=>5000}, :testing=>{:adapter=>"sqlite3",
|
151
|
+
:database=>"db/testing.db", :pool=>2, :timeout=>5000},
|
152
|
+
:production=>{:adapter=>"postgres", :database=>"fixedassets",
|
153
|
+
:pool=>25, :timeout=>50}}>
|
154
|
+
|
155
|
+
config.database.development.adapter
|
156
|
+
# => "sqlite3"
|
157
|
+
|
158
|
+
config.ldap.uri
|
159
|
+
# => "ldap://ldap.acme.com/dc=acme,dc=com"
|
160
|
+
|
161
|
+
config.branding.title
|
162
|
+
# => "#dedede"
|
163
|
+
|
164
|
+
or using a Hash-like interface using either `Symbol`s, `String`s, or a mix of
|
165
|
+
both:
|
166
|
+
|
167
|
+
config[:branding][:title]
|
168
|
+
# => "#dedede"
|
169
|
+
|
170
|
+
config['branding']['header']
|
171
|
+
# => "#333"
|
172
|
+
|
173
|
+
config['branding'][:anchor]
|
174
|
+
# => "#9fc8d4"
|
175
|
+
|
176
|
+
You can install it via the Configurability interface:
|
177
|
+
|
178
|
+
config.install
|
179
|
+
|
180
|
+
Check to see if the file it was loaded from has changed since you
|
181
|
+
loaded it:
|
182
|
+
|
183
|
+
config.changed?
|
184
|
+
# => false
|
185
|
+
|
186
|
+
# Simulate changing the file by manually changing its mtime
|
187
|
+
File.utime( Time.now, Time.now, config.path )
|
188
|
+
config.changed?
|
189
|
+
# => true
|
190
|
+
|
191
|
+
If it has changed (or even if it hasn't), you can reload it, which automatically re-installs it via the Configurability interface:
|
192
|
+
|
193
|
+
config.reload
|
194
|
+
|
195
|
+
You can make modifications via the same Struct- or Hash-like interfaces and write the modified config back out to the same file:
|
196
|
+
|
197
|
+
config.database.testing.adapter = 'mysql'
|
198
|
+
config[:database]['testing'].database = 't_fixedassets'
|
199
|
+
|
200
|
+
then dump it to a YAML string:
|
201
|
+
|
202
|
+
config.dump
|
203
|
+
# => "--- \ndatabase: \n development: \n adapter: sqlite3\n
|
204
|
+
database: db/dev.db\n pool: 5\n timeout: 5000\n testing: \n
|
205
|
+
adapter: mysql\n database: t_fixedassets\n pool: 2\n timeout:
|
206
|
+
5000\n production: \n adapter: postgres\n database:
|
207
|
+
fixedassets\n pool: 25\n timeout: 50\nldap: \n uri:
|
208
|
+
ldap://ldap.acme.com/dc=acme,dc=com\n bind_dn:
|
209
|
+
cn=web,dc=acme,dc=com\n bind_pass: Mut@ge.Mix@ge\nbranding: \n
|
210
|
+
header: \"#333\"\n title: \"#dedede\"\n anchor: \"#9fc8d4\"\n"
|
211
|
+
|
212
|
+
or write it back to the file it was loaded from:
|
213
|
+
|
214
|
+
config.write
|
215
|
+
|
92
216
|
|
93
217
|
|
94
218
|
## Development
|
data/Rakefile
CHANGED
@@ -76,7 +76,7 @@ elsif VERSION_FILE.exist?
|
|
76
76
|
PKG_VERSION = VERSION_FILE.read[ /VERSION\s*=\s*['"](\d+\.\d+\.\d+)['"]/, 1 ]
|
77
77
|
end
|
78
78
|
|
79
|
-
PKG_VERSION = '0.0.0' unless defined?( PKG_VERSION )
|
79
|
+
PKG_VERSION = '0.0.0' unless defined?( PKG_VERSION ) && !PKG_VERSION.nil?
|
80
80
|
|
81
81
|
PKG_FILE_NAME = "#{PKG_NAME.downcase}-#{PKG_VERSION}"
|
82
82
|
GEM_FILE_NAME = "#{PKG_FILE_NAME}.gem"
|
@@ -168,7 +168,7 @@ include RakefileHelpers
|
|
168
168
|
|
169
169
|
# Set the build ID if the mercurial executable is available
|
170
170
|
if hg = which( 'hg' )
|
171
|
-
id =
|
171
|
+
id = `#{hg} id -n`.chomp
|
172
172
|
PKG_BUILD = "pre%03d" % [(id.chomp[ /^[[:xdigit:]]+/ ] || '1')]
|
173
173
|
else
|
174
174
|
PKG_BUILD = 'pre000'
|
@@ -237,7 +237,11 @@ GEMSPEC = Gem::Specification.new do |gem|
|
|
237
237
|
|
238
238
|
gem.summary = PKG_SUMMARY
|
239
239
|
gem.description = [
|
240
|
-
"Configurability is mixin that allows you to add
|
240
|
+
"Configurability is a mixin that allows you to add ",
|
241
|
+
"configurability to one or more classes, assign them ",
|
242
|
+
"each a section of the configuration, and then when ",
|
243
|
+
"the configuration is loaded, the class is given the ",
|
244
|
+
"section it requested.",
|
241
245
|
].join( "\n" )
|
242
246
|
|
243
247
|
gem.authors = "Michael Granger"
|
@@ -0,0 +1,26 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'configurability'
|
4
|
+
require 'spec'
|
5
|
+
|
6
|
+
share_examples_for "an object with Configurability" do
|
7
|
+
|
8
|
+
before( :each ) do
|
9
|
+
fail "this behavior expects the object under test to be in the @object " +
|
10
|
+
"instance variable" unless defined?( @object )
|
11
|
+
end
|
12
|
+
|
13
|
+
it "is extended with Configurability" do
|
14
|
+
Configurability.configurable_objects.should include( @object )
|
15
|
+
end
|
16
|
+
|
17
|
+
it "has a Symbol config key" do
|
18
|
+
@object.config_key.should be_a( Symbol )
|
19
|
+
end
|
20
|
+
|
21
|
+
it "has a config key that is a reasonable section name" do
|
22
|
+
@object.config_key.to_s.should =~ /^[a-z][a-z0-9]*$/i
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
@@ -0,0 +1,561 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'tmpdir'
|
4
|
+
require 'pathname'
|
5
|
+
require 'forwardable'
|
6
|
+
require 'yaml'
|
7
|
+
require 'logger'
|
8
|
+
|
9
|
+
require 'configurability'
|
10
|
+
|
11
|
+
# A configuration object class for systems with Configurability
|
12
|
+
#
|
13
|
+
# @author Michael Granger <ged@FaerieMUD.org>
|
14
|
+
# @author Mahlon E. Smith <mahlon@martini.nu>
|
15
|
+
#
|
16
|
+
# This class also delegates some of its methods to the underlying struct:
|
17
|
+
#
|
18
|
+
# @see Configurability::Config::Struct#to_hash
|
19
|
+
# #to_hash (delegated to its internal Struct)
|
20
|
+
# @see Configurability::Config::Struct#member?
|
21
|
+
# #member? (delegated to its internal Struct)
|
22
|
+
# @see Configurability::Config::Struct#members
|
23
|
+
# #members (delegated to its internal Struct)
|
24
|
+
# @see Configurability::Config::Struct#merge
|
25
|
+
# #merge (delegated to its internal Struct)
|
26
|
+
# @see Configurability::Config::Struct#merge!
|
27
|
+
# #merge! (delegated to its internal Struct)
|
28
|
+
# @see Configurability::Config::Struct#each
|
29
|
+
# #each (delegated to its internal Struct)
|
30
|
+
# @see Configurability::Config::Struct#[]
|
31
|
+
# #[] (delegated to its internal Struct)
|
32
|
+
# @see Configurability::Config::Struct#[]=
|
33
|
+
# #[]= (delegated to its internal Struct)
|
34
|
+
#
|
35
|
+
class Configurability::Config
|
36
|
+
extend Forwardable
|
37
|
+
|
38
|
+
|
39
|
+
#############################################################
|
40
|
+
### C L A S S M E T H O D S
|
41
|
+
#############################################################
|
42
|
+
|
43
|
+
### Read and return a Configurability::Config object from the file at the given +path+.
|
44
|
+
### @param [String] path the path to the config file
|
45
|
+
### @param [Hash] defaults a Hash of default config values which will be
|
46
|
+
### used if the config at +path+ doesn't override
|
47
|
+
### them.
|
48
|
+
### @param block passed through as the block argument to {#initialize}.
|
49
|
+
def self::load( path, defaults=nil, &block )
|
50
|
+
path = Pathname( path ).expand_path
|
51
|
+
source = path.read
|
52
|
+
Configurability.log.debug "Read %d bytes from %s" % [ source.length, path ]
|
53
|
+
return new( source, path, defaults, &block )
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
### Recursive hash-merge function. Used as the block argument to a Hash#merge.
|
58
|
+
### @param [Symbol] key the key that's in conflict
|
59
|
+
### @param [Object] oldval the value in the original Hash
|
60
|
+
### @param [Object] newval the value in the Hash being merged
|
61
|
+
def self::merge_complex_hashes( key, oldval, newval )
|
62
|
+
return oldval.merge( newval, &method(:merge_complex_hashes) ) if
|
63
|
+
oldval.is_a?( Hash ) && newval.is_a?( Hash )
|
64
|
+
return newval
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
|
69
|
+
#############################################################
|
70
|
+
### I N S T A N C E M E T H O D S
|
71
|
+
#############################################################
|
72
|
+
|
73
|
+
### Create a new Configurability::Config object. If the optional +source+ argument
|
74
|
+
### is specified, parse the config from it.
|
75
|
+
###
|
76
|
+
### @param [String] source the YAML source of the configuration
|
77
|
+
### @param [String, Pathname] path the path to the config file (if loaded from a file)
|
78
|
+
### @param [Hash] defaults a Hash containing default values which the loaded values
|
79
|
+
### will be merged into.
|
80
|
+
### @yield The block will be evaluated in the context of the config object after
|
81
|
+
### the config is loaded, unless it accepts an argument, in which case the config
|
82
|
+
### object is passed as the argument.
|
83
|
+
def initialize( source=nil, path=nil, defaults=nil, &block )
|
84
|
+
|
85
|
+
# Shift the hash parameter if it shows up as the path
|
86
|
+
if path.is_a?( Hash )
|
87
|
+
defaults = path
|
88
|
+
path = nil
|
89
|
+
end
|
90
|
+
|
91
|
+
# Make a deep copy of the defaults before loading so we don't modify
|
92
|
+
# the argument
|
93
|
+
@defaults = Marshal.load( Marshal.dump(defaults) ) if defaults
|
94
|
+
@time_created = Time.now
|
95
|
+
@path = path
|
96
|
+
|
97
|
+
if source
|
98
|
+
@struct = self.make_configstruct_from_source( source, @defaults )
|
99
|
+
else
|
100
|
+
@struct = Configurability::Config::Struct.new( @defaults )
|
101
|
+
end
|
102
|
+
|
103
|
+
if block
|
104
|
+
Configurability.log.debug "Block arity is: %p" % [ block.arity ]
|
105
|
+
|
106
|
+
# A block with an argument is called with the config as the argument
|
107
|
+
# instead of instance_evaled
|
108
|
+
case block.arity
|
109
|
+
when 0, -1 # 1.9 and 1.8, respectively
|
110
|
+
Configurability.log.debug "Instance evaling in the context of %p" % [ self ]
|
111
|
+
self.instance_eval( &block )
|
112
|
+
else
|
113
|
+
block.call( self )
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
|
119
|
+
######
|
120
|
+
public
|
121
|
+
######
|
122
|
+
|
123
|
+
# Define delegators to the inner data structure
|
124
|
+
def_delegators :@struct, :to_hash, :to_h, :member?, :members, :merge,
|
125
|
+
:merge!, :each, :[], :[]=
|
126
|
+
|
127
|
+
# @return [Configurability::Config::Struct] The underlying config data structure
|
128
|
+
attr_reader :struct
|
129
|
+
|
130
|
+
# @return [Time] The time the configuration was loaded
|
131
|
+
attr_accessor :time_created
|
132
|
+
|
133
|
+
# @return [Pathname] the path to the config file, if loaded from a file
|
134
|
+
attr_accessor :path
|
135
|
+
|
136
|
+
|
137
|
+
### Install this config object in any objects that have added
|
138
|
+
### Configurability.
|
139
|
+
def install
|
140
|
+
Configurability.configure_objects( self )
|
141
|
+
end
|
142
|
+
|
143
|
+
|
144
|
+
### Return the config object as a YAML hash
|
145
|
+
def dump
|
146
|
+
strhash = stringify_keys( self.to_h )
|
147
|
+
return YAML.dump( strhash )
|
148
|
+
end
|
149
|
+
|
150
|
+
|
151
|
+
### Write the configuration object using the specified name and any
|
152
|
+
### additional +args+.
|
153
|
+
def write( path=@path, *args )
|
154
|
+
raise ArgumentError,
|
155
|
+
"No name associated with this config." unless path
|
156
|
+
path.open( File::WRONLY|File::CREAT|File::TRUNC ) do |ofh|
|
157
|
+
ofh.print( self.dump )
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
|
162
|
+
### Returns +true+ for methods which can be autoloaded
|
163
|
+
def respond_to?( sym )
|
164
|
+
return true if @struct.member?( sym.to_s.sub(/(=|\?)$/, '').to_sym )
|
165
|
+
super
|
166
|
+
end
|
167
|
+
|
168
|
+
|
169
|
+
### Returns +true+ if the configuration has changed since it was last
|
170
|
+
### loaded, either by setting one of its members or changing the file
|
171
|
+
### from which it was loaded.
|
172
|
+
def changed?
|
173
|
+
return self.changed_reason ? true : false
|
174
|
+
end
|
175
|
+
|
176
|
+
|
177
|
+
### If the configuration has changed, return the reason. If it hasn't,
|
178
|
+
### returns nil.
|
179
|
+
def changed_reason
|
180
|
+
if @struct.dirty?
|
181
|
+
Configurability.log.debug "Changed_reason: struct was modified"
|
182
|
+
return "Struct was modified"
|
183
|
+
end
|
184
|
+
|
185
|
+
if self.path && self.is_older_than?( self.path )
|
186
|
+
Configurability.log.debug "Source file (%s) has changed." % [ self.path ]
|
187
|
+
return "Config source (%s) has been updated since %s" %
|
188
|
+
[ self.path, self.time_created ]
|
189
|
+
end
|
190
|
+
|
191
|
+
return nil
|
192
|
+
end
|
193
|
+
|
194
|
+
|
195
|
+
### Return +true+ if the specified +file+ is newer than the time the receiver
|
196
|
+
### was created.
|
197
|
+
def is_older_than?( path )
|
198
|
+
return false unless path.exist?
|
199
|
+
st = path.stat
|
200
|
+
Configurability.log.debug "File mtime is: %s, comparison time is: %s" %
|
201
|
+
[ st.mtime, @time_created ]
|
202
|
+
return st.mtime > @time_created
|
203
|
+
end
|
204
|
+
|
205
|
+
|
206
|
+
### Reload the configuration from the original source if it has
|
207
|
+
### changed. Returns +true+ if it was reloaded and +false+ otherwise.
|
208
|
+
###
|
209
|
+
def reload
|
210
|
+
raise "can't reload from an in-memory source" unless self.path
|
211
|
+
|
212
|
+
if self.changed?
|
213
|
+
self.time_created = Time.now
|
214
|
+
source = self.path.read
|
215
|
+
@struct = self.make_configstruct_from_source( source, @defaults )
|
216
|
+
|
217
|
+
self.install
|
218
|
+
return true
|
219
|
+
else
|
220
|
+
return false
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
|
225
|
+
### Return a human-readable, compact representation of the configuration
|
226
|
+
### suitable for debugging.
|
227
|
+
def inspect
|
228
|
+
return "#<%s:0x%0x16 loaded from %s; %d sections: %s>" % [
|
229
|
+
self.class.name,
|
230
|
+
self.object_id * 2,
|
231
|
+
self.path ? self.path : "memory",
|
232
|
+
self.struct.members.length,
|
233
|
+
self.struct.members.join( ', ' )
|
234
|
+
]
|
235
|
+
end
|
236
|
+
|
237
|
+
|
238
|
+
#########
|
239
|
+
protected
|
240
|
+
#########
|
241
|
+
|
242
|
+
|
243
|
+
### Read in the specified +filename+ and return a config struct.
|
244
|
+
### @param [String] source the YAML source to be converted
|
245
|
+
### @return [Configurability::Config::Struct] the converted config struct
|
246
|
+
def make_configstruct_from_source( source, defaults=nil )
|
247
|
+
defaults ||= {}
|
248
|
+
mergefunc = Configurability::Config.method( :merge_complex_hashes )
|
249
|
+
hash = YAML.load( source )
|
250
|
+
ihash = symbolify_keys( untaint_values(hash) )
|
251
|
+
mergedhash = defaults.merge( ihash, &mergefunc )
|
252
|
+
|
253
|
+
return Configurability::Config::Struct.new( mergedhash )
|
254
|
+
end
|
255
|
+
|
256
|
+
|
257
|
+
### Handle calls to struct-members
|
258
|
+
def method_missing( sym, *args )
|
259
|
+
key = sym.to_s.sub( /(=|\?)$/, '' ).to_sym
|
260
|
+
|
261
|
+
self.class.class_eval %{
|
262
|
+
def #{key}; @struct.#{key}; end
|
263
|
+
def #{key}=(arg); @struct.#{key} = arg; end
|
264
|
+
def #{key}?; @struct.#{key}?; end
|
265
|
+
}
|
266
|
+
|
267
|
+
return self.method( sym ).call( *args )
|
268
|
+
end
|
269
|
+
|
270
|
+
|
271
|
+
#######
|
272
|
+
private
|
273
|
+
#######
|
274
|
+
|
275
|
+
### Return a copy of the specified +hash+ with all of its values
|
276
|
+
### untainted.
|
277
|
+
def untaint_values( hash )
|
278
|
+
newhash = {}
|
279
|
+
hash.each do |key,val|
|
280
|
+
case val
|
281
|
+
when Hash
|
282
|
+
newhash[ key ] = untaint_values( hash[key] )
|
283
|
+
|
284
|
+
when Array
|
285
|
+
newval = val.collect {|v| v.dup.untaint}
|
286
|
+
newhash[ key ] = newval
|
287
|
+
|
288
|
+
when NilClass, TrueClass, FalseClass, Numeric, Symbol
|
289
|
+
newhash[ key ] = val
|
290
|
+
|
291
|
+
else
|
292
|
+
newval = val.dup
|
293
|
+
newval.untaint
|
294
|
+
newhash[ key ] = newval
|
295
|
+
end
|
296
|
+
end
|
297
|
+
return newhash
|
298
|
+
end
|
299
|
+
|
300
|
+
|
301
|
+
### Return a duplicate of the given +hash+ with its identifier-like keys
|
302
|
+
### transformed into symbols from whatever they were before.
|
303
|
+
def symbolify_keys( hash )
|
304
|
+
newhash = {}
|
305
|
+
hash.each do |key,val|
|
306
|
+
if val.is_a?( Hash )
|
307
|
+
newhash[ key.to_sym ] = symbolify_keys( val )
|
308
|
+
else
|
309
|
+
newhash[ key.to_sym ] = val
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
return newhash
|
314
|
+
end
|
315
|
+
|
316
|
+
|
317
|
+
### Return a version of the given +hash+ with its keys transformed
|
318
|
+
### into Strings from whatever they were before.
|
319
|
+
def stringify_keys( hash )
|
320
|
+
newhash = {}
|
321
|
+
hash.each do |key,val|
|
322
|
+
if val.is_a?( Hash )
|
323
|
+
newhash[ key.to_s ] = stringify_keys( val )
|
324
|
+
else
|
325
|
+
newhash[ key.to_s ] = val
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
return newhash
|
330
|
+
end
|
331
|
+
|
332
|
+
|
333
|
+
|
334
|
+
#############################################################
|
335
|
+
### I N T E R I O R C L A S S E S
|
336
|
+
#############################################################
|
337
|
+
|
338
|
+
### Hash-wrapper that allows struct-like accessor calls on nested
|
339
|
+
### hashes.
|
340
|
+
class Struct
|
341
|
+
extend Forwardable
|
342
|
+
include Enumerable
|
343
|
+
|
344
|
+
# Mask most of Kernel's methods away so they don't collide with
|
345
|
+
# config values.
|
346
|
+
Kernel.methods(false).each {|meth|
|
347
|
+
next unless method_defined?( meth )
|
348
|
+
next if /^(?:__|dup|object_id|inspect|class|raise|method_missing)/.match( meth )
|
349
|
+
undef_method( meth )
|
350
|
+
}
|
351
|
+
|
352
|
+
|
353
|
+
### Create a new ConfigStruct using the values from the given +hash+ if specified.
|
354
|
+
### @param [Hash] hash a hash of config values
|
355
|
+
def initialize( hash=nil )
|
356
|
+
hash ||= {}
|
357
|
+
@hash = hash.dup
|
358
|
+
@dirty = false
|
359
|
+
end
|
360
|
+
|
361
|
+
|
362
|
+
######
|
363
|
+
public
|
364
|
+
######
|
365
|
+
|
366
|
+
# Forward some methods to the internal hash
|
367
|
+
def_delegators :@hash, :keys, :key?, :values, :value?, :length,
|
368
|
+
:empty?, :clear, :each
|
369
|
+
|
370
|
+
# Let :each be called as :each_section, too
|
371
|
+
alias_method :each_section, :each
|
372
|
+
|
373
|
+
|
374
|
+
### Return the value associated with the specified +key+.
|
375
|
+
### @param [Symbol, String] key the key of the value to return
|
376
|
+
### @return [Object] the value associated with +key+, or another
|
377
|
+
### Configurability::Config::ConfigStruct if +key+
|
378
|
+
### is a section name.
|
379
|
+
def []( key )
|
380
|
+
key = key.untaint.to_sym
|
381
|
+
|
382
|
+
# Create the config struct on the fly for subsections
|
383
|
+
if !@hash.key?( key )
|
384
|
+
@hash[ key ] = self.class.new
|
385
|
+
elsif @hash[ key ].is_a?( Hash )
|
386
|
+
@hash[ key ] = self.class.new( @hash[key] )
|
387
|
+
end
|
388
|
+
|
389
|
+
return @hash[ key ]
|
390
|
+
end
|
391
|
+
|
392
|
+
|
393
|
+
### Set the value associated with the specified +key+ to +value+.
|
394
|
+
### @param [Symbol, String] key the key of the value to set
|
395
|
+
### @param [Object] value the value to set
|
396
|
+
def []=( key, value )
|
397
|
+
key = key.untaint.to_sym
|
398
|
+
self.mark_dirty if @hash[ key ] != value
|
399
|
+
@hash[ key ] = value
|
400
|
+
end
|
401
|
+
|
402
|
+
|
403
|
+
### Mark the struct has having been modified since its creation.
|
404
|
+
def mark_dirty
|
405
|
+
@dirty = true
|
406
|
+
end
|
407
|
+
|
408
|
+
|
409
|
+
### Returns +true+ if the ConfigStruct or any of its sub-structs
|
410
|
+
### have changed since it was created.
|
411
|
+
def dirty?
|
412
|
+
return true if @dirty
|
413
|
+
return true if @hash.values.find do |obj|
|
414
|
+
obj.respond_to?( :dirty? ) && obj.dirty?
|
415
|
+
end
|
416
|
+
end
|
417
|
+
|
418
|
+
|
419
|
+
### Return the receiver's values as a (possibly multi-dimensional)
|
420
|
+
### Hash with String keys.
|
421
|
+
def to_hash
|
422
|
+
rhash = {}
|
423
|
+
@hash.each {|k,v|
|
424
|
+
case v
|
425
|
+
when Configurability::Config::Struct
|
426
|
+
rhash[k] = v.to_h
|
427
|
+
when NilClass, FalseClass, TrueClass, Numeric
|
428
|
+
# No-op (can't dup)
|
429
|
+
rhash[k] = v
|
430
|
+
when Symbol
|
431
|
+
rhash[k] = v.to_s
|
432
|
+
else
|
433
|
+
rhash[k] = v.dup
|
434
|
+
end
|
435
|
+
}
|
436
|
+
return rhash
|
437
|
+
end
|
438
|
+
alias_method :to_h, :to_hash
|
439
|
+
|
440
|
+
|
441
|
+
### Return +true+ if the receiver responds to the given
|
442
|
+
### method. Overridden to grok autoloaded methods.
|
443
|
+
def respond_to?( sym, priv=false )
|
444
|
+
key = sym.to_s.sub( /(=|\?)$/, '' ).to_sym
|
445
|
+
return true if @hash.key?( key )
|
446
|
+
super
|
447
|
+
end
|
448
|
+
|
449
|
+
|
450
|
+
### Returns an Array of Symbols, one for each of the struct's members.
|
451
|
+
def members
|
452
|
+
return @hash.keys
|
453
|
+
end
|
454
|
+
|
455
|
+
|
456
|
+
### Returns +true+ if the given +name+ is the name of a member of
|
457
|
+
### the receiver.
|
458
|
+
def member?( name )
|
459
|
+
return @hash.key?( name.to_s.to_sym )
|
460
|
+
end
|
461
|
+
|
462
|
+
|
463
|
+
### Merge the specified +other+ object with this config struct. The
|
464
|
+
### +other+ object can be either a Hash, another Configurability::Config::Struct, or an
|
465
|
+
### Configurability::Config.
|
466
|
+
def merge!( other )
|
467
|
+
mergefunc = Configurability::Config.method( :merge_complex_hashes )
|
468
|
+
|
469
|
+
case other
|
470
|
+
when Hash
|
471
|
+
@hash = self.to_h.merge( other, &mergefunc )
|
472
|
+
|
473
|
+
when Configurability::Config::Struct
|
474
|
+
@hash = self.to_h.merge( other.to_h, &mergefunc )
|
475
|
+
|
476
|
+
when Configurability::Config
|
477
|
+
@hash = self.to_h.merge( other.struct.to_h, &mergefunc )
|
478
|
+
|
479
|
+
else
|
480
|
+
raise TypeError,
|
481
|
+
"Don't know how to merge with a %p" % other.class
|
482
|
+
end
|
483
|
+
|
484
|
+
# :TODO: Actually check to see if anything has changed?
|
485
|
+
@dirty = true
|
486
|
+
|
487
|
+
return self
|
488
|
+
end
|
489
|
+
|
490
|
+
|
491
|
+
### Return a new Configurability::Config::Struct which is the result of merging the
|
492
|
+
### receiver with the given +other+ object (a Hash or another
|
493
|
+
### Configurability::Config::Struct).
|
494
|
+
def merge( other )
|
495
|
+
self.dup.merge!( other )
|
496
|
+
end
|
497
|
+
|
498
|
+
|
499
|
+
### Return a human-readable representation of the Struct suitable for debugging.
|
500
|
+
def inspect
|
501
|
+
return "#<%s:%0x16 %p>" % [
|
502
|
+
self.class.name,
|
503
|
+
self.object_id * 2,
|
504
|
+
@hash,
|
505
|
+
]
|
506
|
+
end
|
507
|
+
|
508
|
+
|
509
|
+
#########
|
510
|
+
protected
|
511
|
+
#########
|
512
|
+
|
513
|
+
### Handle calls to key-methods
|
514
|
+
def method_missing( sym, *args )
|
515
|
+
key = sym.to_s.sub( /(=|\?)$/, '' ).to_sym
|
516
|
+
|
517
|
+
# Create new methods for this key
|
518
|
+
reader = self.create_member_reader( key )
|
519
|
+
writer = self.create_member_writer( key )
|
520
|
+
predicate = self.create_member_predicate( key )
|
521
|
+
|
522
|
+
# ...and install them
|
523
|
+
self.class.send( :define_method, key, &reader )
|
524
|
+
self.class.send( :define_method, "#{key}=", &writer )
|
525
|
+
self.class.send( :define_method, "#{key}?", &predicate )
|
526
|
+
|
527
|
+
# Now jump to the requested method in a way that won't come back through
|
528
|
+
# the proxy method if something didn't get defined
|
529
|
+
self.method( sym ).call( *args )
|
530
|
+
end
|
531
|
+
|
532
|
+
|
533
|
+
### Create a reader method for the specified +key+ and return it.
|
534
|
+
### @param [Symbol] key the config key to create the reader method body for
|
535
|
+
### @return [Proc] the body of the new method
|
536
|
+
def create_member_reader( key )
|
537
|
+
return lambda { self[key] }
|
538
|
+
end
|
539
|
+
|
540
|
+
|
541
|
+
### Create a predicate method for the specified +key+ and return it.
|
542
|
+
### @param [Symbol] key the config key to create the predicate method body for
|
543
|
+
### @return [Proc] the body of the new method
|
544
|
+
def create_member_predicate( key )
|
545
|
+
return lambda { self[key] ? true : false }
|
546
|
+
end
|
547
|
+
|
548
|
+
|
549
|
+
### Create a writer method for the specified +key+ and return it.
|
550
|
+
### @param [Symbol] key the config key to create the writer method body for
|
551
|
+
### @return [Proc] the body of the new method
|
552
|
+
def create_member_writer( key )
|
553
|
+
return lambda {|val| self[key] = val }
|
554
|
+
end
|
555
|
+
|
556
|
+
end # class Struct
|
557
|
+
|
558
|
+
end # class Configurability::Config
|
559
|
+
|
560
|
+
# vim: set nosta noet ts=4 sw=4:
|
561
|
+
|
data/lib/configurability.rb
CHANGED
@@ -9,10 +9,10 @@ require 'yaml'
|
|
9
9
|
module Configurability
|
10
10
|
|
11
11
|
# Library version constant
|
12
|
-
VERSION = '1.0.
|
12
|
+
VERSION = '1.0.1'
|
13
13
|
|
14
14
|
# Version-control revision constant
|
15
|
-
REVISION = %q$Revision:
|
15
|
+
REVISION = %q$Revision: 9da882b82786 $
|
16
16
|
|
17
17
|
require 'configurability/logformatter.rb'
|
18
18
|
|
@@ -84,6 +84,9 @@ module Configurability
|
|
84
84
|
### +config_key+, the object's #configure method is called with +nil+
|
85
85
|
### instead.
|
86
86
|
def self::configure_objects( config )
|
87
|
+
self.log.debug "Splitting up config %p between %d objects with configurability." %
|
88
|
+
[ config, self.configurable_objects.length ]
|
89
|
+
|
87
90
|
self.configurable_objects.each do |obj|
|
88
91
|
section = obj.config_key.to_sym
|
89
92
|
self.log.debug "Configuring %p with the %p section of the config." %
|
data/rake/documentation.rb
CHANGED
@@ -53,6 +53,31 @@ begin
|
|
53
53
|
class YARD::RegistryStore; include YardGlobals; end
|
54
54
|
class YARD::Docstring; include YardGlobals; end
|
55
55
|
module YARD::Templates::Helpers::ModuleHelper; include YardGlobals; end
|
56
|
+
|
57
|
+
if vvec(RUBY_VERSION) >= vvec("1.9.1")
|
58
|
+
# Monkeypatched to allow more than two '#' characters at the beginning
|
59
|
+
# of the comment line.
|
60
|
+
# Patched from yard-0.5.8
|
61
|
+
require 'yard/parser/ruby/ruby_parser'
|
62
|
+
class YARD::Parser::Ruby::RipperParser < Ripper
|
63
|
+
def on_comment(comment)
|
64
|
+
$stderr.puts "Adding comment: %p" % [ comment ]
|
65
|
+
visit_ns_token(:comment, comment)
|
66
|
+
|
67
|
+
comment = comment.gsub(/^\#+\s{0,1}/, '').chomp
|
68
|
+
append_comment = @comments[lineno - 1]
|
69
|
+
|
70
|
+
if append_comment && @comments_last_column == column
|
71
|
+
@comments.delete(lineno - 1)
|
72
|
+
comment = append_comment + "\n" + comment
|
73
|
+
end
|
74
|
+
|
75
|
+
@comments[lineno] = comment
|
76
|
+
@comments_last_column = column
|
77
|
+
end
|
78
|
+
end # class YARD::Parser::Ruby::RipperParser
|
79
|
+
end
|
80
|
+
|
56
81
|
# </metamonkeypatch>
|
57
82
|
|
58
83
|
YARD_OPTIONS = [] unless defined?( YARD_OPTIONS )
|
data/rake/hg.rb
CHANGED
@@ -215,13 +215,20 @@ unless defined?( HG_DOTDIR )
|
|
215
215
|
paths = get_repo_paths()
|
216
216
|
if origin_url = paths['default']
|
217
217
|
ask_for_confirmation( "Pull and update from '#{origin_url}'?", false ) do
|
218
|
-
|
218
|
+
Rake::Task['hg:pull_without_confirmation'].invoke
|
219
219
|
end
|
220
220
|
else
|
221
221
|
trace "Skipping pull: No 'default' path."
|
222
222
|
end
|
223
223
|
end
|
224
224
|
|
225
|
+
|
226
|
+
desc "Pull and update without confirmation"
|
227
|
+
task :pull_without_confirmation do
|
228
|
+
run 'hg', 'pull', '-u'
|
229
|
+
end
|
230
|
+
|
231
|
+
|
225
232
|
desc "Check the current code in if tests pass"
|
226
233
|
task :checkin => ['hg:pull', 'hg:newfiles', 'test', COMMIT_MSG_FILE] do
|
227
234
|
targets = get_target_args()
|
@@ -0,0 +1,311 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
BEGIN {
|
4
|
+
require 'pathname'
|
5
|
+
basedir = Pathname.new( __FILE__ ).dirname.parent.parent
|
6
|
+
|
7
|
+
libdir = basedir + "lib"
|
8
|
+
|
9
|
+
$LOAD_PATH.unshift( libdir ) unless $LOAD_PATH.include?( libdir )
|
10
|
+
}
|
11
|
+
|
12
|
+
require 'tempfile'
|
13
|
+
require 'logger'
|
14
|
+
require 'fileutils'
|
15
|
+
|
16
|
+
require 'spec'
|
17
|
+
require 'spec/lib/helpers'
|
18
|
+
|
19
|
+
require 'configurability/config'
|
20
|
+
|
21
|
+
|
22
|
+
|
23
|
+
#####################################################################
|
24
|
+
### C O N T E X T S
|
25
|
+
#####################################################################
|
26
|
+
describe Configurability::Config do
|
27
|
+
include Configurability::SpecHelpers
|
28
|
+
|
29
|
+
TEST_CONFIG = %{
|
30
|
+
---
|
31
|
+
section:
|
32
|
+
subsection:
|
33
|
+
subsubsection: value
|
34
|
+
listsection:
|
35
|
+
- list
|
36
|
+
- values
|
37
|
+
- are
|
38
|
+
- neat
|
39
|
+
mergekey: Yep.
|
40
|
+
textsection: |-
|
41
|
+
With some text as the value
|
42
|
+
...and another line.
|
43
|
+
}.gsub(/^\t/, '')
|
44
|
+
|
45
|
+
|
46
|
+
before( :all ) do
|
47
|
+
setup_logging( :fatal )
|
48
|
+
end
|
49
|
+
|
50
|
+
after( :all ) do
|
51
|
+
reset_logging()
|
52
|
+
end
|
53
|
+
|
54
|
+
it "can dump itself as YAML" do
|
55
|
+
Configurability::Config.new.dump.should == "--- {}\n\n"
|
56
|
+
end
|
57
|
+
|
58
|
+
it "returns nil as its change description" do
|
59
|
+
Configurability::Config.new.changed_reason.should be_nil()
|
60
|
+
end
|
61
|
+
|
62
|
+
it "autogenerates accessors for non-existant struct members" do
|
63
|
+
config = Configurability::Config.new
|
64
|
+
config.plugins.filestore.maxsize = 1024
|
65
|
+
config.plugins.filestore.maxsize.should == 1024
|
66
|
+
end
|
67
|
+
|
68
|
+
it "merges values loaded from the config with any defaults given" do
|
69
|
+
config = Configurability::Config.new( TEST_CONFIG, :defaultkey => "Oh yeah." )
|
70
|
+
config.defaultkey.should == "Oh yeah."
|
71
|
+
end
|
72
|
+
|
73
|
+
it "yields itself if a block is given at creation" do
|
74
|
+
yielded_self = nil
|
75
|
+
config = Configurability::Config.new { yielded_self = self }
|
76
|
+
yielded_self.should equal( config )
|
77
|
+
end
|
78
|
+
|
79
|
+
it "passes itself as the block argument if a block of arity 1 is given at creation" do
|
80
|
+
arg_self = nil
|
81
|
+
yielded_self = nil
|
82
|
+
config = Configurability::Config.new do |arg|
|
83
|
+
yielded_self = self
|
84
|
+
arg_self = arg
|
85
|
+
end
|
86
|
+
yielded_self.should_not equal( config )
|
87
|
+
arg_self.should equal( config )
|
88
|
+
end
|
89
|
+
|
90
|
+
it "supports both Symbols and Strings for Hash-like access" do
|
91
|
+
config = Configurability::Config.new( TEST_CONFIG )
|
92
|
+
config[:section]['subsection'][:subsubsection].should == 'value'
|
93
|
+
end
|
94
|
+
|
95
|
+
|
96
|
+
describe "created with in-memory YAML source" do
|
97
|
+
|
98
|
+
before(:each) do
|
99
|
+
@config = Configurability::Config.new( TEST_CONFIG )
|
100
|
+
end
|
101
|
+
|
102
|
+
it "responds to methods which are the same as struct members" do
|
103
|
+
@config.should respond_to( :section )
|
104
|
+
@config.section.should respond_to( :subsection )
|
105
|
+
@config.should_not respond_to( :pork_sausage )
|
106
|
+
end
|
107
|
+
|
108
|
+
it "contains values specified in the source" do
|
109
|
+
# section:
|
110
|
+
# subsection:
|
111
|
+
# subsubsection: value
|
112
|
+
@config.section.subsection.subsubsection.should == 'value'
|
113
|
+
|
114
|
+
# listsection:
|
115
|
+
# - list
|
116
|
+
# - values
|
117
|
+
# - are
|
118
|
+
# - neat
|
119
|
+
@config.listsection.should == %w[list values are neat]
|
120
|
+
|
121
|
+
# mergekey: Yep.
|
122
|
+
@config.mergekey.should == 'Yep.'
|
123
|
+
|
124
|
+
# textsection: |-
|
125
|
+
# With some text as the value
|
126
|
+
# ...and another line.
|
127
|
+
@config.textsection.should == "With some text as the value\n...and another line."
|
128
|
+
end
|
129
|
+
|
130
|
+
it "returns struct members as an Array of Symbols" do
|
131
|
+
@config.members.should be_an_instance_of( Array )
|
132
|
+
@config.members.should have_at_least( 4 ).things
|
133
|
+
@config.members.each do |member|
|
134
|
+
member.should be_an_instance_of( Symbol)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
it "is able to iterate over sections" do
|
139
|
+
@config.each do |key, struct|
|
140
|
+
key.should be_an_instance_of( Symbol)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
it "dumps values specified in the source" do
|
145
|
+
@config.dump.should =~ /^section:/
|
146
|
+
@config.dump.should =~ /^\s+subsection:/
|
147
|
+
@config.dump.should =~ /^\s+subsubsection:/
|
148
|
+
@config.dump.should =~ /^- list/
|
149
|
+
end
|
150
|
+
|
151
|
+
it "provides a human-readable description of itself when inspected" do
|
152
|
+
@config.inspect.should =~ /4 sections/i
|
153
|
+
@config.inspect.should =~ /mergekey/
|
154
|
+
@config.inspect.should =~ /textsection/
|
155
|
+
@config.inspect.should =~ /from memory/i
|
156
|
+
end
|
157
|
+
|
158
|
+
it "raises an exception when reloaded" do
|
159
|
+
expect {
|
160
|
+
@config.reload
|
161
|
+
}.to raise_exception( RuntimeError, /can't reload from an in-memory source/i )
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|
165
|
+
|
166
|
+
|
167
|
+
# saving if changed since loaded
|
168
|
+
describe " whose internal values have been changed since loaded" do
|
169
|
+
before(:each) do
|
170
|
+
@config = Configurability::Config.new( TEST_CONFIG )
|
171
|
+
@config.section.subsection.anothersection = 11451
|
172
|
+
end
|
173
|
+
|
174
|
+
|
175
|
+
### Specifications
|
176
|
+
it "should report that it is changed" do
|
177
|
+
@config.changed?.should == true
|
178
|
+
end
|
179
|
+
|
180
|
+
it "should report that its internal struct was modified as the reason for the change" do
|
181
|
+
@config.changed_reason.should =~ /struct was modified/i
|
182
|
+
end
|
183
|
+
|
184
|
+
end
|
185
|
+
|
186
|
+
|
187
|
+
# loading from a file
|
188
|
+
describe " loaded from a file" do
|
189
|
+
before(:all) do
|
190
|
+
@tmpfile = Tempfile.new( 'test.conf', '.' )
|
191
|
+
@tmpfile.print( TEST_CONFIG )
|
192
|
+
@tmpfile.close
|
193
|
+
end
|
194
|
+
|
195
|
+
after(:all) do
|
196
|
+
@tmpfile.delete
|
197
|
+
end
|
198
|
+
|
199
|
+
|
200
|
+
before(:each) do
|
201
|
+
@config = Configurability::Config.load( @tmpfile.path )
|
202
|
+
end
|
203
|
+
|
204
|
+
|
205
|
+
### Specifications
|
206
|
+
it "remembers which file it was loaded from" do
|
207
|
+
@config.path.should == Pathname( @tmpfile.path ).expand_path
|
208
|
+
end
|
209
|
+
|
210
|
+
it "writes itself back to the same file by default" do
|
211
|
+
@config.port = 114411
|
212
|
+
@config.write
|
213
|
+
otherconfig = Configurability::Config.load( @tmpfile.path )
|
214
|
+
|
215
|
+
otherconfig.port.should == 114411
|
216
|
+
end
|
217
|
+
|
218
|
+
it "includes the name of the file in its inspect output" do
|
219
|
+
@config.inspect.should include( File.basename(@tmpfile.path) )
|
220
|
+
end
|
221
|
+
|
222
|
+
it "yields itself if a block is given at load-time" do
|
223
|
+
yielded_self = nil
|
224
|
+
config = Configurability::Config.load( @tmpfile.path ) do
|
225
|
+
yielded_self = self
|
226
|
+
end
|
227
|
+
yielded_self.should equal( config )
|
228
|
+
end
|
229
|
+
|
230
|
+
it "passes itself as the block argument if a block of arity 1 is given at load-time" do
|
231
|
+
arg_self = nil
|
232
|
+
yielded_self = nil
|
233
|
+
config = Configurability::Config.load( @tmpfile.path ) do |arg|
|
234
|
+
yielded_self = self
|
235
|
+
arg_self = arg
|
236
|
+
end
|
237
|
+
yielded_self.should_not equal( config )
|
238
|
+
arg_self.should equal( config )
|
239
|
+
end
|
240
|
+
|
241
|
+
it "doesn't re-read its source file if it hasn't changed" do
|
242
|
+
@config.path.should_not_receive( :read )
|
243
|
+
Configurability.should_not_receive( :configure_objects )
|
244
|
+
@config.reload.should be_false()
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
|
249
|
+
# reload if file changes
|
250
|
+
describe " whose file changes after loading" do
|
251
|
+
before(:all) do
|
252
|
+
@tmpfile = Tempfile.new( 'test.conf', '.' )
|
253
|
+
@tmpfile.print( TEST_CONFIG )
|
254
|
+
@tmpfile.close
|
255
|
+
end
|
256
|
+
|
257
|
+
after(:all) do
|
258
|
+
@tmpfile.delete
|
259
|
+
end
|
260
|
+
|
261
|
+
|
262
|
+
before(:each) do
|
263
|
+
old_date = Time.now - 3600
|
264
|
+
File.utime( old_date, old_date, @tmpfile.path )
|
265
|
+
@config = Configurability::Config.load( @tmpfile.path )
|
266
|
+
now = Time.now + 10
|
267
|
+
File.utime( now, now, @tmpfile.path )
|
268
|
+
end
|
269
|
+
|
270
|
+
|
271
|
+
### Specifications
|
272
|
+
it "reports that it is changed" do
|
273
|
+
@config.should be_changed
|
274
|
+
end
|
275
|
+
|
276
|
+
it "reports that its source was updated as the reason for the change" do
|
277
|
+
@config.changed_reason.should =~ /source.*updated/i
|
278
|
+
end
|
279
|
+
|
280
|
+
it "re-reads its file when reloaded" do
|
281
|
+
@config.path.should_receive( :read ).and_return( TEST_CONFIG )
|
282
|
+
Configurability.should_receive( :configure_objects ).with( @config )
|
283
|
+
@config.reload.should be_true()
|
284
|
+
end
|
285
|
+
|
286
|
+
it "reapplies its defaults when reloading" do
|
287
|
+
config = Configurability::Config.load( @tmpfile.path, :defaultskey => 8 )
|
288
|
+
config.reload
|
289
|
+
config.defaultskey.should == 8
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
|
294
|
+
# merging
|
295
|
+
describe " created by merging two other configs" do
|
296
|
+
before(:each) do
|
297
|
+
@config1 = Configurability::Config.new
|
298
|
+
@config2 = Configurability::Config.new( TEST_CONFIG )
|
299
|
+
@merged = @config1.merge( @config2 )
|
300
|
+
end
|
301
|
+
|
302
|
+
|
303
|
+
### Specifications
|
304
|
+
it "should contain values from both" do
|
305
|
+
@merged.mergekey.should == @config2.mergekey
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
end
|
310
|
+
|
311
|
+
# vim: set nosta noet ts=4 sw=4:
|
metadata
CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
|
|
5
5
|
segments:
|
6
6
|
- 1
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
version: 1.0.
|
8
|
+
- 1
|
9
|
+
version: 1.0.1
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Michael Granger
|
@@ -14,11 +14,16 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2010-
|
17
|
+
date: 2010-08-08 00:00:00 -07:00
|
18
18
|
default_executable:
|
19
19
|
dependencies: []
|
20
20
|
|
21
|
-
description:
|
21
|
+
description: |-
|
22
|
+
Configurability is a mixin that allows you to add
|
23
|
+
configurability to one or more classes, assign them
|
24
|
+
each a section of the configuration, and then when
|
25
|
+
the configuration is loaded, the class is given the
|
26
|
+
section it requested.
|
22
27
|
email:
|
23
28
|
- ged@FaerieMUD.org
|
24
29
|
executables: []
|
@@ -34,8 +39,11 @@ files:
|
|
34
39
|
- ChangeLog
|
35
40
|
- README.md
|
36
41
|
- LICENSE
|
42
|
+
- spec/configurability/config_spec.rb
|
37
43
|
- spec/configurability_spec.rb
|
38
44
|
- spec/lib/helpers.rb
|
45
|
+
- lib/configurability/behavior.rb
|
46
|
+
- lib/configurability/config.rb
|
39
47
|
- lib/configurability/logformatter.rb
|
40
48
|
- lib/configurability.rb
|
41
49
|
- rake/191_compat.rb
|
@@ -86,5 +94,6 @@ signing_key:
|
|
86
94
|
specification_version: 3
|
87
95
|
summary: A configurability mixin for Ruby
|
88
96
|
test_files:
|
97
|
+
- spec/configurability/config_spec.rb
|
89
98
|
- spec/configurability_spec.rb
|
90
99
|
- spec/lib/helpers.rb
|