courtier 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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