command-set 0.8.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,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
+