courtier 0.2.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.
@@ -0,0 +1,249 @@
1
+ module Courtier
2
+
3
+ # Config encapsulates a single configuration entry as defined
4
+ # in a project's configuration file.
5
+ #
6
+ class Config
7
+
8
+ #
9
+ # Initialize Config instance. Config instances are per-configuration,
10
+ # which means they are associated with one and only one config entry.
11
+ #
12
+ # @param [#to_sym] tool
13
+ # The tool's name.
14
+ #
15
+ # @param [#to_sym,nil] profile
16
+ # Profile name, or +nil+.
17
+ #
18
+ # @param [Hash] properties
19
+ # Any additional properties associated with the config entry.
20
+ #
21
+ def initialize(tool, properties={}, &block)
22
+ @property = {:profile=>'default'}
23
+
24
+ @property[:command] = tool.to_s
25
+ @property[:feature] = tool.to_s
26
+
27
+ @block = block
28
+
29
+ properties.each do |k, v|
30
+ property(k,v)
31
+ end
32
+ end
33
+
34
+ #
35
+ # Get/set property.
36
+ #
37
+ def property(name, value=ArgumentError)
38
+ name = name.to_sym
39
+
40
+ return @property[name] if value == ArgumentError
41
+
42
+ case name
43
+ when :feature, :command, :profile
44
+ @property[name] = value.to_s
45
+ else
46
+ @property[name] = value
47
+ end
48
+ end
49
+
50
+ #
51
+ # The feature being configured.
52
+ #
53
+ def feature
54
+ @property[:feature]
55
+ end
56
+
57
+ #
58
+ # The name of command being configured.
59
+ #
60
+ def command
61
+ @property[:command]
62
+ end
63
+
64
+ #
65
+ # @todo Deprecate?
66
+ #
67
+ alias :tool :command
68
+
69
+ #
70
+ # The name of the profile to which this configuration belongs.
71
+ #
72
+ def profile
73
+ @property[:profile]
74
+ end
75
+
76
+ #
77
+ # The library from which this configuration derives.
78
+ # This is a shortcut for `property(:from)`.
79
+ #
80
+ def from
81
+ @property[:from]
82
+ end
83
+
84
+ #
85
+ #
86
+ #
87
+ def onload?
88
+ @property[:onload]
89
+ end
90
+
91
+ #
92
+ # Most configuration are scripted. In thos cases the
93
+ # `@block` attributes holds the Proc instance, otherwise
94
+ # it is `nil`.
95
+ #
96
+ attr :block
97
+
98
+ #
99
+ # IDEA: Presets would be processed first and not require the underlying feature.
100
+ #
101
+ #def preset?
102
+ # @property[:preset]
103
+ #end
104
+
105
+ #
106
+ # The arity of the configuration procedure.
107
+ #
108
+ # @return [Fixnum] number of arguments
109
+ #
110
+ def arity
111
+ @block ? @block.arity : 0
112
+ end
113
+
114
+ #
115
+ # Require the feature.
116
+ #
117
+ def require_feature
118
+ begin
119
+ require feature
120
+ rescue LoadError
121
+ #warn "No such feature -- `#{feature}'"
122
+ end
123
+ end
124
+
125
+ #
126
+ # Call the configuration procedure.
127
+ #
128
+ def call(*args)
129
+ block.call(*args) if block
130
+ end
131
+
132
+ #
133
+ # Returns underlying block.
134
+ #
135
+ def to_proc
136
+ block
137
+ end
138
+
139
+ ##
140
+ ## Convert block into a Hash.
141
+ ##
142
+ ## @return [Hash]
143
+ ##
144
+ #def to_h
145
+ # HashBuilder.new(&self)).to_h
146
+ #end
147
+
148
+ #
149
+ # Copy the configuration with alterations.
150
+ #
151
+ # @param [Hash] alt
152
+ # Alternate values for configuration attributes.
153
+ #
154
+ # @return [Config] copied config
155
+ #
156
+ def copy(alt={})
157
+ tool = @property[:feature] || @property[:command]
158
+ copy = self.class.new(tool, @property.dup, &@block)
159
+ alt.each do |k,v|
160
+ copy.property(k, v)
161
+ end
162
+ copy
163
+ end
164
+
165
+ #
166
+ # Match config against tool and/or profile names.
167
+ #
168
+ # @return [Boolean]
169
+ #
170
+ def match?(*args)
171
+ props = Hash === args.last ? args.pop : {}
172
+
173
+ if tool = args.shift
174
+ props[:command] = tool.to_s
175
+ props[:feature] = tool.to_s
176
+ end
177
+
178
+ if props[:profile]
179
+ props[:profile] = (props[:profile] || :default).to_s
180
+ end
181
+
182
+ props.each do |k,v|
183
+ return false unless property(k) == v
184
+ end
185
+
186
+ return true
187
+ end
188
+
189
+ #
190
+ # Does the given `feature` match the config's feature?
191
+ #
192
+ # @return [Boolean]
193
+ #
194
+ def feature?(feature=Courtier.current_feature)
195
+ self.feature == feature.to_s
196
+ end
197
+
198
+ #
199
+ # Does the given `command` match the config's command?
200
+ #
201
+ # @return [Boolean]
202
+ #
203
+ def command?(command=Courtier.current_command)
204
+ self.command == command.to_s
205
+ end
206
+
207
+ #
208
+ # Does the given `profile` match the config's profile?
209
+ #
210
+ # @return [Boolean]
211
+ #
212
+ def profile?(profile=Courtier.current_profile)
213
+ self.profile == (profile || :default).to_s
214
+ end
215
+
216
+ #
217
+ # @todo The feature argument might not be needed.
218
+ #
219
+ #def configure(feature)
220
+ # return false if self.feature != feature
221
+ #
222
+ # if setup = Courtier.setup(feature)
223
+ # setup.call(self)
224
+ # else
225
+ # block.call if command?
226
+ # end
227
+ #end
228
+
229
+ ##
230
+ ## Ruby 1.9 defines #inspect as #to_s, ugh.
231
+ ##
232
+ #def inspect
233
+ # "#<#{self.class.name}:#{object_id} @tool=%s @profile=%s>" % [tool.inspect, profile.inspect]
234
+ #end
235
+
236
+ #
237
+ # Does the configuration apply?
238
+ #
239
+ # @return [Boolean]
240
+ #
241
+ def apply?()
242
+ return false unless command? if command
243
+ return false unless profile? if profile
244
+ return true
245
+ end
246
+
247
+ end
248
+
249
+ end
@@ -0,0 +1,104 @@
1
+ module Courtier
2
+
3
+ #
4
+ class ConfigFilter
5
+ include Enumerable
6
+
7
+ #
8
+ # Initialize new ConfigFilter.
9
+ #
10
+ # @param [Array<Config>] List if Config instances.
11
+ #
12
+ def initialize(configuration, criteria={})
13
+ @configuration = configuration
14
+ @criteria = criteria
15
+
16
+ @list = []
17
+
18
+ configuration.each do |c|
19
+ @list << c if c.match?(criteria)
20
+ end
21
+ end
22
+
23
+ #
24
+ #
25
+ #
26
+ def tool
27
+ @criteria[:tool]
28
+ end
29
+
30
+ #
31
+ #
32
+ #
33
+ def profile
34
+ @criteria[:profile]
35
+ end
36
+
37
+ #
38
+ # Returns list of profiles.
39
+ #
40
+ def profiles
41
+ @list.map{ |c| c.profile }
42
+ end
43
+
44
+ #
45
+ #
46
+ #
47
+ def [](subset)
48
+ return method_missing(:[]) if profile
49
+ if tool
50
+ criteria = @criteria.dup
51
+ criteria[:profile] = subset
52
+ else
53
+ criteria = @criteria.dup
54
+ criteria[:tool] = subset
55
+ end
56
+ self.class.new(@configuration, criteria)
57
+ end
58
+
59
+ #
60
+ #
61
+ #
62
+ def each(&block)
63
+ @list.each do
64
+ block
65
+ end
66
+ end
67
+
68
+ #
69
+ #
70
+ #
71
+ def size
72
+ @list.size
73
+ end
74
+
75
+ #
76
+ # Call each config.
77
+ #
78
+ def call(*args)
79
+ @list.each do |c|
80
+ if c.profile?(RC.current_profile)
81
+ c.call(*args)
82
+ end
83
+ end
84
+ end
85
+
86
+ #
87
+ # Convert to Proc.
88
+ #
89
+ def to_proc(exec=false)
90
+ list = @list
91
+ if exec
92
+ Proc.new do |*args|
93
+ list.each{ |c| instance_exec(*args, &c) }
94
+ end
95
+ else
96
+ Proc.new do |*args|
97
+ list.each{ |c| c.call(*args) }
98
+ end
99
+ end
100
+ end
101
+
102
+ end
103
+
104
+ end
@@ -0,0 +1,335 @@
1
+ module Courtier
2
+
3
+ # The Configuration class encapsulates a project/library's tool
4
+ # configuration.
5
+ #
6
+ class Configuration < Module
7
+
8
+ #
9
+ # Configuration is Enumerable.
10
+ #
11
+ include Enumerable
12
+
13
+ #
14
+ # Configuration file pattern. The standard configuration file name is
15
+ # `Config.rb`, and that name should be used in most cases. However,
16
+ # `.config.rb` can also be use and will take precedence if found.
17
+ # Conversely, `config.rb` (lowercase form) can also be used but has
18
+ # the least precedence.
19
+ #
20
+ # Config files looked for in the order or precedence:
21
+ #
22
+ # * `.config.rb` or `.confile.rb`
23
+ # * `Config.rb` or `Confile.rb`
24
+ # * `config.rb` or `confile.rb`
25
+ #
26
+ # The `.rb` suffix is optional for `confile` variations, but recommended.
27
+ # It is not optional for `config` b/c very old version of setup.rb script
28
+ # still in use by some projects use `.config` for it's own purposes.
29
+ #
30
+ # TODO: Yes, there are really too many choices for config file name, but
31
+ # we haven't been able to settle on a smaller list just yet. Please come
32
+ # argue with us about what's best.
33
+ #
34
+ CONFIG_FILE = '{.c,C,c}onfi{g.rb,le.rb,le}'
35
+
36
+ #
37
+ # When looking up config file, it one of these is found
38
+ # then there is no point to looking further.
39
+ #
40
+ ROOT_INDICATORS = %w{.git .hg _darcs .ruby}
41
+
42
+ #
43
+ # Load configuration file from local project or other gem.
44
+ #
45
+ # @param options [Hash] Load options.
46
+ #
47
+ # @option options [String] :from
48
+ # Name of gem or library.
49
+ #
50
+ def self.load(options={})
51
+ if from = options[:from]
52
+ file = Find.path(CONFIG_FILE, :from=>from).first
53
+ else
54
+ file = lookup(CONFIG_FILE)
55
+ end
56
+ new(file)
57
+ end
58
+
59
+ #
60
+ # Initialize new Configuration object.
61
+ #
62
+ # @param [String] file
63
+ # Configuration file (optional).
64
+ #
65
+ def initialize(file=nil)
66
+ @file = file
67
+
68
+ @_config = Hash.new{ |h,k| h[k]=[] }
69
+ #@_onload = Hash.new{ |h,k| h[k]=[] }
70
+
71
+ # TODO: does this rescue make sense here?
72
+ begin
73
+ dsl = DSL.new(self)
74
+ dsl.instance_eval(File.read(file), file) if file
75
+ rescue => e
76
+ raise e if $DEBUG
77
+ warn e.message
78
+ end
79
+ end
80
+
81
+ #
82
+ def evaluate(*args, &block)
83
+ dsl = DSL.new(self)
84
+ dsl.instance_eval(*args, &block)
85
+ end
86
+
87
+ #
88
+ # Configure a tool.
89
+ #
90
+ # @param [Symbol] tool
91
+ # The name of the command or feature to configure.
92
+ #
93
+ # @param [Hash] opts
94
+ # Configuration options.
95
+ #
96
+ # @options opts [String] :command
97
+ # Name of command, or false if not a command configuration.
98
+ #
99
+ # @options opts [String] :feature
100
+ # Alternate require if differnt than command name.
101
+ #
102
+ # @options opts [String] :from
103
+ # Library from which to import configuration.
104
+ #
105
+ # @example
106
+ # profile :coverage do
107
+ # config :qed, :from=>'qed'
108
+ # end
109
+ #
110
+ def config(target, options={}, &block)
111
+ #options[:profile] = (options[:profile] || 'default').to_s
112
+ #options[:command] = command.to_s unless options.key?(:command)
113
+ #options[:feature] = command.to_s unless options.key?(:feature)
114
+ #command = options[:command].to_s
115
+
116
+ # IDEA: other import options such as local file?
117
+
118
+ configs_from(options).each do |c|
119
+ @_config[target.to_s] << c.copy(options)
120
+ end
121
+
122
+ return unless block
123
+
124
+ @_config[target.to_s] << Config.new(target, options, &block)
125
+ end
126
+
127
+ =begin
128
+ #
129
+ #
130
+ #
131
+ def onload(feature, options={}, &block)
132
+ #options[:profile] = (options[:profile] || 'default').to_s
133
+
134
+ #options[:feature] = feature.to_s unless options.key?(:feature)
135
+ #options[:command] = feature.to_s unless options.key?(:command)
136
+
137
+ feature = options[:feature].to_s
138
+
139
+ # IDEA: what about local file import?
140
+ configs_from(options).each do |c|
141
+ @_onload[feature] << c.copy(options)
142
+ end
143
+
144
+ return unless block
145
+
146
+ @_onload[feature] << Config.new(feature, options, &block)
147
+ end
148
+ =end
149
+
150
+ #
151
+ #
152
+ #
153
+ def [](feature)
154
+ @_config[feature.to_s]
155
+ end
156
+
157
+ #
158
+ # Iterate over each feature config.
159
+ #
160
+ # @example
161
+ # confgiuration.each do |feature, configs|
162
+ # configs.each do |config|
163
+ # ...
164
+ # end
165
+ # end
166
+ #
167
+ def each(&block)
168
+ @_config.each(&block)
169
+ end
170
+
171
+ #
172
+ # The number of feature configs.
173
+ #
174
+ def size
175
+ @_config.size
176
+ end
177
+
178
+ #
179
+ # Get a list of the defined configurations.
180
+ #
181
+ # @return [Array] List of all defined configurations.
182
+ #
183
+ def to_a
184
+ list = []
185
+ @_config.each do |feature, configs|
186
+ list.concat(configs)
187
+ end
188
+ list
189
+ end
190
+
191
+ #
192
+ # @deprecated
193
+ #
194
+ alias :configurations :to_a
195
+
196
+ #
197
+ # Get a list of defined profiles names for the given +command+.
198
+ # use the current command if no +command+ is given.
199
+ #
200
+ def profile_names(command=nil)
201
+ command = command || Courtier.current_command
202
+
203
+ list = []
204
+ @_config.each do |feature, configs|
205
+ configs.each do |c|
206
+ if c.command?(command)
207
+ list << c.profile
208
+ end
209
+ end
210
+ end
211
+ list.uniq
212
+ end
213
+
214
+ #def inspect
215
+ # "#<Courtier::Configuration:#{object_id} @file=#{@file}>"
216
+ #end
217
+
218
+ private
219
+
220
+ #
221
+ # Search upward from working directory.
222
+ #
223
+ def self.lookup(glob, flags=0)
224
+ pwd = File.expand_path(Dir.pwd)
225
+ home = File.expand_path('~')
226
+ while pwd != '/' && pwd != home
227
+ if file = Dir.glob(File.join(pwd, glob), flags).first
228
+ return file
229
+ end
230
+ break if ROOT_INDICATORS.any?{ |r| File.exist?(File.join(pwd, r)) }
231
+ pwd = File.dirname(pwd)
232
+ end
233
+ return nil
234
+ end
235
+
236
+ # TODO: other import options such as local file?
237
+
238
+ #
239
+ #
240
+ #
241
+ def configs_from(options)
242
+ from = options[:from]
243
+ list = []
244
+
245
+ return list unless from
246
+
247
+ if Array === from
248
+ from_name, from_opts = *from
249
+ else
250
+ from_name, from_opts = from, {}
251
+ end
252
+
253
+ from_config = Courtier.configuration(from_name)
254
+
255
+ from_opts[:feature] = options[:feature] unless from_opts.key?(:feature) if options[:feature]
256
+ from_opts[:command] = options[:command] unless from_opts.key?(:command) if options[:command]
257
+ from_opts[:profile] = options[:profile] unless from_opts.key?(:profile) if options[:profile]
258
+
259
+ from_opts[:feature] = from_opts[:feature].to_s if from_opts[:feature]
260
+ from_opts[:command] = from_opts[:command].to_s if from_opts[:command]
261
+ from_opts[:profile] = from_opts[:profile].to_s if from_opts[:profile]
262
+
263
+ from_config.each do |ftr, confs|
264
+ confs.each_with_index do |c, i|
265
+ if c.match?(from_opts)
266
+ list << c.copy(options)
267
+ end
268
+ end
269
+ end
270
+
271
+ list
272
+ end
273
+
274
+ #
275
+ class DSL
276
+
277
+ #
278
+ #
279
+ #
280
+ def initialize(configuration)
281
+ @configuration = configuration
282
+ @_options = {}
283
+ end
284
+
285
+ #
286
+ #
287
+ #
288
+ def profile(name, &block)
289
+ raise SyntaxError, "nested profile sections" if @_options[:profile]
290
+ @_options[:profile] = name.to_s
291
+ instance_eval(&block)
292
+ @_options.delete(:profile)
293
+ end
294
+
295
+ #
296
+ # Profile block.
297
+ #
298
+ # @param [String,Symbol] name
299
+ # A profile name.
300
+ #
301
+ def profile(name, state={}, &block)
302
+ raise SyntaxError, "nested profile sections" if @_options[:profile]
303
+ original_state = @_options.dup
304
+ @_options.update(state)
305
+ @_options[:profile] = name.to_s
306
+ instance_eval(&block)
307
+ @_options = original_state
308
+ end
309
+
310
+ #
311
+ #
312
+ def config(command, options={}, &block)
313
+ nested_keys = @_options.keys & options.keys.map{|k| k.to_sym}
314
+ raise ArgumentError, "nested #{nested_keys.join(', ')}" unless nested_keys.empty?
315
+
316
+ options = @_options.merge(options)
317
+ @configuration.config(command, options, &block)
318
+ end
319
+
320
+ #
321
+ #
322
+ def onload(feature, options={}, &block)
323
+ nested_keys = @_options.keys & options.keys.map{|k| k.to_sym}
324
+ raise ArgumentError, "nested #{nested_keys.join(', ')}" unless nested_keys.empty?
325
+
326
+ options = @_options.merge(options)
327
+ options[:onload] = true
328
+ @configuration.config(feature, options, &block)
329
+ end
330
+
331
+ end
332
+
333
+ end
334
+
335
+ end