boson-more 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. data/.gemspec +22 -0
  2. data/LICENSE.txt +22 -0
  3. data/README.md +97 -0
  4. data/Rakefile +35 -0
  5. data/deps.rip +1 -0
  6. data/lib/boson/alias.rb +75 -0
  7. data/lib/boson/argument_inspector.rb +90 -0
  8. data/lib/boson/commands/core.rb +67 -0
  9. data/lib/boson/commands/view_core.rb +19 -0
  10. data/lib/boson/commands/web_core.rb +153 -0
  11. data/lib/boson/comment_inspector.rb +100 -0
  12. data/lib/boson/console.rb +40 -0
  13. data/lib/boson/console_runner.rb +60 -0
  14. data/lib/boson/index.rb +48 -0
  15. data/lib/boson/libraries/file_library.rb +144 -0
  16. data/lib/boson/libraries/gem_library.rb +30 -0
  17. data/lib/boson/libraries/local_file_library.rb +30 -0
  18. data/lib/boson/libraries/module_library.rb +37 -0
  19. data/lib/boson/libraries/require_library.rb +23 -0
  20. data/lib/boson/libraries.rb +183 -0
  21. data/lib/boson/more/version.rb +5 -0
  22. data/lib/boson/more.rb +18 -0
  23. data/lib/boson/more_commands.rb +14 -0
  24. data/lib/boson/more_inspector.rb +42 -0
  25. data/lib/boson/more_manager.rb +34 -0
  26. data/lib/boson/more_method_inspector.rb +74 -0
  27. data/lib/boson/more_option_parser.rb +28 -0
  28. data/lib/boson/more_scientist.rb +68 -0
  29. data/lib/boson/more_util.rb +30 -0
  30. data/lib/boson/namespace.rb +31 -0
  31. data/lib/boson/namespacer.rb +117 -0
  32. data/lib/boson/pipe.rb +156 -0
  33. data/lib/boson/pipe_runner.rb +44 -0
  34. data/lib/boson/pipes.rb +75 -0
  35. data/lib/boson/repo.rb +96 -0
  36. data/lib/boson/repo_index.rb +135 -0
  37. data/lib/boson/runner_options.rb +88 -0
  38. data/lib/boson/save.rb +198 -0
  39. data/lib/boson/science.rb +273 -0
  40. data/lib/boson/view.rb +98 -0
  41. data/lib/boson/viewable.rb +48 -0
  42. data/test/alias_test.rb +55 -0
  43. data/test/argument_inspector_test.rb +40 -0
  44. data/test/command_test.rb +22 -0
  45. data/test/commands_test.rb +53 -0
  46. data/test/comment_inspector_test.rb +126 -0
  47. data/test/console_runner_test.rb +58 -0
  48. data/test/deps.rip +4 -0
  49. data/test/file_library_test.rb +41 -0
  50. data/test/gem_library_test.rb +40 -0
  51. data/test/libraries_test.rb +55 -0
  52. data/test/loader_test.rb +38 -0
  53. data/test/module_library_test.rb +30 -0
  54. data/test/more_manager_test.rb +29 -0
  55. data/test/more_method_inspector_test.rb +42 -0
  56. data/test/more_scientist_test.rb +10 -0
  57. data/test/namespacer_test.rb +61 -0
  58. data/test/pipes_test.rb +65 -0
  59. data/test/repo_index_test.rb +123 -0
  60. data/test/repo_test.rb +23 -0
  61. data/test/runner_options_test.rb +29 -0
  62. data/test/save_test.rb +86 -0
  63. data/test/science_test.rb +58 -0
  64. data/test/scientist_test.rb +195 -0
  65. data/test/test_helper.rb +165 -0
  66. data/test/web_test.rb +22 -0
  67. metadata +169 -0
@@ -0,0 +1,100 @@
1
+ module Boson
2
+ # Scrapes comments right before a method for its attributes. Method attributes must begin with '@' i.e.:
3
+ # # @desc Does foo
4
+ # # @options :verbose=>true
5
+ # def foo(options={})
6
+ #
7
+ # Some rules about these attributes:
8
+ # * Attribute definitions can span multiple lines. When a new attribute starts a line or the comments end,
9
+ # then a definition ends.
10
+ # * If no @desc is found in the comment block, then the first comment line directly above the method
11
+ # is assumed to be the value for @desc. This means that no multi-line attribute definitions can occur
12
+ # without a description since the last line is assumed to be a description.
13
+ # * options, option and config attributes can take any valid ruby since they're evaled in
14
+ # their module's context.
15
+ # * desc attribute is not evaled and is simply text to be set as a string.
16
+ #
17
+ # This module was inspired by
18
+ # {pragdave}[http://github.com/pragdavespc/rake/commit/45231ac094854da9f4f2ac93465ed9b9ca67b2da].
19
+ module CommentInspector
20
+ extend self
21
+ EVAL_ATTRIBUTES = [:options, :config]
22
+
23
+ # Given a method's file string, line number and defining module, returns a hash
24
+ # of attributes defined for that method.
25
+ def scrape(file_string, line, mod, attribute=nil)
26
+ hash = scrape_file(file_string, line) || {}
27
+ options = (arr = hash.delete(:option)) ? parse_option_comments(arr, mod) : {}
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, k) : v.join(' ')
30
+ end
31
+ (hash[:options] ||= {}).merge!(options) if !options.empty?
32
+ attribute ? hash[attribute] : hash
33
+ end
34
+
35
+ #:stopdoc:
36
+ def parse_option_comments(arr, mod)
37
+ arr.inject({}) {|t,e|
38
+ key, val = e.join(' ').split(/\s*,\s*/, 2)
39
+ if val
40
+ key = key.sub(/^\s*:/, '').to_sym
41
+ t[key] = eval_comment(val, mod, 'option')
42
+ end
43
+ t
44
+ }
45
+ end
46
+
47
+ def eval_comment(value, mod, mattr)
48
+ value = "{#{value}}" if !value[/^\s*\{/] && value[/=>/]
49
+ mod.module_eval(value)
50
+ rescue Exception
51
+ if Boson.debug
52
+ warn "DEBUG: Error while evaluating @#{mattr} in module #{mod.to_s[/\w+$/]}:\n " +
53
+ $!.message.gsub(/\n/, "\n ")
54
+ end
55
+ nil
56
+ end
57
+
58
+ # Scrapes a given string for commented @keywords, starting with the line above the given line
59
+ def scrape_file(file_string, line)
60
+ lines = file_string.split("\n")
61
+ saved = []
62
+ i = line -2
63
+ while lines[i] =~ /^\s*#\s*(\S+)/ && i >= 0
64
+ saved << lines[i]
65
+ i -= 1
66
+ end
67
+
68
+ saved.empty? ? {} : splitter(saved.reverse)
69
+ end
70
+
71
+ def splitter(lines)
72
+ hash = {}
73
+ i = 0
74
+ # to magically make the last comment a description
75
+ unless lines.any? {|e| e =~ /^\s*#\s*@desc/ }
76
+ last_line = lines.pop
77
+ hash[:desc] = (last_line =~ /^\s*#\s*([^@\s].*)/) ? [$1] : nil
78
+ lines << last_line unless hash[:desc]
79
+ end
80
+
81
+ option = []
82
+ while i < lines.size
83
+ while lines[i] =~ /^\s*#\s*@(\w+)\s*(.*)/
84
+ key = $1.to_sym
85
+ hash[key] = [$2]
86
+ i += 1
87
+ while lines[i] =~ /^\s*#\s*([^@\s].*)/
88
+ hash[key] << $1
89
+ i+= 1
90
+ end
91
+ option << hash.delete(:option) if key == :option
92
+ end
93
+ i += 1
94
+ end
95
+ hash[:option] = option if !option.empty?
96
+ hash
97
+ end
98
+ #:startdoc:
99
+ end
100
+ end
@@ -0,0 +1,40 @@
1
+ require 'boson/console_runner'
2
+
3
+ module Boson
4
+ CONFIG.update console_defaults: []
5
+
6
+ # Additional options added to Repo:
7
+ # [:console_defaults] Array of libraries to load at start up when used in irb. Default is to load all library files and libraries
8
+ # defined in the config.
9
+ # [:console] Console to load when using --console from commandline. Default is irb.
10
+ module Console
11
+ # Start Boson by loading repositories and their configured libraries.
12
+ # See ConsoleRunner.start for its options.
13
+ def start(options={})
14
+ ConsoleRunner.start(options)
15
+ end
16
+ end
17
+ extend Console
18
+
19
+ # [:console] This drops Boson into irb after having loaded default commands and any explict libraries with
20
+ # :load option. This is a good way to start irb with only certain libraries loaded.
21
+ module ConsoleOptions
22
+ def early_option?(args)
23
+ if @options[:console]
24
+ ConsoleRunner.bin_start(@options[:console], @options[:load])
25
+ true
26
+ else
27
+ super
28
+ end
29
+ end
30
+ end
31
+
32
+ if defined? BinRunner
33
+ class BinRunner < BareRunner
34
+ GLOBAL_OPTIONS.update console:
35
+ {:type=>:boolean,
36
+ :desc=>"Drops into irb with default and explicit libraries loaded"}
37
+ extend ConsoleOptions
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,60 @@
1
+ require 'boson/save'
2
+
3
+ module Boson
4
+ # Runner used when starting irb. To use in irb, drop this in your ~/.irbrc:
5
+ # require 'boson'
6
+ # Boson.start
7
+ class ConsoleRunner < BareRunner
8
+ class <<self
9
+ # Starts Boson by loading configured libraries. If no default libraries are specified in the config,
10
+ # it will load up all detected libraries. Options:
11
+ # [:libraries] Array of libraries to load.
12
+ # [:verbose] Boolean to be verbose about libraries loading. Default is true.
13
+ # [:no_defaults] Boolean or :all which turns off loading default libraries. If set to true,
14
+ # effects loading user's console default libraries. If set to :all, effects
15
+ # all libraries including boson's. Default is false.
16
+ # [:autoload_libraries] Boolean which makes any command execution easier. It redefines
17
+ # method_missing on Boson.main_object so that commands with unloaded
18
+ # libraries are automatically loaded. Default is false.
19
+ def start(options={})
20
+ super
21
+ @options = {:verbose=>true}.merge options
22
+ init unless @initialized
23
+ Manager.load(@options[:libraries], load_options) if @options[:libraries]
24
+ end
25
+
26
+ # Loads libraries and then starts irb (or the configured console) from the commandline.
27
+ def bin_start(repl, libraries)
28
+ start :no_defaults=>true, :libraries=>libraries
29
+ repl = Boson.repo.config[:console] if Boson.repo.config[:console]
30
+ repl = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb' unless repl.is_a?(String)
31
+ unless repl.index('/') == 0 || (repl = Util.which(repl))
32
+ abort "Console not found. Please specify full path in config[:console]."
33
+ else
34
+ load_repl(repl)
35
+ end
36
+ end
37
+
38
+ def load_repl(repl) #:nodoc:
39
+ ARGV.replace ['-f']
40
+ $progname = $0
41
+ alias $0 $progname
42
+ Kernel.load $0 = repl
43
+ end
44
+
45
+ def init #:nodoc:
46
+ super
47
+ define_autoloader if @options[:autoload_libraries]
48
+ @initialized = true
49
+ end
50
+
51
+ def default_libraries #:nodoc:
52
+ return [] if @options[:no_defaults] == :all
53
+ return super if @options[:no_defaults]
54
+ defaults = super + Boson.repos.map {|e| e.config[:console_defaults] }.flatten
55
+ defaults += detected_libraries if defaults.empty?
56
+ defaults.uniq
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,48 @@
1
+ module Boson
2
+ # This class manages indexing/storing all commands and libraries. See RepoIndex for details
3
+ # about the index created for each Repo.
4
+ module Index
5
+ extend self
6
+ # Array of indexes, one per repo in Boson.repos.
7
+ def indexes
8
+ @indexes ||= Boson.repos.map {|e| RepoIndex.new(e) }
9
+ end
10
+
11
+ # Updates all repo indexes.
12
+ def update(options={})
13
+ indexes.each {|e| e.update(options) }
14
+ end
15
+
16
+ #:stopdoc:
17
+ def read
18
+ indexes.each {|e| e.read }
19
+ end
20
+
21
+ def find_library(command, object=false)
22
+ indexes.each {|e|
23
+ (lib = e.find_library(command, object)) and return lib
24
+ }
25
+ nil
26
+ end
27
+
28
+ def find_command(command)
29
+ indexes.each {|e|
30
+ (cmd = Command.find(command, e.commands)) and return(cmd)
31
+ }
32
+ nil
33
+ end
34
+
35
+ def commands
36
+ indexes.map {|e| e.commands}.flatten
37
+ end
38
+
39
+ def libraries
40
+ indexes.map {|e| e.libraries}.flatten
41
+ end
42
+
43
+ def all_main_methods
44
+ indexes.map {|e| e.all_main_methods}.flatten
45
+ end
46
+ #:startdoc:
47
+ end
48
+ end
@@ -0,0 +1,144 @@
1
+ module Boson
2
+ # This class loads a file by its path relative to the commands directory of a repository.
3
+ # For example the library 'public/misc' could refer to the file '~/.boson/commands/public/misc.rb'.
4
+ # If a file's basename is unique in its repository, then it can be loaded with its basename i.e. 'misc'
5
+ # for the previous example. When loading a library, this class searches repositories in the order given by
6
+ # Boson.repos.
7
+ #
8
+ # === Creating a FileLibrary
9
+ # Start by creating a file with a module and some methods (See Library for naming a module).
10
+ # Non-private methods are automatically loaded as a library's commands.
11
+ #
12
+ # Take for example a library brain.rb:
13
+ # # Drop this in ~/.boson/commands/brain.rb
14
+ # module Brain
15
+ # def take_over(destination)
16
+ # puts "Pinky, it's time to take over the #{destination}!"
17
+ # end
18
+ # end
19
+ #
20
+ # Once loaded, this library can be run from the commandline or irb:
21
+ # $ boson take_over world
22
+ # >> take_over 'world'
23
+ #
24
+ # If the library is namespaced, the command would be run as brain.take_over.
25
+ #
26
+ # Let's give Brain an option in his conquest:
27
+ # module Brain
28
+ # options :execute=>:string
29
+ # def take_over(destination, options={})
30
+ # puts "Pinky, it's time to take over the #{destination}!"
31
+ # system(options[:execute]) if options[:execute]
32
+ # end
33
+ # end
34
+ #
35
+ # From the commandline and irb this runs as:
36
+ # $ boson take_over world -e initiate_brainiac
37
+ # >> take_over 'world -e initiate_brainiac'
38
+ #
39
+ # Since Boson aims to make your libraries just standard ruby, we can achieve the above
40
+ # by making options a commented method attribute:
41
+ # module Brain
42
+ # # @options :execute=>:string
43
+ # # Help Brain live the dream
44
+ # def take_over(destination, options={})
45
+ # puts "Pinky, it's time to take over the #{destination}!"
46
+ # system(options[:execute]) if options[:execute]
47
+ # end
48
+ # end
49
+ #
50
+ # Some points about the above:
51
+ # * A '@' must prefix options and other method attributes that become comments.
52
+ # * Note the comment above the method. One-line comments right before a method set a command's description.
53
+ # * See Inspector for other method attributes, like config that can be placed above a method.
54
+ #
55
+ # Once a command has a defined option, a command can also recognize a slew of global options:
56
+ # >> take_over '-h'
57
+ # take_over [destination] [--execute=STRING]
58
+ #
59
+ # # prints much more verbose help
60
+ # >> take_over '-hv'
61
+ #
62
+ # For more about these global options see OptionCommand and View.
63
+ class FileLibrary < Library
64
+ #:stopdoc:
65
+ def self.library_file(library, dir)
66
+ File.join(Repo.commands_dir(dir), library + ".rb")
67
+ end
68
+
69
+ def self.matched_repo; @repo; end
70
+
71
+ def self.read_library_file(file, reload=false)
72
+ @file_cache ||= {}
73
+ @file_cache[file] = File.read(file) if (!@file_cache.has_key?(file) || reload)
74
+ @file_cache[file]
75
+ end
76
+
77
+ def self.reset_file_cache(name=nil)
78
+ if name && @file_cache
79
+ @file_cache.delete(library_file(name, (matched_repo || Boson.repo).dir))
80
+ else
81
+ @file_cache = nil
82
+ end
83
+ end
84
+
85
+ handles {|source|
86
+ @repo = Boson.repos.find {|e| File.exists? library_file(source.to_s, e.dir) } ||
87
+ Boson.repos.find {|e|
88
+ Dir["#{e.commands_dir}/**/*.rb"].grep(/\/#{source}\.rb/).size == 1
89
+ }
90
+ !!@repo
91
+ }
92
+
93
+ def library_file(name=@name)
94
+ self.class.library_file(name, @repo_dir)
95
+ end
96
+
97
+ def set_repo
98
+ self.class.matched_repo
99
+ end
100
+
101
+ def set_name(name)
102
+ @lib_file = File.exists?(library_file(name.to_s)) ? library_file(name.to_s) :
103
+ Dir[self.class.matched_repo.commands_dir.to_s+'/**/*.rb'].find {|e| e =~ /\/#{name}\.rb$/}
104
+ @lib_file.gsub(/^#{self.class.matched_repo.commands_dir}\/|\.rb$/, '')
105
+ end
106
+
107
+ def base_module
108
+ @base_module ||= @name.include?('/') ? create_module_from_path : Commands
109
+ end
110
+
111
+ def load_source(reload=false)
112
+ library_string = self.class.read_library_file(@lib_file, reload)
113
+ Inspector.enable
114
+ base_module.module_eval(library_string, @lib_file)
115
+ Inspector.disable
116
+ end
117
+
118
+ def create_module_from_path(index=-2)
119
+ @name.split('/')[0..index].inject(Boson::Commands) {|base, e|
120
+ base.const_defined?(sub_mod = Util.camelize(e)) ? base.const_get(sub_mod) :
121
+ Util.create_module(base, e)
122
+ }
123
+ end
124
+
125
+ def load_source_and_set_module
126
+ detected = detect_additions(:modules=>true) { load_source }
127
+ @module = determine_lib_module(detected[:modules]) unless @module
128
+ end
129
+
130
+ def determine_lib_module(detected_modules)
131
+ detected_modules = detected_modules.select {|e| e.to_s[/^#{base_module}::/] }
132
+ case detected_modules.size
133
+ when 1 then lib_module = detected_modules[0]
134
+ when 0 then lib_module = create_module_from_path(-1)
135
+ else
136
+ unless (lib_module = Util.constantize("boson/commands/#{@name}")) && lib_module.to_s[/^Boson::Commands/]
137
+ raise LoaderError, "Can't detect module. Specify a module in this library's config."
138
+ end
139
+ end
140
+ lib_module
141
+ end
142
+ #:startdoc:
143
+ end
144
+ end
@@ -0,0 +1,30 @@
1
+ module Boson
2
+ # This library loads a gem by the given name. Unlike FileLibrary or ModuleLibrary, this library
3
+ # doesn't need a module to provide its functionality.
4
+ #
5
+ # Example:
6
+ # >> load_library 'httparty', :class_commands=>{'put'=>'HTTParty.put',
7
+ # 'delete'=>'HTTParty.delete' }
8
+ # => true
9
+ # >> put 'http://someurl.com'
10
+ class GemLibrary < Library
11
+ #:stopdoc:
12
+ def self.is_a_gem?(name)
13
+ return false unless defined? Gem
14
+ Gem::VERSION >= '1.8.0' ?
15
+ Gem::Specification.find_all_by_name(name)[0].is_a?(Gem::Specification) :
16
+ Gem.searcher.find(name).is_a?(Gem::Specification)
17
+ end
18
+
19
+ handles {|source| is_a_gem?(source.to_s) }
20
+
21
+ def loaded_correctly?
22
+ !@gems.empty? || !@commands.empty? || !!@module
23
+ end
24
+
25
+ def load_source_and_set_module
26
+ detect_additions { Util.safe_require @name }
27
+ end
28
+ #:startdoc:
29
+ end
30
+ end
@@ -0,0 +1,30 @@
1
+ # This class loads any local file and is most commonly used to load a local
2
+ # Bosonfile. Since this file doesn't exist inside a normal Repo, it is not indexed with any repo.
3
+ # Since file-based libraries need to be associated with a repository, Boson associates it
4
+ # with a local repository if it exists or defaults to Boson.repo. See Boson::FileLibrary
5
+ # for more info about this library.
6
+ #
7
+ # Example:
8
+ # >> load_library 'Bosonfile'
9
+ # => true
10
+ class Boson::LocalFileLibrary < Boson::FileLibrary
11
+ handles {|source|
12
+ @repo = (File.exists?(source.to_s) ? (Boson.local_repo || Boson.repo) : nil)
13
+ !!@repo
14
+ }
15
+
16
+ #:stopdoc:
17
+ def set_name(name)
18
+ @lib_file = File.expand_path(name.to_s)
19
+ File.basename(@lib_file).downcase
20
+ end
21
+
22
+ def base_module
23
+ Boson::Commands
24
+ end
25
+
26
+ def library_file
27
+ @lib_file
28
+ end
29
+ #:startdoc:
30
+ end
@@ -0,0 +1,37 @@
1
+ module Boson
2
+ # This library takes a module or class as a library's name and loads its class methods
3
+ # as commands. If no commands are given it defaults to loading all of its class methods
4
+ # as commands. The only method callback (see Loader) this library calls on the
5
+ # original module/class is config().
6
+ #
7
+ # Example:
8
+ # >> load_library Math, :commands=>%w{sin cos tan}
9
+ # => true
10
+ #
11
+ # # Let's brush up on ol trig
12
+ # >> sin (Math::PI/2)
13
+ # => 1.0
14
+ # >> tan (Math::PI/4)
15
+ # => 1.0
16
+ # # Close enough :)
17
+ # >> cos (Math::PI/2)
18
+ # => 6.12323399573677e-17
19
+
20
+ class ModuleLibrary < Library
21
+ #:stopdoc:
22
+ handles {|source| source.is_a?(Module) }
23
+
24
+ def set_name(name)
25
+ @module = name
26
+ underscore_lib = name.to_s[/^Boson::Commands/] ? name.to_s.split('::')[-1] : name.to_s
27
+ Util.underscore(underscore_lib)
28
+ end
29
+
30
+ def load_commands
31
+ @class_commands = {@module.to_s=>Array(@commands).empty? ? @module.methods(false) : @commands }
32
+ @module = nil
33
+ super
34
+ end
35
+ #:startdoc:
36
+ end
37
+ end
@@ -0,0 +1,23 @@
1
+ # This library requires the given name. This is useful for loading standard libraries,
2
+ # non-gem libraries (i.e. rip packages) and anything else in $LOAD_PATH.
3
+ #
4
+ # Example:
5
+ # >> load_library 'fileutils', :class_commands=>{'cd'=>'FileUtils.cd', 'cp'=>'FileUtils.cp'}
6
+ # => true
7
+ # >> cd '/home'
8
+ # => 0
9
+ # >> Dir.pwd
10
+ # >> '/home'
11
+ class Boson::RequireLibrary < Boson::GemLibrary
12
+ EXTENSIONS = ['', '.rb', '.rbw', '.so', '.bundle', '.dll', '.sl', '.jar']
13
+ handles {|source|
14
+ extensions_glob = "{#{EXTENSIONS.join(',')}}"
15
+ ($LOAD_PATH - ['.']).any? {|dir|
16
+ Dir["#{File.expand_path source.to_s, dir}#{extensions_glob}"].size > 0
17
+ }
18
+ }
19
+
20
+ def loaded_correctly?
21
+ super || $".grep(/^#{@name}\.([a-z]+)?$/).size > 0
22
+ end
23
+ end