entp-astrotrain 0.2.0

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 (57) hide show
  1. data/.gitignore +26 -0
  2. data/LICENSE +20 -0
  3. data/README +47 -0
  4. data/Rakefile +145 -0
  5. data/VERSION +1 -0
  6. data/astrotrain.gemspec +96 -0
  7. data/config/sample.rb +12 -0
  8. data/lib/astrotrain/api.rb +53 -0
  9. data/lib/astrotrain/logged_mail.rb +41 -0
  10. data/lib/astrotrain/mapping/http_post.rb +18 -0
  11. data/lib/astrotrain/mapping/jabber.rb +23 -0
  12. data/lib/astrotrain/mapping/transport.rb +55 -0
  13. data/lib/astrotrain/mapping.rb +157 -0
  14. data/lib/astrotrain/message.rb +313 -0
  15. data/lib/astrotrain/tmail.rb +48 -0
  16. data/lib/astrotrain.rb +55 -0
  17. data/lib/vendor/rest-client/README.rdoc +104 -0
  18. data/lib/vendor/rest-client/Rakefile +84 -0
  19. data/lib/vendor/rest-client/bin/restclient +65 -0
  20. data/lib/vendor/rest-client/foo.diff +66 -0
  21. data/lib/vendor/rest-client/lib/rest_client/net_http_ext.rb +21 -0
  22. data/lib/vendor/rest-client/lib/rest_client/payload.rb +185 -0
  23. data/lib/vendor/rest-client/lib/rest_client/request_errors.rb +75 -0
  24. data/lib/vendor/rest-client/lib/rest_client/resource.rb +103 -0
  25. data/lib/vendor/rest-client/lib/rest_client.rb +189 -0
  26. data/lib/vendor/rest-client/rest-client.gemspec +18 -0
  27. data/lib/vendor/rest-client/spec/base.rb +5 -0
  28. data/lib/vendor/rest-client/spec/master_shake.jpg +0 -0
  29. data/lib/vendor/rest-client/spec/payload_spec.rb +71 -0
  30. data/lib/vendor/rest-client/spec/request_errors_spec.rb +44 -0
  31. data/lib/vendor/rest-client/spec/resource_spec.rb +52 -0
  32. data/lib/vendor/rest-client/spec/rest_client_spec.rb +219 -0
  33. data/tasks/doc.thor +149 -0
  34. data/tasks/merb.thor +2020 -0
  35. data/test/api_test.rb +28 -0
  36. data/test/fixtures/apple_multipart.txt +100 -0
  37. data/test/fixtures/basic.txt +14 -0
  38. data/test/fixtures/custom.txt +15 -0
  39. data/test/fixtures/fwd.txt +0 -0
  40. data/test/fixtures/gb2312_encoding.txt +16 -0
  41. data/test/fixtures/gb2312_encoding_invalid.txt +15 -0
  42. data/test/fixtures/html.txt +16 -0
  43. data/test/fixtures/iso-8859-1.txt +13 -0
  44. data/test/fixtures/mapped.txt +13 -0
  45. data/test/fixtures/multipart.txt +213 -0
  46. data/test/fixtures/multipart2.txt +213 -0
  47. data/test/fixtures/multiple.txt +13 -0
  48. data/test/fixtures/multiple_delivered_to.txt +14 -0
  49. data/test/fixtures/multiple_with_body_recipients.txt +15 -0
  50. data/test/fixtures/reply.txt +16 -0
  51. data/test/fixtures/utf-8.txt +13 -0
  52. data/test/logged_mail_test.rb +63 -0
  53. data/test/mapping_test.rb +129 -0
  54. data/test/message_test.rb +424 -0
  55. data/test/test_helper.rb +54 -0
  56. data/test/transport_test.rb +111 -0
  57. metadata +115 -0
@@ -0,0 +1,84 @@
1
+ require 'rake'
2
+ require 'spec/rake/spectask'
3
+
4
+ desc "Run all specs"
5
+ Spec::Rake::SpecTask.new('spec') do |t|
6
+ t.spec_opts = ['--colour --format progress --loadby mtime --reverse']
7
+ t.spec_files = FileList['spec/*_spec.rb']
8
+ end
9
+
10
+ desc "Print specdocs"
11
+ Spec::Rake::SpecTask.new(:doc) do |t|
12
+ t.spec_opts = ["--format", "specdoc", "--dry-run"]
13
+ t.spec_files = FileList['spec/*_spec.rb']
14
+ end
15
+
16
+ desc "Run all examples with RCov"
17
+ Spec::Rake::SpecTask.new('rcov') do |t|
18
+ t.spec_files = FileList['spec/*_spec.rb']
19
+ t.rcov = true
20
+ t.rcov_opts = ['--exclude', 'examples']
21
+ end
22
+
23
+ task :default => :spec
24
+
25
+ ######################################################
26
+
27
+ require 'rake'
28
+ require 'rake/testtask'
29
+ require 'rake/clean'
30
+ require 'rake/gempackagetask'
31
+ require 'rake/rdoctask'
32
+ require 'fileutils'
33
+
34
+ version = "0.6.2"
35
+ name = "rest-client"
36
+
37
+ spec = Gem::Specification.new do |s|
38
+ s.name = name
39
+ s.version = version
40
+ s.summary = "Simple REST client for Ruby, inspired by microframework syntax for specifying actions."
41
+ s.description = "A simple REST client for Ruby, inspired by the Sinatra microframework style of specifying actions: get, put, post, delete."
42
+ s.author = "Adam Wiggins"
43
+ s.email = "adam@heroku.com"
44
+ s.homepage = "http://rest-client.heroku.com/"
45
+ s.rubyforge_project = "rest-client"
46
+
47
+ s.platform = Gem::Platform::RUBY
48
+ s.has_rdoc = true
49
+
50
+ s.files = %w(Rakefile) + Dir.glob("{lib,spec}/**/*")
51
+ s.executables = ['restclient']
52
+
53
+ s.require_path = "lib"
54
+ end
55
+
56
+ Rake::GemPackageTask.new(spec) do |p|
57
+ p.need_tar = true if RUBY_PLATFORM !~ /mswin/
58
+ end
59
+
60
+ task :install => [ :package ] do
61
+ sh %{sudo gem install pkg/#{name}-#{version}.gem}
62
+ end
63
+
64
+ task :uninstall => [ :clean ] do
65
+ sh %{sudo gem uninstall #{name}}
66
+ end
67
+
68
+ Rake::TestTask.new do |t|
69
+ t.libs << "spec"
70
+ t.test_files = FileList['spec/*_spec.rb']
71
+ t.verbose = true
72
+ end
73
+
74
+ Rake::RDocTask.new do |t|
75
+ t.rdoc_dir = 'rdoc'
76
+ t.title = "rest-client, fetch RESTful resources effortlessly"
77
+ t.options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object'
78
+ t.options << '--charset' << 'utf-8'
79
+ t.rdoc_files.include('README.rdoc')
80
+ t.rdoc_files.include('lib/*.rb')
81
+ end
82
+
83
+ CLEAN.include [ 'pkg', '*.gem', '.config' ]
84
+
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift File.dirname(__FILE__) + "/../lib"
4
+ require 'rest_client'
5
+
6
+ require "yaml"
7
+
8
+ def usage(why = nil)
9
+ puts "failed for reason: #{why}" if why
10
+ puts "usage: restclient url|name [username] [password]"
11
+ exit(1)
12
+ end
13
+
14
+ @url = ARGV.shift || 'http://localhost:4567'
15
+
16
+ config = YAML.load(File.read(ENV['HOME'] + "/.restclient")) rescue {}
17
+
18
+ @url, @username, @password = if c = config[@url]
19
+ [c['url'], c['username'], c['password']]
20
+ else
21
+ [@url, *ARGV]
22
+ end
23
+
24
+ usage("invalid url '#{@url}") unless @url =~ /^https?/
25
+ usage("to few args") unless ARGV.size < 3
26
+
27
+ def r
28
+ @r ||= RestClient::Resource.new(@url, @username, @password)
29
+ end
30
+
31
+ r # force rc to load
32
+
33
+ %w(get post put delete).each do |m|
34
+ eval <<-end_eval
35
+ def #{m}(path, *args, &b)
36
+ r[path].#{m}(*args, &b)
37
+ end
38
+ end_eval
39
+ end
40
+
41
+ def method_missing(s, *args, &b)
42
+ super unless r.respond_to?(s)
43
+ begin
44
+ r.send(s, *args, &b)
45
+ rescue RestClient::RequestFailed => e
46
+ puts e.response.body
47
+ raise e
48
+ end
49
+ end
50
+
51
+ require 'irb'
52
+ require 'irb/completion'
53
+
54
+ if File.exists? ".irbrc"
55
+ ENV['IRBRC'] = ".irbrc"
56
+ end
57
+
58
+ if File.exists?(rcfile = "~/.restclientrc")
59
+ load(rcfile)
60
+ end
61
+
62
+ ARGV.clear
63
+
64
+ IRB.start
65
+ exit!
@@ -0,0 +1,66 @@
1
+ commit 1a1a16d6abda6b40091b49d3ad082400018e665b
2
+ Author: rick <technoweenie@gmail.com>
3
+ Date: Wed Dec 10 20:01:02 2008 -0800
4
+
5
+ fix spacing issues with multipart bodies:
6
+ * the final boundary should be on a line below the final param's content.
7
+ * the ending double dash should be on the same line as the final boundary
8
+
9
+ diff --git a/lib/rest_client/payload.rb b/lib/rest_client/payload.rb
10
+ index dfc2921..e896be0 100644
11
+ --- a/lib/rest_client/payload.rb
12
+ +++ b/lib/rest_client/payload.rb
13
+ @@ -76,9 +76,10 @@ module RestClient
14
+ else
15
+ create_regular_field(@stream, k,v)
16
+ end
17
+ - @stream.write(b + EOL)
18
+ + @stream.write(EOL + b)
19
+ end
20
+ @stream.write('--')
21
+ + @stream.write(EOL)
22
+ @stream.seek(0)
23
+ end
24
+
25
+ diff --git a/spec/payload_spec.rb b/spec/payload_spec.rb
26
+ index 22b5a5a..35564ee 100644
27
+ --- a/spec/payload_spec.rb
28
+ +++ b/spec/payload_spec.rb
29
+ @@ -20,29 +20,27 @@ describe RestClient::Payload do
30
+ m.headers['Content-Type'].should == 'multipart/form-data; boundary="123"'
31
+ end
32
+
33
+ - xit "should form properly seperated multipart data" do
34
+ + it "should form properly seperated multipart data" do
35
+ m = RestClient::Payload::Multipart.new({:foo => "bar"})
36
+ - m.stub!(:boundary).and_return("123")
37
+ m.to_s.should == <<-EOS
38
+ ---123\r
39
+ +--#{m.boundary}\r
40
+ Content-Disposition: multipart/form-data; name="foo"\r
41
+ \r
42
+ bar\r
43
+ ---123--\r
44
+ +--#{m.boundary}--\r
45
+ EOS
46
+ end
47
+
48
+ - xit "should form properly seperated multipart data" do
49
+ + it "should form properly seperated multipart data" do
50
+ f = File.new(File.dirname(__FILE__) + "/master_shake.jpg")
51
+ m = RestClient::Payload::Multipart.new({:foo => f})
52
+ - m.stub!(:boundary).and_return("123")
53
+ m.to_s.should == <<-EOS
54
+ ---123\r
55
+ -Content-Disposition: multipart/form-data; name="foo"; filename="master_shake.jpg"\r
56
+ +--#{m.boundary}\r
57
+ +Content-Disposition: multipart/form-data; name="foo"; filename="./spec/master_shake.jpg"\r
58
+ Content-Type: image/jpeg\r
59
+ \r
60
+ -datadatadata\r
61
+ ---123--\r
62
+ +#{IO.read(f.path)}\r
63
+ +--#{m.boundary}--\r
64
+ EOS
65
+ end
66
+ end
@@ -0,0 +1,21 @@
1
+ #
2
+ # Replace the request method in Net::HTTP to sniff the body type
3
+ # and set the stream if appropriate
4
+ #
5
+ # Taken from:
6
+ # http://www.missiondata.com/blog/ruby/29/streaming-data-to-s3-with-ruby/
7
+
8
+ module Net
9
+ class HTTP
10
+ alias __request__ request
11
+
12
+ def request(req, body=nil, &block)
13
+ if body != nil && body.respond_to?(:read)
14
+ req.body_stream = body
15
+ return __request__(req, nil, &block)
16
+ else
17
+ return __request__(req, body, &block)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,185 @@
1
+ require "tempfile"
2
+ require "stringio"
3
+
4
+ module RestClient
5
+ module Payload
6
+ extend self
7
+
8
+ def generate(params)
9
+ if params.is_a?(String)
10
+ Base.new(params)
11
+ elsif params.delete(:multipart) == true ||
12
+ params.any? { |_,v| v.respond_to?(:path) && v.respond_to?(:read) }
13
+ Multipart.new(params)
14
+ else
15
+ UrlEncoded.new(params)
16
+ end
17
+ end
18
+
19
+ class Base
20
+ def initialize(params)
21
+ build_stream(params)
22
+ end
23
+
24
+ def build_stream(params)
25
+ @stream = StringIO.new(params)
26
+ @stream.seek(0)
27
+ end
28
+
29
+ def read(bytes=nil)
30
+ @stream.read(bytes)
31
+ end
32
+ alias :to_s :read
33
+
34
+ def escape(v)
35
+ URI.escape(v.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
36
+ end
37
+
38
+ def headers
39
+ { 'Content-Length' => size.to_s }
40
+ end
41
+
42
+ def size
43
+ @stream.size
44
+ end
45
+ alias :length :size
46
+
47
+ def close
48
+ @stream.close
49
+ end
50
+ end
51
+
52
+ class UrlEncoded < Base
53
+ def build_stream(params)
54
+ @stream = StringIO.new(params.map do |k,v|
55
+ "#{escape(k)}=#{escape(v)}"
56
+ end.join("&"))
57
+ @stream.seek(0)
58
+ end
59
+
60
+ def headers
61
+ super.merge({ 'Content-Type' => 'application/x-www-form-urlencoded' })
62
+ end
63
+ end
64
+
65
+ class Multipart < Base
66
+ EOL = "\r\n"
67
+
68
+ def build_stream(params)
69
+ b = "--#{boundary}"
70
+
71
+ @stream = Tempfile.new("RESTClient.Stream.#{rand(1000)}")
72
+ @stream.write(b)
73
+ params.each do |k,v|
74
+ @stream.write(EOL)
75
+ if v.respond_to?(:read) && v.respond_to?(:path)
76
+ create_file_field(@stream, k,v)
77
+ else
78
+ create_regular_field(@stream, k,v)
79
+ end
80
+ @stream.write(EOL + b)
81
+ end
82
+ @stream.write('--')
83
+ @stream.write(EOL)
84
+ @stream.seek(0)
85
+ end
86
+
87
+ def create_regular_field(s, k, v)
88
+ s.write("Content-Disposition: multipart/form-data; name=\"#{k}\"")
89
+ s.write(EOL)
90
+ s.write(EOL)
91
+ s.write(v)
92
+ end
93
+
94
+ def create_file_field(s, k, v)
95
+ begin
96
+ s.write("Content-Disposition: multipart/form-data; name=\"#{k}\"; filename=\"#{v.path}\"#{EOL}")
97
+ s.write("Content-Type: #{mime_for(v.path)}#{EOL}")
98
+ s.write(EOL)
99
+ while data = v.read(8124)
100
+ s.write(data)
101
+ end
102
+ ensure
103
+ v.close
104
+ end
105
+ end
106
+
107
+ def mime_for(path)
108
+ ext = File.extname(path)[1..-1]
109
+ MIME_TYPES[ext] || 'text/plain'
110
+ end
111
+
112
+ def boundary
113
+ @boundary ||= rand(1_000_000).to_s
114
+ end
115
+
116
+ def headers
117
+ super.merge({'Content-Type' => %Q{multipart/form-data; boundary="#{boundary}"}})
118
+ end
119
+
120
+ def close
121
+ @stream.close
122
+ end
123
+ end
124
+
125
+ # :stopdoc:
126
+ # From WEBrick.
127
+ MIME_TYPES = {
128
+ "ai" => "application/postscript",
129
+ "asc" => "text/plain",
130
+ "avi" => "video/x-msvideo",
131
+ "bin" => "application/octet-stream",
132
+ "bmp" => "image/bmp",
133
+ "class" => "application/octet-stream",
134
+ "cer" => "application/pkix-cert",
135
+ "crl" => "application/pkix-crl",
136
+ "crt" => "application/x-x509-ca-cert",
137
+ "css" => "text/css",
138
+ "dms" => "application/octet-stream",
139
+ "doc" => "application/msword",
140
+ "dvi" => "application/x-dvi",
141
+ "eps" => "application/postscript",
142
+ "etx" => "text/x-setext",
143
+ "exe" => "application/octet-stream",
144
+ "gif" => "image/gif",
145
+ "gz" => "application/x-gzip",
146
+ "htm" => "text/html",
147
+ "html" => "text/html",
148
+ "jpe" => "image/jpeg",
149
+ "jpeg" => "image/jpeg",
150
+ "jpg" => "image/jpeg",
151
+ "js" => "text/javascript",
152
+ "lha" => "application/octet-stream",
153
+ "lzh" => "application/octet-stream",
154
+ "mov" => "video/quicktime",
155
+ "mpe" => "video/mpeg",
156
+ "mpeg" => "video/mpeg",
157
+ "mpg" => "video/mpeg",
158
+ "pbm" => "image/x-portable-bitmap",
159
+ "pdf" => "application/pdf",
160
+ "pgm" => "image/x-portable-graymap",
161
+ "png" => "image/png",
162
+ "pnm" => "image/x-portable-anymap",
163
+ "ppm" => "image/x-portable-pixmap",
164
+ "ppt" => "application/vnd.ms-powerpoint",
165
+ "ps" => "application/postscript",
166
+ "qt" => "video/quicktime",
167
+ "ras" => "image/x-cmu-raster",
168
+ "rb" => "text/plain",
169
+ "rd" => "text/plain",
170
+ "rtf" => "application/rtf",
171
+ "sgm" => "text/sgml",
172
+ "sgml" => "text/sgml",
173
+ "tif" => "image/tiff",
174
+ "tiff" => "image/tiff",
175
+ "txt" => "text/plain",
176
+ "xbm" => "image/x-xbitmap",
177
+ "xls" => "application/vnd.ms-excel",
178
+ "xml" => "text/xml",
179
+ "xpm" => "image/x-xpixmap",
180
+ "xwd" => "image/x-xwindowdump",
181
+ "zip" => "application/zip",
182
+ }
183
+ # :startdoc:
184
+ end
185
+ end
@@ -0,0 +1,75 @@
1
+ module RestClient
2
+ # This is the base RestClient exception class. Rescue it if you want to
3
+ # catch any exception that your request might raise
4
+ class Exception < RuntimeError
5
+ def message(default=nil)
6
+ self.class::ErrorMessage
7
+ end
8
+ end
9
+
10
+ # A redirect was encountered; caught by execute to retry with the new url.
11
+ class Redirect < Exception
12
+ ErrorMessage = "Redirect"
13
+
14
+ attr_accessor :url
15
+ def initialize(url)
16
+ @url = url
17
+ end
18
+ end
19
+
20
+ # Authorization is required to access the resource specified.
21
+ class Unauthorized < Exception
22
+ ErrorMessage = 'Unauthorized'
23
+ end
24
+
25
+ # No resource was found at the given URL.
26
+ class ResourceNotFound < Exception
27
+ ErrorMessage = 'Resource not found'
28
+ end
29
+
30
+ # The server broke the connection prior to the request completing.
31
+ class ServerBrokeConnection < Exception
32
+ ErrorMessage = 'Server broke connection'
33
+ end
34
+
35
+ # The server took too long to respond.
36
+ class RequestTimeout < Exception
37
+ ErrorMessage = 'Request timed out'
38
+ end
39
+
40
+ # The request failed, meaning the remote HTTP server returned a code other
41
+ # than success, unauthorized, or redirect.
42
+ #
43
+ # The exception message attempts to extract the error from the XML, using
44
+ # format returned by Rails: <errors><error>some message</error></errors>
45
+ #
46
+ # You can get the status code by e.http_code, or see anything about the
47
+ # response via e.response. For example, the entire result body (which is
48
+ # probably an HTML error page) is e.response.body.
49
+ class RequestFailed < Exception
50
+ attr_accessor :response
51
+
52
+ def initialize(response=nil)
53
+ @response = response
54
+ end
55
+
56
+ def http_code
57
+ @response.code.to_i if @response
58
+ end
59
+
60
+ def message
61
+ "HTTP status code #{http_code}"
62
+ end
63
+
64
+ def to_s
65
+ message
66
+ end
67
+ end
68
+ end
69
+
70
+ # backwards compatibility
71
+ class RestClient::Request
72
+ Redirect = RestClient::Redirect
73
+ Unauthorized = RestClient::Unauthorized
74
+ RequestFailed = RestClient::RequestFailed
75
+ end
@@ -0,0 +1,103 @@
1
+ module RestClient
2
+ # A class that can be instantiated for access to a RESTful resource,
3
+ # including authentication.
4
+ #
5
+ # Example:
6
+ #
7
+ # resource = RestClient::Resource.new('http://some/resource')
8
+ # jpg = resource.get(:accept => 'image/jpg')
9
+ #
10
+ # With HTTP basic authentication:
11
+ #
12
+ # resource = RestClient::Resource.new('http://protected/resource', 'user', 'pass')
13
+ # resource.delete
14
+ #
15
+ # Use the [] syntax to allocate subresources:
16
+ #
17
+ # site = RestClient::Resource.new('http://example.com', 'adam', 'mypasswd')
18
+ # site['posts/1/comments'].post 'Good article.', :content_type => 'text/plain'
19
+ #
20
+ class Resource
21
+ attr_reader :url, :user, :password
22
+
23
+ def initialize(url, user=nil, password=nil)
24
+ @url = url
25
+ @user = user
26
+ @password = password
27
+ end
28
+
29
+ def get(headers={}, &b)
30
+ Request.execute(:method => :get,
31
+ :url => url,
32
+ :user => user,
33
+ :password => password,
34
+ :headers => headers, &b)
35
+ end
36
+
37
+ def post(payload, headers={}, &b)
38
+ Request.execute(:method => :post,
39
+ :url => url,
40
+ :payload => payload,
41
+ :user => user,
42
+ :password => password,
43
+ :headers => headers, &b)
44
+ end
45
+
46
+ def put(payload, headers={}, &b)
47
+ Request.execute(:method => :put,
48
+ :url => url,
49
+ :payload => payload,
50
+ :user => user,
51
+ :password => password,
52
+ :headers => headers, &b)
53
+ end
54
+
55
+ def delete(headers={}, &b)
56
+ Request.execute(:method => :delete,
57
+ :url => url,
58
+ :user => user,
59
+ :password => password,
60
+ :headers => headers, &b)
61
+ end
62
+
63
+ # Construct a subresource, preserving authentication.
64
+ #
65
+ # Example:
66
+ #
67
+ # site = RestClient::Resource.new('http://example.com', 'adam', 'mypasswd')
68
+ # site['posts/1/comments'].post 'Good article.', :content_type => 'text/plain'
69
+ #
70
+ # This is especially useful if you wish to define your site in one place and
71
+ # call it in multiple locations:
72
+ #
73
+ # def orders
74
+ # RestClient::Resource.new('http://example.com/orders', 'admin', 'mypasswd')
75
+ # end
76
+ #
77
+ # orders.get # GET http://example.com/orders
78
+ # orders['1'].get # GET http://example.com/orders/1
79
+ # orders['1/items'].delete # DELETE http://example.com/orders/1/items
80
+ #
81
+ # Nest resources as far as you want:
82
+ #
83
+ # site = RestClient::Resource.new('http://example.com')
84
+ # posts = site['posts']
85
+ # first_post = posts['1']
86
+ # comments = first_post['comments']
87
+ # comments.post 'Hello', :content_type => 'text/plain'
88
+ #
89
+ def [](suburl)
90
+ self.class.new(concat_urls(url, suburl), user, password)
91
+ end
92
+
93
+ def concat_urls(url, suburl) # :nodoc:
94
+ url = url.to_s
95
+ suburl = suburl.to_s
96
+ if url.slice(-1, 1) == '/' or suburl.slice(0, 1) == '/'
97
+ url + suburl
98
+ else
99
+ "#{url}/#{suburl}"
100
+ end
101
+ end
102
+ end
103
+ end