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.
- data/LICENSE.txt +22 -0
- data/README.rdoc +133 -0
- data/Rakefile +52 -0
- data/VERSION.yml +4 -0
- data/bin/boson +6 -0
- data/lib/boson.rb +72 -0
- data/lib/boson/command.rb +117 -0
- data/lib/boson/commands.rb +7 -0
- data/lib/boson/commands/core.rb +66 -0
- data/lib/boson/commands/web_core.rb +36 -0
- data/lib/boson/index.rb +95 -0
- data/lib/boson/inspector.rb +80 -0
- data/lib/boson/inspectors/argument_inspector.rb +92 -0
- data/lib/boson/inspectors/comment_inspector.rb +79 -0
- data/lib/boson/inspectors/method_inspector.rb +94 -0
- data/lib/boson/libraries/file_library.rb +76 -0
- data/lib/boson/libraries/gem_library.rb +21 -0
- data/lib/boson/libraries/module_library.rb +17 -0
- data/lib/boson/libraries/require_library.rb +11 -0
- data/lib/boson/library.rb +108 -0
- data/lib/boson/loader.rb +103 -0
- data/lib/boson/manager.rb +184 -0
- data/lib/boson/namespace.rb +45 -0
- data/lib/boson/option_parser.rb +318 -0
- data/lib/boson/repo.rb +38 -0
- data/lib/boson/runner.rb +51 -0
- data/lib/boson/runners/bin_runner.rb +100 -0
- data/lib/boson/runners/repl_runner.rb +40 -0
- data/lib/boson/scientist.rb +168 -0
- data/lib/boson/util.rb +93 -0
- data/lib/boson/view.rb +31 -0
- data/test/argument_inspector_test.rb +62 -0
- data/test/bin_runner_test.rb +136 -0
- data/test/commands_test.rb +51 -0
- data/test/comment_inspector_test.rb +99 -0
- data/test/config/index.marshal +0 -0
- data/test/file_library_test.rb +50 -0
- data/test/index_test.rb +117 -0
- data/test/loader_test.rb +181 -0
- data/test/manager_test.rb +110 -0
- data/test/method_inspector_test.rb +64 -0
- data/test/option_parser_test.rb +365 -0
- data/test/repo_test.rb +22 -0
- data/test/runner_test.rb +43 -0
- data/test/scientist_test.rb +291 -0
- data/test/test_helper.rb +119 -0
- 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
|
data/lib/boson/runner.rb
ADDED
@@ -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
|