ing 0.1.5 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -32,8 +32,8 @@ The ing command line is generally parsed as
32
32
 
33
33
  [ing command] [ing command options] [subcommand] [args] [subcommand options]
34
34
 
35
- But in cases where the first argument isn't an built-in ing command or options, it's
36
- simplified to
35
+ But in cases where the first argument isn't a built-in ing command or options,
36
+ it's simplified to
37
37
 
38
38
  [subcommand] [args] [subcommand options]
39
39
 
data/lib/ing.rb CHANGED
@@ -1,25 +1,26 @@
1
1
  ['ing/version',
2
+ 'ing/util',
2
3
  'ing/lib_trollop',
3
4
  'ing/trollop/parser',
4
- 'ing/util',
5
- 'ing/dispatcher',
5
+ 'ing/option_parsers/trollop',
6
6
  'ing/shell',
7
7
  'ing/common_options',
8
- 'ing/boot',
9
8
  'ing/files',
10
9
  'ing/task',
11
10
  'ing/generator',
11
+ 'ing/command',
12
+ 'ing/commands/boot',
12
13
  'ing/commands/implicit',
14
+ 'ing/commands/generate',
13
15
  'ing/commands/list',
14
- 'ing/commands/help',
15
- 'ing/commands/generate'
16
+ 'ing/commands/help'
16
17
  ].each do |f|
17
18
  require_relative f
18
19
  end
19
20
 
20
21
  module Ing
21
22
  extend self
22
-
23
+
23
24
  Error = Class.new(StandardError)
24
25
  FileNotFoundError = Class.new(Error)
25
26
 
@@ -29,53 +30,74 @@ module Ing
29
30
  @shell_class ||= Shell::Basic
30
31
  end
31
32
 
32
- def implicit_booter
33
- ["Implicit"]
33
+
34
+ class << (Callstack = Object.new)
35
+
36
+ def index(klass, meth)
37
+ stack.index {|e| e == [klass,meth]}
38
+ end
39
+
40
+ def push(klass, meth)
41
+ stack << [klass, meth]
42
+ end
43
+
44
+ def clear
45
+ stack.clear
46
+ end
47
+
48
+ def to_a
49
+ stack.dup
50
+ end
51
+
52
+ private
53
+ def stack
54
+ @stack ||= []
55
+ end
56
+
57
+ end
58
+
59
+ def run(args=ARGV)
60
+ booter = extract_boot_class!(args) || implicit_booter
61
+ execute booter, *args
34
62
  end
35
63
 
36
- # Dispatch command line to boot class (if specified, or Implicit otherwise),
37
- # which in turn dispatches the command after parsing args.
38
- #
39
- # Note boot dispatch happens within +Ing::Commands+ namespace.
40
- #
41
- def run(argv=ARGV)
42
- booter = extract_boot_class!(argv) || implicit_booter
43
- run_boot booter, "call", *argv
64
+ def execute(klass, meth=:call, *args, &config)
65
+ cmd = command.new(klass, meth, *args)
66
+ _callstack.push(cmd.command_class, cmd.command_meth)
67
+ cmd.execute(&config)
44
68
  end
45
69
 
46
- # Dispatch to the command via +Ing::Boot#call_invoke+
47
- # Use this when you want to invoke a command from another command, but only
48
- # if it hasn't been run yet. For example,
49
- #
50
- # invoke Some::Task, :some_instance_method, some_argument, :some_option => true
51
- #
52
- # You can skip the method and it will assume +#call+ :
53
- #
54
- # invoke Some::Task, :some_option => true
55
- def invoke(klass, *args)
56
- run_boot implicit_booter, "call_invoke", klass, *args
70
+ def invoke(klass, meth=:call, *args, &config)
71
+ execute(klass, meth, *args, &config) unless executed?(klass, meth)
57
72
  end
58
73
 
59
- # Dispatch to the command via +Ing::Boot#call_execute+
60
- # Use this when you want to execute a command from another command, and you
61
- # don't care if it has been run yet or not. See equivalent examples for
62
- # +invoke+.
63
- #
64
- def execute(klass, *args)
65
- run_boot implicit_booter, "call_execute", klass, *args
74
+ def executed?(klass, meth)
75
+ !!_callstack.index(klass, meth)
76
+ end
77
+
78
+ def callstack
79
+ _callstack.to_a
66
80
  end
67
81
 
68
82
  private
69
83
 
70
- def run_boot(booter, *args)
71
- Dispatcher.new(["Ing","Commands"], booter, *args).dispatch
84
+ def command
85
+ Command
86
+ end
87
+
88
+ def _callstack
89
+ Callstack
90
+ end
91
+
92
+ def implicit_booter
93
+ Commands::Implicit
72
94
  end
73
95
 
74
96
  def extract_boot_class!(args)
75
- c = Util.to_class_names(args.first)
76
- if (Commands.const_defined?(c.first, false) rescue nil)
77
- args.shift; c
78
- end
97
+ c = Util.decode_class(args.first, Ing::Commands)
98
+ args.shift; c
99
+ rescue NameError
100
+ nil
79
101
  end
80
-
81
- end
102
+
103
+ end
@@ -0,0 +1,96 @@
1
+ module Ing
2
+
3
+ class Command
4
+
5
+ class << self
6
+ attr_writer :parser
7
+ def parser
8
+ OptionParsers::Trollop
9
+ end
10
+
11
+ def execute(klass, *args, &config)
12
+ new(klass, *args).execute(&config)
13
+ end
14
+ end
15
+
16
+ attr_accessor :options, :command_class, :command_meth, :args
17
+
18
+ def initialize(klass, *args)
19
+ self.options = (Hash === args.last ? args.pop : {})
20
+ self.command_class = klass
21
+ self.command_meth = extract_method!(args, command_class)
22
+ self.args = args
23
+ end
24
+
25
+ def classy?
26
+ command_class.respond_to?(:new)
27
+ end
28
+
29
+ def instance
30
+ @instance ||= build_command
31
+ end
32
+
33
+ def execute
34
+ yield instance if block_given?
35
+ classy? ? instance.send(command_meth, *args) :
36
+ instance.send(command_meth, *args, options)
37
+ end
38
+
39
+ def describe
40
+ with_option_parser {|p| p.describe}
41
+ end
42
+
43
+ def help
44
+ with_option_parser {|p| p.help}
45
+ end
46
+
47
+ def with_option_parser
48
+ return unless command_class.respond_to?(:specify_options)
49
+ p = self.class.parser.new
50
+ command_class.specify_options(p.parser)
51
+ yield p
52
+ end
53
+
54
+ private
55
+
56
+ def build_command
57
+ parse_options!
58
+ classy? ? command_class.new(options) : command_class
59
+ end
60
+
61
+ # Note options merged into parsed options (reverse merge)
62
+ # so that passed options (in direct invoke or execute) override defaults
63
+ def parse_options!
64
+ self.options = (parsed_options_from_args || {}).merge(self.options)
65
+ end
66
+
67
+ # memoized to avoid duplicate args processing
68
+ def parsed_options_from_args
69
+ @parsed_options ||= with_option_parser do |p|
70
+ p.parse! self.args
71
+ end
72
+ end
73
+
74
+ def extract_method!(args, klass)
75
+ return :call if args.empty?
76
+ if meth = whitelist(args.first, klass)
77
+ args.shift
78
+ else
79
+ meth = :call
80
+ end
81
+ meth
82
+ end
83
+
84
+ # Note this currently does no filtering, but basically checks for respond_to
85
+ def whitelist(meth, klass)
86
+ finder = Proc.new {|m| m == meth.to_sym}
87
+ if klass.respond_to?(:new)
88
+ klass.public_instance_methods(true).find(&finder)
89
+ else
90
+ klass.public_methods.find(&finder)
91
+ end
92
+ end
93
+
94
+ end
95
+
96
+ end
@@ -8,6 +8,14 @@
8
8
  #
9
9
  module Boot
10
10
 
11
+ # Before hook passed unprocessed args, override in target class
12
+ def before(*args)
13
+ end
14
+
15
+ # After hook, override in target class
16
+ def after
17
+ end
18
+
11
19
  # Configure the command prior to dispatch.
12
20
  # Default behavior is to set the shell of the dispatched command.
13
21
  # Override in target class as needed; if you want to keep the default
@@ -15,7 +23,7 @@
15
23
  def configure_command(cmd)
16
24
  cmd.shell = Ing.shell_class.new if cmd.respond_to?(:"shell=")
17
25
  end
18
-
26
+
19
27
  # Main processing of arguments and dispatch from command line (+Ing.run+)
20
28
  # Note that three hooks are provided for target classes,
21
29
  # +before+:: runs before any processing of arguments or dispatch of command
@@ -23,32 +31,24 @@
23
31
  # +after+:: runs after command dispatched
24
32
  #
25
33
  def call(*args)
26
- before *args if respond_to?(:before)
27
- ns = Ing::Util.to_class_names(options[:namespace] || 'object')
28
- classes = Ing::Util.to_class_names(args.shift)
29
- Dispatcher.new(ns, classes, *args).dispatch do |cmd|
34
+ before *args
35
+ klass = _extract_class!(args)
36
+ Ing.execute(klass, *args) do |cmd|
30
37
  configure_command cmd
31
38
  end
32
- after if respond_to?(:after)
39
+ after
33
40
  end
41
+
42
+ private
34
43
 
35
- # Dispatch from +Ing.invoke+
36
- def call_invoke(klass, meth, *args)
37
- before *args if respond_to?(:before)
38
- Dispatcher.invoke(klass, meth, *args) do |cmd|
39
- configure_command cmd
40
- end
41
- after if respond_to?(:after)
44
+ def _extract_class!(args)
45
+ Util.decode_class(args.shift, _namespace_class)
42
46
  end
43
47
 
44
- # Dispatch from +Ing.execute+
45
- def call_execute(klass, meth, *args)
46
- before *args if respond_to?(:before)
47
- Dispatcher.execute(klass, meth, *args) do |cmd|
48
- configure_command cmd
49
- end
50
- after if respond_to?(:after)
48
+ def _namespace_class
49
+ return ::Object unless ns = options[:namespace]
50
+ Util.decode_class(ns)
51
51
  end
52
-
52
+
53
53
  end
54
54
  end
@@ -26,6 +26,11 @@ module Ing
26
26
  @generator_root ||= File.expand_path(options[:gen_root])
27
27
  end
28
28
 
29
+ attr_accessor :options
30
+ def initialize(options)
31
+ self.options = options
32
+ end
33
+
29
34
  # Require libs and ing_file, then
30
35
  # locate and require the generator ruby file identified by the first arg,
31
36
  # before dispatching to it.
@@ -36,14 +36,19 @@
36
36
  require_ing_file
37
37
  end
38
38
 
39
- def call(cmd="help")
39
+ def call(cmd="help")
40
40
  before
41
- ns = Ing::Util.to_class_names(options[:namespace] || 'object')
42
- cs = Ing::Util.to_class_names(cmd)
43
- help = Dispatcher.new(ns, cs).help
44
- shell.say help.read
41
+ klass = Util.decode_class(cmd, _namespace_class)
42
+ help = Command.new(klass).help
43
+ shell.say help
45
44
  end
46
45
 
46
+ private
47
+ def _namespace_class
48
+ return ::Object unless ns = options[:namespace]
49
+ Util.decode_class(ns)
50
+ end
51
+
47
52
  end
48
53
 
49
54
  H = Help
@@ -36,28 +36,32 @@
36
36
  require_ing_file
37
37
  end
38
38
 
39
- def call(namespace=options[:namespace])
39
+ def call(ns=nil)
40
40
  before
41
- ns = Ing::Util.to_class_names(namespace)
42
- mod = Ing::Util.namespaced_const_get(ns)
41
+ mod = _namespace_class(ns)
43
42
  data = mod.constants.map do |c|
44
- desc = Dispatcher.new(ns, [c]).describe
45
- [ "ing #{Ing::Util.encode_class_names(ns + [c])}",
46
- (desc.gets || '(no description)').chomp
43
+ klass = mod.const_get(c)
44
+ desc = (Command.new(klass).describe || '')[/.+$/]
45
+ [ "ing #{Ing::Util.encode_class(mod)}:#{Ing::Util.encode_class_names([c])}",
46
+ (desc || '(no description)').chomp
47
47
  ]
48
48
  end.sort
49
- shell.say desc_lines(ns, data).join("\n")
49
+ shell.say desc_lines(mod, data).join("\n")
50
50
  end
51
51
 
52
52
  private
53
+ def _namespace_class(ns=options[:namespace])
54
+ return ::Ing::Commands unless ns
55
+ Util.decode_class(ns)
56
+ end
53
57
 
54
- def desc_lines(ns, data)
58
+ def desc_lines(mod, data)
55
59
  colwidths = data.inject([0,0]) {|max, (line, desc)|
56
60
  max[0] = line.length if line.length > max[0]
57
61
  max[1] = desc.length if desc.length > max[1]
58
62
  max
59
63
  }
60
- ["#{ns.join(' ')}: all tasks",
64
+ ["#{mod}: all tasks",
61
65
  "-" * 80
62
66
  ] +
63
67
  data.map {|line, desc|
@@ -40,26 +40,11 @@ module Ing
40
40
  # attr_reader :source_root, :destination_root
41
41
  # attr_reader :shell, :options
42
42
  #
43
- # Adds to target class options:
44
- # verbose, force, pretend, revoke, quiet, skip.
43
+ # and should provide these options (otherwise all defaulted nil):
44
+ # verbose, force, pretend, revoke, quiet, skip.
45
45
  #
46
46
  module Files
47
47
 
48
- # a bit of trickiness to change a singleton method...
49
- def self.included(base)
50
- meth = base.method(:specify_options) if base.respond_to?(:specify_options)
51
- base.send(:define_singleton_method, :specify_options) do |expect|
52
- meth.call(expect) if meth
53
- expect.text "\nCommon Options:"
54
- expect.opt :verbose, "Run verbosely by default", :short => nil
55
- expect.opt :force, "Overwrite files that already exist", :short => nil
56
- expect.opt :pretend, "Run but do not make any changes", :short => nil
57
- expect.opt :revoke, "Revoke action (not available for all generators)", :short => nil
58
- expect.opt :quiet, "Suppress status output", :short => nil
59
- expect.opt :skip, "Skip files that already exist", :short => nil
60
- end
61
- end
62
-
63
48
  def pretend?
64
49
  !!options[:pretend]
65
50
  end
@@ -3,6 +3,12 @@
3
3
 
4
4
  opt :dest, "Destination root", :type => :string
5
5
  opt :source, "Source root", :type => :string
6
+ opt :verbose, "Run verbosely by default"
7
+ opt :force, "Overwrite files that already exist"
8
+ opt :pretend, "Run but do not make any changes"
9
+ opt :revoke, "Revoke action (not available for all generators)"
10
+ opt :quiet, "Suppress status output"
11
+ opt :skip, "Skip files that already exist"
6
12
 
7
13
  include Ing::Files
8
14
 
@@ -0,0 +1,35 @@
1
+ require 'stringio'
2
+ module Ing
3
+
4
+ # Classes in this namespace provide a uniform interface to different
5
+ # option parsers.
6
+ #
7
+ module OptionParsers
8
+
9
+ class Trollop
10
+
11
+ def parser
12
+ @parser ||= ::Trollop::Parser.new
13
+ end
14
+
15
+ def parse!(args)
16
+ ::Trollop.with_standard_exception_handling(parser) { parser.parse(args) }
17
+ end
18
+
19
+ def describe
20
+ s=StringIO.new
21
+ parser.educate_banner s
22
+ s.rewind; s.read
23
+ end
24
+
25
+ def help
26
+ s=StringIO.new
27
+ parser.educate s
28
+ s.rewind; s.read
29
+ end
30
+
31
+ end
32
+
33
+ end
34
+
35
+ end
@@ -11,8 +11,18 @@ module Ing
11
11
 
12
12
  class << self
13
13
 
14
+ attr_accessor :inherited_options
15
+
14
16
  def inherited(subclass)
15
- subclass.set_options self.options.dup
17
+ subclass.inherited_options = self.options.dup
18
+ end
19
+
20
+ def inherited_option?(name)
21
+ inherited_options.has_key?(name)
22
+ end
23
+
24
+ def option?(name)
25
+ options.has_key?(name)
16
26
  end
17
27
 
18
28
  # Modify the option named +name+ according to +specs+ (Hash).
@@ -23,8 +33,13 @@ module Ing
23
33
  # modify_option :file, :required => true
24
34
  #
25
35
  def modify_option(name, specs)
26
- opt(name) unless options[name]
27
- options[name].opts.merge!(specs)
36
+ if inherited_option?(name)
37
+ inherited_options[name].opts.merge!(specs)
38
+ elsif option?(name)
39
+ options[name].opts.merge!(specs)
40
+ else
41
+ opt(name, '', specs)
42
+ end
28
43
  end
29
44
 
30
45
  # Modify the default for option +name+ to +val+.
@@ -69,6 +84,12 @@ module Ing
69
84
  parser.opt *opt.to_args
70
85
  end
71
86
  end
87
+ unless inherited_options.empty?
88
+ parser.text "\nCommon Options:"
89
+ inherited_options.each do |name, opt|
90
+ parser.opt *opt.to_args
91
+ end
92
+ end
72
93
  end
73
94
 
74
95
  # Description lines
@@ -82,7 +103,7 @@ module Ing
82
103
  end
83
104
 
84
105
  # Options hash. Note that in a subclass, options are copied down from
85
- # superclass.
106
+ # superclass into inherited_options.
86
107
  def options
87
108
  @options ||= {}
88
109
  end
@@ -105,6 +126,31 @@ module Ing
105
126
  given
106
127
  end
107
128
 
129
+ # Build a hash of options that weren't given from command line via +ask+
130
+ # (i.e., $stdin.gets).
131
+ #
132
+ # Note it currently does not cast options to appropriate types.
133
+ # Also note because the shell is not available until after initialization,
134
+ # this must be called from command method(s), e.g. +#call+
135
+ #
136
+ def ask_unless_given(*opts)
137
+ opts.inject({}) do |memo, opt|
138
+ next memo if options[:"#{opt}_given"]
139
+ msg = self.class.options[opt].desc + "?"
140
+ df = self.class.options[opt].default
141
+ memo[opt] = shell.ask(msg, :default => df)
142
+ memo
143
+ end
144
+ end
145
+
146
+ # Shortcut for:
147
+ #
148
+ # options.merge! ask_unless_given :opt1, :opt2
149
+ #
150
+ def ask_unless_given!(*opts)
151
+ self.options.merge! ask_unless_given(*opts)
152
+ end
153
+
108
154
  # Use in initialization for option validation (post-parsing).
109
155
  #
110
156
  # Example:
@@ -2,10 +2,17 @@
2
2
  module Util
3
3
  extend self
4
4
 
5
- def to_class_names(str)
5
+ def decode_class(str, base=::Object)
6
+ namespaced_const_get( decode_class_names(str), base )
7
+ end
8
+
9
+ def decode_class_names(str)
6
10
  str.split(':').map {|c| c.gsub(/(?:\A|_+)(\w)/) {$1.upcase} }
11
+ end
12
+
13
+ def encode_class(klass)
14
+ encode_class_names(klass.to_s.split('::'))
7
15
  end
8
- alias decode_class_names to_class_names
9
16
 
10
17
  def encode_class_names(list)
11
18
  list.map {|c| c.to_s.gsub(/([A-Z])/) {
@@ -13,15 +20,7 @@
13
20
  }
14
21
  }.join(':')
15
22
  end
16
-
17
- def encode_class(klass)
18
- encode_class_names(klass.to_s.split('::'))
19
- end
20
-
21
- def to_classes(str, base=::Object)
22
- namespaced_const_get( to_class_names(str), base )
23
- end
24
-
23
+
25
24
  def namespaced_const_get(list, base=::Object)
26
25
  list.inject(base) {|m, klass| m.const_get(klass, false)}
27
26
  end
@@ -1,3 +1,3 @@
1
1
  module Ing
2
- VERSION = '0.1.5'
2
+ VERSION = '0.2.1'
3
3
  end
@@ -8,9 +8,15 @@ describe Ing do
8
8
  end
9
9
 
10
10
  def reset
11
- Ing::Dispatcher.dispatched.clear
11
+ Ing::Callstack.clear
12
12
  end
13
13
 
14
+ def count_executions_of(klass, meth)
15
+ Ing.callstack.count {|(k, m)|
16
+ k == klass && m == meth
17
+ }
18
+ end
19
+
14
20
  describe "#run" do
15
21
 
16
22
  describe "no method or args given" do
@@ -150,6 +156,13 @@ describe Ing do
150
156
  it 'should run with expected output' do
151
157
  assert_equal "1\n2\n3\n", capture_run(subject)
152
158
  end
159
+
160
+ it 'should only list one execution in the call stack for invoked commands' do
161
+ capture_run(subject)
162
+ assert_equal 1, count_executions_of(Invoking::Counter, :one)
163
+ assert_equal 1, count_executions_of(Invoking::Counter, :two)
164
+ assert_equal 1, count_executions_of(Invoking::Counter, :three)
165
+ end
153
166
  end
154
167
 
155
168
  describe "executing within tasks" do
@@ -159,6 +172,14 @@ describe Ing do
159
172
  it 'should run with expected output' do
160
173
  assert_equal "1\n2\n3\n3\n", capture_run(subject)
161
174
  end
175
+
176
+ it 'should only list each execution in the call stack for executed commands' do
177
+ capture_run(subject)
178
+ assert_equal 1, count_executions_of(Executing::Counter, :one)
179
+ assert_equal 1, count_executions_of(Executing::Counter, :two)
180
+ assert_equal 2, count_executions_of(Executing::Counter, :three)
181
+ end
182
+
162
183
  end
163
184
 
164
185
  end
@@ -1,4 +1,4 @@
1
- Dir[ File.expand_path('{actions,acceptance}/*.rb',
1
+ Dir[ File.expand_path('{unit,actions,acceptance}/*.rb',
2
2
  File.dirname(__FILE__))
3
3
  ].each do |f|
4
4
  puts "Loading tests: #{f.gsub(Dir.pwd,'.')}"
@@ -0,0 +1,334 @@
1
+ require File.expand_path('../test_helper', File.dirname(__FILE__))
2
+
3
+ describe Ing::Command do
4
+
5
+ def dummy_command_class
6
+ Class.new do
7
+ attr_reader :options
8
+ def initialize(options); @options = options; end
9
+ def dummy; end
10
+ end
11
+ end
12
+
13
+ def dummy_command_class_with_specify_options
14
+ Class.new do
15
+ def self.specify_options(expect); end
16
+ attr_reader :options
17
+ def initialize(options); @options = options; end
18
+ def dummy; end
19
+ end
20
+ end
21
+
22
+ def dummy_command_proc
23
+ x = Proc.new { }
24
+ def x.dummy; end
25
+ x
26
+ end
27
+
28
+ #-----------------------------------------------------------------------------
29
+ describe ".new" do
30
+
31
+ describe "when only command class passed" do
32
+ subject { Ing::Command.new dummy_command_class }
33
+
34
+ it "should have command_meth == :call" do
35
+ assert_equal :call, subject.command_meth
36
+ end
37
+
38
+ it "should have options == {}" do
39
+ assert_equal Hash.new, subject.options
40
+ end
41
+
42
+ it "should have args == []" do
43
+ assert_equal [], subject.args
44
+ end
45
+ end
46
+
47
+ describe "when command class and options hash passed" do
48
+ subject { Ing::Command.new dummy_command_class, options }
49
+ let(:options) { {:one => 1, :two => 2} }
50
+
51
+ it "should have command_meth == :call" do
52
+ assert_equal :call, subject.command_meth
53
+ end
54
+
55
+ it "should have options == passed options" do
56
+ assert_equal options, subject.options
57
+ end
58
+
59
+ it "should have args == []" do
60
+ assert_equal [], subject.args
61
+ end
62
+ end
63
+
64
+ describe "when command class and method passed" do
65
+ subject { Ing::Command.new dummy_command_class, meth, *args }
66
+ let(:meth) { "dummy" }
67
+ let(:args) { ["one", "two"] }
68
+
69
+ it "should have command_meth == passed method" do
70
+ assert_equal meth.to_sym, subject.command_meth
71
+ end
72
+
73
+ it "should have options == {}" do
74
+ assert_equal Hash.new, subject.options
75
+ end
76
+
77
+ it "should have args == remaining args" do
78
+ assert_equal args, subject.args
79
+ end
80
+ end
81
+
82
+ describe "when command proc and method passed" do
83
+ subject { Ing::Command.new dummy_command_proc, meth, *args }
84
+ let(:meth) { "dummy" }
85
+ let(:args) { ["one", "two"] }
86
+
87
+ it "should have command_meth == passed method" do
88
+ assert_equal meth.to_sym, subject.command_meth
89
+ end
90
+
91
+ it "should have options == {}" do
92
+ assert_equal Hash.new, subject.options
93
+ end
94
+
95
+ it "should have args == remaining args" do
96
+ assert_equal args, subject.args
97
+ end
98
+ end
99
+
100
+ describe "when command class and non-public-instance-method arg passed" do
101
+ subject { Ing::Command.new dummy_command_class, arg, *args }
102
+ let(:arg) { "remove_instance_variable" }
103
+ let(:args) { ["one", "two"] }
104
+
105
+ it "should have command_meth == :call" do
106
+ assert_equal :call, subject.command_meth
107
+ end
108
+
109
+ it "should have options == {}" do
110
+ assert_equal Hash.new, subject.options
111
+ end
112
+
113
+ it "should have args == all passed args" do
114
+ assert_equal [arg] + args, subject.args
115
+ end
116
+ end
117
+
118
+ describe "when command proc and non-public-method arg passed" do
119
+ subject { Ing::Command.new dummy_command_proc, arg, *args }
120
+ let(:arg) { "extended" }
121
+ let(:args) { ["one", "two"] }
122
+
123
+ it "should have command_meth == :call" do
124
+ assert_equal :call, subject.command_meth
125
+ end
126
+
127
+ it "should have options == {}" do
128
+ assert_equal Hash.new, subject.options
129
+ end
130
+
131
+ it "should have args == all passed args" do
132
+ assert_equal [arg] + args, subject.args
133
+ end
134
+ end
135
+
136
+ describe "when command class and option arg passed" do
137
+ subject { Ing::Command.new dummy_command_class, arg, *args }
138
+ let(:arg) { "--nonsense" }
139
+ let(:args) { ["one", "two"] }
140
+
141
+ it "should have command_meth == :call" do
142
+ assert_equal :call, subject.command_meth
143
+ end
144
+
145
+ it "should have options == {}" do
146
+ assert_equal Hash.new, subject.options
147
+ end
148
+
149
+ it "should have args == all passed args" do
150
+ assert_equal [arg] + args, subject.args
151
+ end
152
+ end
153
+
154
+ describe "when command proc and option arg passed" do
155
+ subject { Ing::Command.new dummy_command_proc, arg, *args }
156
+ let(:arg) { "--help" }
157
+ let(:args) { ["one", "two"] }
158
+
159
+ it "should have command_meth == :call" do
160
+ assert_equal :call, subject.command_meth
161
+ end
162
+
163
+ it "should have options == {}" do
164
+ assert_equal Hash.new, subject.options
165
+ end
166
+
167
+ it "should have args == all passed args" do
168
+ assert_equal [arg] + args, subject.args
169
+ end
170
+ end
171
+
172
+ end
173
+
174
+ #-----------------------------------------------------------------------------
175
+ describe "#instance" do
176
+ subject { Ing::Command.new command_class }
177
+ let(:command_class) { dummy_command_class }
178
+
179
+ it "should return an instance of the passed class" do
180
+ assert_kind_of command_class, subject.instance
181
+ end
182
+
183
+ describe "and passed class defines specify_options" do
184
+
185
+ def mock_parser_given(args, ret={})
186
+ parser = MiniTest::Mock.new
187
+ parser.expect(:parser,nil)
188
+ parser.expect(:"parse!",ret,[args])
189
+ parserclass = MiniTest::Mock.new
190
+ parserclass.expect(:new, parser)
191
+ parserclass
192
+ end
193
+
194
+ def expecting_parser_given(args, ret={})
195
+ Ing::Command.stub(:parser, mock_parser_given(args, ret)) do |stub|
196
+ #puts stub.parser.inspect
197
+ yield
198
+ end
199
+ end
200
+
201
+ # These don't work because `parse_options!` is indivisible from `instance`
202
+ # and you need an unstubbed :command_class for `parse_options!`
203
+ #
204
+ # def mock_command_constructor_given(opts)
205
+ # cmd = MiniTest::Mock.new
206
+ # cmd.expect(:new,nil,[opts])
207
+ # cmd
208
+ # end
209
+ #
210
+ # def expecting_command_constructor_given(ing_cmd, opts)
211
+ # ing_cmd.stub(:command_class,
212
+ # mock_command_constructor_given(opts)) do |stub|
213
+ # #puts stub.command_class.inspect
214
+ # yield
215
+ # end
216
+ # end
217
+
218
+ subject { Ing::Command.new command_class, *args }
219
+ let(:command_class) { dummy_command_class_with_specify_options }
220
+ let(:args) { ["one", "two", "three"] }
221
+
222
+ it "should interact with parser as expected" do
223
+ expecting_parser_given(args) do
224
+ subject.instance
225
+ end
226
+ end
227
+
228
+ describe "and an option hash is passed as the last arg" do
229
+ subject { Ing::Command.new command_class, *args, options }
230
+ let(:command_class) { dummy_command_class_with_specify_options }
231
+ let(:args) { ["--two", "2", "--four", "4", "--three", "1"] }
232
+ let(:options) { {:one => 1, :two => 2, :three => 3} }
233
+ let(:parsed_options) { {:two => 2, :four => 4, :three => 1} }
234
+
235
+ it "should merge passed options into the options parsed from other args" do
236
+ expecting_parser_given(args, parsed_options) do
237
+ merged_options = parsed_options.merge(options)
238
+ it = subject.instance
239
+ # note dummy.options returns what it was passed in constructor
240
+ assert_equal merged_options, it.options
241
+ end
242
+ end
243
+
244
+ end
245
+
246
+ end
247
+
248
+ end
249
+
250
+ #-----------------------------------------------------------------------------
251
+ describe "#execute" do
252
+
253
+ def mock_instance_sent(meth, args=[])
254
+ inst = MiniTest::Mock.new
255
+ inst.expect(:send, nil, [meth] + args)
256
+ inst
257
+ end
258
+
259
+ def expecting_instance_sent(ing_cmd, meth, args=[])
260
+ ing_cmd.stub(:instance, mock_instance_sent(meth, args)) do |stub|
261
+ #puts stub.instance.inspect
262
+ yield
263
+ end
264
+ end
265
+
266
+ describe "when only command class passed" do
267
+ subject { Ing::Command.new dummy_command_class }
268
+
269
+ it "instance should receive :call and no args" do
270
+ subject.instance
271
+ expecting_instance_sent(subject, :call) do
272
+ subject.execute
273
+ end
274
+ end
275
+
276
+ end
277
+
278
+ describe "when command class and options hash passed" do
279
+ subject { Ing::Command.new dummy_command_class, options }
280
+ let(:options) { {:one => 1, :two => 2} }
281
+
282
+ it "instance should receive :call and no args" do
283
+ subject.instance
284
+ expecting_instance_sent(subject, :call) do
285
+ subject.execute
286
+ end
287
+ end
288
+
289
+ end
290
+
291
+ describe "when command class and method passed with args" do
292
+ subject { Ing::Command.new dummy_command_class, meth, *args }
293
+ let(:meth) { "dummy" }
294
+ let(:args) { ["one", "two"] }
295
+
296
+ it "instance should receive passed method and args" do
297
+ subject.instance
298
+ expecting_instance_sent(subject, meth.to_sym, args) do
299
+ subject.execute
300
+ end
301
+ end
302
+
303
+ end
304
+
305
+ describe "when command proc and method passed" do
306
+ subject { Ing::Command.new dummy_command_proc, meth, *args }
307
+ let(:meth) { "dummy" }
308
+ let(:args) { ["one", "two"] }
309
+
310
+ it "instance should receive passed method, args, and empty options hash" do
311
+ subject.instance
312
+ expecting_instance_sent(subject, meth.to_sym, args + [{}]) do
313
+ subject.execute
314
+ end
315
+ end
316
+
317
+ end
318
+
319
+ describe "when command proc and options hash passed" do
320
+ subject { Ing::Command.new dummy_command_proc, options }
321
+ let(:options) { {:one => 1, :two => 2} }
322
+
323
+ it "instance should receive :call and passed options hash" do
324
+ subject.instance
325
+ expecting_instance_sent(subject, :call, [options]) do
326
+ subject.execute
327
+ end
328
+ end
329
+
330
+ end
331
+
332
+ end
333
+
334
+ end
@@ -0,0 +1,67 @@
1
+ require File.expand_path('../test_helper', File.dirname(__FILE__))
2
+
3
+ # These don't test much, basically just the `extract_boot_class!` logic.
4
+ # More comprehensive tests under acceptance/ing_run_tests.
5
+ #
6
+ describe Ing do
7
+
8
+ #-----------------------------------------------------------------------------
9
+ describe ".run" do
10
+
11
+ def mock_command_execute(klass, args)
12
+ cmd = MiniTest::Mock.new
13
+ cmd.expect(:execute, nil)
14
+ cmd.expect(:command_class, nil)
15
+ cmd.expect(:command_meth, nil)
16
+ cmdclass = MiniTest::Mock.new
17
+ cmdclass.expect(:new, cmd, [klass] + args)
18
+ cmdclass
19
+ end
20
+
21
+ def stubbing_command_execute(klass, args)
22
+ ::Ing.stub(:command, mock_command_execute(klass, args)) do |stub|
23
+ #puts stub.command.inspect
24
+ yield
25
+ end
26
+ end
27
+
28
+ def mock_callstack_push(klass, meth)
29
+ callst = MiniTest::Mock.new
30
+ callst.expect(:push, nil, [klass, meth])
31
+ callst
32
+ end
33
+
34
+ def stubbing_callstack_push(klass, meth)
35
+ ::Ing.stub(:_callstack, mock_callstack_push(klass, meth)) do |stub|
36
+ yield
37
+ end
38
+ end
39
+
40
+
41
+ describe "when first arg is built-in command" do
42
+ subject { ["generate"] + args }
43
+ let(:args) { ["something"] }
44
+
45
+ it "should execute with the specified command class and remaining args" do
46
+ stubbing_command_execute(::Ing::Commands::Generate, args) do
47
+ Ing.run subject
48
+ end
49
+ end
50
+
51
+ end
52
+
53
+ describe "when first arg is not a built-in command" do
54
+ subject { ["foo:die"] + args }
55
+ let(:args) { ["do_it"] }
56
+
57
+ it "should execute with the implicit command class and whole command line" do
58
+ stubbing_command_execute(::Ing::Commands::Implicit, subject) do
59
+ Ing.run subject
60
+ end
61
+ end
62
+ end
63
+
64
+ end
65
+
66
+
67
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ing
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.2.1
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-09-17 00:00:00.000000000Z
12
+ date: 2012-09-20 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: minitest
16
- requirement: &14733000 !ruby/object:Gem::Requirement
16
+ requirement: &7756700 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: '0'
22
22
  type: :development
23
23
  prerelease: false
24
- version_requirements: *14733000
24
+ version_requirements: *7756700
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: fakeweb
27
- requirement: &14677360 !ruby/object:Gem::Requirement
27
+ requirement: &7756220 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,7 +32,7 @@ dependencies:
32
32
  version: '0'
33
33
  type: :development
34
34
  prerelease: false
35
- version_requirements: *14677360
35
+ version_requirements: *7756220
36
36
  description: ! "\nAn alternative to Rake and Thor, Ing has a command-line syntax similar
37
37
  to \nThor's, and it incorporates Thor's (Rails') generator methods and shell \nconventions.
38
38
  But unlike Thor or Rake, it does not define its own DSL. Your tasks\ncorrespond
@@ -62,16 +62,17 @@ files:
62
62
  - lib/ing/actions/empty_directory.rb
63
63
  - lib/ing/actions/file_manipulation.rb
64
64
  - lib/ing/actions/inject_into_file.rb
65
- - lib/ing/boot.rb
65
+ - lib/ing/command.rb
66
+ - lib/ing/commands/boot.rb
66
67
  - lib/ing/commands/generate.rb
67
68
  - lib/ing/commands/help.rb
68
69
  - lib/ing/commands/implicit.rb
69
70
  - lib/ing/commands/list.rb
70
71
  - lib/ing/common_options.rb
71
- - lib/ing/dispatcher.rb
72
72
  - lib/ing/files.rb
73
73
  - lib/ing/generator.rb
74
74
  - lib/ing/lib_trollop.rb
75
+ - lib/ing/option_parsers/trollop.rb
75
76
  - lib/ing/shell.rb
76
77
  - lib/ing/task.rb
77
78
  - lib/ing/trollop/parser.rb
@@ -107,6 +108,8 @@ files:
107
108
  - test/spec_helper.rb
108
109
  - test/suite.rb
109
110
  - test/test_helper.rb
111
+ - test/unit/command_test.rb
112
+ - test/unit/ing_test.rb
110
113
  - todo.yml
111
114
  homepage: https://github.com/ericgj/ing
112
115
  licenses: []
@@ -162,4 +165,6 @@ test_files:
162
165
  - test/spec_helper.rb
163
166
  - test/suite.rb
164
167
  - test/test_helper.rb
168
+ - test/unit/command_test.rb
169
+ - test/unit/ing_test.rb
165
170
  has_rdoc:
@@ -1,143 +0,0 @@
1
- require 'stringio'
2
- require 'set'
3
-
4
- module Ing
5
-
6
- # Generic router for ing commands (both built-in and user-defined).
7
- # Resolves class, parses options with Trollop if target class
8
- # defines +specify_options+, then dispatches like (simplifying):
9
- #
10
- # Target.new(options).send(*args)
11
- #
12
- # if the target is class-like, i.e. responds to +new+. Otherwise it dispatches
13
- # to the target as a callable:
14
- #
15
- # Target.call(*args, options)
16
- #
17
- class Dispatcher
18
-
19
- # Global set of dispatched commands as [dispatch_class, dispatch_meth],
20
- # updated before dispatch
21
- def self.dispatched
22
- @dispatched ||= Set.new
23
- end
24
-
25
- # +Ing.invoke+
26
- def self.invoke(klass, *args, &config)
27
- allocate.tap {|d| d.initialize_preloaded(true, klass, *args) }.
28
- dispatch(&config)
29
- end
30
-
31
- # +Ing.execute+
32
- def self.execute(klass, *args, &config)
33
- allocate.tap {|d| d.initialize_preloaded(false, klass, *args) }.
34
- dispatch(&config)
35
- end
36
-
37
- attr_accessor :dispatch_class, :dispatch_meth, :args, :options
38
-
39
- # True if current dispatch class/method has been dispatched before
40
- def dispatched?
41
- Dispatcher.dispatched.include?([dispatch_class,dispatch_meth])
42
- end
43
-
44
- # Default constructor from +Ing.run+ (command line)
45
- def initialize(namespaces, classes, *args)
46
- ns = Util.namespaced_const_get(namespaces)
47
- self.dispatch_class = Util.namespaced_const_get(classes, ns)
48
- self.dispatch_meth = extract_method!(args, dispatch_class)
49
- self.args = args
50
- @invoking = false
51
- end
52
-
53
- # Alternate constructor for preloaded object and arguments
54
- # i.e. from +invoke+ or +execute+ instead of +run+
55
- def initialize_preloaded(invoking, klass, *args)
56
- self.options = (Hash === args.last ? args.pop : {})
57
- self.dispatch_class = klass
58
- self.dispatch_meth = extract_method!(args, dispatch_class)
59
- self.args = args
60
- @invoking = invoking
61
- end
62
-
63
- # Returns stream (StringIO) of description text from specify_options.
64
- # Note this does not parse the options. Used by +Ing::Commands::List+.
65
- def describe
66
- s=StringIO.new
67
- with_option_parser(self.dispatch_class) do |p|
68
- p.educate_banner s
69
- end
70
- s.rewind; s
71
- end
72
-
73
- # Returns stream (StringIO) of help text from specify_options.
74
- # Note this does not parse the options. Used by +Ing::Commands::Help+.
75
- def help
76
- s=StringIO.new
77
- with_option_parser(self.dispatch_class) do |p|
78
- p.educate s
79
- end
80
- s.rewind; s
81
- end
82
-
83
- # Public dispatch method used by all types of dispatch (run, invoke,
84
- # execute). Does not dispatch if invoking and already dispatched.
85
- def dispatch(&config)
86
- unless @invoking && dispatched?
87
- record_dispatch
88
- execute(&config)
89
- end
90
- end
91
-
92
- def with_option_parser(klass) # :nodoc:
93
- return unless klass.respond_to?(:specify_options)
94
- klass.specify_options(p = Trollop::Parser.new)
95
- yield p
96
- end
97
-
98
- private
99
-
100
- def record_dispatch
101
- Dispatcher.dispatched.add [dispatch_class, dispatch_meth]
102
- end
103
-
104
- def execute
105
- self.options = parse_options!(args, dispatch_class) || {}
106
- if dispatch_class.respond_to?(:new)
107
- cmd = dispatch_class.new(options)
108
- yield cmd if block_given?
109
- cmd.send(dispatch_meth, *args)
110
- else
111
- dispatch_class.call *args, options
112
- end
113
- end
114
-
115
- def parse_options!(args, klass)
116
- with_option_parser(klass) do |p|
117
- Trollop.with_standard_exception_handling(p) { p.parse(args) }
118
- end
119
- end
120
-
121
- def extract_method!(args, klass)
122
- return :call if args.empty?
123
- if meth = whitelist(args.first, klass)
124
- args.shift
125
- else
126
- meth = :call
127
- end
128
- meth
129
- end
130
-
131
- # Note this currently does no filtering, but basically checks for respond_to
132
- def whitelist(meth, klass)
133
- finder = Proc.new {|m| m == meth.to_sym}
134
- if klass.respond_to?(:new)
135
- klass.public_instance_methods(true).find(&finder)
136
- else
137
- klass.public_methods.find(&finder)
138
- end
139
- end
140
-
141
- end
142
-
143
- end