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