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,615 @@
1
+ require 'command-set/command'
2
+ require 'command-set/arguments'
3
+ require 'facet/kernel/constant'
4
+
5
+ module Command
6
+ class OgArgument < Argument
7
+ def initialize(name, klass, find_by="name", options={})
8
+ super(name)
9
+ @klass = klass
10
+ @key = find_by
11
+ @options = options
12
+ end
13
+
14
+ def complete(prefix, subject)
15
+ entities = @klass.find(@options.merge({:condition => ["#{@key} like ?", prefix + "%" ]}))
16
+ return entities.map{|entity| entity.__send__(@key)}
17
+ end
18
+
19
+ def validate(term, subject)
20
+ found = @klass.find(@options.merge({:condition => ["#{@key} = ?", term]}))
21
+ if found.empty?
22
+ #THINK! Is it a good idea to create missing items in the Argument?
23
+ #Should the Command's action have to do that?
24
+ if(@options[:accept_missing])
25
+ return true
26
+ elsif(@options[:create_missing])
27
+ new_attributes = @options[:default_values] || {}
28
+ new_attributes.merge!( @key => term )
29
+ @klass.create_with(new_attributes)
30
+ return true
31
+ else
32
+ return false
33
+ end
34
+ end
35
+ return true
36
+ end
37
+
38
+ def parse(subject, term)
39
+ found = @klass.find(@options.merge({:condition => ["#{@key} = ?", term]}))
40
+ raise RuntimeError, "Couldn't find item on parse!" if found.empty?
41
+ found[0]
42
+ end
43
+ end
44
+
45
+ module OgCommands
46
+ class OgModeCommand < Command
47
+ subject_methods :interpreter, :current_state
48
+
49
+ doesnt_undo
50
+
51
+ action do
52
+ subject.current_state << current
53
+ subject.interpreter.push_mode(my_mode)
54
+ end
55
+
56
+ class << self
57
+ def switch_to(model_class, find_by, og_options={})
58
+ og_options.merge!(:create_missing => true)
59
+ argument OgArgument.new(:current, model_class, find_by, og_options)
60
+
61
+ define_method(:found_current) do
62
+ current.__send__(find_by)
63
+ end
64
+ end
65
+
66
+ def mode(command_set)
67
+ define_method(:my_mode) do
68
+ command_set.set_prompt(/$/, "#{found_current} : ")
69
+ return command_set
70
+ end
71
+ end
72
+ end
73
+ end
74
+
75
+ class HasOneCommand < Command
76
+ subject_methods :current_state
77
+
78
+ action do
79
+ if value.nil?
80
+ dont_undo
81
+ current_value = get_value
82
+ puts get_label(current_value)
83
+ else
84
+ @old_value = get_value
85
+ set_value value
86
+ entity.save
87
+ end
88
+ end
89
+
90
+ undo do
91
+ set_value @old_value
92
+ entity.save
93
+ end
94
+
95
+ def entity
96
+ subject.current_state.last
97
+ end
98
+
99
+ class << self
100
+ def optional_value(target_class, select_by)
101
+ optional_argument OgArgument.new(:value, target_class, select_by)
102
+ end
103
+
104
+ def has_one(field)
105
+ define_method(:get_value) do
106
+ entity.__send__(field)
107
+ end
108
+
109
+ define_method(:set_value) do |value|
110
+ entity.__send__("#{field}=", value)
111
+ end
112
+ end
113
+
114
+ def identified_by(name, &block)
115
+ if block.nil?
116
+ define_method(:get_label) do |value|
117
+ return "" unless value.respond_to?(name.intern)
118
+ value.__send__(name.intern).to_s
119
+ end
120
+ else
121
+ define_method(:get_label) do |value|
122
+ return "" unless value.respond_to?(name.intern)
123
+ block[value.__send__(name.intern)]
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
129
+
130
+ class HasManyListCommand < Command
131
+ optional_argument :search, "A substring of the name of the thing you're looking for"
132
+
133
+ doesnt_undo
134
+
135
+ subject_methods :current_state
136
+
137
+ action do
138
+ options = {}
139
+ unless search.nil?
140
+ options.update({:condition =>
141
+ ["#{list_attribute} like ?", "%#{search_term}%"]})
142
+ end
143
+
144
+ object_list(options).each do |item|
145
+ puts item
146
+ end
147
+ end
148
+
149
+ def entity
150
+ subject.current_state.last
151
+ end
152
+
153
+ def object_list(options={})
154
+ objects=entity.__send__("find_#{target_name}", options)
155
+ return objects.map do |object|
156
+ object.__send__(list_as)
157
+ end
158
+ end
159
+
160
+ class << self
161
+ def has_many(target_name)
162
+ define_method(:target_name) do
163
+ target_name
164
+ end
165
+ end
166
+
167
+ def listed_as(list_as)
168
+ define_method(:list_as) do
169
+ list_as
170
+ end
171
+ end
172
+ end
173
+ end
174
+
175
+ class HasManyEditCommand < Command
176
+ def add(item)
177
+ owner.__send__("add_#{kind}", item)
178
+ end
179
+
180
+ def remove(item)
181
+ owner.__send__("remove_#{kind}", item)
182
+ end
183
+
184
+ subject_methods :current_state
185
+
186
+ def owner
187
+ subject.current_state.last
188
+ end
189
+
190
+ class << self
191
+ def no_more_add_or_remove
192
+ raise NoMethodError, "Can't add_to and remove_from with same command!"
193
+ end
194
+ private :no_more_add_or_remove
195
+
196
+ def defined_sense
197
+ class << self
198
+ alias_method :add_to, :no_more_add_or_remove
199
+ alias_method :remove_from, :no_more_add_or_remove
200
+ def defined_sense
201
+ end
202
+ end
203
+ end
204
+
205
+ def find_a(target_class, find_by)
206
+ argument OgArgument.new(:item, target_class, find_by)
207
+ end
208
+
209
+ def remove_from(kind)
210
+ define_method(:kind) do
211
+ return kind.to_s.singular
212
+ end
213
+
214
+ action do
215
+ remove(item)
216
+ end
217
+
218
+ undo do
219
+ add(item)
220
+ end
221
+ defined_sense
222
+ end
223
+
224
+ def add_to(kind)
225
+ define_method(:kind) do
226
+ return kind.to_s.singular
227
+ end
228
+
229
+ action do
230
+ add(item)
231
+ end
232
+
233
+ undo do
234
+ remove(item)
235
+ end
236
+ defined_sense
237
+ end
238
+
239
+ end
240
+ end
241
+
242
+ class DisplayCommand < Command
243
+ subject_methods :current_state
244
+
245
+ doesnt_undo
246
+
247
+ action do
248
+ pairs = []
249
+ simple_fields.each_pair do |field, value|
250
+ pairs << [field.to_s, value]
251
+ end
252
+ single_relations.each_pair do |field, value|
253
+ pairs << [field.to_s, value]
254
+ end
255
+ many_relations.each_pair do |field, number|
256
+ pairs << [field.to_s, "#{number} items"]
257
+ end
258
+ field_width = pairs.map{|f,v| f.length}.max + 1
259
+ pairs.each do |field, value|
260
+ puts "#{field.rjust(field_width)}: #{value}"
261
+ end
262
+ end
263
+
264
+ def simple_fields
265
+ {}
266
+ end
267
+
268
+ def single_relations
269
+ {}
270
+ end
271
+
272
+ def many_relations
273
+ {}
274
+ end
275
+
276
+ def entity
277
+ subject.current_state.last
278
+ end
279
+
280
+ class << self
281
+ def simple_fields(list)
282
+ define_method(:simple_fields) do
283
+ fields = {}
284
+ list.each do |name, field_name|
285
+ fields[name] = entity.__send__(field_name)
286
+ end
287
+ return fields
288
+ end
289
+ end
290
+
291
+ def single_relations(list)
292
+ define_method(:single_relations) do
293
+ fields = {}
294
+ list.each do |name, field_name, called|
295
+ target = entity.__send__(field_name)
296
+ if target.nil?
297
+ fields[name] = ""
298
+ else
299
+ fields[name] = target.__send__(called)
300
+ end
301
+ end
302
+ return fields
303
+ end
304
+ end
305
+
306
+ def many_relations(list)
307
+ define_method(:many_relations) do
308
+ fields = {}
309
+ list.each do |name, field_name|
310
+ fields[name] = entity.__send__("count_#{field_name}")
311
+ end
312
+ return fields
313
+ end
314
+ end
315
+ end
316
+ end
317
+
318
+ class PropertyCommand < Command
319
+ subject_methods :current_state
320
+
321
+ action do
322
+ if value.nil?
323
+ dont_undo
324
+ puts get_value
325
+ else
326
+ @old_value = get_value
327
+ set_value value
328
+ end
329
+ end
330
+
331
+ undo do
332
+ set_value @old_value
333
+ end
334
+
335
+ def entity
336
+ subject.current_state.last
337
+ end
338
+
339
+ def value
340
+ nil
341
+ end
342
+
343
+ class << self
344
+ def field_name(field)
345
+ define_method(:get_value) do
346
+ entity.__send__(field)
347
+ end
348
+
349
+ define_method(:set_value) do |value|
350
+ entity.__send__("#{field}=", value)
351
+ end
352
+ end
353
+
354
+ def editable
355
+ optional_argument :value, "The new value"
356
+ end
357
+ end
358
+ end
359
+
360
+ class << self
361
+ def command_set(config)
362
+ set = CommandSet.new("og_commands")
363
+ def set.entity_modes
364
+ return @entity_modes||={}
365
+ end
366
+ normalize_config(config)
367
+ entity_commands_from_config(set, config)
368
+ list_command_from_config(set, config)
369
+ return set
370
+ end
371
+
372
+ # This is a messy method that ensures that the configuration hash for the
373
+ # CommandSet is well constructed. It should catch errors and do coversions,
374
+ # set defaults, etc. to set up the creation of the CommandSet. It's source
375
+ # may be instructive for creating or editing config hashes.
376
+ def normalize_config(config)
377
+ entity_count = 1
378
+ required_names = ["class"]
379
+ config.each do |entity_config|
380
+ required_names.each do |name|
381
+ if entity_config[name].nil?
382
+ raise RuntimeError, "Entity #{entity_config.pretty_inspect} missing #{name}!"
383
+ end
384
+ end
385
+
386
+ begin
387
+ real_class = constant(entity_config["class"])
388
+ entity_config["real_class"] = real_class
389
+ rescue NameError
390
+ raise RuntimeError, "Class: #{entity_config["class"]} unrecognized. Missing require?"
391
+ end
392
+
393
+ entity_config["select_by"] = "primary_key" if entity_config["select_by"].nil?
394
+
395
+ unless entity_config["edit_command"].nil?
396
+ if entity_config["edit_config"].nil?
397
+ raise RuntimeError, "Class: #{item["class"]} has an edit_command but no edit_config!"
398
+ end
399
+ end
400
+
401
+ if entity_config["listable?"]
402
+ if entity_config["list_as"].nil?
403
+ if not entity_config["plural_name"].nil?
404
+ entity_config["list_as"] = entity_config["plural_name"]
405
+ elsif not entity_config["edit_command"].nil?
406
+ entity_config["list_as"] = entity_config["edit_command"] + "s"
407
+ else
408
+ raise RuntimeError, "Class: #{entity_config["class"]} marked listable, " +
409
+ "but without a list name!"
410
+ end
411
+ end
412
+ end
413
+ entity_count += 1
414
+ end
415
+
416
+ #fields pass - so that relations can reuse entity defs
417
+ config.each do |entity_config|
418
+ fields_config = entity_config["edit_config"]
419
+ next if fields_config.nil?
420
+
421
+ fields_config["simple_fields"]||=[]
422
+ fields_config["simple_fields"].each do |field_config|
423
+ if field_config["field_name"].nil?
424
+ raise RuntimeError, "Class: #{entity_config["class"]} field missing field_name!"
425
+ end
426
+
427
+ field_config["name"]||=field_config["field_name"]
428
+ end
429
+
430
+ fields_config["single_relations"]||=[]
431
+ fields_config["single_relations"].each do |relation_config|
432
+ if relation_config["field_name"].nil?
433
+ raise RuntimeError, "Class: #{entity_config["class"]} relation " +
434
+ "missing field_name!"
435
+ end
436
+
437
+ relation_config["name"]||=relation_config["field_name"]
438
+ if relation_config["target"].nil?
439
+ raise RuntimeError, "Class: #{entity_config["class"]} relation " +
440
+ "#{relation_config["name"]} has no target!"
441
+ end
442
+
443
+ relation_config["target"]["select_by"]||="name"
444
+ end
445
+
446
+ fields_config["many_relations"]||=[]
447
+ fields_config["many_relations"].each do |relation_config|
448
+ if relation_config["field_name"].nil?
449
+ raise RuntimeError, "Class: #{entity_config["class"]} field missing field_name!"
450
+ end
451
+
452
+ relation_config["name"]||=relation_config["field_name"]
453
+
454
+ if relation_config["target"].nil?
455
+ raise RuntimeError, "Class: #{entity_config["class"]} relation " +
456
+ "#{relation_config["name"]} has no target!"
457
+ end
458
+
459
+ relation_config["target"]["select_by"]||="name"
460
+ unless Class === relation_config["target"]["real_class"]
461
+ if relation_config["target"]["class"].nil?
462
+ raise RuntimeError,
463
+ "Class: #{entity_config["class"]} relation " +
464
+ "#{relation_config["name"]} target has no class!"
465
+ else
466
+ relation_config["target"]["real_class"] =
467
+ constant(relation_config["target"]["real_class"])
468
+ end
469
+ end
470
+ end
471
+ end
472
+ end
473
+
474
+
475
+ def entity_commands_from_config(set, config)
476
+ config.each do |entity_config|
477
+ next if entity_config["edit_command"].nil?
478
+
479
+ my_mode = entity_mode(entity_config["edit_config"])
480
+ set.entity_modes[entity_config["class"]]=my_mode
481
+
482
+ set.command(OgModeCommand, entity_config["edit_command"]) do
483
+ switch_to entity_config["real_class"], entity_config["select_by"]
484
+ mode my_mode
485
+ end
486
+ end
487
+ end
488
+
489
+ def list_command_from_config(set, config)
490
+ lists = {}
491
+ config.find_all{|item| item["listable?"] }.each do |item|
492
+ entry = item["list_as"]
493
+ entry_class = item["real_class"]
494
+ find_by = item["select_by"]
495
+
496
+ lists[entry] = [entry_class, find_by]
497
+ end
498
+
499
+ set.command "list" do
500
+ argument :what, lists.keys
501
+
502
+ optional_argument :search, "A substring of the name of the thing you're looking for"
503
+
504
+ doesnt_undo
505
+
506
+ define_method(:make_list) do |klass, list_attribute, search_term|
507
+ objects = []
508
+ if search_term.nil?
509
+ objects = klass.find
510
+ else
511
+ objects = klass.find(:condition => ["#{list_attribute} like ?",
512
+ "%#{search_term}%"])
513
+ end
514
+
515
+ objects.map do |object|
516
+ object.__send__(list_attribute)
517
+ end.each do |item|
518
+ puts item
519
+ end
520
+ end
521
+
522
+ action do
523
+ list_me = lists[what]
524
+ raise CommandException if list_me.nil?
525
+ make_list(*(list_me + [search]))
526
+ end
527
+ end
528
+ end
529
+
530
+
531
+ def entity_mode(fields_config)
532
+ mode = CommandSet.define_commands do
533
+ include_commands StandardCommands::Quit
534
+
535
+ command "exit" do
536
+ subject_methods :interpreter, :current_state
537
+
538
+ doesnt_undo
539
+ action do
540
+ entity = subject.current_state.pop
541
+ entity.save
542
+ subject.interpreter.pop_mode
543
+ end
544
+ end
545
+
546
+ command "delete!" do
547
+ subject_methods :interpreter, :current_state
548
+
549
+ action do
550
+ entity = subject.current_state.pop
551
+ entity.delete
552
+ subject.interpreter.pop_mode
553
+ end
554
+ end
555
+
556
+ command DisplayCommand, "display" do
557
+ simple_fields fields_config["simple_fields"].map{|f| [f["name"], f["field_name"]]}
558
+ singles = fields_config["single_relations"].map do |f|
559
+ [ f["name"], f["field_name"], f["target"]["select_by"] ]
560
+ end
561
+ single_relations singles
562
+ many_relations fields_config["many_relations"].map{|f| [f["name"], f["field_name"]]}
563
+ end
564
+
565
+ fields_config["simple_fields"].each do |property|
566
+ command PropertyCommand, property["name"] do
567
+ field_name property["field_name"]
568
+ if property["edit?"]
569
+ editable
570
+ end
571
+ end
572
+ end
573
+
574
+ fields_config["single_relations"].each do |relation|
575
+ command HasOneCommand, relation["name"] do
576
+ optional_value relation["target"]["real_class"], relation["target"]["select_by"]
577
+ has_one relation["field_name"]
578
+ identified_by relation["target"]["select_by"]
579
+ end
580
+ end
581
+
582
+ fields_config["many_relations"].each do |relation_config|
583
+ unless relation_config["list?"] or relation_config["edit?"]
584
+ next
585
+ end
586
+ sub_command relation_config["name"] do
587
+ if relation_config["list?"]
588
+ command HasManyListCommand, :list do
589
+ has_many relation_config["field_name"]
590
+ listed_as relation_config["target"]["select_by"]
591
+ end
592
+ end
593
+
594
+ if relation_config["edit?"]
595
+ command HasManyEditCommand, :add do
596
+ find_a relation_config["target"]["real_class"],
597
+ relation_config["target"]["select_by"]
598
+ add_to relation_config["field_name"]
599
+ end
600
+
601
+ command HasManyEditCommand, :remove do
602
+ find_a relation_config["target"]["real_class"],
603
+ relation_config["target"]["select_by"]
604
+ remove_from relation_config["field_name"]
605
+ end
606
+ end
607
+ end
608
+ end
609
+ end
610
+ return mode
611
+ end
612
+ end
613
+ end
614
+ end
615
+
@@ -0,0 +1,91 @@
1
+ require 'command-set/interpreter'
2
+
3
+ module Command
4
+ #A looser Subject, used by QuickInterpreter for testing purposes.
5
+ #Normally, a Subject doesn't allow reads from it's fields - you have to
6
+ #explicitly call get_image. This class allows reads, which makes testing
7
+ #easier.
8
+ class PermissiveSubject < Subject
9
+ protected
10
+ def add_accessor(name)
11
+ super
12
+ (class << self; self; end).instance_eval do
13
+ define_method(name) do
14
+ return instance_variable_get("@#{name}")
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+
21
+ #This class exists mostly to make spec and unit test writing easier.
22
+ #Because Commands need so much care and feeding on the back end, it can be
23
+ #troublesome to write specs on the directly. QuickInterpreter is designed
24
+ #for programmatic access, and easy setup.
25
+ #
26
+ #Example usage:
27
+ #
28
+ # @interpreter = QuickInterpreter::define_interpreter do #unique to QI
29
+ # command :test do
30
+ # subject.a_field << "one"
31
+ # end
32
+ # end
33
+ #
34
+ # @subject = @interpreter.subject_template
35
+ # @subject.a_field = []
36
+ # @interpreter.subject = @subject
37
+ #
38
+ # @interpreter.process_input(:test, "args")
39
+ # @subject.a_field.length # => 1; note that normally you need to
40
+ # #+get_image+ to access fields of the subject
41
+ #
42
+ class QuickInterpreter < BaseInterpreter
43
+ class << self
44
+ #You can use this method to create a new interpreter with a command
45
+ #set. The block is passed to a new CommandSet, so you can use
46
+ #DSL::CommandSetDefinition there.
47
+ def define_interpreter(&block)
48
+ interpreter = new
49
+ interpreter.command_set = CommandSet::define_commands(&block)
50
+ return interpreter
51
+ end
52
+
53
+ alias define_commands define_interpreter
54
+ end
55
+
56
+ def initialize
57
+ @formatter_factory = proc {Results::Formatter.new(::Command::raw_stdout)}
58
+ super
59
+ end
60
+
61
+ #Saves the block passed to create formatters with. Cleaner a singleton
62
+ #get_formatter definition.
63
+ def make_formatter(&block)
64
+ @formatter_factory = proc &block
65
+ end
66
+
67
+ def get_formatter #:nodoc:
68
+ return @formatter_factory.call
69
+ end
70
+
71
+ #Accepts it's input as multiple arguments for convenience
72
+ def process_input(*words)
73
+ super(words)
74
+ end
75
+
76
+ #Always returns "yes" so that undo warnings can be ignored.
77
+ def prompt_user(message)
78
+ "yes"
79
+ end
80
+
81
+ #Passes the arguments to process_input directly to CommandSetup
82
+ def cook_input(words)
83
+ CommandSetup.new(words)
84
+ end
85
+
86
+ #See PermissiveSubject
87
+ def get_subject
88
+ return PermissiveSubject.new
89
+ end
90
+ end
91
+ end