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,76 @@
1
+ module Boson
2
+ # This library loads a file in the commands subdirectory of a Boson::Repo. This library looks for files
3
+ # in repositories in the order given by Boson.repos.
4
+ # TODO: explain file format, modules, inspectors
5
+ class FileLibrary < Library
6
+ #:stopdoc:
7
+ def self.library_file(library, dir)
8
+ File.join(Repo.commands_dir(dir), library + ".rb")
9
+ end
10
+
11
+ def self.matched_repo; @repo; end
12
+
13
+ def self.read_library_file(file, reload=false)
14
+ @file_cache ||= {}
15
+ @file_cache[file] = File.read(file) if (!@file_cache.has_key?(file) || reload)
16
+ @file_cache[file]
17
+ end
18
+
19
+ def self.reset_file_cache(name=nil)
20
+ if name && @file_cache
21
+ #td: tia other repos
22
+ @file_cache.delete(library_file(name, Boson.repo.dir))
23
+ else
24
+ @file_cache = nil
25
+ end
26
+ end
27
+
28
+ handles {|source|
29
+ @repo = Boson.repos.find {|e|
30
+ File.exists? library_file(source.to_s, e.dir)
31
+ }
32
+ !!@repo
33
+ }
34
+
35
+ def library_file
36
+ self.class.library_file(@name, @repo_dir)
37
+ end
38
+
39
+ def set_repo
40
+ self.class.matched_repo
41
+ end
42
+
43
+ def load_source(reload=false)
44
+ library_string = self.class.read_library_file(library_file, reload)
45
+ Inspector.enable
46
+ Commands.module_eval(library_string, library_file)
47
+ Inspector.disable
48
+ end
49
+
50
+ def load_source_and_set_module
51
+ detected = detect_additions(:modules=>true) { load_source }
52
+ @module = determine_lib_module(detected[:modules]) unless @module
53
+ end
54
+
55
+ def reload_source_and_set_module
56
+ detected = detect_additions(:modules=>true) { load_source(true) }
57
+ if (@new_module = !detected[:modules].empty?)
58
+ @commands = []
59
+ @module = determine_lib_module(detected[:modules])
60
+ end
61
+ end
62
+
63
+ def determine_lib_module(detected_modules)
64
+ case detected_modules.size
65
+ when 1 then lib_module = detected_modules[0]
66
+ when 0 then raise LoaderError, "Can't detect module. Make sure at least one module is defined in the library."
67
+ else
68
+ unless ((lib_module = Util.constantize("boson/commands/#{@name}")) && lib_module.to_s[/^Boson::Commands/])
69
+ raise LoaderError, "Can't detect module. Specify a module in this library's config."
70
+ end
71
+ end
72
+ lib_module
73
+ end
74
+ #:startdoc:
75
+ end
76
+ end
@@ -0,0 +1,21 @@
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
+ class GemLibrary < Library
5
+ #:stopdoc:
6
+ def self.is_a_gem?(name)
7
+ Object.const_defined?(:Gem) && Gem.searcher.find(name).is_a?(Gem::Specification)
8
+ end
9
+
10
+ handles {|source| is_a_gem?(source.to_s) }
11
+
12
+ def loaded_correctly?
13
+ !@gems.empty? || !@commands.empty? || !!@module
14
+ end
15
+
16
+ def load_source_and_set_module
17
+ detect_additions { Util.safe_require @name }
18
+ end
19
+ #:startdoc:
20
+ end
21
+ end
@@ -0,0 +1,17 @@
1
+ module Boson
2
+ # A library which takes a module as a library's name. Reload for this library
3
+ # subclass is disabled.
4
+ class ModuleLibrary < Library
5
+ #:stopdoc:
6
+ handles {|source| source.is_a?(Module) }
7
+
8
+ def set_name(name)
9
+ @module = name
10
+ underscore_lib = name.to_s[/^Boson::Commands/] ? name.to_s.split('::')[-1] : name.to_s
11
+ super Util.underscore(underscore_lib)
12
+ end
13
+
14
+ def reload; false; end
15
+ #:startdoc:
16
+ end
17
+ end
@@ -0,0 +1,11 @@
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
+ class Boson::RequireLibrary < Boson::GemLibrary
4
+ handles {|source|
5
+ begin
6
+ Kernel.load("#{source}.rb", true)
7
+ rescue LoadError
8
+ false
9
+ end
10
+ }
11
+ end
@@ -0,0 +1,108 @@
1
+ module Boson
2
+ # A library is a group of commands (Command objects) usually grouped together by a module.
3
+ # Libraries are loaded from different sources depending on the library subclass. Default library
4
+ # subclasses are FileLibrary, GemLibrary, RequireLibrary and ModuleLibrary.
5
+ #
6
+ # To create your own subclass you need to define what sources the subclass can handle with handles().
7
+ # If handles() returns true then the subclass is chosen to load. See Loader to see what instance methods
8
+ # to override for a subclass.
9
+ class Library
10
+ include Loader
11
+ class <<self
12
+ #:stopdoc:
13
+ attr_accessor :handle_blocks
14
+ def handles(&block)
15
+ (Library.handle_blocks ||= []) << [self,block]
16
+ end
17
+ #:startdoc:
18
+ end
19
+
20
+ # Public attributes for use outside of Boson.
21
+ ATTRIBUTES = [:gems, :dependencies, :commands, :loaded, :module, :name, :namespace]
22
+ attr_reader *(ATTRIBUTES + [:commands_hash, :library_file, :object_namespace])
23
+ # Private attribute for use within Boson.
24
+ attr_reader :except, :no_alias_creation, :new_module, :new_commands
25
+ # Optional namespace name for a library. When enabled defaults to a library's name.
26
+ attr_writer :namespace
27
+ # Creates a library object with a hash of attributes which must include a :name attribute.
28
+ # Each hash pair maps directly to an instance variable and value. Defaults for attributes
29
+ # are read from config[:libraries][@library_name]. See Boson::Repo.config for more details.
30
+ def initialize(hash)
31
+ @name = set_name hash.delete(:name)
32
+ @loaded = false
33
+ repo = set_repo
34
+ @repo_dir = repo.dir
35
+ @commands_hash = {}
36
+ @commands = []
37
+ set_config (repo.config[:libraries][@name] || {}).merge(hash)
38
+ @commands_hash = repo.config[:commands].merge @commands_hash
39
+ set_command_aliases(repo.config[:command_aliases])
40
+ @namespace = true if Boson.repo.config[:auto_namespace] && @namespace.nil? &&
41
+ !Boson::Runner.default_libraries.include?(@module)
42
+ @namespace = clean_name if @namespace
43
+ end
44
+
45
+ # A concise symbol version of a library type i.e. FileLibrary -> :file.
46
+ def library_type
47
+ str = self.class.to_s[/::(\w+)Library$/, 1] || 'library'
48
+ str.downcase.to_sym
49
+ end
50
+
51
+ # The object a library uses for executing its commands.
52
+ def namespace_object
53
+ @namespace_object ||= @namespace ? Boson.invoke(@namespace) : Boson.main_object
54
+ end
55
+
56
+ #:stopdoc:
57
+ # handles names under directories
58
+ def clean_name
59
+ @name[/\w+$/]
60
+ end
61
+
62
+ def set_name(name)
63
+ name.to_s or raise ArgumentError, "New library missing required key :name"
64
+ end
65
+
66
+ def set_config(config)
67
+ if (commands = config.delete(:commands))
68
+ if commands.is_a?(Array)
69
+ @commands += commands
70
+ @pre_defined_commands = true
71
+ elsif commands.is_a?(Hash)
72
+ @commands += commands.keys
73
+ @commands_hash = Util.recursive_hash_merge commands, @commands_hash
74
+ end
75
+ end
76
+ set_command_aliases config.delete(:command_aliases) if config[:command_aliases]
77
+ set_attributes config, true
78
+ end
79
+
80
+ def set_command_aliases(command_aliases)
81
+ (command_aliases || {}).each do |cmd, cmd_alias|
82
+ @commands_hash[cmd] ||= {}
83
+ @commands_hash[cmd][:alias] ||= cmd_alias
84
+ end
85
+ end
86
+
87
+ def set_repo
88
+ Boson.repo
89
+ end
90
+
91
+ def set_attributes(hash, force=false)
92
+ hash.each {|k,v| instance_variable_set("@#{k}", v) if instance_variable_get("@#{k}").nil? || force }
93
+ end
94
+
95
+ def command_objects(names)
96
+ Boson.commands.select {|e| names.include?(e.name) && e.lib == self.name }
97
+ end
98
+
99
+ def marshal_dump
100
+ [@name, @commands, @gems, @module.to_s, @repo_dir]
101
+ end
102
+
103
+ def marshal_load(ary)
104
+ @name, @commands, @gems, @module, @repo_dir = ary
105
+ end
106
+ #:startdoc:
107
+ end
108
+ end
@@ -0,0 +1,103 @@
1
+ module Boson
2
+ # Raised if a library has a method which conflicts with existing methods in Boson.main_object.
3
+ class MethodConflictError < LoaderError; end
4
+
5
+ # This module is mixed into Library to give it load() and reload() functionality.
6
+ # When creating your own Library subclass, you should override load_source_and_set_module and
7
+ # reload_source_and_set_module. You can override other methods in this module as needed.
8
+ module Loader
9
+ # Loads a library and its dependencies and returns true if library loads correctly.
10
+ def load
11
+ @gems ||= []
12
+ load_source_and_set_module
13
+ module_callbacks if @module
14
+ yield if block_given?
15
+ (@module || @class_commands) ? detect_additions { load_module_commands } : @namespace = nil
16
+ @init_methods.each {|m| namespace_object.send(m) if namespace_object.respond_to?(m) } if @init_methods && !@index
17
+ set_library_commands
18
+ loaded_correctly? && (@loaded = true)
19
+ end
20
+
21
+ # Load the source and set instance variables necessary to make a library valid i.e. @module.
22
+ def load_source_and_set_module; end
23
+
24
+ # Boolean which indicates if library loaded correctly.
25
+ def loaded_correctly?
26
+ !!@module
27
+ end
28
+
29
+ # Reloads a library from its source and adds new commands.
30
+ def reload
31
+ original_commands = @commands
32
+ reload_source_and_set_module
33
+ detect_additions { load_module_commands } if @new_module
34
+ @new_commands = @commands - original_commands
35
+ true
36
+ end
37
+
38
+ # Same as load_source_and_set_module except it reloads.
39
+ def reload_source_and_set_module
40
+ raise LoaderError, "Reload not implemented"
41
+ end
42
+
43
+ #:stopdoc:
44
+ def module_callbacks
45
+ set_config(@module.config) if @module.respond_to?(:config)
46
+ if @module.respond_to?(:append_features)
47
+ raise AppendFeaturesFalseError unless @module.append_features(Module.new)
48
+ end
49
+ end
50
+
51
+ def load_module_commands
52
+ initialize_library_module
53
+ rescue MethodConflictError=>e
54
+ if Boson.repo.config[:error_method_conflicts] || @namespace
55
+ raise MethodConflictError, e.message
56
+ else
57
+ @namespace = clean_name
58
+ $stderr.puts "#{e.message}. Attempting load into the namespace #{@namespace}..."
59
+ initialize_library_module
60
+ end
61
+ end
62
+
63
+ def detect_additions(options={}, &block)
64
+ options[:object_methods] = @object_methods if !@object_methods.nil?
65
+ detected = Util.detect(options, &block)
66
+ @gems += detected[:gems] if detected[:gems]
67
+ @commands += detected[:methods]
68
+ detected
69
+ end
70
+
71
+ def initialize_library_module
72
+ @module = @module ? Util.constantize(@module) : Util.create_module(Boson::Commands, clean_name)
73
+ raise(LoaderError, "No module for library #{@name}") unless @module
74
+ Manager.create_class_aliases(@module, @class_commands) unless @class_commands.to_s.empty?
75
+ check_for_method_conflicts unless @force
76
+ @namespace = clean_name if @object_namespace
77
+ @namespace ? Namespace.create(@namespace, self) : include_in_universe
78
+ end
79
+
80
+ def include_in_universe(lib_module=@module)
81
+ Boson::Universe.send :include, lib_module
82
+ Boson::Universe.send :extend_object, Boson.main_object
83
+ end
84
+
85
+ def check_for_method_conflicts
86
+ conflicts = @namespace ? (Boson.can_invoke?(@namespace) ? [@namespace] : []) :
87
+ Util.common_instance_methods(@module, Boson::Universe)
88
+ unless conflicts.empty?
89
+ raise MethodConflictError,"The following commands conflict with existing commands: #{conflicts.join(', ')}"
90
+ end
91
+ end
92
+
93
+ def set_library_commands
94
+ aliases = @commands_hash.select {|k,v| @commands.include?(k) }.map {|k,v| v[:alias]}.compact
95
+ @commands -= aliases
96
+ @commands.delete(@namespace) if @namespace
97
+ @commands += Boson.invoke(@namespace).boson_commands if @namespace && !@pre_defined_commands
98
+ @commands -= @except if @except
99
+ @commands.uniq!
100
+ end
101
+ #:startdoc:
102
+ end
103
+ end
@@ -0,0 +1,184 @@
1
+ module Boson
2
+ # Base class for library loading errors. Raised mostly in Boson::Loader and rescued by Boson::Manager.
3
+ class LoaderError < StandardError; end
4
+ # Raised when a library's append_features returns false.
5
+ class AppendFeaturesFalseError < StandardError; end
6
+
7
+ class Manager
8
+ class <<self
9
+ # Loads a library or an array of libraries with options. Manager loads the first library subclass
10
+ # to meet a library subclass' criteria in this order: ModuleLibrary, FileLibrary, GemLibrary, RequireLibrary.
11
+ # ==== Examples:
12
+ # Manager.load 'my_commands' -> Loads a FileLibrary object from ~/.boson/commands/my_commands.rb
13
+ # Manager.load 'method_lister' -> Loads a GemLibrary object which requires the method_lister gem
14
+ # ==== Options:
15
+ # [:verbose] Boolean to print each library's loaded status along with more verbose errors. Default is false.
16
+ # [:index] Boolean to load in index mode. Default is false.
17
+ def load(libraries, options={})
18
+ libraries = [libraries] unless libraries.is_a?(Array)
19
+ libraries.map {|e|
20
+ (@library = load_once(e, options)) ? after_load : false
21
+ }.all?
22
+ end
23
+
24
+ # Reloads a library or an array of libraries with the following options:
25
+ # * :verbose: Boolean to print reload status. Default is false.
26
+ def reload(source, options={})
27
+ if (lib = Boson.library(source))
28
+ if lib.loaded
29
+ command_size = Boson.commands.size
30
+ @options = options
31
+ if (result = rescue_load_action(lib.name, :reload) { lib.reload })
32
+ after_reload(lib)
33
+ puts "Reloaded library #{source}: Added #{Boson.commands.size - command_size} commands" if options[:verbose]
34
+ end
35
+ result
36
+ else
37
+ puts "Library hasn't been loaded yet. Loading library #{source}..." if options[:verbose]
38
+ load(source, options)
39
+ end
40
+ else
41
+ puts "Library #{source} doesn't exist." if options[:verbose]
42
+ false
43
+ end
44
+ end
45
+
46
+ #:stopdoc:
47
+ def add_library(lib)
48
+ Boson.libraries.delete(Boson.library(lib.name))
49
+ Boson.libraries << lib
50
+ end
51
+
52
+ def loaded?(lib_name)
53
+ ((lib = Boson.library(lib_name)) && lib.loaded) ? true : false
54
+ end
55
+
56
+ def rescue_load_action(library, load_method)
57
+ yield
58
+ rescue AppendFeaturesFalseError
59
+ rescue LoaderError=>e
60
+ FileLibrary.reset_file_cache(library.to_s)
61
+ print_error_message "Unable to #{load_method} library #{library}. Reason: #{e.message}"
62
+ rescue Exception=>e
63
+ FileLibrary.reset_file_cache(library.to_s)
64
+ print_error_message "Unable to #{load_method} library #{library}. Reason: #{$!}" + "\n" +
65
+ e.backtrace.slice(0,3).join("\n")
66
+ ensure
67
+ Inspector.disable if Inspector.enabled
68
+ end
69
+
70
+ def print_error_message(message)
71
+ $stderr.puts message if !@options[:index] || (@options[:index] && @options[:verbose])
72
+ end
73
+
74
+ def load_once(source, options={})
75
+ @options = options
76
+ rescue_load_action(source, :load) do
77
+ lib = loader_create(source)
78
+ if loaded?(lib.name)
79
+ $stderr.puts "Library #{lib.name} already exists" if options[:verbose] && !options[:dependency]
80
+ false
81
+ else
82
+ if lib.load { load_dependencies(lib, options) }
83
+ lib
84
+ else
85
+ $stderr.puts "Unable to load library #{lib.name}." if !options[:dependency]
86
+ false
87
+ end
88
+ end
89
+ end
90
+ end
91
+
92
+ def lib_dependencies
93
+ @lib_dependencies ||= {}
94
+ end
95
+
96
+ def load_dependencies(lib, options={})
97
+ lib_dependencies[lib] = (lib.dependencies || []).map do |e|
98
+ next if loaded?(e)
99
+ load_once(e, options.merge(:dependency=>true)) ||
100
+ raise(LoaderError, "Can't load dependency #{e}")
101
+ end.compact
102
+ end
103
+
104
+ def loader_create(source)
105
+ lib_class = Library.handle_blocks.find {|k,v| v.call(source) } or raise(LoaderError, "Library #{source} not found.")
106
+ lib_class[0].new(:name=>source, :index=>@options[:index])
107
+ end
108
+
109
+ def after_load
110
+ create_commands(@library)
111
+ add_library(@library)
112
+ puts "Loaded library #{@library.name}" if @options[:verbose]
113
+ (lib_dependencies[@library] || []).each do |e|
114
+ create_commands(e)
115
+ add_library(e)
116
+ puts "Loaded library dependency #{e.name}" if @options[:verbose]
117
+ end
118
+ true
119
+ end
120
+
121
+ def after_reload(lib)
122
+ Boson.commands.delete_if {|e| e.lib == lib.name } if lib.new_module
123
+ create_commands(lib, lib.new_commands)
124
+ end
125
+
126
+ def before_create_commands(lib)
127
+ lib.is_a?(FileLibrary) && lib.module && Inspector.add_method_data_to_library(lib)
128
+ end
129
+
130
+ def create_commands(lib, commands=lib.commands)
131
+ if lib.except
132
+ commands -= lib.except
133
+ lib.except.each {|e| lib.namespace_object.instance_eval("class<<self;self;end").send :undef_method, e }
134
+ end
135
+ before_create_commands(lib)
136
+ commands.each {|e| Boson.commands << Command.create(e, lib)}
137
+ create_command_aliases(lib, commands) if commands.size > 0 && !lib.no_alias_creation
138
+ create_option_commands(lib, commands)
139
+ end
140
+
141
+ def create_option_commands(lib, commands)
142
+ option_commands = lib.command_objects(commands).select {|e| e.option_command? }
143
+ accepted, rejected = option_commands.partition {|e| e.args(lib) || e.arg_size }
144
+ if @options[:verbose] && rejected.size > 0
145
+ puts "Following commands cannot have options until their arguments are configured: " +
146
+ rejected.map {|e| e.name}.join(', ')
147
+ end
148
+ accepted.each {|cmd| Scientist.create_option_command(lib.namespace_object, cmd) }
149
+ end
150
+
151
+ def create_command_aliases(lib, commands)
152
+ lib.module ? prep_and_create_instance_aliases(commands, lib.module) : check_for_uncreated_aliases(lib, commands)
153
+ end
154
+
155
+ def prep_and_create_instance_aliases(commands, lib_module)
156
+ aliases_hash = {}
157
+ select_commands = Boson.commands.select {|e| commands.include?(e.name)}
158
+ select_commands.each do |e|
159
+ if e.alias
160
+ aliases_hash[lib_module.to_s] ||= {}
161
+ aliases_hash[lib_module.to_s][e.name] = e.alias
162
+ end
163
+ end
164
+ create_instance_aliases(aliases_hash)
165
+ end
166
+
167
+ def create_instance_aliases(aliases_hash)
168
+ Alias.manager.create_aliases(:instance_method, aliases_hash)
169
+ end
170
+
171
+ def create_class_aliases(mod, class_commands)
172
+ Alias.manager.create_aliases(:any_to_instance_method, mod.to_s=>class_commands.invert)
173
+ end
174
+
175
+ def check_for_uncreated_aliases(lib, commands)
176
+ return if lib.is_a?(GemLibrary)
177
+ if (found_commands = Boson.commands.select {|e| commands.include?(e.name)}) && found_commands.find {|e| e.alias }
178
+ $stderr.puts "No aliases created for library #{lib.name} because it has no module"
179
+ end
180
+ end
181
+ #:startdoc:
182
+ end
183
+ end
184
+ end