boson 0.0.1 → 0.1.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,54 @@
1
+ module Boson
2
+ # Runner used when starting irb. To use in irb, drop this in your ~/.irbrc:
3
+ # require 'boson'
4
+ # Boson.start
5
+ class ConsoleRunner < Runner
6
+ class <<self
7
+ # Starts Boson by loading configured libraries. If no default libraries are specified in the config,
8
+ # it will load up all detected libraries.
9
+ # ==== Options
10
+ # [:libraries] Array of libraries to load.
11
+ # [:verbose] Boolean to be verbose about libraries loading. Default is true.
12
+ # [:no_defaults] Boolean which turns off loading any default libraries. Default is false.
13
+ # [:autoload_libraries] Boolean which makes any command execution easier. It redefines
14
+ # method_missing on Boson.main_object so that commands with unloaded
15
+ # libraries are automatically loaded. Default is false.
16
+ def start(options={})
17
+ @options = {:verbose=>true}.merge options
18
+ init unless @initialized
19
+ Manager.load(@options[:libraries], load_options) if @options[:libraries]
20
+ end
21
+
22
+ # Loads libraries and then starts irb (or the configured console) from the commandline.
23
+ def bin_start(repl, libraries)
24
+ start :no_defaults=>true, :libraries=>libraries
25
+ repl = Boson.repo.config[:console] if Boson.repo.config[:console]
26
+ repl = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb' unless repl.is_a?(String)
27
+ unless repl.index('/') == 0 || (repl = Util.which(repl))
28
+ $stderr.puts "Console not found. Please specify full path in config[:console]."
29
+ return
30
+ end
31
+ ARGV.replace ['-f']
32
+ Kernel.load $0 = repl
33
+ end
34
+
35
+ def init #:nodoc:
36
+ super
37
+ define_autoloader if @options[:autoload_libraries]
38
+ @initialized = true
39
+ end
40
+
41
+ def default_libraries #:nodoc:
42
+ defaults = super
43
+ unless @options[:no_defaults]
44
+ new_defaults = Boson.repos.map {|e| e.config[:console_defaults] }.flatten
45
+ new_defaults = detected_libraries if new_defaults.empty?
46
+ defaults += new_defaults
47
+ defaults.uniq!
48
+ end
49
+ defaults
50
+ end
51
+ end
52
+ end
53
+ end
54
+
@@ -1,27 +1,96 @@
1
1
  require 'shellwords'
2
2
  module Boson
3
+ # Scientist redefines the methods of commands that have options and/or take global options. This redefinition
4
+ # allows a command to receive its arguments normally or as a commandline app does. For a command's
5
+ # method to be redefined correctly, its last argument _must_ expect a hash.
6
+ #
7
+ # Take for example this basic method/command with an options definition:
8
+ # options :level=>:numeric, :verbose=>:boolean
9
+ # def foo(arg='', options={})
10
+ # [arg, options]
11
+ # end
12
+ #
13
+ # When Scientist wraps around foo(), argument defaults are respected:
14
+ # foo '', :verbose=>true # normal call
15
+ # foo '-v' # commandline call
16
+ #
17
+ # Both calls return: ['', {:verbose=>true}]
18
+ #
19
+ # Non-string arguments can be passed in:
20
+ # foo Object, :level=>1
21
+ # foo Object, 'l1'
22
+ #
23
+ # Both calls return: [Object, {:level=>1}]
24
+ #
25
+ # === Global Options
26
+ # Any command with options comes with default global options. For example '-hv' on such a command
27
+ # prints a help summarizing a command's options as well as the global options.
28
+ # When using global options along with command options, global options _must_ precede command options.
29
+ # Take for example using the global --pretend option with the method above:
30
+ # irb>> foo '-p -l=1'
31
+ # Arguments: ["", {:level=>1}]
32
+ # Global options: {:pretend=>true}
33
+ #
34
+ # If a global option conflicts with a command's option, the command's option takes precedence. You can get around
35
+ # this by passing a --global option which takes a string of options without their dashes. For example:
36
+ # foo '-p --fields=f1,f2 -l=1'
37
+ # # is the same as
38
+ # foo ' -g "p fields=f1,f2" -l=1 '
39
+ #
40
+ # === Rendering Views With Global Options
41
+ # Perhaps the most important global option is --render. This option toggles the rendering of your command's output
42
+ # with Hirb[http://github.com/cldwalker/hirb]. Since Hirb can be customized to generate any view, this option allows
43
+ # you toggle a predefined view for a command without embedding view code in your command!
44
+ #
45
+ # Here's a simple example, toggling Hirb's table view:
46
+ # # Defined in a library file:
47
+ # #@options {}
48
+ # def list(options={})
49
+ # [1,2,3]
50
+ # end
51
+ #
52
+ # Using it in irb:
53
+ # >> list
54
+ # => [1,2,3]
55
+ # >> list '-r'
56
+ # +-------+
57
+ # | value |
58
+ # +-------+
59
+ # | 1 |
60
+ # | 2 |
61
+ # | 3 |
62
+ # +-------+
63
+ # 3 rows in set
64
+ # => true
65
+ #
66
+ # To default to rendering a view for a command, add a render_options {method attribute}[link:classes/Boson/MethodInspector.html]
67
+ # above list() along with any options you want to pass to your Hirb helper class. In this case, using '-r' gives you the
68
+ # command's returned object instead of a formatted view!
3
69
  module Scientist
4
70
  extend self
71
+ # Handles all Scientist errors.
5
72
  class Error < StandardError; end
6
73
  class EscapeGlobalOption < StandardError; end
74
+
7
75
  attr_reader :global_options, :rendered
8
76
  @no_option_commands ||= []
9
77
  GLOBAL_OPTIONS = {
10
78
  :help=>{:type=>:boolean, :desc=>"Display a command's help"},
11
- :render=>{:type=>:boolean, :desc=>"Toggle a command's default render behavior"},
79
+ :render=>{:type=>:boolean, :desc=>"Toggle a command's default rendering behavior"},
12
80
  :verbose=>{:type=>:boolean, :desc=>"Increase verbosity for help, errors, etc."},
13
- :global=>{:type=>:string, :desc=>"Pass a string of global options without the dashes i.e. '-p -f=f1,f2' -> 'p f=f1,f2'"},
81
+ :global=>{:type=>:string, :desc=>"Pass a string of global options without the dashes"},
14
82
  :pretend=>{:type=>:boolean, :desc=>"Display what a command would execute without executing it"}
15
- }
83
+ } #:nodoc:
16
84
  RENDER_OPTIONS = {
17
85
  :fields=>{:type=>:array, :desc=>"Displays fields in the order given"},
18
86
  :sort=>{:type=>:string, :desc=>"Sort by given field"},
19
- :as=>{:type=>:string, :desc=>"Hirb helper class which renders"},
87
+ :class=>{:type=>:string, :desc=>"Hirb helper class which renders"},
20
88
  :reverse_sort=>{:type=>:boolean, :desc=>"Reverse a given sort"},
21
89
  :max_width=>{:type=>:numeric, :desc=>"Max width of a table"},
22
90
  :vertical=>{:type=>:boolean, :desc=>"Display a vertical table"}
23
- }
91
+ } #:nodoc:
24
92
 
93
+ # Redefines a command's method for the given object.
25
94
  def create_option_command(obj, command)
26
95
  cmd_block = create_option_command_block(obj, command)
27
96
  @no_option_commands << command if command.options.nil?
@@ -30,12 +99,14 @@ module Boson
30
99
  }
31
100
  end
32
101
 
102
+ # The actual method which replaces a command's original method
33
103
  def create_option_command_block(obj, command)
34
104
  lambda {|*args|
35
105
  Boson::Scientist.translate_and_render(obj, command, args) {|args| super(*args) }
36
106
  }
37
107
  end
38
108
 
109
+ #:stopdoc:
39
110
  def translate_and_render(obj, command, args)
40
111
  @global_options = {}
41
112
  args = translate_args(obj, command, args)
@@ -58,13 +129,9 @@ module Boson
58
129
  return @args if @no_option_commands.include?(@command)
59
130
  @args << parsed_options
60
131
  if @args.size != command.arg_size && !command.has_splat_args?
61
- command_size = @args.size > command.arg_size ? command.arg_size : command.arg_size - 1
62
- if @args.size - 1 == command_size
63
- raise Error, "Arguments are misaligned. Possible causes are incorrect argument "+
64
- "size or no argument for this method's options."
65
- else
66
- raise ArgumentError, "wrong number of arguments (#{@args.size - 1} for #{command_size})"
67
- end
132
+ command_size, args_size = @args.size > command.arg_size ? [command.arg_size, @args.size] :
133
+ [command.arg_size - 1, @args.size - 1]
134
+ raise ArgumentError, "wrong number of arguments (#{args_size} for #{command_size})"
68
135
  end
69
136
  end
70
137
  @args
@@ -110,7 +177,7 @@ module Boson
110
177
  @command.render_options[k] = {:default=>v}
111
178
  end
112
179
  }
113
- opts = Util.recursive_hash_merge(@command.render_options, RENDER_OPTIONS)
180
+ opts = Util.recursive_hash_merge(@command.render_options, Util.deep_copy(RENDER_OPTIONS))
114
181
  opts[:sort][:values] ||= opts[:fields][:values] if opts[:fields][:values]
115
182
  opts
116
183
  end
@@ -129,11 +196,17 @@ module Boson
129
196
  parsed_options, @args = parse_options Shellwords.shellwords(@args[0])
130
197
  # last string argument interpreted as args + options
131
198
  elsif @args.size > 1 && @args[-1].is_a?(String)
132
- parsed_options, new_args = parse_options @args.pop.split(/\s+/)
199
+ args = caller.grep(/bin_runner.rb:/).empty? ? Shellwords.shellwords(@args.pop) : @args
200
+ parsed_options, new_args = parse_options args
133
201
  @args += new_args
134
- # default options
135
- elsif (@args.size <= @command.arg_size - 1) || (@command.has_splat_args? && !@args[-1].is_a?(Hash))
202
+ # add default options
203
+ elsif (!@command.has_splat_args? && @args.size <= @command.arg_size - 1) ||
204
+ (@command.has_splat_args? && !@args[-1].is_a?(Hash))
205
+ parsed_options = parse_options([])[0]
206
+ # merge default options with given hash of options
207
+ elsif (@command.has_splat_args? || (@args.size == @command.arg_size)) && @args[-1].is_a?(Hash)
136
208
  parsed_options = parse_options([])[0]
209
+ parsed_options.merge!(@args.pop)
137
210
  end
138
211
  parsed_options
139
212
  end
@@ -143,7 +216,8 @@ module Boson
143
216
  @global_options = option_parser.parse @command.option_parser.leading_non_opts
144
217
  new_args = option_parser.non_opts.dup + @command.option_parser.trailing_non_opts
145
218
  if @global_options[:global]
146
- global_opts = Shellwords.shellwords(@global_options[:global]).map {|str| (str.length > 1 ? "--" : "-") + str }
219
+ global_opts = Shellwords.shellwords(@global_options[:global]).map {|str|
220
+ ((str[/^(.*?)=/,1] || str).length > 1 ? "--" : "-") + str }
147
221
  @global_options.merge! option_parser.parse(global_opts)
148
222
  end
149
223
  raise EscapeGlobalOption if @global_options[:help]
@@ -164,5 +238,6 @@ module Boson
164
238
  }
165
239
  end
166
240
  end
241
+ #:startdoc:
167
242
  end
168
243
  end
@@ -1,7 +1,9 @@
1
1
  module Boson
2
+ # Collection of utility methods used throughout Boson.
2
3
  module Util
3
4
  extend self
4
- #From Rails ActiveSupport
5
+ # From Rails ActiveSupport, converts a camelcased string to an underscored string:
6
+ # 'Boson::MethodInspector' -> 'boson/method_inspector'
5
7
  def underscore(camel_cased_word)
6
8
  camel_cased_word.to_s.gsub(/::/, '/').
7
9
  gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
@@ -10,11 +12,14 @@ module Boson
10
12
  downcase
11
13
  end
12
14
 
13
- # from Rails ActiveSupport
15
+ # From Rails ActiveSupport, does the reverse of underscore:
16
+ # 'boson/method_inspector' -> 'Boson::MethodInspector'
14
17
  def camelize(string)
15
18
  string.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
16
19
  end
17
-
20
+
21
+ # Converts a module/class string to the actual constant.
22
+ # Returns nil if not found.
18
23
  def constantize(string)
19
24
  any_const_get(camelize(string))
20
25
  end
@@ -40,6 +45,9 @@ module Boson
40
45
  end
41
46
  end
42
47
 
48
+ # Detects new object/kernel methods, gems and modules created within a block.
49
+ # Returns a hash of what's detected.
50
+ # Valid options and possible returned keys are :methods, :object_methods, :modules, :gems.
43
51
  def detect(options={}, &block)
44
52
  options = {:methods=>true, :object_methods=>true}.merge!(options)
45
53
  original_gems = Gem.loaded_specs.keys if Object.const_defined? :Gem
@@ -56,6 +64,7 @@ module Boson
56
64
  detected
57
65
  end
58
66
 
67
+ # Safely calls require, returning false if LoadError occurs.
59
68
  def safe_require(lib)
60
69
  begin
61
70
  require lib
@@ -64,16 +73,20 @@ module Boson
64
73
  end
65
74
  end
66
75
 
76
+ # Returns all modules that currently exist.
67
77
  def modules
68
78
  all_modules = []
69
79
  ObjectSpace.each_object(Module) {|e| all_modules << e}
70
80
  all_modules
71
81
  end
72
82
 
83
+ # Returns array of _all_ common instance methods between two modules/classes.
73
84
  def common_instance_methods(module1, module2)
74
85
  (module1.instance_methods + module1.private_instance_methods) & (module2.instance_methods + module2.private_instance_methods)
75
86
  end
76
87
 
88
+ # Creates a module under a given base module and possible name. If the module already exists, it attempts
89
+ # to create one with a number appended to the name.
77
90
  def create_module(base_module, name)
78
91
  desired_class = camelize(name)
79
92
  if (suffix = ([""] + (1..10).to_a).find {|e| !base_module.const_defined?(desired_class+e)})
@@ -81,10 +94,16 @@ module Boson
81
94
  end
82
95
  end
83
96
 
97
+ # Behaves just like the unix which command, returning the full path to an executable based on ENV['PATH'].
84
98
  def which(command)
85
99
  ENV['PATH'].split(File::PATH_SEPARATOR).map {|e| File.join(e, command) }.find {|e| File.exists?(e) }
86
100
  end
87
101
 
102
+ # Deep copies any object if it can be marshaled. Useful for deep hashes.
103
+ def deep_copy(obj)
104
+ Marshal::load(Marshal::dump(obj))
105
+ end
106
+
88
107
  # Recursively merge hash1 with hash2.
89
108
  def recursive_hash_merge(hash1, hash2)
90
109
  hash1.merge(hash2) {|k,o,n| (o.is_a?(Hash)) ? recursive_hash_merge(o,n) : n}
@@ -1,18 +1,30 @@
1
1
  module Boson
2
+ # Handles {Hirb}[http://tagaholic.me/hirb/]-based views.
2
3
  module View
3
4
  extend self
4
5
 
6
+ # Enables hirb and reads a config file from the main repo's config/hirb.yml.
5
7
  def enable
6
8
  Hirb::View.enable(:config_file=>File.join(Boson.repo.config_dir, 'hirb.yml')) unless @enabled
7
9
  @enabled = true
8
10
  end
9
11
 
12
+ # Renders any object via Hirb. Options are passed directly to
13
+ # {Hirb::Console.render_output}[http://tagaholic.me/hirb/doc/classes/Hirb/Console.html#M000011].
10
14
  def render(object, options={})
11
- [nil,false,true].include?(object) ? puts(object.inspect) : render_object(object, options)
15
+ if silent_object?(object)
16
+ puts(object.inspect) unless options[:silence_booleans]
17
+ else
18
+ render_object(object, options)
19
+ end
20
+ end
21
+
22
+ def silent_object?(obj)
23
+ [nil,false,true].include?(obj)
12
24
  end
13
25
 
14
- def render_object(object, options={})
15
- options[:class] = options.delete(:as) || :auto_table
26
+ def render_object(object, options={}) #:nodoc:
27
+ options[:class] ||= :auto_table
16
28
  if object.is_a?(Array) && object.size > 0 && (sort = options.delete(:sort))
17
29
  begin
18
30
  sort_lambda = object[0].is_a?(Hash) ? (object[0][sort].respond_to?(:<=>) ?
@@ -36,17 +36,17 @@ module Boson
36
36
  start('-l', 'blah', 'libraries')
37
37
  end
38
38
 
39
- test "repl option starts repl" do
40
- ReplRunner.expects(:start)
39
+ test "console option starts irb" do
40
+ ConsoleRunner.expects(:start)
41
41
  Util.expects(:which).returns("/usr/bin/irb")
42
42
  Kernel.expects(:load).with("/usr/bin/irb")
43
- start("--repl")
43
+ start("--console")
44
44
  end
45
45
 
46
- test "repl option but no repl found prints error" do
47
- ReplRunner.expects(:start)
46
+ test "console option but no irb found prints error" do
47
+ ConsoleRunner.expects(:start)
48
48
  Util.expects(:which).returns(nil)
49
- capture_stderr { start("--repl") } =~ /Repl not found/
49
+ capture_stderr { start("--console") }.should =~ /Console not found/
50
50
  end
51
51
 
52
52
  test "execute option executes string" do
@@ -54,17 +54,27 @@ module Boson
54
54
  capture_stdout { start("-e", "p 1 + 1") }.should == "2\n"
55
55
  end
56
56
 
57
+ test "global option takes value with whitespace" do
58
+ View.expects(:render).with(anything, {:sort => :lib, :fields => [:name, :lib]})
59
+ start('commands', '-g', 'f=name,lib s=lib')
60
+ end
61
+
57
62
  test "execute option errors are caught" do
58
63
  capture_stderr { start("-e", "raise 'blah'") }.should =~ /^Error:/
59
64
  end
60
65
 
61
66
  test "command and too many arguments prints error" do
62
- capture_stdout { start('commands','1','2','3') }.should =~ /Wrong number/
67
+ capture_stdout { capture_stderr { start('commands','1','2','3') }.should =~ /'commands'.*incorrect/ }
68
+ end
69
+
70
+ test "failed subcommand prints error and not command not found" do
71
+ BinRunner.expects(:execute_command).raises("bling")
72
+ capture_stderr { start("commands.blah") }.should =~ /Error: bling/
63
73
  end
64
74
 
65
75
  test "undiscovered command prints error" do
66
76
  BinRunner.expects(:load_command_by_index).returns(false)
67
- capture_stderr { start('blah') }.should =~ /Error.*blah/
77
+ capture_stderr { start('blah') }.should =~ /Error.*not found/
68
78
  end
69
79
 
70
80
  test "basic command executes" do
@@ -30,7 +30,7 @@ module Boson
30
30
  end
31
31
 
32
32
  test "loads and strips aliases from a library's commands" do
33
- with_config(:commands=>{"blah"=>{:alias=>'b'}}) do
33
+ with_config(:command_aliases=>{"blah"=>'b'}) do
34
34
  load :blah, :file_string=>"module Blah; def blah; end; alias_method(:b, :blah); end"
35
35
  library_loaded?('blah')
36
36
  library('blah').commands.should == ['blah']
@@ -37,7 +37,7 @@ module Boson
37
37
  after(:each) { Object.send(:remove_const, "Aquateen") }
38
38
 
39
39
  test "created with command specific config" do
40
- with_config(:commands=>{'frylock'=>{:alias=>'fr'}}) do
40
+ with_config(:command_aliases=>{'frylock'=>'fr'}) do
41
41
  Manager.expects(:create_instance_aliases).with({"Aquateen"=>{"frylock"=>"fr"}})
42
42
  load_library :name=>'aquateen', :commands=>['frylock'], :module=>Aquateen
43
43
  library_loaded? 'aquateen'
@@ -53,7 +53,7 @@ module Boson
53
53
  end
54
54
 
55
55
  test "not created and warns for commands with no module" do
56
- with_config(:commands=>{'frylock'=>{:alias=>'fr'}}) do
56
+ with_config(:command_aliases=>{'frylock'=>'fr'}) do
57
57
  capture_stderr {
58
58
  load_library(:name=>'aquateen', :commands=>['frylock'])
59
59
  }.should =~ /No aliases/
@@ -16,7 +16,7 @@ class Boson::RepoTest < Test::Unit::TestCase
16
16
  end
17
17
 
18
18
  test "ignores nonexistent file and sets config defaults" do
19
- assert @repo.config[:commands].is_a?(Hash) && @repo.config[:libraries].is_a?(Hash)
19
+ assert @repo.config[:command_aliases].is_a?(Hash) && @repo.config[:libraries].is_a?(Hash)
20
20
  end
21
21
  end
22
22
  end
@@ -3,17 +3,17 @@ require File.join(File.dirname(__FILE__), 'test_helper')
3
3
  module Boson
4
4
  class RunnerTest < Test::Unit::TestCase
5
5
  context "repl_runner" do
6
- def start(*args)
6
+ def start(hash={})
7
7
  Hirb.stubs(:enable)
8
- Boson.start(*args)
8
+ Boson.start(hash.merge(:verbose=>false))
9
9
  end
10
10
 
11
11
  before(:all) { reset }
12
- before(:each) { Boson::ReplRunner.instance_eval("@initialized = false") }
12
+ before(:each) { Boson::ConsoleRunner.instance_eval("@initialized = false") }
13
13
 
14
- test "loads default libraries and libraries in :defaults config" do
14
+ test "loads default libraries and libraries in :console_defaults config" do
15
15
  defaults = Boson::Runner.default_libraries + ['yo']
16
- with_config(:defaults=>['yo']) do
16
+ with_config(:console_defaults=>['yo']) do
17
17
  Manager.expects(:load).with {|*args| args[0] == defaults }
18
18
  start
19
19
  end
@@ -21,12 +21,12 @@ module Boson
21
21
 
22
22
  test "doesn't call init twice" do
23
23
  start
24
- ReplRunner.expects(:init).never
24
+ ConsoleRunner.expects(:init).never
25
25
  start
26
26
  end
27
27
 
28
28
  test "loads multiple libraries with :libraries option" do
29
- ReplRunner.expects(:init)
29
+ ConsoleRunner.expects(:init)
30
30
  Manager.expects(:load).with([:lib1,:lib2], anything)
31
31
  start(:libraries=>[:lib1, :lib2])
32
32
  end