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