bitly4r 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +3 -0
- data/LICENSE +22 -0
- data/README.rdoc +67 -0
- data/Rakefile +162 -0
- data/lib/bitly4r.rb +20 -0
- data/lib/bitly4r/client.rb +211 -0
- data/lib/bitly4r/definitions.rb +34 -0
- data/lib/bitly4r/objects.rb +123 -0
- data/test/client_test.rb +130 -0
- data/test/definitions_test.rb +50 -0
- data/test/objects_test.rb +101 -0
- data/test/test_helper.rb +41 -0
- metadata +70 -0
data/CHANGELOG
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2009 Dan Foley / CantRemember.com
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person
|
4
|
+
obtaining a copy of this software and associated documentation
|
5
|
+
files (the "Software"), to deal in the Software without
|
6
|
+
restriction, including without limitation the rights to use,
|
7
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
copies of the Software, and to permit persons to whom the
|
9
|
+
Software is furnished to do so, subject to the following
|
10
|
+
conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be
|
13
|
+
included in all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
17
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
19
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
20
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
21
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
= Bitly4R
|
2
|
+
|
3
|
+
A Ruby API for the http://bit.ly URL-shortening service
|
4
|
+
|
5
|
+
== SimpleClient
|
6
|
+
|
7
|
+
This example uses a simpler construction and invocation technique.
|
8
|
+
For when you just need the URLs, and not any additional data.
|
9
|
+
|
10
|
+
require 'rubygems'
|
11
|
+
require 'bitly4r'
|
12
|
+
|
13
|
+
login, api_key = 'bitlyapidemo', 'R_0da49e0a9118ff35f52f629d2d71bf07'
|
14
|
+
long_url = 'http://rubyforge.org/'
|
15
|
+
|
16
|
+
client = Bitly4R.Keyed(login, api_key)
|
17
|
+
|
18
|
+
# shorten
|
19
|
+
short_url = client.shorten(long_url)
|
20
|
+
|
21
|
+
# re-expand
|
22
|
+
raise 'what the?' unless (long_url == client.expand(short_url))
|
23
|
+
|
24
|
+
== Client
|
25
|
+
|
26
|
+
This example uses a full-fledged construction and invocation technique.
|
27
|
+
The URLs are returned by the <tt>to_s</tt> methods of the Response objects, and additional data is made available.
|
28
|
+
|
29
|
+
require 'rubygems'
|
30
|
+
require 'bitly4r'
|
31
|
+
|
32
|
+
login, api_key = 'bitlyapidemo', 'R_0da49e0a9118ff35f52f629d2d71bf07'
|
33
|
+
long_url = 'http://rubyforge.org/'
|
34
|
+
|
35
|
+
client = Bitly4R::Client.new(:login => login, :api_key => api_key)
|
36
|
+
|
37
|
+
# shorten
|
38
|
+
short_url = client.shorten(long_url).to_s
|
39
|
+
|
40
|
+
# re-expand
|
41
|
+
raise 'aww, cmon!' unless (long_url == client.expand(short_url).to_s)
|
42
|
+
|
43
|
+
== Support
|
44
|
+
|
45
|
+
This gem supports the following API commands:
|
46
|
+
|
47
|
+
* shorten
|
48
|
+
* expand
|
49
|
+
* info
|
50
|
+
* stats
|
51
|
+
* errors
|
52
|
+
|
53
|
+
For more information, see the API documentation:
|
54
|
+
|
55
|
+
* http://code.google.com/p/bitly-api/wiki/ApiDocumentation
|
56
|
+
|
57
|
+
== Contributing
|
58
|
+
|
59
|
+
=== Issue Tracking and Feature Requests
|
60
|
+
|
61
|
+
* http://bitly4r.rubyforge.org
|
62
|
+
|
63
|
+
== Community
|
64
|
+
|
65
|
+
=== Wiki
|
66
|
+
|
67
|
+
* http://wiki.cantremember.com/Bitly4R
|
data/Rakefile
ADDED
@@ -0,0 +1,162 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake/clean'
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
task :default => :test
|
6
|
+
|
7
|
+
# SPECS ===============================================================
|
8
|
+
|
9
|
+
desc 'Run specs with story style output'
|
10
|
+
task :spec do
|
11
|
+
sh 'specrb --specdox -Ilib:test test/*_test.rb'
|
12
|
+
end
|
13
|
+
|
14
|
+
desc 'Run specs with unit test style output'
|
15
|
+
task :test => FileList['test/*_test.rb'] do |t|
|
16
|
+
suite = t.prerequisites.map{|f| "-r#{f.chomp('.rb')}"}.join(' ')
|
17
|
+
sh "ruby -Ilib:test #{suite} -e ''", :verbose => false
|
18
|
+
end
|
19
|
+
|
20
|
+
# PACKAGING ============================================================
|
21
|
+
|
22
|
+
# Load the gemspec using the same limitations as github
|
23
|
+
def spec
|
24
|
+
@spec ||=
|
25
|
+
begin
|
26
|
+
require 'rubygems/specification'
|
27
|
+
data = File.read('sinatra.gemspec')
|
28
|
+
spec = nil
|
29
|
+
Thread.new { spec = eval("$SAFE = 3\n#{data}") }.join
|
30
|
+
spec
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def package(ext='')
|
35
|
+
"dist/sinatra-#{spec.version}" + ext
|
36
|
+
end
|
37
|
+
|
38
|
+
desc 'Build packages'
|
39
|
+
task :package => %w[.gem .tar.gz].map {|e| package(e)}
|
40
|
+
|
41
|
+
desc 'Build and install as local gem'
|
42
|
+
task :install => package('.gem') do
|
43
|
+
sh "gem install #{package('.gem')}"
|
44
|
+
end
|
45
|
+
|
46
|
+
directory 'dist/'
|
47
|
+
|
48
|
+
file package('.gem') => %w[dist/ sinatra.gemspec] + spec.files do |f|
|
49
|
+
sh "gem build sinatra.gemspec"
|
50
|
+
mv File.basename(f.name), f.name
|
51
|
+
end
|
52
|
+
|
53
|
+
file package('.tar.gz') => %w[dist/] + spec.files do |f|
|
54
|
+
sh "git archive --format=tar HEAD | gzip > #{f.name}"
|
55
|
+
end
|
56
|
+
|
57
|
+
# Rubyforge Release / Publish Tasks ==================================
|
58
|
+
|
59
|
+
desc 'Publish website to rubyforge'
|
60
|
+
task 'publish:doc' => 'doc/api/index.html' do
|
61
|
+
sh 'scp -rp doc/* rubyforge.org:/var/www/gforge-projects/sinatra/'
|
62
|
+
end
|
63
|
+
|
64
|
+
task 'publish:gem' => [package('.gem'), package('.tar.gz')] do |t|
|
65
|
+
sh <<-end
|
66
|
+
rubyforge add_release sinatra sinatra #{spec.version} #{package('.gem')} &&
|
67
|
+
rubyforge add_file sinatra sinatra #{spec.version} #{package('.tar.gz')}
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Website ============================================================
|
72
|
+
# Building docs requires HAML and the hanna gem:
|
73
|
+
# gem install mislav-hanna --source=http://gems.github.com
|
74
|
+
|
75
|
+
task 'doc' => ['doc:api','doc:site']
|
76
|
+
|
77
|
+
desc 'Generate Hanna RDoc under doc/api'
|
78
|
+
task 'doc:api' => ['doc/api/index.html']
|
79
|
+
|
80
|
+
file 'doc/api/index.html' => FileList['lib/**/*.rb','README.rdoc'] do |f|
|
81
|
+
rb_files = f.prerequisites
|
82
|
+
sh((<<-end).gsub(/\s+/, ' '))
|
83
|
+
hanna --charset utf8 \
|
84
|
+
--fmt html \
|
85
|
+
--inline-source \
|
86
|
+
--line-numbers \
|
87
|
+
--main README.rdoc \
|
88
|
+
--op doc/api \
|
89
|
+
--title 'Sinatra API Documentation' \
|
90
|
+
#{rb_files.join(' ')}
|
91
|
+
end
|
92
|
+
end
|
93
|
+
CLEAN.include 'doc/api'
|
94
|
+
|
95
|
+
def rdoc_to_html(file_name)
|
96
|
+
require 'rdoc/markup/to_html'
|
97
|
+
rdoc = RDoc::Markup::ToHtml.new
|
98
|
+
rdoc.convert(File.read(file_name))
|
99
|
+
end
|
100
|
+
|
101
|
+
def haml(locals={})
|
102
|
+
require 'haml'
|
103
|
+
template = File.read('doc/template.haml')
|
104
|
+
haml = Haml::Engine.new(template, :format => :html4, :attr_wrapper => '"')
|
105
|
+
haml.render(Object.new, locals)
|
106
|
+
end
|
107
|
+
|
108
|
+
desc 'Build website HTML and stuff'
|
109
|
+
task 'doc:site' => ['doc/index.html', 'doc/book.html']
|
110
|
+
|
111
|
+
file 'doc/index.html' => %w[README.rdoc doc/template.haml] do |file|
|
112
|
+
File.open(file.name, 'w') do |file|
|
113
|
+
file << haml(:title => 'Sinatra', :content => rdoc_to_html('README.rdoc'))
|
114
|
+
end
|
115
|
+
end
|
116
|
+
CLEAN.include 'doc/index.html'
|
117
|
+
|
118
|
+
file 'doc/book.html' => ['book/output/sinatra-book.html'] do |file|
|
119
|
+
File.open(file.name, 'w') do |file|
|
120
|
+
book_content = File.read('book/output/sinatra-book.html')
|
121
|
+
file << haml(:title => 'Sinatra Book', :content => book_content)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
CLEAN.include 'doc/book.html'
|
125
|
+
|
126
|
+
file 'book/output/sinatra-book.html' => FileList['book/**'] do |f|
|
127
|
+
unless File.directory?('book')
|
128
|
+
sh 'git clone git://github.com/cschneid/sinatra-book.git book'
|
129
|
+
end
|
130
|
+
sh((<<-SH).strip.gsub(/\s+/, ' '))
|
131
|
+
cd book &&
|
132
|
+
git fetch origin &&
|
133
|
+
git rebase origin/master &&
|
134
|
+
thor book:build
|
135
|
+
SH
|
136
|
+
end
|
137
|
+
CLEAN.include 'book/output/sinatra-book.html'
|
138
|
+
|
139
|
+
desc 'Build the Sinatra book'
|
140
|
+
task 'doc:book' => ['book/output/sinatra-book.html']
|
141
|
+
|
142
|
+
# Gemspec Helpers ====================================================
|
143
|
+
|
144
|
+
file 'sinatra.gemspec' => FileList['{lib,test,images}/**','Rakefile'] do |f|
|
145
|
+
# read spec file and split out manifest section
|
146
|
+
spec = File.read(f.name)
|
147
|
+
parts = spec.split(" # = MANIFEST =\n")
|
148
|
+
fail 'bad spec' if parts.length != 3
|
149
|
+
# determine file list from git ls-files
|
150
|
+
files = `git ls-files`.
|
151
|
+
split("\n").
|
152
|
+
sort.
|
153
|
+
reject{ |file| file =~ /^\./ }.
|
154
|
+
reject { |file| file =~ /^doc/ }.
|
155
|
+
map{ |file| " #{file}" }.
|
156
|
+
join("\n")
|
157
|
+
# piece file back together and write...
|
158
|
+
parts[1] = " s.files = %w[\n#{files}\n ]\n"
|
159
|
+
spec = parts.join(" # = MANIFEST =\n")
|
160
|
+
File.open(f.name, 'w') { |io| io.write(spec) }
|
161
|
+
puts "updated #{f.name}"
|
162
|
+
end
|
data/lib/bitly4r.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
#Requires all the individual Bitly4R components, in the proper sequence.
|
2
|
+
#That makes the use of this gem as easy as:
|
3
|
+
#
|
4
|
+
# require 'bitly4r'
|
5
|
+
#
|
6
|
+
#See:
|
7
|
+
#* Bitly4R::Client
|
8
|
+
#--
|
9
|
+
|
10
|
+
# external
|
11
|
+
%w{ net/http cgi }.each {|lib| require lib }
|
12
|
+
|
13
|
+
# internal, and in the proper sequence
|
14
|
+
%w{
|
15
|
+
bitly4r/objects
|
16
|
+
bitly4r/client
|
17
|
+
bitly4r/definitions
|
18
|
+
}.each do |file|
|
19
|
+
require File.expand_path(File.join(File.dirname(__FILE__), file))
|
20
|
+
end
|
@@ -0,0 +1,211 @@
|
|
1
|
+
#Client objects for Bitly4R.
|
2
|
+
#
|
3
|
+
#See:
|
4
|
+
#* Bitly4R::Client
|
5
|
+
#* Bitly4R::SimpleClient
|
6
|
+
|
7
|
+
module Bitly4R
|
8
|
+
#= Client
|
9
|
+
#
|
10
|
+
#A client object for accessing the bit.ly API.
|
11
|
+
#
|
12
|
+
#* Supports both API key and HTTP Auth credentials (although HTTP Auth / password-based doesn't seem to work)
|
13
|
+
#* Works with version 2.0.1 of the API
|
14
|
+
#* Uses XML for marshalling, for cheap & easy text parsing.
|
15
|
+
#
|
16
|
+
#See the API documentation:
|
17
|
+
#* http://code.google.com/p/bitly-api/wiki/ApiDocumentation
|
18
|
+
#* http://bit.ly
|
19
|
+
class Client
|
20
|
+
#:nodoc:
|
21
|
+
BASE_PARAMS = Params.new
|
22
|
+
BASE_PARAMS[:version] = '2.0.1'
|
23
|
+
BASE_PARAMS[:format] = 'xml'
|
24
|
+
|
25
|
+
#The login credential provided at construction.
|
26
|
+
attr_reader :login
|
27
|
+
#The password credential provided at construction.
|
28
|
+
attr_reader :password
|
29
|
+
#The API key credential provided at construction.
|
30
|
+
attr_reader :api_key
|
31
|
+
|
32
|
+
#Constructs a new client.
|
33
|
+
#
|
34
|
+
#Any tuples provided in the optional Hash are injected into instance variables.
|
35
|
+
#
|
36
|
+
#You must provide a login, and either an API key (<tt>api_key</tt>) or a password.
|
37
|
+
def initialize(ops={})
|
38
|
+
# for the readers
|
39
|
+
# not necessary, but polite
|
40
|
+
ops.each do |k, v|
|
41
|
+
instance_variable_set "@#{k}".to_sym, v
|
42
|
+
end
|
43
|
+
|
44
|
+
raise Error.new("you must provide a login") unless self.login
|
45
|
+
raise Error.new("you must provide either an API key or a password") unless (self.api_key or self.password)
|
46
|
+
|
47
|
+
# now, a client-izec set of parameters
|
48
|
+
@client_params = BASE_PARAMS.clone.merge(ops)
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
#Invokes the API's <b>shorten</b> method.
|
53
|
+
#That's pretty much what makes bit.ly a valuable service.
|
54
|
+
#
|
55
|
+
#A Response is returned.
|
56
|
+
#Response.to_s will return the <tt>shortUrl</tt> / <tt>short_url</tt> value.
|
57
|
+
#
|
58
|
+
#You can take the shortened URL and re- expand it.
|
59
|
+
def shorten(long_url)
|
60
|
+
return nil unless long_url
|
61
|
+
assert_codes(execute(:shorten, :short_url) do |params|
|
62
|
+
params[:longUrl] = long_url
|
63
|
+
end)
|
64
|
+
end
|
65
|
+
|
66
|
+
#Invokes the API's <b>expand</b> method.
|
67
|
+
#It reverses a shorten; the original full URL is re-hydrated.
|
68
|
+
#
|
69
|
+
#For <i>param</i>, you can provide a previously-shortened URL from the bit.ly service.
|
70
|
+
#If so, you do not need to provide <i>param_type</i> (though <tt>:short_url</tt> would be the proper symbol).
|
71
|
+
#
|
72
|
+
#Alternately, you can provide a hash code returned by the bit.ly service.
|
73
|
+
#In this case, provide <tt>:hash</tt> as the <i>param_type</i>.
|
74
|
+
#
|
75
|
+
#A Response is returned.
|
76
|
+
#Response.to_s will return the <tt>longUrl</tt> / <tt>long_url</tt> value.
|
77
|
+
def expand(param, param_type=:short_url)
|
78
|
+
return nil unless param && param_type
|
79
|
+
assert_codes(execute(:expand, :long_url) do |params|
|
80
|
+
params[Utility::camelize(param_type).to_sym] = param
|
81
|
+
end)
|
82
|
+
end
|
83
|
+
|
84
|
+
#Invokes the API's <b>info</b> method.
|
85
|
+
#Information about the shortened URL is returned by the service.
|
86
|
+
#
|
87
|
+
#For <i>param</i>, you can provide a previously-shortened URL from the bit.ly service.
|
88
|
+
#If so, you do not need to provide <i>param_type</i> (though <tt>:short_url</tt> would be the proper symbol).
|
89
|
+
#
|
90
|
+
#Alternately, you can provide a hash code returned by the bit.ly service.
|
91
|
+
#In this case, provide <tt>:hash</tt> as the <i>param_type</i>.
|
92
|
+
#
|
93
|
+
#You can optionally provide an arbitrary Hash of HTTP parameters.
|
94
|
+
#The one that bit.ly cares about is called <tt>:key</tt>.
|
95
|
+
#It should be an array of camel-cased or underscored element names which you would like to receive.
|
96
|
+
#In theory, this should limit the response content, but I haven't seen that work yet.
|
97
|
+
#The arbitrary Hash capability is left intact for future re-purposing.
|
98
|
+
#
|
99
|
+
#A Response is returned.
|
100
|
+
#Response.to_s will return the <tt>longUrl</tt> / <tt>long_url</tt> value.
|
101
|
+
#
|
102
|
+
#There is plenty of other data in the response besides the original full URL.
|
103
|
+
#Feel free to access the Response.body and use Response.method_missing to pull out specific element values.
|
104
|
+
def info(param, param_type=:short_url, ops={})
|
105
|
+
return nil unless param_type && param
|
106
|
+
assert_codes(execute(:info, :long_url) do |params|
|
107
|
+
params[Utility::camelize(param_type).to_sym] = param
|
108
|
+
|
109
|
+
# optional keys
|
110
|
+
if (keys = ops[:keys])
|
111
|
+
keys = [keys] unless Array === keys
|
112
|
+
params[:keys] = (keys.inject([]) do |a, key|
|
113
|
+
a << Utility::camelize(key)
|
114
|
+
a
|
115
|
+
end).join(',')
|
116
|
+
end
|
117
|
+
end)
|
118
|
+
end
|
119
|
+
|
120
|
+
#Invokes the API's <b>stats</b> method.
|
121
|
+
#Statistics about the shortened URL are returned by the service.
|
122
|
+
#
|
123
|
+
#For <i>param</i>, you can provide a previously-shortened URL from the bit.ly service.
|
124
|
+
#If so, you do not need to provide <i>param_type</i> (though <tt>:short_url</tt> would be the proper symbol).
|
125
|
+
#
|
126
|
+
#Alternately, you can provide a hash code returned by the bit.ly service.
|
127
|
+
#In this case, provide <tt>:hash</tt> as the <i>param_type</i>.
|
128
|
+
#
|
129
|
+
#A Response is returned.
|
130
|
+
#Response.to_s will return the <tt>longUrl</tt> / <tt>long_url</tt> value.
|
131
|
+
#
|
132
|
+
#There is plenty of other data in the response besides the original full URL.
|
133
|
+
#Feel free to access the Response.body and use Response.method_missing to pull out specific element values.
|
134
|
+
def stats(param, param_type=:short_url)
|
135
|
+
return nil unless param_type && param
|
136
|
+
assert_codes(execute(:info, :long_url) do |params|
|
137
|
+
params[Utility::camelize(param_type).to_sym] = param
|
138
|
+
end)
|
139
|
+
end
|
140
|
+
|
141
|
+
def errors
|
142
|
+
execute(:errors)
|
143
|
+
end
|
144
|
+
|
145
|
+
|
146
|
+
|
147
|
+
# - - - - -
|
148
|
+
protected
|
149
|
+
|
150
|
+
def execute(command, to_s_sym=nil) #:nodoc:
|
151
|
+
#http://api.bit.ly/shorten?version=2.0.1&longUrl=http://cnn.com&login=bitlyapidemo&apiKey=R_0da49e0a9118ff35f52f629d2d71bf07
|
152
|
+
uri = URI.parse('http://api.bit.ly')
|
153
|
+
|
154
|
+
# the client-izec set
|
155
|
+
params = @client_params.clone
|
156
|
+
params[:login] = self.login
|
157
|
+
params[:apiKey] = self.api_key if self.api_key
|
158
|
+
|
159
|
+
# altered in whatever way the caller desires
|
160
|
+
yield params if block_given?
|
161
|
+
|
162
|
+
response = Net::HTTP.start(uri.host, uri.port) do |http|
|
163
|
+
path = "/#{command}?#{params}"
|
164
|
+
request = Net::HTTP::Get.new(path)
|
165
|
+
if self.password
|
166
|
+
# HTTP auth expected
|
167
|
+
request.basic_auth self.login, self.password
|
168
|
+
end
|
169
|
+
|
170
|
+
http.request request
|
171
|
+
end
|
172
|
+
|
173
|
+
raise Error.new('did not receive HTTP 200 OK') unless Net::HTTPOK === response
|
174
|
+
|
175
|
+
# a parsing response
|
176
|
+
Response.new(response, to_s_sym)
|
177
|
+
end
|
178
|
+
|
179
|
+
def assert_error_code(response) #:nodoc:
|
180
|
+
raise Error.new("errorCode #{response.errorCode} : #{response.errorMesage}") unless '0' == response.errorCode
|
181
|
+
response
|
182
|
+
end
|
183
|
+
|
184
|
+
def assert_status_code(response) #:nodoc:
|
185
|
+
raise Error.new("status #{response.statusCode}") unless 'OK' == response.statusCode
|
186
|
+
response
|
187
|
+
end
|
188
|
+
|
189
|
+
def assert_codes(response) #:nodoc:
|
190
|
+
assert_error_code(assert_status_code(response))
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
|
195
|
+
|
196
|
+
#Constructs a new 'simple' client.
|
197
|
+
#
|
198
|
+
#Just like a standard Client, except that several methods are overridden to provide the 'likely' value, vs. a Response.
|
199
|
+
class SimpleClient < Client
|
200
|
+
#Same as Client.shorten, except that the shortened URL is returned (vs. a Response)
|
201
|
+
def shorten(*args)
|
202
|
+
(super *args).to_s
|
203
|
+
end
|
204
|
+
|
205
|
+
#Same as Client.expand, except that the long URL is returned (vs. a Response)
|
206
|
+
def expand(*args)
|
207
|
+
# just the default value, not the Response
|
208
|
+
(super *args).to_s
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
#Simple constructor methods, so that you don't have to deal directly with Client if you don't want to.
|
2
|
+
#
|
3
|
+
#See also:
|
4
|
+
#* Bitly4R.Keyed
|
5
|
+
#* Bitly4R.Authed
|
6
|
+
#
|
7
|
+
#--
|
8
|
+
#Many thanks to Hpricot for introducing me to this convention.
|
9
|
+
#++
|
10
|
+
|
11
|
+
#Constructs a SimpleClient with the usual Hash of arguments.
|
12
|
+
|
13
|
+
def Bitly4R(ops={})
|
14
|
+
# general options
|
15
|
+
Bitly4R::SimpleClient.new(ops)
|
16
|
+
end
|
17
|
+
|
18
|
+
#Constructs a SimpleClient with login and API key credentials.
|
19
|
+
#
|
20
|
+
#No password is involved.
|
21
|
+
#Simple HTTP GETs will be the order of the day.
|
22
|
+
def Bitly4R.Keyed(login, api_key)
|
23
|
+
# using an API key
|
24
|
+
Bitly4R::SimpleClient.new(:login => login, :api_key => api_key)
|
25
|
+
end
|
26
|
+
|
27
|
+
#Constructs a SimpleClient with login and password credentials.
|
28
|
+
#
|
29
|
+
#No API key is involved.
|
30
|
+
#HTTP GETs with HTTP Basic Auth will be the order of the day.
|
31
|
+
def Bitly4R.Authed(login, password)
|
32
|
+
# using an API key
|
33
|
+
Bitly4R::SimpleClient.new(:login => login, :password => password)
|
34
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
#Supporting objects for Bitly4R.
|
2
|
+
#
|
3
|
+
#See:
|
4
|
+
#* Bitly4R::Error
|
5
|
+
#* Bitly4R::Params
|
6
|
+
#* Bitly4R::Response
|
7
|
+
|
8
|
+
module Bitly4R
|
9
|
+
|
10
|
+
#= Error
|
11
|
+
#
|
12
|
+
#A Module-specific Exception class
|
13
|
+
#
|
14
|
+
#--
|
15
|
+
# i would have called it Bitly4R::Exception
|
16
|
+
# except that i don't know how to access Kernel::Exception within the initialize logic
|
17
|
+
#++
|
18
|
+
class Error < Exception
|
19
|
+
#The propagated cause of this Exception, if appropriate
|
20
|
+
attr_accessor :cause
|
21
|
+
|
22
|
+
#Provide a message and an optional 'causing' Exception.
|
23
|
+
#
|
24
|
+
#If no message is passed -- eg. only an Exception -- then this Exception inherits its message.
|
25
|
+
def initialize(message, cause=nil)
|
26
|
+
if (Exception === message)
|
27
|
+
super message.to_s
|
28
|
+
@cause = message
|
29
|
+
else
|
30
|
+
super message
|
31
|
+
@cause = cause
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
#= Params
|
37
|
+
#
|
38
|
+
#Extends the Hash class to provide simply composition of a URL-encoded string.
|
39
|
+
#
|
40
|
+
#Could have extended Hash, but chose instead to 'leave no trace'.
|
41
|
+
class Params < Hash
|
42
|
+
# An encoded composite of the parameters, ready for use in a URL
|
43
|
+
def to_s
|
44
|
+
(self.to_a.collect do |k, v|
|
45
|
+
"#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}"
|
46
|
+
end).join('&')
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
|
52
|
+
#= Response
|
53
|
+
#
|
54
|
+
#A response from the bit.ly API.
|
55
|
+
#
|
56
|
+
#The to_s method should always return the 'likely' value, assuming that there is a likely one.
|
57
|
+
#For example:
|
58
|
+
#* <tt>shorten => shortUrl</tt>
|
59
|
+
#* <tt>expand => longUrl</tt>
|
60
|
+
#
|
61
|
+
#All other response values can be retrieved via method_missing.
|
62
|
+
#
|
63
|
+
#<b>NOTE:</b> This is <i>not</i> a sophisticated XML parser. It's just Regexp's, with handling for CDATA blocks.
|
64
|
+
class Response
|
65
|
+
#The body of the bit.ly API response, as XML
|
66
|
+
attr_reader :body
|
67
|
+
|
68
|
+
#Constructs a bit.ly API response wrapper.
|
69
|
+
#
|
70
|
+
#<i>response</i> can be:
|
71
|
+
#* a String, which becomes the body
|
72
|
+
#* a Net::HTTPResponse, in which case its body is extracted
|
73
|
+
#
|
74
|
+
#<i>to_s_sym</i> is optional, and it references the element which will become the to_s value of this Response.
|
75
|
+
#It can be either camel-case or underscored.
|
76
|
+
#See method_missing.
|
77
|
+
def initialize(response, to_s_sym=nil)
|
78
|
+
response = response.body if Net::HTTPResponse === response
|
79
|
+
@body = response
|
80
|
+
@to_s_sym = to_s_sym
|
81
|
+
end
|
82
|
+
|
83
|
+
#Provides access the other text elements of the response via camel-case or underscored names.
|
84
|
+
#For example, <tt>longUrl</tt> and <tt>long_url</tt> are equivalent.
|
85
|
+
#If no such element exists, you'll get nil.
|
86
|
+
def method_missing(sym, *args)
|
87
|
+
sym = Utility::camelize(sym.to_s)
|
88
|
+
match = (self.body || '').match(%r{<#{sym}>(.*)</#{sym}>})
|
89
|
+
|
90
|
+
unless match
|
91
|
+
nil
|
92
|
+
else
|
93
|
+
match[1].gsub(/^<!\[CDATA\[(.*)\]\]>$/, '\1')
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
#Provides the 'likely' value from the response.
|
98
|
+
def to_s
|
99
|
+
@to_s_sym ? self.__send__(@to_s_sym) : super
|
100
|
+
end
|
101
|
+
alias :to_str :to_s
|
102
|
+
|
103
|
+
#Provides the 'likely' value from the response, as a symbol.
|
104
|
+
def to_sym
|
105
|
+
@to_s_sym ? self.to_s.to_sym : super
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
class Utility #:nodoc: all
|
110
|
+
class << self
|
111
|
+
def camelize(string)
|
112
|
+
((string || '').to_s.split(/_/).inject([]) do |a, s|
|
113
|
+
s = s[0].chr.upcase + s[1..s.size] unless a.empty?
|
114
|
+
a << s
|
115
|
+
a
|
116
|
+
end).join('')
|
117
|
+
end
|
118
|
+
def decamelize(string)
|
119
|
+
(string.to_s || '').gsub(/([^_])([A-Z])/, '\1_\2').downcase
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
data/test/client_test.rb
ADDED
@@ -0,0 +1,130 @@
|
|
1
|
+
#--
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
|
5
|
+
|
6
|
+
|
7
|
+
class ClientTest < Test::Unit::TestCase #:nodoc: all
|
8
|
+
def test_construction
|
9
|
+
any = 'any'
|
10
|
+
|
11
|
+
assert_raises Bitly4R::Error do
|
12
|
+
# no login
|
13
|
+
Bitly4R::Client.new
|
14
|
+
end
|
15
|
+
|
16
|
+
assert_raises Bitly4R::Error do
|
17
|
+
# needs API key or password
|
18
|
+
Bitly4R::Client.new(:login => any)
|
19
|
+
end
|
20
|
+
|
21
|
+
Bitly4R::Client.new(:login => any, :api_key => any)
|
22
|
+
Bitly4R::Client.new(:login => any, :password => any)
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_shorten
|
26
|
+
# shorten
|
27
|
+
client = new_client
|
28
|
+
response = client.shorten LONG_URL
|
29
|
+
|
30
|
+
assert_is_response_ok response
|
31
|
+
|
32
|
+
# we get back what we provided
|
33
|
+
assert_equal LONG_URL, response.node_key
|
34
|
+
|
35
|
+
# no assumption as to the value, just how they inter-relate
|
36
|
+
hash = response.user_hash
|
37
|
+
assert hash && (! hash.empty?)
|
38
|
+
short = response.short_url
|
39
|
+
assert short =~ Regexp.compile(hash + '$')
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_expand
|
43
|
+
# shorten ...
|
44
|
+
client = new_client
|
45
|
+
|
46
|
+
response = client.shorten(LONG_URL)
|
47
|
+
assert_is_response_ok response
|
48
|
+
|
49
|
+
hash = response.user_hash
|
50
|
+
short = response.to_s
|
51
|
+
|
52
|
+
# ... and re-expand
|
53
|
+
# again, we don't have to know anything
|
54
|
+
response = client.expand(short)
|
55
|
+
assert_is_response_ok response
|
56
|
+
|
57
|
+
assert_equal LONG_URL, response.to_s
|
58
|
+
|
59
|
+
# note sure what purpose it serves
|
60
|
+
# but it will contain a hash-wrapped element
|
61
|
+
assert ! response.__send__(hash.to_sym).empty?
|
62
|
+
|
63
|
+
# ... and re-expand
|
64
|
+
# again, we don't have to know anything
|
65
|
+
response = client.expand(hash, :hash)
|
66
|
+
assert_is_response_ok response
|
67
|
+
|
68
|
+
assert_equal LONG_URL, response.to_s
|
69
|
+
|
70
|
+
# again...
|
71
|
+
assert ! response.__send__(hash.to_sym).empty?
|
72
|
+
end
|
73
|
+
|
74
|
+
def test_info
|
75
|
+
# shorten ...
|
76
|
+
client = new_client
|
77
|
+
|
78
|
+
response = client.shorten(LONG_URL)
|
79
|
+
assert_is_response_ok response
|
80
|
+
|
81
|
+
hash = response.user_hash
|
82
|
+
short = response.to_s
|
83
|
+
|
84
|
+
# short url, with no key limit
|
85
|
+
response = client.info(short)
|
86
|
+
assert_is_response_ok response
|
87
|
+
|
88
|
+
assert_equal LONG_URL, response.long_url
|
89
|
+
|
90
|
+
# hash, key limit
|
91
|
+
response = client.info(hash, :hash, :keys => [:long_url, :html_title])
|
92
|
+
assert_is_response_ok response
|
93
|
+
|
94
|
+
# well, we're getting non-included keys back
|
95
|
+
# then again, the demo doesn't constrain the keys either
|
96
|
+
# http://code.google.com/p/bitly-api/wiki/ApiDocumentation
|
97
|
+
###assert response.thumbnail.empty?
|
98
|
+
###assert ! response.html_title.empty?
|
99
|
+
assert_equal LONG_URL, response.to_s
|
100
|
+
end
|
101
|
+
|
102
|
+
def test_stats
|
103
|
+
# shorten ...
|
104
|
+
client = new_client
|
105
|
+
|
106
|
+
response = client.shorten(LONG_URL)
|
107
|
+
assert_is_response_ok response
|
108
|
+
|
109
|
+
hash = response.user_hash
|
110
|
+
short = response.to_s
|
111
|
+
|
112
|
+
{ :hash => hash, :short_url => short }.each do |param_type, param|
|
113
|
+
response = client.info(param, param_type)
|
114
|
+
assert_is_response_ok response
|
115
|
+
|
116
|
+
# we could choose anything
|
117
|
+
assert_equal LONG_URL, response.to_s
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def test_errors
|
122
|
+
# errors ...
|
123
|
+
client = new_client
|
124
|
+
|
125
|
+
response = client.errors
|
126
|
+
assert ! response.results.empty?
|
127
|
+
assert ! response.error_code.empty?
|
128
|
+
assert ! response.status_code.empty?
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
#--
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
|
5
|
+
|
6
|
+
|
7
|
+
class DefinitionsTest < Test::Unit::TestCase #:nodoc: all
|
8
|
+
def test_keyed
|
9
|
+
# shorten ...
|
10
|
+
client = Bitly4R(:login => LOGIN, :api_key => API_KEY)
|
11
|
+
short = client.shorten(LONG_URL)
|
12
|
+
assert short && (! short.empty?)
|
13
|
+
|
14
|
+
# ... and expand
|
15
|
+
assert_equal LONG_URL, Bitly4R.Keyed(LOGIN, API_KEY).expand(short)
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_authed
|
19
|
+
unless PASSWORD
|
20
|
+
puts %Q{
|
21
|
+
NOTE:
|
22
|
+
the text login (#{LOGIN}) did not publish a password
|
23
|
+
cannot be tested without that private information
|
24
|
+
}.strip
|
25
|
+
return
|
26
|
+
end
|
27
|
+
|
28
|
+
# http://code.google.com/p/bitly-api/wiki/ApiDocumentation
|
29
|
+
# sure, the documentation claims there's HTTP Auth support
|
30
|
+
# but i don't see it yet
|
31
|
+
short = nil
|
32
|
+
assert_raises Bitly4R::Error do
|
33
|
+
# shorten ...
|
34
|
+
client = Bitly4R(:login => LOGIN, :password => API_KEY)
|
35
|
+
short = client.shorten(LONG_URL)
|
36
|
+
assert short && (! short.empty?)
|
37
|
+
end
|
38
|
+
|
39
|
+
# alright, let's use the API key
|
40
|
+
client = Bitly4R(:login => LOGIN, :api_key => API_KEY)
|
41
|
+
short = client.shorten(LONG_URL)
|
42
|
+
assert short && (! short.empty?)
|
43
|
+
|
44
|
+
# same deal. *sigh*
|
45
|
+
assert_raises Bitly4R::Error do
|
46
|
+
# ... and expand
|
47
|
+
assert_equal LONG_URL, Bitly4R.Authed(LOGIN, PASSWORD).expand(short)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
#--
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
|
5
|
+
|
6
|
+
|
7
|
+
class ObjectsTest < Test::Unit::TestCase #:nodoc: all
|
8
|
+
def test_decamelize
|
9
|
+
u = Bitly4R::Utility
|
10
|
+
|
11
|
+
assert_equal '', u.decamelize(nil)
|
12
|
+
assert_equal '', u.decamelize('')
|
13
|
+
assert_equal 'a', u.decamelize('a')
|
14
|
+
assert_equal 'a', u.decamelize('A')
|
15
|
+
assert_equal 'aa', u.decamelize('Aa')
|
16
|
+
assert_equal 'a_a', u.decamelize('aA')
|
17
|
+
assert_equal 'a_a', u.decamelize('AA')
|
18
|
+
assert_equal 'a_a', u.decamelize('a_a')
|
19
|
+
assert_equal 'a_a', u.decamelize(:A_A)
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_camelize
|
23
|
+
u = Bitly4R::Utility
|
24
|
+
|
25
|
+
assert_equal '', u.camelize(nil)
|
26
|
+
assert_equal '', u.camelize('')
|
27
|
+
assert_equal '', u.camelize('_')
|
28
|
+
assert_equal 'a', u.camelize('a_')
|
29
|
+
assert_equal 'A', u.camelize('_a')
|
30
|
+
assert_equal 'aA', u.camelize('aA')
|
31
|
+
assert_equal 'aA', u.camelize('a_a')
|
32
|
+
assert_equal 'AA', u.camelize(:A_A)
|
33
|
+
assert_equal 'aA1.', u.camelize('a_A_1_.')
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_error
|
37
|
+
# string only
|
38
|
+
e = Bitly4R::Error.new('message')
|
39
|
+
assert_equal 'message', e.message
|
40
|
+
assert_nil e.cause
|
41
|
+
|
42
|
+
e = Bitly4R::Error.new(Exception.new('exception'))
|
43
|
+
assert_equal 'exception', e.message
|
44
|
+
assert_not_nil e.cause
|
45
|
+
|
46
|
+
# exception
|
47
|
+
ee = nil
|
48
|
+
begin
|
49
|
+
1/0
|
50
|
+
rescue ZeroDivisionError => raised
|
51
|
+
ee = raised
|
52
|
+
e = Bitly4R::Error.new('rescued', raised)
|
53
|
+
end
|
54
|
+
assert_equal 'rescued', e.message
|
55
|
+
assert_not_nil e.cause
|
56
|
+
assert_equal ee, e.cause
|
57
|
+
end
|
58
|
+
|
59
|
+
def test_params
|
60
|
+
params = Bitly4R::Params.new
|
61
|
+
assert_equal '', params.to_s
|
62
|
+
|
63
|
+
params[1] = :one
|
64
|
+
assert_equal '1=one', params.to_s
|
65
|
+
|
66
|
+
params[:b] = Exception.new('an exception')
|
67
|
+
assert_equal ['1=one', 'b=an+exception'], params.to_s.split('&').sort
|
68
|
+
|
69
|
+
params['key is'] = '&escaped'
|
70
|
+
assert_equal ['1=one', 'b=an+exception', 'key+is=%26escaped'], params.to_s.split('&').sort
|
71
|
+
end
|
72
|
+
|
73
|
+
def test_response
|
74
|
+
# nil response *and* to_s symbol
|
75
|
+
response = Bitly4R::Response.new(nil)
|
76
|
+
assert_nil response.body
|
77
|
+
assert_not_nil response.to_s
|
78
|
+
assert_nil response.to_sym
|
79
|
+
|
80
|
+
# crap
|
81
|
+
response = Bitly4R::Response.new('string', :bogus)
|
82
|
+
assert_equal 'string', response.body
|
83
|
+
assert_nil response.to_s
|
84
|
+
|
85
|
+
# simple
|
86
|
+
response = Bitly4R::Response.new('<a>value</a>', :b)
|
87
|
+
assert_equal 'value', response.a
|
88
|
+
assert_nil response.to_s
|
89
|
+
|
90
|
+
# camelization
|
91
|
+
response = Bitly4R::Response.new('<uD>down</uD><dU>up</dU>', :d_u)
|
92
|
+
assert_equal 'down', response.uD
|
93
|
+
assert_equal 'down', response.u_d
|
94
|
+
assert_equal 'up', response.dU
|
95
|
+
assert_equal 'up', response.to_s
|
96
|
+
|
97
|
+
# CDATA, plus to_sym
|
98
|
+
response = Bitly4R::Response.new('<a><b>b</b><cData><![CDATA[data]]></cData><c>c</c></a>', :c_data)
|
99
|
+
assert_equal :data, response.to_sym
|
100
|
+
end
|
101
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
#--
|
2
|
+
|
3
|
+
#
|
4
|
+
# Thank you masked man. Or, rather, Hpricot.
|
5
|
+
#
|
6
|
+
$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__)) + "/../lib"
|
7
|
+
|
8
|
+
%w{ rubygems bitly4r test/unit }.each {|lib| require lib }
|
9
|
+
|
10
|
+
# if we're lucky...
|
11
|
+
begin
|
12
|
+
require 'ruby-debug'
|
13
|
+
rescue Object => e
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
|
18
|
+
class Test::Unit::TestCase #:nodoc: all
|
19
|
+
|
20
|
+
# trailing slash makes a difference! they normalize!
|
21
|
+
LONG_URL = 'http://rubyforge.org/'
|
22
|
+
|
23
|
+
# credentials from
|
24
|
+
# http://code.google.com/p/bitly-api/wiki/ApiDocumentation
|
25
|
+
# no password provided
|
26
|
+
LOGIN = 'bitlyapidemo'
|
27
|
+
API_KEY = 'R_0da49e0a9118ff35f52f629d2d71bf07'
|
28
|
+
PASSWORD = nil
|
29
|
+
|
30
|
+
|
31
|
+
|
32
|
+
def new_client
|
33
|
+
Bitly4R::Client.new(:login => LOGIN, :api_key => API_KEY)
|
34
|
+
end
|
35
|
+
|
36
|
+
def assert_is_response_ok(response)
|
37
|
+
assert_equal '0', response.error_code
|
38
|
+
assert_equal '', response.error_message
|
39
|
+
assert_equal 'OK', response.status_code
|
40
|
+
end
|
41
|
+
end
|
metadata
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: bitly4r
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Dan Foley
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-02-02 00:00:00 -08:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: "Bitly4R : A Ruby API for the http://bit.ly URL-shortening service"
|
17
|
+
email: admin@cantremember.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README.rdoc
|
24
|
+
- CHANGELOG
|
25
|
+
- LICENSE
|
26
|
+
files:
|
27
|
+
- CHANGELOG
|
28
|
+
- LICENSE
|
29
|
+
- README.rdoc
|
30
|
+
- Rakefile
|
31
|
+
- lib/bitly4r/client.rb
|
32
|
+
- lib/bitly4r/definitions.rb
|
33
|
+
- lib/bitly4r/objects.rb
|
34
|
+
- lib/bitly4r.rb
|
35
|
+
has_rdoc: true
|
36
|
+
homepage: http://wiki.cantremember.com/Bitly4R
|
37
|
+
post_install_message:
|
38
|
+
rdoc_options:
|
39
|
+
- --line-numbers
|
40
|
+
- --inline-source
|
41
|
+
- --title
|
42
|
+
- Bitly4R
|
43
|
+
- --main
|
44
|
+
- README.rdoc
|
45
|
+
require_paths:
|
46
|
+
- lib
|
47
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: "1.8"
|
52
|
+
version:
|
53
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: "0"
|
58
|
+
version:
|
59
|
+
requirements: []
|
60
|
+
|
61
|
+
rubyforge_project: bitly4r
|
62
|
+
rubygems_version: 1.3.1
|
63
|
+
signing_key:
|
64
|
+
specification_version: 2
|
65
|
+
summary: bitly4r 0.1.0
|
66
|
+
test_files:
|
67
|
+
- test/client_test.rb
|
68
|
+
- test/definitions_test.rb
|
69
|
+
- test/objects_test.rb
|
70
|
+
- test/test_helper.rb
|