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,45 @@
1
+ module Boson
2
+ class Namespace
3
+ def self.create_object_namespace(name, library)
4
+ obj = library.namespace_object
5
+ obj.instance_eval("class<<self;self;end").send(:define_method, :boson_commands) {
6
+ self.class.instance_methods(false) }
7
+ obj.instance_eval("class<<self;self;end").send(:define_method, :object_delegate?) { true }
8
+ namespaces[name.to_s] = obj
9
+ end
10
+
11
+ def self.namespaces
12
+ @namespaces ||= {}
13
+ end
14
+
15
+ def self.create(name, library)
16
+ if library.object_namespace && library.module.instance_methods.include?(name)
17
+ library.include_in_universe
18
+ create_object_namespace(name, library)
19
+ else
20
+ create_basic_namespace(name, library)
21
+ end
22
+ end
23
+
24
+ def self.create_basic_namespace(name, library)
25
+ namespaces[name.to_s] = new(name, library)
26
+ Commands::Namespace.send(:define_method, name) { Boson::Namespace.namespaces[name.to_s] }
27
+ end
28
+
29
+ def initialize(name, library)
30
+ raise ArgumentError unless library.module
31
+ @name, @library = name.to_s, library
32
+ class <<self; self end.send :include, @library.module
33
+ end
34
+
35
+ def boson_commands
36
+ @library.module.instance_methods
37
+ end
38
+
39
+ def object_delegate?; false; end
40
+
41
+ def method_missing(method, *args, &block)
42
+ Boson.can_invoke?(method) ? Boson.invoke(method, *args, &block) : super
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,318 @@
1
+ module Boson
2
+ # Simple Hash with indifferent access
3
+ class IndifferentAccessHash < ::Hash
4
+ def initialize(hash)
5
+ super()
6
+ update hash.each {|k,v| hash[convert_key(k)] = hash.delete(k) }
7
+ end
8
+
9
+ def [](key)
10
+ super convert_key(key)
11
+ end
12
+
13
+ def []=(key, value)
14
+ super convert_key(key), value
15
+ end
16
+
17
+ def values_at(*indices)
18
+ indices.collect { |key| self[convert_key(key)] }
19
+ end
20
+
21
+ protected
22
+ def convert_key(key)
23
+ key.kind_of?(String) ? key.to_sym : key
24
+ end
25
+ end
26
+
27
+ # This is a modified version of Yehuda Katz's Thor::Options class which is a modified version
28
+ # of Daniel Berger's Getopt::Long class, licensed under Ruby's license.
29
+ class OptionParser
30
+ class Error < StandardError; end
31
+
32
+ NUMERIC = /(\d*\.\d+|\d+)/
33
+ LONG_RE = /^(--\w+[-\w+]*)$/
34
+ SHORT_RE = /^(-[a-zA-Z])$/i
35
+ EQ_RE = /^(--\w+[-\w+]*|-[a-zA-Z])=(.*)$/i
36
+ SHORT_SQ_RE = /^-([a-zA-Z]{2,})$/i # Allow either -x -v or -xv style for single char args
37
+ SHORT_NUM = /^(-[a-zA-Z])#{NUMERIC}$/i
38
+
39
+ attr_reader :leading_non_opts, :trailing_non_opts, :opt_aliases
40
+
41
+ def non_opts
42
+ leading_non_opts + trailing_non_opts
43
+ end
44
+
45
+ # Takes an array of options. Each array consists of up to three
46
+ # elements that indicate the name and type of option. Returns a hash
47
+ # containing each option name, minus the '-', as a key. The value
48
+ # for each key depends on the type of option and/or the value provided
49
+ # by the user.
50
+ #
51
+ # The long option _must_ be provided. The short option defaults to the
52
+ # first letter of the option. The default type is :boolean.
53
+ #
54
+ # Example:
55
+ #
56
+ # opts = Boson::OptionParser.new(
57
+ # "--debug" => true,
58
+ # ["--verbose", "-v"] => true,
59
+ # ["--level", "-l"] => :numeric
60
+ # ).parse(args)
61
+ #
62
+ def initialize(opts)
63
+ @defaults = {}
64
+ @opt_aliases = {}
65
+ @leading_non_opts, @trailing_non_opts = [], []
66
+
67
+ # build hash of dashed options to option types
68
+ # type can be a hash of opt attributes, a default value or a type symbol
69
+ @opt_types = opts.inject({}) do |mem, (name, type)|
70
+ name, *aliases = name if name.is_a?(Array)
71
+ name = name.to_s
72
+ # we need both nice and dasherized form of option name
73
+ if name.index('-') == 0
74
+ nice_name = undasherize name
75
+ else
76
+ nice_name = name
77
+ name = dasherize name
78
+ end
79
+ # store for later
80
+ @opt_aliases[nice_name] = aliases || []
81
+
82
+ if type.is_a?(Hash)
83
+ @option_attributes ||= {}
84
+ @option_attributes[nice_name] = type
85
+ @defaults[nice_name] = type[:default] if type[:default]
86
+ @option_attributes[nice_name][:enum] = true if type.key?(:values) && !type.key?(:enum)
87
+ type = determine_option_type(type[:default]) || type[:type] || :boolean
88
+ end
89
+
90
+ # set defaults
91
+ case type
92
+ when TrueClass then @defaults[nice_name] = true
93
+ when FalseClass then @defaults[nice_name] = false
94
+ when String, Numeric, Array then @defaults[nice_name] = type
95
+ end
96
+
97
+ mem[name] = determine_option_type(type) || type
98
+ mem
99
+ end
100
+
101
+ # generate hash of dashed aliases to dashed options
102
+ @opt_aliases = @opt_aliases.sort.inject({}) {|h, (nice_name, aliases)|
103
+ name = dasherize nice_name
104
+ # allow for aliases as symbols
105
+ aliases.map! {|e| e.to_s.index('-') != 0 ? dasherize(e.to_s) : e }
106
+ if aliases.empty? and nice_name.length > 1
107
+ opt_alias = nice_name[0,1]
108
+ opt_alias = h.key?("-"+opt_alias) ? "-"+opt_alias.capitalize : "-"+opt_alias
109
+ h[opt_alias] ||= name unless @opt_types.key?(opt_alias)
110
+ else
111
+ aliases.each { |e| h[e] = name unless @opt_types.key?(e) }
112
+ end
113
+ h
114
+ }
115
+ end
116
+
117
+ def parse(args, flags={})
118
+ @args = args
119
+ # start with defaults
120
+ hash = IndifferentAccessHash.new @defaults
121
+
122
+ @leading_non_opts = []
123
+ unless flags[:opts_before_args]
124
+ @leading_non_opts << shift until current_is_option? || @args.empty?
125
+ end
126
+
127
+ while current_is_option?
128
+ case shift
129
+ when SHORT_SQ_RE
130
+ unshift $1.split('').map { |f| "-#{f}" }
131
+ next
132
+ when EQ_RE, SHORT_NUM
133
+ unshift $2
134
+ option = $1
135
+ when LONG_RE, SHORT_RE
136
+ option = $1
137
+ end
138
+
139
+ dashed_option = normalize_option(option)
140
+ @current_option = undasherize(dashed_option)
141
+ type = option_type(dashed_option)
142
+ validate_option_value(type)
143
+ value = get_option_value(type, dashed_option)
144
+ # set on different line since current_option may change
145
+ hash[@current_option.to_sym] = value
146
+ end
147
+
148
+ @trailing_non_opts = @args
149
+ check_required! hash
150
+ delete_invalid_opts if flags[:delete_invalid_opts]
151
+ hash
152
+ end
153
+
154
+ def formatted_usage
155
+ return "" if @opt_types.empty?
156
+ @opt_types.map do |opt, type|
157
+ case type
158
+ when :boolean
159
+ "[#{opt}]"
160
+ when :required
161
+ opt + "=" + opt.gsub(/\-/, "").upcase
162
+ else
163
+ sample = @defaults[undasherize(opt)]
164
+ sample ||= case type
165
+ when :string then undasherize(opt).gsub(/\-/, "_").upcase
166
+ when :numeric then "N"
167
+ when :array then "A,B,C"
168
+ end
169
+ "[" + opt + "=" + sample.to_s + "]"
170
+ end
171
+ end.join(" ")
172
+ end
173
+
174
+ alias :to_s :formatted_usage
175
+
176
+ def print_usage_table(render_options={})
177
+ aliases = @opt_aliases.invert
178
+ additional = [:desc, :values].select {|e| (@option_attributes || {}).values.any? {|f| f.key?(e) } }
179
+ opts = @opt_types.keys.sort.inject([]) {|t,e|
180
+ h = {:name=>e, :aliases=>aliases[e], :type=>@opt_types[e]}
181
+ additional.each {|f| h[f] = (@option_attributes[undasherize(e)] || {})[f] }
182
+ t << h
183
+ }
184
+ render_options = {:headers=>{:name=>"Option", :aliases=>"Alias", :desc=>'Description', :values=>'Values'},
185
+ :fields=>[:name, :aliases, :type] + additional, :description=>false, :filters=>{:values=>lambda {|e| (e || []).join(',')} }
186
+ }.merge(render_options)
187
+ View.render opts, render_options
188
+ end
189
+
190
+ private
191
+ def determine_option_type(value)
192
+ case value
193
+ when TrueClass, FalseClass then :boolean
194
+ when String then :string
195
+ when Numeric then :numeric
196
+ when Array then :array
197
+ else nil
198
+ end
199
+ end
200
+
201
+ def get_option_value(type, opt)
202
+ case type
203
+ when :required
204
+ shift
205
+ when :string
206
+ value = shift
207
+ if (values = @option_attributes[@current_option][:values].sort_by {|e| e.to_s} rescue nil)
208
+ (val = auto_alias_value(values, value)) && value = val
209
+ end
210
+ value
211
+ when :boolean
212
+ (!@opt_types.key?(opt) && @current_option =~ /^no-(\w+)$/) ? (@current_option.replace($1) && false) : true
213
+ when :numeric
214
+ peek.index('.') ? shift.to_f : shift.to_i
215
+ when :array
216
+ splitter = (@option_attributes[@current_option][:split] rescue nil) || ','
217
+ array = shift.split(splitter)
218
+ if values = @option_attributes[@current_option][:values].sort_by {|e| e.to_s } rescue nil
219
+ array.each_with_index {|e,i|
220
+ (value = auto_alias_value(values, e)) && array[i] = value
221
+ }
222
+ end
223
+ array
224
+ end
225
+ end
226
+
227
+ def auto_alias_value(values, possible_value)
228
+ values.find {|v| v.to_s =~ /^#{possible_value}/ } or (@option_attributes[@current_option][:enum] ?
229
+ raise(Error, "invalid value '#{possible_value}' for option '#{@current_option}'") : possible_value)
230
+ end
231
+
232
+ def validate_option_value(type)
233
+ if type != :boolean && peek.nil?
234
+ raise Error, "no value provided for option '#{@current_option}'"
235
+ end
236
+
237
+ case type
238
+ when :required, :string
239
+ raise Error, "cannot pass '#{peek}' as an argument to option '#{@current_option}'" if valid?(peek)
240
+ when :numeric
241
+ unless peek =~ NUMERIC and $& == peek
242
+ raise Error, "expected numeric value for option '#{@current_option}'; got #{peek.inspect}"
243
+ end
244
+ end
245
+ end
246
+
247
+ def delete_invalid_opts
248
+ [@trailing_non_opts].each do |args|
249
+ args.delete_if {|e|
250
+ invalid = e.to_s[/^-/]
251
+ $stderr.puts "Deleted invalid option '#{e}'" if invalid
252
+ invalid
253
+ }
254
+ end
255
+ end
256
+
257
+ def undasherize(str)
258
+ str.sub(/^-{1,2}/, '')
259
+ end
260
+
261
+ def dasherize(str)
262
+ (str.length > 1 ? "--" : "-") + str
263
+ end
264
+
265
+ def peek
266
+ @args.first
267
+ end
268
+
269
+ def shift
270
+ @args.shift
271
+ end
272
+
273
+ def unshift(arg)
274
+ unless arg.kind_of?(Array)
275
+ @args.unshift(arg)
276
+ else
277
+ @args = arg + @args
278
+ end
279
+ end
280
+
281
+ def valid?(arg)
282
+ if arg.to_s =~ /^--no-(\w+)$/
283
+ @opt_types.key?(arg) or (@opt_types["--#{$1}"] == :boolean)
284
+ else
285
+ @opt_types.key?(arg) or @opt_aliases.key?(arg)
286
+ end
287
+ end
288
+
289
+ def current_is_option?
290
+ case peek
291
+ when LONG_RE, SHORT_RE, EQ_RE, SHORT_NUM
292
+ valid?($1)
293
+ when SHORT_SQ_RE
294
+ $1.split('').any? { |f| valid?("-#{f}") }
295
+ end
296
+ end
297
+
298
+ def normalize_option(opt)
299
+ @opt_aliases.key?(opt) ? @opt_aliases[opt] : opt
300
+ end
301
+
302
+ def option_type(opt)
303
+ if opt =~ /^--no-(\w+)$/
304
+ @opt_types[opt] || @opt_types["--#{$1}"]
305
+ else
306
+ @opt_types[opt]
307
+ end
308
+ end
309
+
310
+ def check_required!(hash)
311
+ for name, type in @opt_types
312
+ if type == :required and !hash[undasherize(name)]
313
+ raise Error, "no value provided for required option '#{undasherize(name)}'"
314
+ end
315
+ end
316
+ end
317
+ end
318
+ end
data/lib/boson/repo.rb ADDED
@@ -0,0 +1,38 @@
1
+ %w{yaml fileutils}.each {|e| require e }
2
+ module Boson
3
+ class Repo
4
+ def self.commands_dir(dir)
5
+ File.join(dir, 'commands')
6
+ end
7
+
8
+ attr_accessor :dir, :config
9
+ def initialize(dir)
10
+ @dir = dir
11
+ end
12
+
13
+ def config_dir
14
+ @config_dir ||= FileUtils.mkdir_p File.join(dir, 'config')
15
+ end
16
+
17
+ def commands_dir
18
+ @commands_dir ||= FileUtils.mkdir_p self.class.commands_dir(@dir)
19
+ end
20
+
21
+ # ==== Valid config keys:
22
+ # [:libraries] Hash of libraries mapping their name to attribute hashes.
23
+ # [:commands] Hash of commands mapping their name to attribute hashes.
24
+ # [:defaults] Array of libraries to load at start up.
25
+ # [:add_load_path] Boolean specifying whether to add a load path pointing to the lib under boson's directory. Defaults to false if
26
+ # the lib directory isn't defined in the boson directory. Default is false.
27
+ # [:error_method_conflicts] Boolean specifying library loading behavior when one of its methods conflicts with existing methods in
28
+ # the global namespace. When set to false, Boson automatically puts the library in its own namespace.
29
+ # When set to true, the library fails to load explicitly. Default is false.
30
+ def config(reload=false)
31
+ if reload || @config.nil?
32
+ default = {:commands=>{}, :libraries=>{}, :command_aliases=>{}, :defaults=>[]}
33
+ @config = default.merge(YAML::load_file(config_dir + '/boson.yml')) rescue default
34
+ end
35
+ @config
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,51 @@
1
+ module Boson
2
+ class Runner
3
+ class<<self
4
+ def init
5
+ View.enable
6
+ add_load_path
7
+ Manager.load default_libraries, load_options
8
+ end
9
+
10
+ def add_load_path
11
+ Boson.repos.each {|repo|
12
+ if repo.config[:add_load_path] || File.exists?(File.join(repo.dir, 'lib'))
13
+ $: << File.join(repo.dir, 'lib') unless $:.include? File.expand_path(File.join(repo.dir, 'lib'))
14
+ end
15
+ }
16
+ end
17
+
18
+ def load_options
19
+ {:verbose=>@options[:verbose]}
20
+ end
21
+
22
+ def default_libraries
23
+ [Boson::Commands::Core, Boson::Commands::WebCore]
24
+ end
25
+
26
+ def detected_libraries
27
+ Boson.repos.map {|repo| Dir[File.join(repo.commands_dir, '**/*.rb')].
28
+ map {|e| e.gsub(/.*commands\//,'').gsub('.rb','') } }.flatten
29
+ end
30
+
31
+ def all_libraries
32
+ (detected_libraries + Boson.repos.map {|e| e.config[:libraries].keys}.flatten).uniq
33
+ end
34
+
35
+ def define_autoloader
36
+ class << ::Boson.main_object
37
+ def method_missing(method, *args, &block)
38
+ Boson::Index.read
39
+ if lib = Boson::Index.find_library(method.to_s)
40
+ Boson::Manager.load lib, :verbose=>true
41
+ send(method, *args, &block) if respond_to?(method)
42
+ else
43
+ super
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ end
50
+ end
51
+ end