boson 0.0.1

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.
Files changed (47) hide show
  1. data/LICENSE.txt +22 -0
  2. data/README.rdoc +133 -0
  3. data/Rakefile +52 -0
  4. data/VERSION.yml +4 -0
  5. data/bin/boson +6 -0
  6. data/lib/boson.rb +72 -0
  7. data/lib/boson/command.rb +117 -0
  8. data/lib/boson/commands.rb +7 -0
  9. data/lib/boson/commands/core.rb +66 -0
  10. data/lib/boson/commands/web_core.rb +36 -0
  11. data/lib/boson/index.rb +95 -0
  12. data/lib/boson/inspector.rb +80 -0
  13. data/lib/boson/inspectors/argument_inspector.rb +92 -0
  14. data/lib/boson/inspectors/comment_inspector.rb +79 -0
  15. data/lib/boson/inspectors/method_inspector.rb +94 -0
  16. data/lib/boson/libraries/file_library.rb +76 -0
  17. data/lib/boson/libraries/gem_library.rb +21 -0
  18. data/lib/boson/libraries/module_library.rb +17 -0
  19. data/lib/boson/libraries/require_library.rb +11 -0
  20. data/lib/boson/library.rb +108 -0
  21. data/lib/boson/loader.rb +103 -0
  22. data/lib/boson/manager.rb +184 -0
  23. data/lib/boson/namespace.rb +45 -0
  24. data/lib/boson/option_parser.rb +318 -0
  25. data/lib/boson/repo.rb +38 -0
  26. data/lib/boson/runner.rb +51 -0
  27. data/lib/boson/runners/bin_runner.rb +100 -0
  28. data/lib/boson/runners/repl_runner.rb +40 -0
  29. data/lib/boson/scientist.rb +168 -0
  30. data/lib/boson/util.rb +93 -0
  31. data/lib/boson/view.rb +31 -0
  32. data/test/argument_inspector_test.rb +62 -0
  33. data/test/bin_runner_test.rb +136 -0
  34. data/test/commands_test.rb +51 -0
  35. data/test/comment_inspector_test.rb +99 -0
  36. data/test/config/index.marshal +0 -0
  37. data/test/file_library_test.rb +50 -0
  38. data/test/index_test.rb +117 -0
  39. data/test/loader_test.rb +181 -0
  40. data/test/manager_test.rb +110 -0
  41. data/test/method_inspector_test.rb +64 -0
  42. data/test/option_parser_test.rb +365 -0
  43. data/test/repo_test.rb +22 -0
  44. data/test/runner_test.rb +43 -0
  45. data/test/scientist_test.rb +291 -0
  46. data/test/test_helper.rb +119 -0
  47. metadata +133 -0
@@ -0,0 +1,100 @@
1
+ module Boson
2
+ class BinRunner < Runner
3
+ GLOBAL_OPTIONS = {
4
+ :verbose=>{:type=>:boolean, :desc=>"Verbose description of loading libraries or help"},
5
+ :index=>{:type=>:boolean, :desc=>"Updates index"},
6
+ :execute=>{:type=>:string, :desc=>"Executes given arguments as a one line script"},
7
+ :repl=>{:type=>:boolean, :desc=>"Drops into irb or another given repl/shell with default and explicit libraries loaded"},
8
+ :help=>{:type=>:boolean, :desc=>"Displays this help message or a command's help if given a command"},
9
+ :load=>{:type=>:array, :values=>all_libraries, :enum=>false, :desc=>"A comma delimited array of libraries to load"}
10
+ }
11
+
12
+ class <<self
13
+ attr_accessor :command
14
+ def start(args=ARGV)
15
+ @command, @options, @args = parse_args(args)
16
+ return print_usage if args.empty? || (@command.nil? && !@options[:repl] && !@options[:execute])
17
+ return ReplRunner.bin_start(@options[:repl], @options[:load]) if @options[:repl]
18
+ init
19
+
20
+ if @options[:help]
21
+ print_command_help
22
+ elsif @options[:execute]
23
+ Boson.main_object.instance_eval @options[:execute]
24
+ else
25
+ execute_command
26
+ end
27
+ rescue Exception
28
+ message = (@command && !Boson.can_invoke?(@command)) ?
29
+ "Error: Command '#{@command}' not found" : "Error: #{$!.message}"
30
+ message += "\nActual error: #{$!}\n" + $!.backtrace.inspect if @options && @options[:verbose]
31
+ $stderr.puts message
32
+ end
33
+
34
+ def init
35
+ super
36
+ Index.update(:verbose=>true) if @options[:index]
37
+ if @options[:load]
38
+ Manager.load @options[:load], load_options
39
+ elsif @options[:execute]
40
+ define_autoloader
41
+ else
42
+ load_command_by_index
43
+ end
44
+ end
45
+
46
+ def load_command_by_index
47
+ Index.update(:verbose=>@options[:verbose]) if !@options[:index] && Boson.can_invoke?(@command) && !@options[:help]
48
+ if !Boson.can_invoke?(@command) && ((lib = Index.find_library(@command)) ||
49
+ (Index.update(:verbose=>@options[:verbose]) && (lib = Index.find_library(@command))))
50
+ Manager.load lib, load_options
51
+ end
52
+ end
53
+
54
+ def default_libraries
55
+ super + (Boson.repo.config[:bin_defaults] || [])
56
+ end
57
+
58
+ def execute_command
59
+ command, subcommand = @command.include?('.') ? @command.split('.', 2) : [@command, nil]
60
+ dispatcher = subcommand ? Boson.invoke(command) : Boson.main_object
61
+ @args = @args.join(" ") if ((com = Boson::Command.find(@command)) && com.option_command?)
62
+ render_output dispatcher.send(subcommand || command, *@args)
63
+ rescue ArgumentError
64
+ puts "Wrong number of arguments for #{@command}\n\n"
65
+ print_command_help
66
+ end
67
+
68
+ def print_command_help
69
+ Boson.invoke(:usage, @command, :verbose=>@options[:verbose])
70
+ end
71
+
72
+ def parse_args(args)
73
+ @option_parser = OptionParser.new(GLOBAL_OPTIONS)
74
+ options = @option_parser.parse(args.dup, :opts_before_args=>true)
75
+ new_args = @option_parser.non_opts
76
+ [new_args.shift, options, new_args]
77
+ end
78
+
79
+ def render_output(output)
80
+ if Scientist.global_options
81
+ puts output.inspect unless Scientist.rendered
82
+ else
83
+ View.render(output)
84
+ end
85
+ end
86
+
87
+ def print_usage
88
+ puts "boson [GLOBAL OPTIONS] [COMMAND] [ARGS] [COMMAND OPTIONS]\n\n"
89
+ puts "GLOBAL OPTIONS"
90
+ View.enable
91
+ @option_parser.print_usage_table
92
+ if @options[:verbose]
93
+ Manager.load [Boson::Commands::Core]
94
+ puts "\n\nDEFAULT COMMANDS"
95
+ Boson.invoke :commands, "", :fields=>["name", "usage", "description"], :description=>false
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,40 @@
1
+ module Boson
2
+ class ReplRunner < Runner
3
+ class <<self
4
+ def start(options={})
5
+ @options = options
6
+ init unless @initialized
7
+ Manager.load(@options[:libraries], load_options) if @options[:libraries]
8
+ end
9
+
10
+ def init
11
+ super
12
+ define_autoloader if @options[:autoload_libraries]
13
+ @initialized = true
14
+ end
15
+
16
+ def bin_start(repl, libraries)
17
+ start :no_defaults=>true, :libraries=>libraries
18
+ repl = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb' unless repl.is_a?(String)
19
+ unless repl.index('/') == 0 || (repl = Util.which(repl))
20
+ $stderr.puts "Repl not found. Please specify full path of repl."
21
+ return
22
+ end
23
+ ARGV.replace ['-f']
24
+ Kernel.load $0 = repl
25
+ end
26
+
27
+ def default_libraries
28
+ defaults = super
29
+ unless @options[:no_defaults]
30
+ new_defaults = Boson.repos.map {|e| e.config[:defaults] }.flatten
31
+ new_defaults = detected_libraries if new_defaults.empty?
32
+ defaults += new_defaults
33
+ defaults.uniq!
34
+ end
35
+ defaults
36
+ end
37
+ end
38
+ end
39
+ end
40
+
@@ -0,0 +1,168 @@
1
+ require 'shellwords'
2
+ module Boson
3
+ module Scientist
4
+ extend self
5
+ class Error < StandardError; end
6
+ class EscapeGlobalOption < StandardError; end
7
+ attr_reader :global_options, :rendered
8
+ @no_option_commands ||= []
9
+ GLOBAL_OPTIONS = {
10
+ :help=>{:type=>:boolean, :desc=>"Display a command's help"},
11
+ :render=>{:type=>:boolean, :desc=>"Toggle a command's default render behavior"},
12
+ :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'"},
14
+ :pretend=>{:type=>:boolean, :desc=>"Display what a command would execute without executing it"}
15
+ }
16
+ RENDER_OPTIONS = {
17
+ :fields=>{:type=>:array, :desc=>"Displays fields in the order given"},
18
+ :sort=>{:type=>:string, :desc=>"Sort by given field"},
19
+ :as=>{:type=>:string, :desc=>"Hirb helper class which renders"},
20
+ :reverse_sort=>{:type=>:boolean, :desc=>"Reverse a given sort"},
21
+ :max_width=>{:type=>:numeric, :desc=>"Max width of a table"},
22
+ :vertical=>{:type=>:boolean, :desc=>"Display a vertical table"}
23
+ }
24
+
25
+ def create_option_command(obj, command)
26
+ cmd_block = create_option_command_block(obj, command)
27
+ @no_option_commands << command if command.options.nil?
28
+ [command.name, command.alias].compact.each {|e|
29
+ obj.instance_eval("class<<self;self;end").send(:define_method, e, cmd_block)
30
+ }
31
+ end
32
+
33
+ def create_option_command_block(obj, command)
34
+ lambda {|*args|
35
+ Boson::Scientist.translate_and_render(obj, command, args) {|args| super(*args) }
36
+ }
37
+ end
38
+
39
+ def translate_and_render(obj, command, args)
40
+ @global_options = {}
41
+ args = translate_args(obj, command, args)
42
+ if @global_options[:verbose] || @global_options[:pretend]
43
+ puts "Arguments: #{args.inspect}", "Global options: #{@global_options.inspect}"
44
+ end
45
+ return @rendered = true if @global_options[:pretend]
46
+ render_or_raw yield(args)
47
+ rescue EscapeGlobalOption
48
+ Boson.invoke(:usage, command.name, :verbose=>@global_options[:verbose]) if @global_options[:help]
49
+ rescue OptionParser::Error, Error
50
+ $stderr.puts "Error: " + $!.message
51
+ end
52
+
53
+ def translate_args(obj, command, args)
54
+ @obj, @command, @args = obj, command, args
55
+ @command.options ||= {}
56
+ if parsed_options = command_options
57
+ add_default_args(@args)
58
+ return @args if @no_option_commands.include?(@command)
59
+ @args << parsed_options
60
+ 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
68
+ end
69
+ end
70
+ @args
71
+ rescue Error, ArgumentError, EscapeGlobalOption
72
+ raise
73
+ rescue Exception
74
+ message = @global_options[:verbose] ? "#{$!}\n#{$!.backtrace.inspect}" : $!.message
75
+ raise Error, message
76
+ end
77
+
78
+ def render_or_raw(result)
79
+ (@rendered = render?) ? View.render(result, global_render_options) : result
80
+ rescue Exception
81
+ message = @global_options[:verbose] ? "#{$!}\n#{$!.backtrace.inspect}" : $!.message
82
+ raise Error, message
83
+ end
84
+
85
+ def option_parser
86
+ @command.render_options ? command_option_parser : default_option_parser
87
+ end
88
+
89
+ def command_option_parser
90
+ (@option_parsers ||= {})[@command] ||= OptionParser.new render_options.merge(GLOBAL_OPTIONS)
91
+ end
92
+
93
+ def render_option_parser(cmd)
94
+ @command = cmd
95
+ option_parser
96
+ end
97
+
98
+ def default_option_parser
99
+ @default_option_parser ||= OptionParser.new RENDER_OPTIONS.merge(GLOBAL_OPTIONS)
100
+ end
101
+
102
+ def render_options
103
+ @command.render_options ? command_render_options : RENDER_OPTIONS
104
+ end
105
+
106
+ def command_render_options
107
+ (@command_render_options ||= {})[@command] ||= begin
108
+ @command.render_options.each {|k,v|
109
+ if !v.is_a?(Hash) && !v.is_a?(Symbol) && RENDER_OPTIONS.keys.include?(k)
110
+ @command.render_options[k] = {:default=>v}
111
+ end
112
+ }
113
+ opts = Util.recursive_hash_merge(@command.render_options, RENDER_OPTIONS)
114
+ opts[:sort][:values] ||= opts[:fields][:values] if opts[:fields][:values]
115
+ opts
116
+ end
117
+ end
118
+
119
+ def global_render_options
120
+ @global_options.dup.delete_if {|k,v| !render_options.keys.include?(k) }
121
+ end
122
+
123
+ def render?
124
+ (@command.render_options && !@global_options[:render]) || (!@command.render_options && @global_options[:render])
125
+ end
126
+
127
+ def command_options
128
+ if @args.size == 1 && @args[0].is_a?(String)
129
+ parsed_options, @args = parse_options Shellwords.shellwords(@args[0])
130
+ # last string argument interpreted as args + options
131
+ elsif @args.size > 1 && @args[-1].is_a?(String)
132
+ parsed_options, new_args = parse_options @args.pop.split(/\s+/)
133
+ @args += new_args
134
+ # default options
135
+ elsif (@args.size <= @command.arg_size - 1) || (@command.has_splat_args? && !@args[-1].is_a?(Hash))
136
+ parsed_options = parse_options([])[0]
137
+ end
138
+ parsed_options
139
+ end
140
+
141
+ def parse_options(args)
142
+ parsed_options = @command.option_parser.parse(args, :delete_invalid_opts=>true)
143
+ @global_options = option_parser.parse @command.option_parser.leading_non_opts
144
+ new_args = option_parser.non_opts.dup + @command.option_parser.trailing_non_opts
145
+ if @global_options[:global]
146
+ global_opts = Shellwords.shellwords(@global_options[:global]).map {|str| (str.length > 1 ? "--" : "-") + str }
147
+ @global_options.merge! option_parser.parse(global_opts)
148
+ end
149
+ raise EscapeGlobalOption if @global_options[:help]
150
+ [parsed_options, new_args]
151
+ end
152
+
153
+ def add_default_args(args)
154
+ if @command.args && args.size < @command.args.size - 1
155
+ # leave off last arg since its an option
156
+ @command.args.slice(0..-2).each_with_index {|arr,i|
157
+ next if args.size >= i + 1 # only fill in once args run out
158
+ break if arr.size != 2 # a default arg value must exist
159
+ begin
160
+ args[i] = @command.file_parsed_args? ? @obj.instance_eval(arr[1]) : arr[1]
161
+ rescue Exception
162
+ raise Error, "Unable to set default argument at position #{i+1}.\nReason: #{$!.message}"
163
+ end
164
+ }
165
+ end
166
+ end
167
+ end
168
+ end
data/lib/boson/util.rb ADDED
@@ -0,0 +1,93 @@
1
+ module Boson
2
+ module Util
3
+ extend self
4
+ #From Rails ActiveSupport
5
+ def underscore(camel_cased_word)
6
+ camel_cased_word.to_s.gsub(/::/, '/').
7
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
8
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
9
+ tr("-", "_").
10
+ downcase
11
+ end
12
+
13
+ # from Rails ActiveSupport
14
+ def camelize(string)
15
+ string.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
16
+ end
17
+
18
+ def constantize(string)
19
+ any_const_get(camelize(string))
20
+ end
21
+
22
+ def symbolize_keys(hash)
23
+ hash.inject({}) {|options, (key, value)|
24
+ options[key.to_sym] = value; options
25
+ }
26
+ end
27
+
28
+ # Returns a constant like const_get() no matter what namespace it's nested in.
29
+ # Returns nil if the constant is not found.
30
+ def any_const_get(name)
31
+ return name if name.is_a?(Module)
32
+ begin
33
+ klass = Object
34
+ name.split('::').each {|e|
35
+ klass = klass.const_get(e)
36
+ }
37
+ klass
38
+ rescue
39
+ nil
40
+ end
41
+ end
42
+
43
+ def detect(options={}, &block)
44
+ options = {:methods=>true, :object_methods=>true}.merge!(options)
45
+ original_gems = Gem.loaded_specs.keys if Object.const_defined? :Gem
46
+ original_object_methods = Object.instance_methods
47
+ original_instance_methods = class << Boson.main_object; instance_methods end
48
+ original_modules = modules if options[:modules]
49
+ block.call
50
+ detected = {}
51
+ detected[:methods] = options[:methods] ? (class << Boson.main_object; instance_methods end -
52
+ original_instance_methods) : []
53
+ detected[:methods] -= (Object.instance_methods - original_object_methods) unless options[:object_methods]
54
+ detected[:gems] = Gem.loaded_specs.keys - original_gems if Object.const_defined? :Gem
55
+ detected[:modules] = modules - original_modules if options[:modules]
56
+ detected
57
+ end
58
+
59
+ def safe_require(lib)
60
+ begin
61
+ require lib
62
+ rescue LoadError
63
+ false
64
+ end
65
+ end
66
+
67
+ def modules
68
+ all_modules = []
69
+ ObjectSpace.each_object(Module) {|e| all_modules << e}
70
+ all_modules
71
+ end
72
+
73
+ def common_instance_methods(module1, module2)
74
+ (module1.instance_methods + module1.private_instance_methods) & (module2.instance_methods + module2.private_instance_methods)
75
+ end
76
+
77
+ def create_module(base_module, name)
78
+ desired_class = camelize(name)
79
+ if (suffix = ([""] + (1..10).to_a).find {|e| !base_module.const_defined?(desired_class+e)})
80
+ base_module.const_set(desired_class+suffix, Module.new)
81
+ end
82
+ end
83
+
84
+ def which(command)
85
+ ENV['PATH'].split(File::PATH_SEPARATOR).map {|e| File.join(e, command) }.find {|e| File.exists?(e) }
86
+ end
87
+
88
+ # Recursively merge hash1 with hash2.
89
+ def recursive_hash_merge(hash1, hash2)
90
+ hash1.merge(hash2) {|k,o,n| (o.is_a?(Hash)) ? recursive_hash_merge(o,n) : n}
91
+ end
92
+ end
93
+ end
data/lib/boson/view.rb ADDED
@@ -0,0 +1,31 @@
1
+ module Boson
2
+ module View
3
+ extend self
4
+
5
+ def enable
6
+ Hirb::View.enable(:config_file=>File.join(Boson.repo.config_dir, 'hirb.yml')) unless @enabled
7
+ @enabled = true
8
+ end
9
+
10
+ def render(object, options={})
11
+ [nil,false,true].include?(object) ? puts(object.inspect) : render_object(object, options)
12
+ end
13
+
14
+ def render_object(object, options={})
15
+ options[:class] = options.delete(:as) || :auto_table
16
+ if object.is_a?(Array) && object.size > 0 && (sort = options.delete(:sort))
17
+ begin
18
+ sort_lambda = object[0].is_a?(Hash) ? (object[0][sort].respond_to?(:<=>) ?
19
+ lambda {|e| e[sort] } : lambda {|e| e[sort].to_s }) :
20
+ (object[0].send(sort).respond_to?(:<=>) ? lambda {|e| e.send(sort)} :
21
+ lambda {|e| e.send(sort).to_s })
22
+ object = object.sort_by &sort_lambda
23
+ object = object.reverse if options[:reverse_sort]
24
+ rescue NoMethodError, ArgumentError
25
+ $stderr.puts "Sort failed with nonexistant method '#{sort}'"
26
+ end
27
+ end
28
+ Hirb::Console.render_output(object, options)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,62 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ module Boson
4
+ class ArgumentInspectorTest < Test::Unit::TestCase
5
+ context "scrape_with_text" do
6
+ def args_from(file_string)
7
+ ArgumentInspector.scrape_with_text(file_string, "blah")
8
+ end
9
+
10
+ test "parses arguments with no spacing" do
11
+ args_from("def bong; end\ndef blah(arg1,arg2='val2')\nend").should == [["arg1"], ['arg2', "'val2'"]]
12
+ end
13
+
14
+ test "parses arguments with spacing" do
15
+ args_from("\t def blah( arg1=val1, arg2 = val2)").should == [["arg1","val1"], ["arg2", "val2"]]
16
+ end
17
+
18
+ test "parses arguments without parenthesis" do
19
+ args_from(" def blah arg1, arg2, arg3={}").should == [['arg1'], ['arg2'], ['arg3','{}']]
20
+ end
21
+ end
22
+
23
+ context "scrape_with_eval" do
24
+ def args_from(string)
25
+ # methods need options to have their args parsed with ArgumentInspector
26
+ string.gsub!(/(def blah)/, 'options :a=>1; \1')
27
+ Inspector.enable
28
+ ::Boson::Commands::Aaa.module_eval(string)
29
+ Inspector.disable
30
+ MethodInspector.store[:method_args]['blah']
31
+ end
32
+
33
+ before(:all) { eval "module ::Boson::Commands::Aaa; end"; }
34
+ before(:each) { MethodInspector.mod_store[::Boson::Commands::Aaa] = {} }
35
+
36
+ test "determines arguments with literal defaults" do
37
+ args_from("def blah(arg1,arg2='val2'); end").should == [['arg1'], ['arg2','val2']]
38
+ end
39
+
40
+ test "determines splat arguments" do
41
+ args_from("def blah(arg1, *args); end").should == [['arg1'], ["*args"]]
42
+ end
43
+
44
+ test "determines arguments with local values before a method" do
45
+ body = "AWESOME='awesome'; def sweet; 'ok'; end; def blah(arg1=AWESOME, arg2=sweet); end"
46
+ args_from(body).should == [['arg1', 'awesome'], ['arg2', 'ok']]
47
+ end
48
+
49
+ test "doesn't get arguments with local values after a method" do
50
+ args_from("def blah(arg1=nope) end; def nope; 'nope'; end").should == nil
51
+ end
52
+
53
+ test "doesn't determine arguments of a private method" do
54
+ args_from("private; def blah(arg1,arg2); end").should == nil
55
+ end
56
+
57
+ test "doesn't determine arguments if an error occurs" do
58
+ args_from("def blah(arg1,arg2=raise); end").should == nil
59
+ end
60
+ end
61
+ end
62
+ end