bitly4r 0.1.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.
- 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
|