boson 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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