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,36 @@
1
+ module Boson::Commands::WebCore #:nodoc:
2
+ def self.config
3
+ descriptions = {
4
+ :install=>"Installs a library by url. Library should then be loaded with load_library.",
5
+ :browser=>"Opens urls in a browser", :get=>"Gets the body of a url" }
6
+ commands = descriptions.inject({}) {|h,(k,v)| h[k.to_s] = {:description=>v}; h}
7
+ commands['install'][:options] = {:name=>:string, :force=>:boolean}
8
+ {:library_file=>File.expand_path(__FILE__), :commands=>commands}
9
+ end
10
+
11
+ def get(url)
12
+ %w{uri net/http}.each {|e| require e }
13
+ Net::HTTP.get(URI.parse(url))
14
+ rescue
15
+ raise "Error opening #{url}"
16
+ end
17
+
18
+ def install(url, options={})
19
+ options[:name] ||= strip_name_from_url(url)
20
+ return puts("Please give a library name with this url.") unless options[:name]
21
+ filename = File.join Boson.repo.commands_dir, "#{options[:name]}.rb"
22
+ return puts("Library name #{options[:name]} already exists. Try a different name.") if File.exists?(filename) && !options[:force]
23
+ File.open(filename, 'w') {|f| f.write get(url) }
24
+ puts "Saved to #{filename}."
25
+ end
26
+
27
+ # non-mac users should override this with the launchy gem
28
+ def browser(*urls)
29
+ system('open', *urls)
30
+ end
31
+
32
+ private
33
+ def strip_name_from_url(url)
34
+ url[/\/([^\/.]+)(\.[a-z]+)?$/, 1]
35
+ end
36
+ end
@@ -0,0 +1,95 @@
1
+ require 'digest/md5'
2
+ module Boson
3
+ module Index
4
+ extend self
5
+ attr_reader :libraries, :commands
6
+
7
+ def read_and_transfer(ignored_libraries=[])
8
+ read
9
+ existing_libraries = (Boson.libraries.map {|e| e.name} + ignored_libraries).uniq
10
+ Boson.libraries += @libraries.select {|e| !existing_libraries.include?(e.name)}
11
+ existing_commands = Boson.commands.map {|e| e.name} #td: consider namespace
12
+ Boson.commands += @commands.select {|e| !existing_commands.include?(e.name) && !ignored_libraries.include?(e.lib)}
13
+ end
14
+
15
+ def update(options={})
16
+ options[:all] = true if !exists? && !options.key?(:all)
17
+ libraries_to_update = options[:all] ? Runner.all_libraries : changed_libraries
18
+ read_and_transfer(libraries_to_update)
19
+ if options[:verbose]
20
+ puts options[:all] ? "Generating index for all #{libraries_to_update.size} libraries. Patience ... is a bitch" :
21
+ (libraries_to_update.empty? ? "No libraries indexed" :
22
+ "Indexing the following libraries: #{libraries_to_update.join(', ')}")
23
+ end
24
+ Manager.load(libraries_to_update, options.merge(:index=>true)) unless libraries_to_update.empty?
25
+ write
26
+ end
27
+
28
+ def exists?
29
+ File.exists? marshal_file
30
+ end
31
+
32
+ def write
33
+ save_marshal_index Marshal.dump([Boson.libraries, Boson.commands, latest_hashes])
34
+ end
35
+
36
+ def save_marshal_index(marshal_string)
37
+ File.open(marshal_file, 'w') {|f| f.write marshal_string }
38
+ end
39
+
40
+ def read
41
+ return if @read
42
+ @libraries, @commands, @lib_hashes = exists? ? Marshal.load(File.read(marshal_file)) : [[], [], {}]
43
+ set_latest_namespaces
44
+ @read = true
45
+ end
46
+
47
+ # get latest namespaces from config files
48
+ def set_latest_namespaces
49
+ namespace_libs = Boson.repo.config[:auto_namespace] ? @libraries.map {|e| [e.name, {:namespace=>true}]} :
50
+ Boson.repo.config[:libraries].select {|k,v| v && v[:namespace] }
51
+ lib_commands = @commands.inject({}) {|t,e| (t[e.lib] ||= []) << e; t }
52
+ namespace_libs.each {|name, hash|
53
+ if (lib = @libraries.find {|l| l.name == name})
54
+ lib.namespace = (hash[:namespace] == true) ? lib.clean_name : hash[:namespace]
55
+ (lib_commands[lib.name] || []).each {|e| e.namespace = lib.namespace }
56
+ end
57
+ }
58
+ end
59
+
60
+ def namespaces
61
+ @libraries.map {|e| e.namespace }.compact
62
+ end
63
+
64
+ def all_main_methods
65
+ @commands.reject {|e| e.namespace }.map {|e| [e.name, e.alias]}.flatten.compact + namespaces
66
+ end
67
+
68
+ def marshal_file
69
+ File.join(Boson.repo.config_dir, 'index.marshal')
70
+ end
71
+
72
+ def find_library(command)
73
+ read
74
+ namespace_command = command.split('.')[0]
75
+ if (lib = @libraries.find {|e| e.namespace == namespace_command })
76
+ lib.name
77
+ elsif (cmd = Command.find(command, @commands))
78
+ cmd.lib
79
+ end
80
+ end
81
+
82
+ def changed_libraries
83
+ read
84
+ latest_hashes.select {|lib, hash| @lib_hashes[lib] != hash}.map {|e| e[0]}
85
+ end
86
+
87
+ def latest_hashes
88
+ Runner.all_libraries.inject({}) {|h, e|
89
+ lib_file = FileLibrary.library_file(e, Boson.repo.dir)
90
+ h[e] = Digest::MD5.hexdigest(File.read(lib_file)) if File.exists?(lib_file)
91
+ h
92
+ }
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,80 @@
1
+ module Boson
2
+ # Scrapes and processes method metadata with the inspectors (MethodInspector, CommentInspector
3
+ # and ArgumentInspector) and hands off the usable data to FileLibrary objects.
4
+ module Inspector
5
+ extend self
6
+ attr_reader :enabled
7
+
8
+ # Enable scraping by overridding method_added to snoop on a library while it's
9
+ # loading its methods.
10
+ def enable
11
+ @enabled = true
12
+ body = MethodInspector::METHODS.map {|e|
13
+ %[def #{e}(val)
14
+ Boson::MethodInspector.#{e}(self, val)
15
+ end]
16
+ }.join("\n") +
17
+ %[
18
+ def new_method_added(method)
19
+ Boson::MethodInspector.new_method_added(self, method)
20
+ end
21
+
22
+ alias_method :_old_method_added, :method_added
23
+ alias_method :method_added, :new_method_added
24
+ ]
25
+ ::Module.module_eval body
26
+ end
27
+
28
+ # Disable scraping method data.
29
+ def disable
30
+ ::Module.module_eval %[
31
+ Boson::MethodInspector::METHODS.each {|e| remove_method e }
32
+ alias_method :method_added, :_old_method_added
33
+ ]
34
+ @enabled = false
35
+ end
36
+
37
+ # Adds method data scraped for the library's module to the library's commands.
38
+ def add_method_data_to_library(library)
39
+ @commands_hash = library.commands_hash
40
+ @library_file = library.library_file
41
+ MethodInspector.current_module = library.module
42
+ @store = MethodInspector.store
43
+ add_method_scraped_data
44
+ add_comment_scraped_data
45
+ end
46
+
47
+ #:stopdoc:
48
+ def add_method_scraped_data
49
+ (MethodInspector::METHODS + [:method_args]).each do |e|
50
+ key = command_key(e)
51
+ (@store[e] || []).each do |cmd, val|
52
+ if no_command_config_for(cmd, key)
53
+ (@commands_hash[cmd] ||= {})[key] = val
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ def add_comment_scraped_data
60
+ (@store[:method_locations] || []).select {|k,(f,l)| f == @library_file }.each do |cmd, (file, lineno)|
61
+ scraped = CommentInspector.scrape(FileLibrary.read_library_file(file), lineno, MethodInspector.current_module)
62
+ MethodInspector::METHODS.each do |e|
63
+ if no_command_config_for(cmd, e)
64
+ (@commands_hash[cmd] ||= {})[command_key(e)] = scraped[e]
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ # translates from inspector attribute name to command attribute name
71
+ def command_key(key)
72
+ {:method_args=>:args, :desc=>:description}[key] || key
73
+ end
74
+
75
+ def no_command_config_for(cmd, attribute)
76
+ !@commands_hash[cmd] || (@commands_hash[cmd] && !@commands_hash[cmd].key?(attribute))
77
+ end
78
+ #:startdoc:
79
+ end
80
+ end
@@ -0,0 +1,92 @@
1
+ # Extracts arguments and their default values from methods either by
2
+ # by scraping a method's text or with method_added and brute force eval (thanks to
3
+ # {eigenclass}[http://eigenclass.org/hiki/method+arguments+via+introspection]).
4
+ module Boson::ArgumentInspector
5
+ extend self
6
+ # Returns same argument arrays as scrape_with_eval but argument defaults haven't been evaluated.
7
+ def scrape_with_text(file_string, meth)
8
+ tabspace = "[ \t]"
9
+ if match = /^#{tabspace}*def#{tabspace}+#{meth}#{tabspace}*($|\(?\s*([^\)]+)\s*\)?\s*$)/.match(file_string)
10
+ (match.to_a[2] || '').split(/\s*,\s*/).map {|e| e.split(/\s*=\s*/)}
11
+ end
12
+ end
13
+
14
+ # Max number of arguments extracted per method with scrape_with_eval
15
+ MAX_ARGS = 10
16
+ # Scrapes non-private methods for argument names and default values.
17
+ # Returns arguments as array of argument arrays with optional default value as a second element.
18
+ # ====Examples:
19
+ # def meth1(arg1, arg2='val', options={}) -> [['arg1'], ['arg2', 'val'], ['options', {}]]
20
+ # def meth2(*args) -> [['*args']]
21
+ def scrape_with_eval(meth, klass, object)
22
+ unless %w[initialize].include?(meth.to_s)
23
+ return if class << object; private_instance_methods(true) end.include?(meth.to_s)
24
+ end
25
+ params, values, arity, num_args = trace_method_args(meth, klass, object)
26
+ return if local_variables == params # nothing new found
27
+ format_arguments(params, values, arity, num_args)
28
+ rescue Exception
29
+ # puts "#{klass}.#{meth}: #{$!.message}"
30
+ ensure
31
+ set_trace_func(nil)
32
+ end
33
+
34
+ # process params + values to return array of argument arrays
35
+ def format_arguments(params, values, arity, num_args) #:nodoc:
36
+ params ||= []
37
+ params = params[0,num_args]
38
+ params.inject([[], 0]) do |(a, i), x|
39
+ if Array === values[i]
40
+ [a << ["*#{x}"], i+1]
41
+ else
42
+ if arity < 0 && i >= arity.abs - 1
43
+ [a << [x, values[i]], i + 1]
44
+ else
45
+ [a << [x], i+1]
46
+ end
47
+ end
48
+ end.first
49
+ end
50
+
51
+ def trace_method_args(meth, klass, object) #:nodoc:
52
+ file = line = params = values = nil
53
+ arity = klass.instance_method(meth).arity
54
+ set_trace_func lambda{|event, file, line, id, binding, classname|
55
+ begin
56
+ if event[/call/] && classname == klass && id == meth
57
+ params = eval("local_variables", binding)
58
+ values = eval("local_variables.map{|x| eval(x)}", binding)
59
+ throw :done
60
+ end
61
+ rescue Exception
62
+ end
63
+ }
64
+ if arity >= 0
65
+ num_args = arity
66
+ catch(:done){ object.send(meth, *(0...arity)) }
67
+ else
68
+ num_args = 0
69
+ # determine number of args (including splat & block)
70
+ MAX_ARGS.downto(arity.abs - 1) do |i|
71
+ catch(:done) do
72
+ begin
73
+ object.send(meth, *(0...i))
74
+ rescue Exception
75
+ end
76
+ end
77
+ # all nils if there's no splat and we gave too many args
78
+ next if !values || values.compact.empty?
79
+ k = nil
80
+ values.each_with_index{|x,j| break (k = j) if Array === x}
81
+ num_args = k ? k+1 : i
82
+ break
83
+ end
84
+ args = (0...arity.abs-1).to_a
85
+ catch(:done) do
86
+ args.empty? ? object.send(meth) : object.send(meth, *args)
87
+ end
88
+ end
89
+ set_trace_func(nil)
90
+ return [params, values, arity, num_args]
91
+ end
92
+ end
@@ -0,0 +1,79 @@
1
+ module Boson
2
+ # Scrapes comments right before a method for its metadata. Metadata attributes are the
3
+ # same as MethodInspector: desc, options, render_options. Attributes must begin with '@' i.e.:
4
+ #
5
+ # # @desc Does foo
6
+ # # @options :verbose=>true
7
+ # def foo(options={})
8
+ #
9
+ # Some rules about comment attributes:
10
+ # * Attribute definitions can span multiple lines. When a new attribute starts a line or the comments end
11
+ # then a definition ends.
12
+ # * If no @desc is found in the comment block, then the first comment line directly above the method
13
+ # is assumed to be the value for @desc. This means that no multi-line attribute definitions can occur
14
+ # without a description since the last line is assumed to be a description.
15
+ # * options and render_options attributes can take any valid ruby since they're evaled in their module's context.
16
+ # * desc attribute is not evaled and is simply text to be set as a string.
17
+ #
18
+ # This module was inspired by
19
+ # {pragdave}[http://github.com/pragdavespc/rake/commit/45231ac094854da9f4f2ac93465ed9b9ca67b2da].
20
+ module CommentInspector
21
+ extend self
22
+ EVAL_ATTRIBUTES = [:options, :render_options]
23
+
24
+ # Given a method's file string, line number and defining module, returns a hash
25
+ # of attributes defined for that method.
26
+ def scrape(file_string, line, mod, attribute=nil)
27
+ hash = scrape_file(file_string, line) || {}
28
+ hash.select {|k,v| v && (attribute.nil? || attribute == k) }.each do |k,v|
29
+ hash[k] = EVAL_ATTRIBUTES.include?(k) ? eval_comment(v.join(' '), mod) : v.join(' ')
30
+ end
31
+ attribute ? hash[attribute] : hash
32
+ end
33
+
34
+ #:stopdoc:
35
+ def eval_comment(value, mod)
36
+ value = "{#{value}}" if !value[/^\s*\{/] && value[/=>/]
37
+ begin mod.module_eval(value); rescue(Exception); nil end
38
+ end
39
+
40
+ # Scrapes a given string for commented @keywords, starting with the line above the given line
41
+ def scrape_file(file_string, line)
42
+ lines = file_string.split("\n")
43
+ saved = []
44
+ i = line -2
45
+ while lines[i] =~ /^\s*#\s*(\S+)/ && i >= 0
46
+ saved << lines[i]
47
+ i -= 1
48
+ end
49
+
50
+ saved.empty? ? {} : splitter(saved.reverse)
51
+ end
52
+
53
+ def splitter(lines)
54
+ hash = {}
55
+ i = 0
56
+ # to magically make the last comment a description
57
+ unless lines.any? {|e| e =~ /^\s*#\s*@desc/ }
58
+ last_line = lines.pop
59
+ hash[:desc] = (last_line =~ /^\s*#\s*([^@\s].*)/) ? [$1] : nil
60
+ lines << last_line unless hash[:desc]
61
+ end
62
+
63
+ while i < lines.size
64
+ while lines[i] =~ /^\s*#\s*@(\w+)\s*(.*)/
65
+ key = $1.to_sym
66
+ hash[key] = [$2]
67
+ i += 1
68
+ while lines[i] =~ /^\s*#\s*([^@\s].*)/
69
+ hash[key] << $1
70
+ i+= 1
71
+ end
72
+ end
73
+ i += 1
74
+ end
75
+ hash
76
+ end
77
+ #:startdoc:
78
+ end
79
+ end
@@ -0,0 +1,94 @@
1
+ module Boson
2
+ # Allows for defining method metadata with new_method_added and the
3
+ # following Module methods:
4
+ # * desc: Defines a description for a method/command
5
+ # * options: Defines an OptionParser object for a method's options.
6
+ # * render_options: Defines an OptionParser object for a method's rendering options.
7
+ #
8
+ # These method calls must come immediately before a method i.e.:
9
+ #
10
+ # desc "Does foo"
11
+ # options :verbose=>:boolean
12
+ # def foo(options={})
13
+ #
14
+ # This module also allows for defining method metadata as comments. Although the actual
15
+ # scraping of comments is handled by CommentInspector, MethodInspector gather's the method
16
+ # location it needs with with find_method_locations().
17
+ module MethodInspector
18
+ extend self
19
+ attr_accessor :current_module
20
+ attr_reader :mod_store
21
+ @mod_store ||= {}
22
+ METHODS = [:desc, :options, :render_options]
23
+
24
+ # The method_added used while scraping method metadata.
25
+ def new_method_added(mod, meth)
26
+ return unless mod.name[/^Boson::Commands::/]
27
+ self.current_module = mod
28
+ store[:temp] ||= {}
29
+ METHODS.each do |e|
30
+ store[e][meth.to_s] = store[:temp][e] if store[:temp][e]
31
+ end
32
+
33
+ if store[:temp].size < METHODS.size
34
+ store[:method_locations] ||= {}
35
+ if (result = find_method_locations(caller))
36
+ store[:method_locations][meth.to_s] = result
37
+ end
38
+ end
39
+ store[:temp] = {}
40
+ scrape_arguments(meth) if has_inspector_method?(meth, :options) || has_inspector_method?(meth,:render_options)
41
+ end
42
+
43
+ METHODS.each do |e|
44
+ define_method(e) do |mod, val|
45
+ store(mod)[e] ||= {}
46
+ (store(mod)[:temp] ||= {})[e] = val
47
+ end
48
+ end
49
+
50
+ # Scrapes a method's arguments using ArgumentInspector.
51
+ def scrape_arguments(meth)
52
+ store[:method_args] ||= {}
53
+
54
+ o = Object.new
55
+ o.extend(@current_module)
56
+ # private methods return nil
57
+ if (val = ArgumentInspector.scrape_with_eval(meth, @current_module, o))
58
+ store[:method_args][meth.to_s] = val
59
+ end
60
+ end
61
+
62
+ # Returns an array of the file and line number at which a method starts using
63
+ # a caller array. Necessary information for CommentInspector to function.
64
+ def find_method_locations(stack)
65
+ if (line = stack.find {|e| e =~ /in `load_source'/ })
66
+ (line =~ /^(.*):(\d+)/) ? [$1, $2.to_i] : nil
67
+ end
68
+ end
69
+
70
+ #:stopdoc:
71
+ # Hash of a module's method attributes i.e. descriptions, options by method and then attribute
72
+ def store(mod=@current_module)
73
+ @mod_store[mod]
74
+ end
75
+
76
+ def current_module=(mod)
77
+ @current_module = mod
78
+ @mod_store[mod] ||= {}
79
+ end
80
+
81
+ def has_inspector_method?(meth, inspector)
82
+ (store[inspector] && store[inspector].key?(meth.to_s)) || inspector_in_file?(meth.to_s, inspector)
83
+ end
84
+
85
+ def inspector_in_file?(meth, inspector_method)
86
+ return false if !(file_line = store[:method_locations] && store[:method_locations][meth])
87
+ if File.exists?(file_line[0]) && (options = CommentInspector.scrape(
88
+ FileLibrary.read_library_file(file_line[0]), file_line[1], @current_module, inspector_method) )
89
+ (store[inspector_method] ||= {})[meth] = options
90
+ end
91
+ end
92
+ #:startdoc:
93
+ end
94
+ end