bwkfanboy 1.4.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. data/.gitignore +4 -0
  2. data/Gemfile +7 -0
  3. data/Gemfile.lock +51 -0
  4. data/Procfile +1 -0
  5. data/README.rdoc +40 -77
  6. data/Rakefile +13 -48
  7. data/bin/bwkfanboy +47 -166
  8. data/bin/bwkfanboy_generate +7 -19
  9. data/bin/bwkfanboy_parse +21 -17
  10. data/bwkfanboy.gemspec +40 -0
  11. data/config.ru +3 -0
  12. data/doc/NEWS.rdoc +21 -79
  13. data/doc/plugin.rdoc +63 -79
  14. data/etc/bwkfanboy.yaml +2 -0
  15. data/etc/sinatra.rb +34 -0
  16. data/lib/bwkfanboy/cliconfig.rb +141 -0
  17. data/lib/bwkfanboy/cliutils.rb +114 -0
  18. data/lib/bwkfanboy/fetch.rb +22 -24
  19. data/lib/bwkfanboy/generator.rb +78 -0
  20. data/lib/bwkfanboy/home.rb +53 -0
  21. data/lib/bwkfanboy/meta.rb +5 -2
  22. data/lib/bwkfanboy/plugin.rb +247 -0
  23. data/lib/bwkfanboy/plugin_skeleton.erb +19 -23
  24. data/lib/bwkfanboy/server.rb +73 -0
  25. data/lib/bwkfanboy/utils.rb +39 -129
  26. data/plugins/bwk.rb +25 -0
  27. data/plugins/econlib.rb +22 -0
  28. data/plugins/freebsd-ports-update.rb +73 -0
  29. data/plugins/inc.rb +29 -0
  30. data/plugins/test.rb +29 -0
  31. data/public/.gitattributes +1 -0
  32. data/public/favicon.ico +0 -0
  33. data/public/jquery-1.7.2.min.js +0 -0
  34. data/public/list.js +111 -0
  35. data/public/loading.gif +0 -0
  36. data/public/style.css +54 -0
  37. data/shotgun.rb +20 -0
  38. data/test/example/.gitattributes +1 -0
  39. data/test/example/.gitignore +1 -0
  40. data/test/example/02/plugins/bwk.html +0 -0
  41. data/test/{plugins → example/02/plugins}/empty.rb +0 -0
  42. data/test/example/02/plugins/garbage.rb +1 -0
  43. data/test/example/02/plugins/inc.html +0 -0
  44. data/test/helper.rb +30 -27
  45. data/test/helper_cliutils.rb +34 -0
  46. data/test/test_cli.rb +86 -0
  47. data/test/test_fetch.rb +49 -18
  48. data/test/test_generate.rb +43 -16
  49. data/test/test_home.rb +33 -0
  50. data/test/test_plugin.rb +141 -0
  51. data/test/test_server.rb +21 -32
  52. data/views/list.haml +38 -0
  53. metadata +223 -110
  54. data/bin/bwkfanboy_fetch +0 -13
  55. data/bin/bwkfanboy_server +0 -126
  56. data/doc/README.erb +0 -114
  57. data/doc/README.rdoc +0 -141
  58. data/doc/TODO +0 -7
  59. data/doc/bwkfanboy_fetch.rdoc +0 -4
  60. data/doc/bwkfanboy_generate.rdoc +0 -7
  61. data/doc/bwkfanboy_parse.rdoc +0 -7
  62. data/doc/bwkfanboy_server.rdoc +0 -35
  63. data/doc/rakefile.rb +0 -59
  64. data/lib/bwkfanboy/generate.rb +0 -63
  65. data/lib/bwkfanboy/parser.rb +0 -156
  66. data/lib/bwkfanboy/plugins/bwk.rb +0 -33
  67. data/lib/bwkfanboy/plugins/econlib.rb +0 -34
  68. data/lib/bwkfanboy/plugins/freebsd-ports-update.rb +0 -76
  69. data/lib/bwkfanboy/plugins/inc.rb +0 -37
  70. data/lib/bwkfanboy/schema.js +0 -39
  71. data/test/popen4.sh +0 -4
  72. data/test/rake_git.rb +0 -36
  73. data/test/semis/Rakefile +0 -35
  74. data/test/semis/bwk.html +0 -393
  75. data/test/semis/bwk.json +0 -82
  76. data/test/semis/econlib.html +0 -21
  77. data/test/semis/inc.html +0 -1067
  78. data/test/semis/links.txt +0 -4
  79. data/test/test_parse.rb +0 -27
  80. data/test/xml-clean.sh +0 -8
  81. data/web/bwkfanboy.cgi +0 -36
@@ -1,6 +1,9 @@
1
1
  module Bwkfanboy
2
- module Meta
2
+ module Meta # :nodoc:
3
3
  NAME = 'bwkfanboy'
4
- VERSION = '1.4.1'
4
+ VERSION = '2.0.0'
5
+ AUTHOR = 'Alexander Gromnitsky'
6
+ EMAIL = 'alexander.gromnitsky@gmail.com'
7
+ HOMEPAGE = 'http://github.com/gromnitsky/' + NAME
5
8
  end
6
9
  end
@@ -0,0 +1,247 @@
1
+ require 'msgpack'
2
+ require 'nokogiri'
3
+
4
+ module Bwkfanboy
5
+
6
+ # Helpers for plugin authors.
7
+ module BH
8
+ extend self
9
+
10
+ # FIXME: clean unsafe html for 'html' content_type
11
+ def clean t
12
+ return '' unless t
13
+ t
14
+ end
15
+
16
+ # Tries to parse _s_ string as a date.
17
+ # Return the result in ISO 8601 format.
18
+ def date(t)
19
+ DateTime.parse(BH.clean(t)).iso8601
20
+ rescue
21
+ DateTime.now.iso8601
22
+ end
23
+
24
+ # See test_plugin.rb
25
+ def all_set? t
26
+ return false unless t
27
+
28
+ if t.is_a?(Array)
29
+ return false if t.size == 0
30
+
31
+ t.each {|i|
32
+ return false unless i
33
+ return false if i.to_s.strip.size == 0
34
+ }
35
+ end
36
+
37
+ return false if t.to_s.strip.size == 0
38
+ true
39
+ end
40
+
41
+ end
42
+
43
+ class PluginException < StandardError
44
+ def initialize msg
45
+ super msg
46
+ end
47
+
48
+ alias :orig_to_s :to_s
49
+ # looks clumsy
50
+ def to_s
51
+ "plugin: #{orig_to_s}"
52
+ end
53
+ end
54
+
55
+ class PluginInvalidName < PluginException
56
+ end
57
+
58
+ class PluginNotFound < PluginException
59
+ end
60
+
61
+ class PluginNoOptions < PluginException
62
+ end
63
+
64
+ # Requires defined 'parse(streams)' method in plugin.
65
+ #
66
+ # Raises only PluginException on purpose.
67
+ class Plugin
68
+ include Enumerable
69
+
70
+ MAX_ENTRIES = 128
71
+ NAME_RE = /^[a-zA-Z0-9-]+$/
72
+
73
+ # [path] an array
74
+ # [name] plugin's name (without .rb extension)
75
+ # [opt] an array
76
+ # [&block] you can examine the Plugin object there
77
+ def initialize path, name, opt, &block
78
+ @path = path
79
+ raise PluginInvalidName, "name doesn't match #{NAME_RE}" unless validName?(name)
80
+ @name = name
81
+ @origin = nil # a path where plugin was found
82
+ @syslib = File.dirname __FILE__
83
+
84
+ # Variables for plugin authours
85
+ @opt = (opt && opt.map(&:to_s)) || []
86
+ @uri = []
87
+ @enc = 'UTF-8'
88
+ @version = 1
89
+ @copyright = ''
90
+ @title = ''
91
+ @content_type = ''
92
+
93
+ @data = []
94
+ load &block
95
+ end
96
+
97
+ attr_accessor :origin
98
+ attr_accessor :uri, :enc, :version, :copyright, :title, :content_type
99
+
100
+ def validName? name
101
+ name =~ NAME_RE
102
+ end
103
+
104
+ def each &b
105
+ @data.each &b
106
+ end
107
+
108
+ def << obj
109
+ return @data if full?
110
+
111
+ ['title', 'link', 'updated', 'author', 'content'].each {|idx|
112
+ obj[idx] &&= BH.clean obj[idx]
113
+ raise PluginException, "empty '#{idx}' in the entry #{obj.inspect}" if obj[idx].size == 0
114
+ }
115
+
116
+ @data << obj
117
+ end
118
+
119
+ def full?
120
+ @data.size >= MAX_ENTRIES
121
+ end
122
+
123
+ def [] index
124
+ @data[index]
125
+ end
126
+
127
+ def size
128
+ @data.size
129
+ end
130
+
131
+ def pack stream = ''
132
+ # hopefully, urf8 will survive
133
+ MessagePack.pack export, stream
134
+ end
135
+
136
+ def export
137
+ {
138
+ 'channel' => {
139
+ 'updated' => entryMostRecent,
140
+ 'id' => @uri.to_s,
141
+ 'author' => @copyright,
142
+ 'title' => @title,
143
+ 'link' => @uri.first,
144
+ 'x_entries_content_type' => @content_type,
145
+ },
146
+ 'x_entries' => @data
147
+ }
148
+ end
149
+
150
+ # We can do this while adding a new entry, not here
151
+ def entryMostRecent
152
+ return nil if @data.size == 0
153
+
154
+ max = DateTime.parse @data.sample['updated']
155
+ @data.each {|idx|
156
+ cur = DateTime.parse idx['updated']
157
+ max = cur if max < cur
158
+ }
159
+
160
+ return max.iso8601
161
+ end
162
+
163
+ def load
164
+ raise PluginException, 'invalid search path' unless @path && @path.respond_to?(:each)
165
+
166
+ p = nil
167
+ @path.each {|idx|
168
+ contents = Dir.glob "#{idx}/*.rb"
169
+ pos = contents.index "#{idx}/#{@name}.rb"
170
+ if pos && p = contents[pos]
171
+ @origin = idx
172
+ break
173
+ end
174
+ }
175
+
176
+ raise PluginNotFound, "'#{@name}' not found" unless p
177
+
178
+ begin
179
+ instance_eval File.read(p)
180
+ rescue Exception
181
+ raise PluginException, "'#{@name}' failed to parse: #{$!}"
182
+ end
183
+
184
+ unless BH.all_set?(uri)
185
+ raise PluginException, 'uri must be an array of strings' if @opt.size != 0
186
+ raise PluginNoOptions, 'don\'t we forget about additional options?'
187
+ end
188
+ raise PluginException, 'enc is unset' unless BH.all_set?(enc)
189
+ raise PluginException, 'version must be an integer' unless BH.all_set?(version)
190
+ raise PluginException, 'copyright is unset' unless BH.all_set?(copyright)
191
+ raise PluginException, 'title is unset' unless BH.all_set?(title)
192
+ raise PluginException, 'content_type is unset' unless BH.all_set?(content_type)
193
+
194
+ # use this, for example, to print a message to user that loading
195
+ # was fine
196
+ yield self if block_given?
197
+ end
198
+
199
+ # Runs loaded plugin's parser
200
+ def run_parser streams
201
+ ok = streams ? true : false
202
+ streams.each {|i| ok = false unless i.respond_to?(:eof) } if streams
203
+ raise PluginException, 'parser expects a valid array of IO objects' unless ok
204
+
205
+ begin
206
+ parse streams
207
+ rescue Exception
208
+ raise PluginException, "'#{@name}' failed to parse: #{$!}"
209
+ end
210
+
211
+ check
212
+ end
213
+
214
+ def check
215
+ raise PluginException, "it ain't grab anything" if @data.size == 0
216
+ end
217
+
218
+ end
219
+
220
+ module PluginInfo
221
+ extend self
222
+
223
+ def about path, name, opt
224
+ p = Plugin.new path, name, opt
225
+ r = {}
226
+ ['title', 'version', 'copyright', 'uri'].each {|idx|
227
+ r[idx] = p.send(idx)
228
+ }
229
+ r
230
+ end
231
+
232
+ def getList path
233
+ r = []
234
+ path.each {|idx|
235
+ dir = idx.to_s
236
+ e = { dir => [] }
237
+ Dir.glob("#{dir}/*.rb").each {|file|
238
+ e[dir] << File.basename(file, '.rb')
239
+ }
240
+ r << e
241
+ }
242
+
243
+ r
244
+ end
245
+
246
+ end
247
+ end
@@ -1,30 +1,26 @@
1
- # This is a skeleton for a <%= Bwkfanboy::Meta::NAME %> <%= Bwkfanboy::Meta::VERSION %> plugin. To understand how
2
- # plugins work please read doc/plugins.rdoc file from <%= Bwkfanboy::Meta::NAME %>'s
1
+ # This is a skeleton for a <%= Meta::NAME %> <%= Meta::VERSION %> plugin. To understand how
2
+ # plugins work please read doc/plugins.rdoc file from <%= Meta::NAME %>'s
3
3
  # distribution.
4
4
 
5
- require 'nokogiri'
6
-
7
- class Page < Bwkfanboy::Parse
8
- module Meta
9
- URI = 'http://example.org/news'
10
- ENC = 'UTF-8'
11
- VERSION = 1
12
- COPYRIGHT = '(c) <%= DateTime.now.year %> <%= Etc.getpwuid(Process.euid)[:gecos] %>'
13
- TITLE = "News from example.org"
14
- CONTENT_TYPE = 'html'
15
- end
5
+ @uri << 'http://example.org/news'
6
+ @enc = 'UTF-8'
7
+ @version = 1
8
+ @copyright = '(c) <%= DateTime.now.year %> <%= Etc.getpwuid(Process.euid)[:gecos] %>'
9
+ @title = "News from example.org"
10
+ @content_type = 'html'
16
11
 
17
- def myparse(stream)
18
- # read 'stream' IO object and parse it
19
- doc = Nokogiri::HTML(stream, nil, Meta::ENC)
20
- doc.xpath("XPATH QUERY").each {|i|
21
- t = clean(i.xpath("XPATH QUERY").text())
22
- l = clean(i.xpath("XPATH QUERY").text())
23
- u = date(i.xpath("XPATH QUERY").text())
24
- a = clean(i.xpath("XPATH QUERY").text())
25
- c = clean(i.xpath("XPATH QUERY").text())
12
+ def parse streams
13
+ streams.each do |io|
14
+ doc = Nokogiri::HTML io, nil, @enc
15
+ doc.xpath("XPATH QUERY").each {|idx|
16
+ t = idx.xpath("XPATH QUERY").text
17
+ l = idx.xpath("XPATH QUERY").text
18
+ u = BH.date idx.xpath("XPATH QUERY").text
19
+ a = idx.xpath("XPATH QUERY").text
20
+ c = idx.xpath("XPATH QUERY").text
26
21
 
27
- self << { title: t, link: l, updated: u, author: a, content: c }
22
+ self << { 'title' => t, 'link' => l, 'updated' => u,
23
+ 'author' => a, 'content' => c }
28
24
  }
29
25
  end
30
26
  end
@@ -0,0 +1,73 @@
1
+ require 'logger'
2
+ require 'haml'
3
+ require 'sinatra/base'
4
+ require 'json'
5
+
6
+ require_relative 'home'
7
+ require_relative 'utils'
8
+ require_relative '../../etc/sinatra'
9
+
10
+ module Bwkfanboy
11
+ class MyApp < Sinatra::Base
12
+ MySinatraConfig.read self
13
+
14
+ set :home, Home.new
15
+ set :public_folder, CliUtils::DIR_LIB_SRC.parent.parent + 'public'
16
+ set :views, CliUtils::DIR_LIB_SRC.parent.parent + 'views'
17
+
18
+ use Rack::Deflater
19
+
20
+ def getOpts opts
21
+ return [] unless opts
22
+ opts.gsub! /\s+/, ' '
23
+ opts.strip.split ' '
24
+ end
25
+
26
+ # List all plugins
27
+ get '/' do
28
+ list = PluginInfo.getList settings.home.conf[:plugins_path]
29
+ haml :list, locals: {
30
+ meta: Meta,
31
+ list: list
32
+ }
33
+ end
34
+
35
+ get %r{/info/([a-zA-Z0-9_-]+)} do |plugin|
36
+ cache_control :no_cache
37
+ opts = getOpts params['o']
38
+ begin
39
+ PluginInfo.about(settings.home.conf[:plugins_path], plugin, opts).to_json
40
+ rescue PluginInvalidName, PluginNoOptions
41
+ halt 400, $!.to_s
42
+ rescue PluginNotFound
43
+ halt 404, $!.to_s
44
+ rescue PluginException
45
+ halt 500, $!.to_s
46
+ end
47
+ end
48
+
49
+ get %r{/([a-zA-Z0-9_-]+)} do |plugin|
50
+ begin
51
+ opts = getOpts params['o']
52
+ r = Utils.atom(settings.home.conf[:plugins_path], plugin, opts).to_s
53
+
54
+ # Search for <updated> tag and set Last-Modified header
55
+ if (m = r.match('<updated>(.+?)</updated>'))
56
+ headers 'Last-Modified' => DateTime.parse(m.to_s).httpdate
57
+ end
58
+ content_type 'application/atom+xml; charset=UTF-8'
59
+ headers 'Content-Disposition' => "inline; filename=\"#{Meta::NAME}-#{plugin}.xml"
60
+
61
+ r
62
+ rescue PluginInvalidName, PluginNoOptions
63
+ halt 400, $!.to_s
64
+ rescue PluginNotFound
65
+ halt 404, $!.to_s
66
+ rescue FetchException, PluginException, GeneratorException
67
+ halt 500, $!.to_s
68
+ end
69
+ end
70
+
71
+ run! if app_file == $0
72
+ end
73
+ end
@@ -1,148 +1,58 @@
1
- require 'optparse'
2
- require 'logger'
1
+ require 'erb'
2
+ require 'digest/md5'
3
+ require 'etc'
3
4
 
4
- require 'open4'
5
- require 'active_support/core_ext/module/attribute_accessors'
6
-
7
- require_relative 'meta'
5
+ require_relative 'cliutils'
6
+ require_relative 'fetch'
7
+ require_relative 'plugin'
8
+ require_relative 'generator'
8
9
 
9
10
  module Bwkfanboy
10
- module Meta
11
- USER_AGENT = "#{NAME}/#{VERSION} (#{RUBY_PLATFORM}; N; #{Encoding.default_external.name}; #{RUBY_ENGINE}; rv:#{RUBY_VERSION}.#{RUBY_PATCHLEVEL})"
12
- PLUGIN_CLASS = 'Page'
13
- DIR_TMP = "/tmp/#{Meta::NAME}/#{ENV['USER']}"
14
- DIR_LOG = "#{DIR_TMP}/log"
15
- LOG_MAXSIZE = 64*1024
16
- PLUGIN_NAME = /^[ a-zA-Z0-9_-]+$/
17
- PLUGIN_OPTS = /^[ a-zA-Z'"0-9_-]+$/
18
- end
19
-
20
11
  module Utils
21
- mattr_accessor :cfg, :log
22
-
23
- self.cfg = Hash.new()
24
- cfg[:verbose] = 0
25
- cfg[:log] = "#{Meta::DIR_LOG}/general.log"
26
-
27
- def self.warnx(t)
28
- m = File.basename($0) +" warning: "+ t + "\n";
29
- $stderr.print(m);
30
- log.warn(m.chomp) if log
31
- end
32
-
33
- def self.errx(ec, t)
34
- m = File.basename($0) +" error: "+ t + "\n"
35
- $stderr.print(m);
36
- log.error(m.chomp) if log
37
- exit(ec)
38
- end
12
+ extend self
39
13
 
40
- def self.veputs(level, t)
41
- if cfg[:verbose] >= level then
42
- # p log
43
- log.info(t.chomp) if log
44
- print(t)
14
+ # [template] a full path to a .erb file
15
+ # [desiredName] a future skeleton name
16
+ def skeletonCreate template, desiredName
17
+ t = ERB.new File.read template
18
+ t.filename = template # to report errors relative to this file
19
+ begin
20
+ md5_system = Digest::MD5.hexdigest t.result(binding)
21
+ rescue Exception
22
+ CliUtils.errx EX_SOFTWARE, "cannot read the template: #{$!}"
45
23
  end
46
- end
47
-
48
- def self.vewarnx(level, t)
49
- warnx(t) if cfg[:verbose] >= level
50
- end
51
24
 
52
- # Logs and pidfiles the other temporal stuff sits here
53
- def self.dir_tmp_create()
54
- if ! File.writable?(Meta::DIR_TMP) then
25
+ if ! File.exists?(desiredName)
26
+ # create a new skeleton
55
27
  begin
56
- t = '/'
57
- Meta::DIR_TMP.split('/')[1..-1].each {|i|
58
- t += i + '/'
59
- Dir.mkdir(t) if ! Dir.exists?(t)
60
- }
28
+ File.open(desiredName, 'w+') { |fp| fp.puts t.result(binding) }
61
29
  rescue
62
- warnx("cannot create/open directory #{Meta::DIR_TMP} for writing")
30
+ CliUtils.errx EX_IOERR, "cannot write the skeleton: #{$!}"
31
+ end
32
+ else
33
+ # warn a careless user
34
+ if md5_system != Digest::MD5.file(desiredName).hexdigest
35
+ CliUtils.warnx "#{desiredName} already exists"
36
+ return false
63
37
  end
64
38
  end
65
- end
66
-
67
- def self.log_start()
68
- dir_tmp_create()
69
- begin
70
- Dir.mkdir(Meta::DIR_LOG) if ! File.writable?(Meta::DIR_LOG)
71
- log = Logger.new(cfg[:log], 2, Meta::LOG_MAXSIZE)
72
- rescue
73
- warnx("cannot open log #{cfg[:log]}");
74
- return nil
75
- end
76
- log.level = Logger::DEBUG
77
- log.datetime_format = "%H:%M:%S"
78
- log.info("#{$0} starting")
79
- log
80
- end
81
- self.log = log_start()
82
-
83
- # Loads (via <tt>require()</tt>) a Ruby code from _path_ (the full path to
84
- # the file). <em>class_name</em> is the name of the class to check
85
- # for existence after successful plugin loading.
86
- def self.plugin_load(path, class_name)
87
- begin
88
- require(path)
89
- # TODO get rid of eval()
90
- fail "class #{class_name} isn't defined" if (! eval("defined?#{class_name}") || ! eval(class_name).is_a?(Class) )
91
- rescue LoadError
92
- errx(1, "cannot load plugin '#{path}' #{$!}");
93
- rescue Exception
94
- errx(1, "plugin '#{path}' has errors: #{$!}\n\nBacktrace:\n\n#{$!.backtrace.join("\n")}")
95
- end
96
- end
97
39
 
98
- # Get possible options for the parser.
99
- def self.plugin_opts(a)
100
- opt = a.size >= 2 ? a[1..-1] : ''
40
+ true
101
41
  end
102
42
 
43
+ # Do all the work of reading the plugin, parsing and generating the
44
+ # atom feed.
45
+ # FIXME: ensure all streams are closed
46
+ def atom pluginsPath, name, opt
47
+ p = Plugin.new pluginsPath, name, opt
48
+ streams = Fetch.openStreams p.uri
49
+ p.run_parser(streams)
103
50
 
104
- # Parses command line options. _arr_ is an array of options (usually
105
- # +ARGV+). _banner_ is a help string that describes what your
106
- # program does.
107
- #
108
- # If _o_ is non nil function parses _arr_ immediately, otherwise it
109
- # only creates +OptionParser+ object and return it (if _simple_ is
110
- # false). See <tt>bwkfanboy</tt> script for examples.
111
- def self.cl_parse(arr, banner, o = nil, simple = false)
112
- if ! o then
113
- o = OptionParser.new
114
- o.banner = banner
115
- o.on('-v', 'Be more verbose.') { |i| Bwkfanboy::Utils.cfg[:verbose] += 1 }
116
- o.on('-V', 'Show version & exit.') { |i|
117
- puts Bwkfanboy::Meta::VERSION
118
- exit 0
119
- }
120
- return o if ! simple
121
- end
51
+ r = Generator.atom p.export
52
+ Fetch.closeStreams streams
122
53
 
123
- begin
124
- o.parse!(arr)
125
- rescue
126
- Bwkfanboy::Utils.errx(1, $!.to_s)
127
- end
128
- end
129
-
130
- # used in CGI and WEBrick examples
131
- def self.cmd_run(cmd)
132
- so = sr = ''
133
- status = Open4::popen4(cmd) { |pid, stdin, stdout, stderr|
134
- so = stdout.read
135
- sr = stderr.read
136
- }
137
- [status.exitstatus, sr, so]
138
- end
139
-
140
- def self.gem_dir_system
141
- t = ["#{File.dirname(File.expand_path($0))}/../lib/#{Meta::NAME}",
142
- "#{Gem.dir}/gems/#{Meta::NAME}-#{Meta::VERSION}/lib/#{Meta::NAME}"]
143
- t.each {|i| return i if File.readable?(i) }
144
- raise "both paths are invalid: #{t}"
54
+ r
145
55
  end
146
56
 
147
- end # utils
57
+ end
148
58
  end