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,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