command-set 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,456 @@
1
+ require 'command-set/arguments'
2
+ module Command
3
+
4
+ #A thin wrapper on Array to maintain undo/redo state.
5
+ class UndoStack
6
+ def initialize()
7
+ @stack = []
8
+ @now = 0
9
+ end
10
+
11
+ def add(cmd)
12
+ @stack.slice!(0,@now)
13
+ @now=0
14
+ @stack.unshift(cmd)
15
+ end
16
+
17
+ def get_undo
18
+ if @now > (@stack.length - 1) or @stack.length == 0
19
+ raise CommandException, "No more commands to undo"
20
+ end
21
+ cmd = @stack[@now]
22
+ @now+=1
23
+ return cmd
24
+ end
25
+
26
+ def get_redo
27
+ if @now <= 0
28
+ raise CommandException, "Can't redo"
29
+ end
30
+ @now-=1
31
+ return @stack[@now]
32
+ end
33
+ end
34
+
35
+ #An abstraction of the lifecycle of a command. Allows invocations to be
36
+ #postponed temporarily, or for the command to be instantiated and still
37
+ #passed around. Client code should almost never need to see this class,
38
+ #so the methods aren't individually documented.
39
+ class CommandSetup
40
+ def initialize(cmd = [], args = {})
41
+ @task_id = nil
42
+ @args_hash = args
43
+ @terms = []
44
+ @command = cmd
45
+ end
46
+
47
+ attr_accessor :task_id, :command, :args_hash, :terms
48
+
49
+ def resolve_command_class(command_set)
50
+ if Class === @command && Command > @command
51
+ return @command
52
+ end
53
+
54
+ command_path = @command
55
+ @command,terms = command_set.find_command(*command_path)
56
+
57
+ if CommandSet === @command
58
+ if (cmd = @command.get_root).nil?
59
+ raise CommandException, "Incomplete command #{command_path.join(" ")}"
60
+ else
61
+ terms = [@command]
62
+ @command = cmd
63
+ end
64
+ end
65
+
66
+ @terms = terms
67
+
68
+ return @command
69
+ end
70
+
71
+ def assign_terms(cmd)
72
+ terms = @terms.dup
73
+ cmd.each_consumer do |consumer|
74
+ terms = consumer[terms]
75
+ end
76
+
77
+ cmd.consume_hash(@args_hash)
78
+ end
79
+
80
+ def command_instance(command_set, subject)
81
+ command_class = resolve_command_class(command_set)
82
+ command = command_class.new(subject, task_id)
83
+ assign_terms(command)
84
+ return command
85
+ end
86
+ end
87
+
88
+ #An overworked exception class. It captures details about the command
89
+ #being interrupted as it propagates up the stack.
90
+ class ResumeFrom < ::Exception;
91
+ def initialize(pause_deck = nil, msg = "")
92
+ super(msg)
93
+ @setup = CommandSetup.new
94
+ @pause_deck = pause_deck
95
+ end
96
+
97
+ attr_reader :setup, :pause_deck
98
+ end
99
+
100
+ class ResumeFromOnlyThis < ResumeFrom; end
101
+
102
+ class Command
103
+ @name = "unnamed command"
104
+ @parent = nil
105
+ @argument_list=[]
106
+ @doc_text = nil
107
+ @subject_requirements=[:chain_of_command]
108
+ @defined = false
109
+ @advice_block = proc {}
110
+ class << self
111
+ alias_method :instance, :new
112
+
113
+ attr_reader :name, :argument_list
114
+ alias_method :required_argument_list, :argument_list
115
+ alias_method :optional_argument_list, :argument_list
116
+ attr_reader :doc_text, :subject_requirements, :defined, :advice_block
117
+ attr_accessor :parent
118
+
119
+ def inherited(subclass)
120
+ subclass.instance_variable_set("@name", name.dup)
121
+ subclass.instance_variable_set("@argument_list", argument_list.dup )
122
+ subclass.instance_variable_set("@doc_text", doc_text.dup) unless doc_text.nil?
123
+ subclass.instance_variable_set("@subject_requirements", subject_requirements.dup)
124
+ subclass.instance_variable_set("@defined", false)
125
+ subclass.instance_variable_set("@advice_block", advice_block.dup)
126
+ end
127
+
128
+ #Establishes a subclass of Command. This is important because commands are actually
129
+ #classes in CommandSet; their instances are specific executions of the command, which
130
+ #allows for undo's, and history management.
131
+ #The block will get run in the context of the new class, allowing you to quickly
132
+ #define the class completely.
133
+ #
134
+ #For examples, see Command::StandardCommands
135
+ def setup(parent, new_name=nil, &block)
136
+
137
+ unless parent.nil? or CommandSet === parent
138
+ raise RuntimeError, "Command parents must be CommandSets or nil"
139
+ end
140
+
141
+ command_class = Class.new(self)
142
+ new_name = new_name.to_s
143
+ #Of interest: klass.instance_eval { def method } creates klass::method
144
+ #but klass.class_eval { def method } creates klass#method
145
+
146
+ command_class.instance_variable_set("@name", new_name)
147
+ command_class.instance_variable_set("@parent", parent)
148
+
149
+ command_class.instance_eval &block
150
+
151
+ command_class.defined
152
+ return command_class
153
+ end
154
+
155
+ def defined
156
+ @defined = true
157
+ end
158
+
159
+ def defined?
160
+ return @defined
161
+ end
162
+
163
+ def inspect
164
+ arguments = []
165
+ [required_argument_list, optional_argument_list].each do |list|
166
+ next if list.nil?
167
+ list.each do |argument|
168
+ arguments << argument.name
169
+ end
170
+ end
171
+ return "#<Class:#{self.object_id} - Command:#{path().join(" ")}(#{arguments.join(", ")})>"
172
+ end
173
+
174
+ def path
175
+ if @parent.nil?
176
+ [@name]
177
+ else
178
+ @parent.path + [@name]
179
+ end
180
+ end
181
+
182
+ def documentation(width, parent="")
183
+ if @doc_text.nil?
184
+ return short_docs(width)
185
+ else
186
+ return short_docs(width) +
187
+ ["\n"] + @doc_text.wrap(width)
188
+ end
189
+ end
190
+
191
+ def short_docs(width, parent="")
192
+ docs = ((parent.empty? ? [name]:[parent, name]) + required_argument_list.map do |arg|
193
+ "<#{arg.name}> "
194
+ end + optional_argument_list.map do |arg|
195
+ "[<#{arg.name}>]"
196
+ end).join(" ")
197
+
198
+ return docs.wrap(width)
199
+ end
200
+
201
+ def add_requirements(subject)
202
+ subject.required_fields(*(@subject_requirements.uniq))
203
+ end
204
+
205
+ def completion_list(terms, prefix, subject)
206
+ arguments = argument_list.dup
207
+
208
+ until(terms.empty? or arguments.empty?) do
209
+ arguments.first.match_terms(subject, terms, arguments)
210
+ end
211
+
212
+ completion = []
213
+ arguments.each do |handler|
214
+ completion += handler.complete(prefix, subject)
215
+ break unless handler.omittable?
216
+ end
217
+ return completion
218
+ end
219
+
220
+ def create_argument_methods(name)
221
+ define_method(name) do
222
+ @arg_hash[name]
223
+ end
224
+ private(name)
225
+
226
+ define_method(name.to_s + "=") do |value|
227
+ @arg_hash[name] = value
228
+ end
229
+ private(name.to_s + "=")
230
+ end
231
+
232
+ def embed_argument(arg)
233
+ names = [*arg.name]
234
+
235
+ names.each do |name|
236
+ create_argument_methods(name)
237
+ end
238
+
239
+ unless argument_list.last.nil? or argument_list.last.required?
240
+ if arg.required?
241
+ raise NoMethodError, "Can't define required arguments after optionals"
242
+ end
243
+ end
244
+
245
+ argument_list << arg
246
+ end
247
+
248
+ def optional_argument(arg, values=nil, &get_values)
249
+ optional.argument(arg, values, &get_values)
250
+ end
251
+
252
+ def optional_arguments(name, &block)
253
+ optional.multiword_argument(name, block)
254
+ end
255
+
256
+ def no_more_required_arguments(*args) #:nodoc:
257
+ raise NoMethodError, "Can't define required arguments after optionals"
258
+ end
259
+ private :no_more_required_arguments
260
+
261
+ def stop_requireds #:nodoc:
262
+ class << self
263
+ alias_method :argument, :no_more_required_arguments
264
+ def stop_requireds
265
+ end
266
+ end
267
+ end
268
+ private :stop_requireds
269
+ end
270
+
271
+ extend DSL::CommandDefinition
272
+ extend DSL::Argument
273
+ include DSL::Action
274
+
275
+ def initialize(subject, resume=nil)
276
+ raise CommandException, "#{@name}: unrecognized command" unless self.class.defined?
277
+ fields = subject_requirements || []
278
+ @subject = subject.get_image(fields)
279
+ @puts_box = nil
280
+ @puts_lock = Mutex.new
281
+ @arg_hash = {}
282
+ @should_undo = true
283
+ @validation_problem = CommandException.new("No arguments provided!")
284
+ @last_completed_task = nil
285
+ @resume_from = resume
286
+ @main_collector = collector
287
+ end
288
+
289
+ attr_reader :arg_hash
290
+
291
+ def name
292
+ self.class.name
293
+ end
294
+
295
+ def required_arguments
296
+ self.class.argument_list.find_all do |arg|
297
+ arg.required?
298
+ end
299
+ end
300
+
301
+ def optional_arguments
302
+ self.class.argument_list.find_all do |arg|
303
+ not arg.required?
304
+ end
305
+ end
306
+
307
+ def all_arguments
308
+ self.class.argument_list
309
+ end
310
+
311
+ def subject_requirements
312
+ self.class.subject_requirements
313
+ end
314
+
315
+ def inspect
316
+ name = self.class.name
317
+ return "#<Command:#{name}>:#{self.object_id} #{@arg_hash.inspect}"
318
+ end
319
+
320
+ def parent
321
+ self.class.parent
322
+ end
323
+
324
+ def go(collector)
325
+ unless self.respond_to? :execute
326
+ raise CommandException, "#{@name}: command declared but no action defined"
327
+ end
328
+
329
+ @main_collector = collector
330
+ begin
331
+ validate_arguments
332
+ verify_subject
333
+ execute
334
+ join_undo
335
+ rescue Interrupt
336
+ puts "Command cancelled"
337
+ rescue ResumeFrom => rf
338
+ rf.setup.task_id = @last_completed_task
339
+ raise rf
340
+ end
341
+ end
342
+
343
+ def advise_formatter(formatter)
344
+ formatter.receive_advice(&self.class.advice_block)
345
+ end
346
+
347
+ def verify_subject
348
+ return if subject_requirements.nil?
349
+ subject_requirements.each do |requirement|
350
+ begin
351
+ if Subject::UndefinedField === @subject.send(requirement)
352
+ raise CommandException, "\"#{name}\" requires \"#{requirement.to_s}\" to be set"
353
+ end
354
+ rescue NameError => ne
355
+ raise CommandException, "\"#{name}\" requires subject to include \"#{requirement.to_s}\""
356
+ end
357
+ end
358
+ end
359
+
360
+ def consume_hash(args_hash)
361
+ allowed_arguments = all_arguments.inject([]) do |allowed, argument|
362
+ allowed += [*argument.name]
363
+ end
364
+
365
+ wrong_values = {}
366
+ required_names = []
367
+
368
+ args_hash.keys.each do |name|
369
+ args_hash[name.to_s] = args_hash[name]
370
+ end
371
+
372
+ all_arguments.each do |argument|
373
+ begin
374
+ @arg_hash.merge! argument.consume_hash(@subject, args_hash)
375
+ rescue ArgumentInvalidException => aie
376
+ wrong_values.merge! aie.pairs
377
+ rescue OutOfArgumentsException
378
+ required_names += ([*argument.name].find_all {|name| not @arg_hash.has_key?(name)})
379
+ end
380
+ end
381
+
382
+ wrong_names = @arg_hash.keys - allowed_arguments
383
+
384
+ unless wrong_values.empty?
385
+ aie = ArgumentInvalidException.new(wrong_values)
386
+ @validation_problem = aie
387
+ raise aie
388
+ end
389
+
390
+ unless wrong_names.empty?
391
+ aue = ArgumentUnrecognizedException.new("Unrecognized arguments: #{wrong_names.join(", ")}")
392
+ @validation_problem = aue
393
+ raise aue
394
+ end
395
+
396
+ unless required_names.empty?
397
+ ooae = OutOfArgumentsException.new("Missing arguments: #{required_names.join(", ")}")
398
+ @validation_problem = ooae
399
+ raise ooae
400
+ end
401
+
402
+ @validation_problem = nil
403
+ end
404
+
405
+ def yield_consumer(argument, &block)
406
+ blk = proc(&block)
407
+ consumer = proc do |args|
408
+ raise TypeError, "#{args.inspect} isn't an Array!" unless Array === args
409
+ begin
410
+ if args.empty?
411
+ raise OutOfArgumentsException, "argument #{argument.inspect} required!"
412
+ end
413
+ @arg_hash.merge! argument.consume(@subject, args)
414
+ return args
415
+ rescue ArgumentInvalidException => aie
416
+ @validation_problem = aie
417
+ raise
418
+ end
419
+ end
420
+ blk[consumer]
421
+ end
422
+
423
+ def each_consumer(&block)
424
+ begin
425
+ all_arguments.each do |argument|
426
+ yield_consumer(argument, &block)
427
+ end
428
+ rescue OutOfArgumentsException
429
+ end
430
+ @validation_problem = nil
431
+ end
432
+
433
+ def undoable?
434
+ return false
435
+ end
436
+
437
+ def join_undo
438
+ if(undoable? && @should_undo && subject.undo_stack != nil)
439
+ subject.undo_stack.add(self)
440
+ end
441
+ return nil
442
+ end
443
+
444
+ def collector
445
+ end
446
+
447
+ def validate_arguments
448
+ raise @validation_problem if Exception === @validation_problem
449
+ required_arguments.each do |argument|
450
+ argument.check_present(@arg_hash.keys)
451
+ end
452
+ end
453
+ end
454
+ end
455
+
456
+