guillotine 1.2.1 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +17 -0
- data/README.md +39 -4
- data/config/cassandra_config.json +10 -0
- data/guillotine.gemspec +6 -2
- data/lib/guillotine.rb +49 -7
- data/lib/guillotine/adapters/active_record_adapter.rb +6 -4
- data/lib/guillotine/adapters/cassandra_adapter.rb +67 -0
- data/lib/guillotine/adapters/memory_adapter.rb +7 -4
- data/lib/guillotine/adapters/mongo_adapter.rb +8 -6
- data/lib/guillotine/adapters/redis_adapter.rb +6 -4
- data/lib/guillotine/adapters/riak_adapter.rb +6 -5
- data/lib/guillotine/adapters/sequel_adapter.rb +5 -4
- data/lib/guillotine/app.rb +7 -0
- data/lib/guillotine/service.rb +56 -9
- data/test/app_test.rb +83 -11
- data/test/cassandra_adapter_test.rb +67 -0
- data/test/helper.rb +3 -0
- data/test/options_test.rb +59 -0
- data/test/service_test.rb +30 -1
- metadata +76 -48
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,22 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## v1.3.0
|
4
|
+
|
5
|
+
* Add Cassandra Adapter [rajeshucsb]
|
6
|
+
* Add option to stop stripping URL queries or Anchors. [mrtazz]
|
7
|
+
|
8
|
+
class MyApp < Guillotine::App
|
9
|
+
db = Sequel.sqlite
|
10
|
+
adapter = Guillotine::Adapters::SequelAdapter.new(db)
|
11
|
+
|
12
|
+
# OLD required host configuration, deprecated
|
13
|
+
set :service => Guillotine::Service.new(adapter, 'github.com')
|
14
|
+
|
15
|
+
# NEW required host configuration.
|
16
|
+
set :service => Guillotine::Service.new(adapter,
|
17
|
+
:required_host => 'github.com', :strip_query => false)
|
18
|
+
end
|
19
|
+
|
3
20
|
## v1.2.1
|
4
21
|
|
5
22
|
* Fix WildcardHostChecker error responses.
|
data/README.md
CHANGED
@@ -50,7 +50,7 @@ require 'guillotine'
|
|
50
50
|
require 'sequel'
|
51
51
|
module MyApp
|
52
52
|
class App < Guillotine::App
|
53
|
-
db = Sequel.sqlite
|
53
|
+
db = Sequel.sqlite
|
54
54
|
adapter = Guillotine::Adapters::SequelAdapter.new(db)
|
55
55
|
set :service => Guillotine::Service.new(adapter)
|
56
56
|
end
|
@@ -106,6 +106,41 @@ module MyApp
|
|
106
106
|
end
|
107
107
|
```
|
108
108
|
|
109
|
+
## Cassandra
|
110
|
+
|
111
|
+
you can use Cassandra!
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
require 'guillotine'
|
115
|
+
require 'cassandra'
|
116
|
+
|
117
|
+
module MyApp
|
118
|
+
class App < Guillotine::App
|
119
|
+
cassandra = Cassandra.new('url_shortener', '127.0.0.1:9160')
|
120
|
+
adapter = Guillotine::Adapters::CassandraAdapter.new(cassandra)
|
121
|
+
|
122
|
+
set :service => Guillotine::Service.new(adapter)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
```
|
126
|
+
|
127
|
+
You need to create keyspace and column families as below
|
128
|
+
|
129
|
+
```sql
|
130
|
+
CREATE KEYSPACE url_shortener;
|
131
|
+
USE url_shortener;
|
132
|
+
|
133
|
+
CREATE COLUMN FAMILY urls
|
134
|
+
WITH comparator = UTF8Type
|
135
|
+
AND key_validation_class=UTF8Type
|
136
|
+
AND column_metadata = [{column_name: code, validation_class: UTF8Type}];
|
137
|
+
|
138
|
+
CREATE COLUMN FAMILY codes
|
139
|
+
WITH comparator = UTF8Type
|
140
|
+
AND key_validation_class=UTF8Type
|
141
|
+
AND column_metadata = [{column_name: url, validation_class: UTF8Type}];
|
142
|
+
```
|
143
|
+
|
109
144
|
## Domain Restriction
|
110
145
|
|
111
146
|
You can restrict what domains that Guillotine will shorten.
|
@@ -117,15 +152,15 @@ module MyApp
|
|
117
152
|
adapter = Guillotine::Adapters::MemoryAdapter.new
|
118
153
|
# only this domain
|
119
154
|
set :service => Guillotine::Service.new(adapter,
|
120
|
-
'github.com')
|
155
|
+
:required_host => 'github.com')
|
121
156
|
|
122
157
|
# or, any *.github.com domain
|
123
158
|
set :service => Guillotine::Service.new(adapter,
|
124
|
-
/(^|\.)github\.com$/)
|
159
|
+
:required_host => /(^|\.)github\.com$/)
|
125
160
|
|
126
161
|
# or set a simple wildcard
|
127
162
|
set :service => Guillotine::Servicew.new(adapter,
|
128
|
-
'*.github.com')
|
163
|
+
:required_host => '*.github.com')
|
129
164
|
end
|
130
165
|
end
|
131
166
|
```
|
data/guillotine.gemspec
CHANGED
@@ -13,8 +13,8 @@ Gem::Specification.new do |s|
|
|
13
13
|
## If your rubyforge_project name is different, then edit it and comment out
|
14
14
|
## the sub! line in the Rakefile
|
15
15
|
s.name = 'guillotine'
|
16
|
-
s.version = '1.
|
17
|
-
s.date = '2012-
|
16
|
+
s.version = '1.3.0'
|
17
|
+
s.date = '2012-08-19'
|
18
18
|
s.rubyforge_project = 'guillotine'
|
19
19
|
|
20
20
|
## Make sure your summary is short. The description may be as long
|
@@ -52,10 +52,12 @@ Gem::Specification.new do |s|
|
|
52
52
|
LICENSE
|
53
53
|
README.md
|
54
54
|
Rakefile
|
55
|
+
config/cassandra_config.json
|
55
56
|
config/haproxy.riak.cfg
|
56
57
|
guillotine.gemspec
|
57
58
|
lib/guillotine.rb
|
58
59
|
lib/guillotine/adapters/active_record_adapter.rb
|
60
|
+
lib/guillotine/adapters/cassandra_adapter.rb
|
59
61
|
lib/guillotine/adapters/memory_adapter.rb
|
60
62
|
lib/guillotine/adapters/mongo_adapter.rb
|
61
63
|
lib/guillotine/adapters/redis_adapter.rb
|
@@ -67,10 +69,12 @@ Gem::Specification.new do |s|
|
|
67
69
|
script/cibuild
|
68
70
|
test/active_record_adapter_test.rb
|
69
71
|
test/app_test.rb
|
72
|
+
test/cassandra_adapter_test.rb
|
70
73
|
test/helper.rb
|
71
74
|
test/host_checker_test.rb
|
72
75
|
test/memory_adapter_test.rb
|
73
76
|
test/mongo_adapter_test.rb
|
77
|
+
test/options_test.rb
|
74
78
|
test/redis_adapter_test.rb
|
75
79
|
test/riak_adapter_test.rb
|
76
80
|
test/sequel_adapter_test.rb
|
data/lib/guillotine.rb
CHANGED
@@ -3,7 +3,7 @@ require 'digest/md5'
|
|
3
3
|
require 'addressable/uri'
|
4
4
|
|
5
5
|
module Guillotine
|
6
|
-
VERSION = "1.
|
6
|
+
VERSION = "1.3.0"
|
7
7
|
|
8
8
|
class Error < StandardError
|
9
9
|
end
|
@@ -23,7 +23,7 @@ module Guillotine
|
|
23
23
|
# use whatever you want, as long as it implements the #add and #find
|
24
24
|
# methods. See MemoryAdapter for a simple solution.
|
25
25
|
class Adapter
|
26
|
-
#
|
26
|
+
# Internal: Shortens a given URL to a short code.
|
27
27
|
#
|
28
28
|
# 1) MD5 hash the URL to the hexdigest
|
29
29
|
# 2) Convert it to a Bignum
|
@@ -37,16 +37,57 @@ module Guillotine
|
|
37
37
|
Base64.urlsafe_encode64([Digest::MD5.hexdigest(url).to_i(16)].pack("N")).sub(/==\n?$/, '')
|
38
38
|
end
|
39
39
|
|
40
|
+
# Internal: Shortens a URL with a specific character set at a certain
|
41
|
+
# length.
|
42
|
+
#
|
43
|
+
# url - String URL to shorten.
|
44
|
+
# length - Optional Integer maximum length of the short code desired.
|
45
|
+
# charset - Optional Array of String characters which will be present in
|
46
|
+
# short code. eg. ['a', 'b', 'c', 'd', 'e', 'f']
|
47
|
+
#
|
48
|
+
# Returns an encoded String code for the URL.
|
49
|
+
def shorten_fixed_charset(url, length, char_set)
|
50
|
+
number = (Digest::MD5.hexdigest(url).to_i(16) % (char_set.size**length))
|
51
|
+
|
52
|
+
code = ""
|
53
|
+
|
54
|
+
while (number > 0)
|
55
|
+
code = code + char_set[number % char_set.size]
|
56
|
+
number /= char_set.size
|
57
|
+
end
|
58
|
+
|
59
|
+
code
|
60
|
+
end
|
61
|
+
|
40
62
|
# Parses and sanitizes a URL.
|
41
63
|
#
|
42
|
-
# url
|
64
|
+
# url - A String URL.
|
65
|
+
# options - A Guillotine::Service::Options object.
|
43
66
|
#
|
44
67
|
# Returns an Addressable::URI.
|
45
|
-
def parse_url(url)
|
46
|
-
url.gsub!
|
47
|
-
url.gsub!
|
48
|
-
|
68
|
+
def parse_url(url, options)
|
69
|
+
url.gsub!(/\s/, '')
|
70
|
+
url.gsub!(/\?.*/, '') if options.strip_query?
|
71
|
+
url.gsub!(/\#.*/, '') if options.strip_anchor?
|
72
|
+
Addressable::URI.parse(url)
|
49
73
|
end
|
74
|
+
|
75
|
+
# Internal: Shortens a URL with the given options.
|
76
|
+
#
|
77
|
+
# url - A String URL.
|
78
|
+
# code - Optional String code.
|
79
|
+
# options - Optional Guillotine::Service::Options to specify how the code
|
80
|
+
# is generated.
|
81
|
+
#
|
82
|
+
# returns a String code.
|
83
|
+
def get_code(url, code = nil, options = nil)
|
84
|
+
code ||= if options && options.with_charset?
|
85
|
+
shorten_fixed_charset(url, options.length, options.charset)
|
86
|
+
else
|
87
|
+
shorten(url)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
50
91
|
end
|
51
92
|
|
52
93
|
dir = File.expand_path '../guillotine/adapters', __FILE__
|
@@ -56,6 +97,7 @@ module Guillotine
|
|
56
97
|
autoload :ActiveRecordAdapter, dir + "/active_record_adapter"
|
57
98
|
autoload :RedisAdapter, dir + "/redis_adapter"
|
58
99
|
autoload :MongoAdapter, dir + "/mongo_adapter"
|
100
|
+
autoload :CassandraAdapter, dir + "/cassandra_adapter"
|
59
101
|
|
60
102
|
dir = File.expand_path '../guillotine', __FILE__
|
61
103
|
autoload :App, "#{dir}/app"
|
@@ -10,16 +10,18 @@ module Guillotine
|
|
10
10
|
|
11
11
|
# Public: Stores the shortened version of a URL.
|
12
12
|
#
|
13
|
-
# url
|
14
|
-
# code
|
13
|
+
# url - The String URL to shorten and store.
|
14
|
+
# code - Optional String code for the URL.
|
15
|
+
# options - Optional Guillotine::Service::Options
|
15
16
|
#
|
16
17
|
# Returns the unique String code for the URL. If the URL is added
|
17
18
|
# multiple times, this should return the same code.
|
18
|
-
def add(url, code = nil)
|
19
|
+
def add(url, code = nil, options = nil)
|
19
20
|
if row = Url.select(:code).where(:url => url).first
|
20
21
|
row[:code]
|
21
22
|
else
|
22
|
-
code
|
23
|
+
code = get_code(url, code, options)
|
24
|
+
|
23
25
|
begin
|
24
26
|
Url.create :url => url, :code => code
|
25
27
|
rescue ActiveRecord::RecordNotUnique, ActiveRecord::StatementInvalid
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Guillotine
|
2
|
+
class CassandraAdapter < Adapter
|
3
|
+
# Public: Initialise the adapter with a Redis instance.
|
4
|
+
#
|
5
|
+
# cassandra - A Cassandra instance to persist urls and codes to.
|
6
|
+
def initialize(cassandra, read_only = false)
|
7
|
+
@cassandra = cassandra
|
8
|
+
@read_only = read_only
|
9
|
+
end
|
10
|
+
|
11
|
+
# Public: Stores the shortened version of a URL.
|
12
|
+
#
|
13
|
+
# url - The String URL to shorten and store.
|
14
|
+
# code - Optional String code for the URL.
|
15
|
+
# options - Optional Guillotine::Service::Options
|
16
|
+
#
|
17
|
+
# Returns the unique String code for the URL. If the URL is added
|
18
|
+
# multiple times, this should return the same code.
|
19
|
+
def add(url, code = nil, options = nil)
|
20
|
+
return if @read_only
|
21
|
+
if existing_code = code_for(url)
|
22
|
+
existing_code
|
23
|
+
else
|
24
|
+
code = get_code(url, code, options)
|
25
|
+
|
26
|
+
if existing_url = find(code)
|
27
|
+
raise DuplicateCodeError.new(existing_url, url, code) if url != existing_url
|
28
|
+
end
|
29
|
+
@cassandra.insert("codes", code, 'url' => url)
|
30
|
+
@cassandra.insert("urls", url, 'code' => code)
|
31
|
+
code
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Public: Retrieves a URL from the code.
|
36
|
+
#
|
37
|
+
# code - The String code to lookup the URL.
|
38
|
+
#
|
39
|
+
# Returns the String URL, or nil if none is found.
|
40
|
+
def find(code)
|
41
|
+
obj = @cassandra.get("codes", code)
|
42
|
+
obj.nil? ? nil : obj["url"]
|
43
|
+
end
|
44
|
+
|
45
|
+
# Public: Retrieves the code for a given URL.
|
46
|
+
#
|
47
|
+
# url - The String URL to lookup.
|
48
|
+
#
|
49
|
+
# Returns the String code, or nil if none is found.
|
50
|
+
def code_for(url)
|
51
|
+
obj = @cassandra.get("urls", url)
|
52
|
+
obj.nil? ? nil : obj["code"]
|
53
|
+
end
|
54
|
+
|
55
|
+
# Public: Removes the assigned short code for a URL.
|
56
|
+
#
|
57
|
+
# url - The String URL to remove.
|
58
|
+
#
|
59
|
+
# Returns nothing.
|
60
|
+
def clear(url)
|
61
|
+
if code = code_for(url)
|
62
|
+
@cassandra.remove("urls", url)
|
63
|
+
@cassandra.remove("codes", code)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -8,16 +8,18 @@ module Guillotine
|
|
8
8
|
|
9
9
|
# Public: Stores the shortened version of a URL.
|
10
10
|
#
|
11
|
-
# url
|
12
|
-
# code
|
11
|
+
# url - The String URL to shorten and store.
|
12
|
+
# code - Optional String code for the URL.
|
13
|
+
# options - Optional Guillotine::Service::Options
|
13
14
|
#
|
14
15
|
# Returns the unique String code for the URL. If the URL is added
|
15
16
|
# multiple times, this should return the same code.
|
16
|
-
def add(url, code = nil)
|
17
|
+
def add(url, code = nil, options = nil)
|
17
18
|
if existing_code = @urls[url]
|
18
19
|
existing_code
|
19
20
|
else
|
20
|
-
code
|
21
|
+
code = get_code(url, code, options)
|
22
|
+
|
21
23
|
if existing_url = @hash[code]
|
22
24
|
raise DuplicateCodeError.new(existing_url, url, code) if url != existing_url
|
23
25
|
end
|
@@ -62,3 +64,4 @@ module Guillotine
|
|
62
64
|
end
|
63
65
|
end
|
64
66
|
end
|
67
|
+
|
@@ -15,13 +15,14 @@ module Guillotine
|
|
15
15
|
|
16
16
|
# Public: Stores the shortened version of a URL.
|
17
17
|
#
|
18
|
-
# url
|
19
|
-
# code
|
18
|
+
# url - The String URL to shorten and store.
|
19
|
+
# code - Optional String code for the URL.
|
20
|
+
# options - Optional Guillotine::Service::Options
|
20
21
|
#
|
21
22
|
# Returns the unique String code for the URL. If the URL is added
|
22
23
|
# multiple times, this should return the same code.
|
23
|
-
def add(url, code = nil)
|
24
|
-
code_for(url) || insert(url, code
|
24
|
+
def add(url, code = nil, options = nil)
|
25
|
+
code_for(url) || insert(url, get_code(url, code, options))
|
25
26
|
end
|
26
27
|
|
27
28
|
|
@@ -31,7 +32,7 @@ module Guillotine
|
|
31
32
|
#
|
32
33
|
# Returns the String URL, or nil if none is found.
|
33
34
|
def find(code)
|
34
|
-
select
|
35
|
+
select(:url, :_id => code)
|
35
36
|
end
|
36
37
|
|
37
38
|
# Public: Retrieves the code for a given URL.
|
@@ -40,7 +41,7 @@ module Guillotine
|
|
40
41
|
#
|
41
42
|
# Returns the String code, or nil if none is found.
|
42
43
|
def code_for(url)
|
43
|
-
select
|
44
|
+
select(:code, :url => url)
|
44
45
|
end
|
45
46
|
|
46
47
|
# Public: Removes the assigned short code for a URL.
|
@@ -66,3 +67,4 @@ module Guillotine
|
|
66
67
|
end
|
67
68
|
end
|
68
69
|
end
|
70
|
+
|
@@ -9,16 +9,18 @@ module Guillotine
|
|
9
9
|
|
10
10
|
# Public: Stores the shortened version of a URL.
|
11
11
|
#
|
12
|
-
# url
|
13
|
-
# code
|
12
|
+
# url - The String URL to shorten and store.
|
13
|
+
# code - Optional String code for the URL.
|
14
|
+
# options - Optional Guillotine::Service::Options
|
14
15
|
#
|
15
16
|
# Returns the unique String code for the URL. If the URL is added
|
16
17
|
# multiple times, this should return the same code.
|
17
|
-
def add(url, code = nil)
|
18
|
+
def add(url, code = nil, options = nil)
|
18
19
|
if existing_code = @redis.get(url_key(url))
|
19
20
|
existing_code
|
20
21
|
else
|
21
|
-
code
|
22
|
+
code = get_code(url, code, options)
|
23
|
+
|
22
24
|
if existing_url = @redis.get(code_key(code))
|
23
25
|
raise DuplicateCodeError.new(existing_url, url, code) if url != existing_url
|
24
26
|
end
|
@@ -17,13 +17,14 @@ module Guillotine
|
|
17
17
|
end
|
18
18
|
|
19
19
|
# Public: Stores the shortened version of a URL.
|
20
|
-
#
|
21
|
-
# url
|
22
|
-
# code
|
20
|
+
#
|
21
|
+
# url - The String URL to shorten and store.
|
22
|
+
# code - Optional String code for the URL.
|
23
|
+
# options - Optional Guillotine::Service::Options
|
23
24
|
#
|
24
25
|
# Returns the unique String code for the URL. If the URL is added
|
25
26
|
# multiple times, this should return the same code.
|
26
|
-
def add(url, code = nil)
|
27
|
+
def add(url, code = nil, options = nil)
|
27
28
|
sha = url_key url
|
28
29
|
url_obj = @url_bucket.get_or_new sha, :r => 1
|
29
30
|
if url_obj.raw_data
|
@@ -31,7 +32,7 @@ module Guillotine
|
|
31
32
|
code = url_obj.data
|
32
33
|
end
|
33
34
|
|
34
|
-
code
|
35
|
+
code = get_code(url, code, options)
|
35
36
|
code_obj = @code_bucket.get_or_new code
|
36
37
|
code_obj.content_type = url_obj.content_type = PLAIN
|
37
38
|
|
@@ -7,16 +7,17 @@ module Guillotine
|
|
7
7
|
|
8
8
|
# Public: Stores the shortened version of a URL.
|
9
9
|
#
|
10
|
-
# url
|
11
|
-
# code
|
10
|
+
# url - The String URL to shorten and store.
|
11
|
+
# code - Optional String code for the URL.
|
12
|
+
# options - Optional Guillotine::Service::Options
|
12
13
|
#
|
13
14
|
# Returns the unique String code for the URL. If the URL is added
|
14
15
|
# multiple times, this should return the same code.
|
15
|
-
def add(url, code = nil)
|
16
|
+
def add(url, code = nil, options = nil)
|
16
17
|
if existing = code_for(url)
|
17
18
|
existing
|
18
19
|
else
|
19
|
-
code
|
20
|
+
code = get_code(url, code, options)
|
20
21
|
begin
|
21
22
|
@table << {:url => url, :code => code}
|
22
23
|
rescue Sequel::DatabaseError
|
data/lib/guillotine/app.rb
CHANGED
@@ -6,6 +6,13 @@ module Guillotine
|
|
6
6
|
class App < Sinatra::Base
|
7
7
|
set :service, nil
|
8
8
|
|
9
|
+
get "/" do
|
10
|
+
if params[:code].nil?
|
11
|
+
default_url = settings.service.default_url
|
12
|
+
redirect default_url if !default_url.nil?
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
9
16
|
get "/:code" do
|
10
17
|
escaped = Addressable::URI.escape(params[:code])
|
11
18
|
status, head, body = settings.service.get(escaped)
|
data/lib/guillotine/service.rb
CHANGED
@@ -3,6 +3,46 @@ module Guillotine
|
|
3
3
|
# Deprecated until v2
|
4
4
|
NullChecker = Guillotine::HostChecker
|
5
5
|
|
6
|
+
# length - Optional Integer maximum length of the short code desired.
|
7
|
+
# charset - Optional Array of String characters which will be present in
|
8
|
+
# short code. eg. ['a', 'b', 'c', 'd', 'e', 'f']
|
9
|
+
class Options < Struct.new(:required_host, :strip_query, :strip_anchor,
|
10
|
+
:length, :charset, :default_url)
|
11
|
+
def self.from(value)
|
12
|
+
case value
|
13
|
+
when nil, "" then new
|
14
|
+
when String, Regexp then new(value)
|
15
|
+
when Hash then
|
16
|
+
opt = new
|
17
|
+
value.each do |key, value|
|
18
|
+
opt[key] = value
|
19
|
+
end
|
20
|
+
opt
|
21
|
+
when self then value
|
22
|
+
else
|
23
|
+
raise ArgumentError, "Unable to convert to Options: #{value.inspect}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def strip_query?
|
28
|
+
strip_query != false
|
29
|
+
end
|
30
|
+
|
31
|
+
def strip_anchor?
|
32
|
+
strip_anchor != false
|
33
|
+
end
|
34
|
+
|
35
|
+
def with_charset?
|
36
|
+
!(length.nil? || charset.nil?)
|
37
|
+
end
|
38
|
+
|
39
|
+
def host_checker
|
40
|
+
@host_checker ||= HostChecker.matching(required_host)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
attr_reader :db, :options
|
45
|
+
|
6
46
|
# This is the public API to the Guillotine service. Wire this up to Sinatra
|
7
47
|
# or whatever. Every public method should return a compatible Rack Response:
|
8
48
|
# [Integer Status, Hash headers, String body].
|
@@ -11,9 +51,9 @@ module Guillotine
|
|
11
51
|
# required_host - Either a String or Regex limiting which domains the
|
12
52
|
# shortened URLs can come from.
|
13
53
|
#
|
14
|
-
def initialize(db,
|
54
|
+
def initialize(db, value = nil)
|
15
55
|
@db = db
|
16
|
-
@
|
56
|
+
@options = Options.from(value)
|
17
57
|
end
|
18
58
|
|
19
59
|
# Public: Gets the full URL for a shortened code.
|
@@ -24,7 +64,7 @@ module Guillotine
|
|
24
64
|
# or 404 on a miss.
|
25
65
|
def get(code)
|
26
66
|
if url = @db.find(code)
|
27
|
-
[302, {"Location" =>
|
67
|
+
[302, {"Location" => parse_url(url).to_s}]
|
28
68
|
else
|
29
69
|
[404, {}, "No url found for #{code}"]
|
30
70
|
end
|
@@ -44,7 +84,7 @@ module Guillotine
|
|
44
84
|
end
|
45
85
|
|
46
86
|
begin
|
47
|
-
if code = @db.add(url.to_s, code)
|
87
|
+
if code = @db.add(url.to_s, code, @options)
|
48
88
|
[201, {"Location" => code}]
|
49
89
|
else
|
50
90
|
[422, {}, "Unable to shorten #{url}"]
|
@@ -64,7 +104,7 @@ module Guillotine
|
|
64
104
|
if url.scheme !~ /^https?$/
|
65
105
|
[422, {}, "Invalid url: #{url}"]
|
66
106
|
else
|
67
|
-
@
|
107
|
+
@options.host_checker.call url
|
68
108
|
end
|
69
109
|
end
|
70
110
|
|
@@ -77,11 +117,18 @@ module Guillotine
|
|
77
117
|
if str.respond_to?(:scheme)
|
78
118
|
str
|
79
119
|
else
|
80
|
-
str
|
81
|
-
str.gsub! /\s/, ''
|
82
|
-
str.gsub! /(\#|\?).*/, ''
|
83
|
-
Addressable::URI.parse str
|
120
|
+
parse_url(str.to_s)
|
84
121
|
end
|
85
122
|
end
|
123
|
+
|
124
|
+
# Internal
|
125
|
+
def parse_url(url)
|
126
|
+
@db.parse_url(url, @options)
|
127
|
+
end
|
128
|
+
|
129
|
+
# Public
|
130
|
+
def default_url
|
131
|
+
@options.default_url
|
132
|
+
end
|
86
133
|
end
|
87
134
|
end
|
data/test/app_test.rb
CHANGED
@@ -10,7 +10,7 @@ module Guillotine
|
|
10
10
|
|
11
11
|
def test_adding_a_link_returns_code
|
12
12
|
url = 'http://github.com'
|
13
|
-
post '/', :url => url
|
13
|
+
post '/', :url => url
|
14
14
|
assert_equal 201, last_response.status
|
15
15
|
assert code_url = last_response.headers['Location']
|
16
16
|
code = code_url.gsub(/.*\//, '')
|
@@ -20,6 +20,60 @@ module Guillotine
|
|
20
20
|
assert_equal url, last_response.headers['Location']
|
21
21
|
end
|
22
22
|
|
23
|
+
def test_adding_a_link_with_query_params_strips_query
|
24
|
+
query_url = 'http://github.com?a=1'
|
25
|
+
url = 'http://github.com'
|
26
|
+
post '/', :url => query_url
|
27
|
+
assert_equal 201, last_response.status
|
28
|
+
assert code_url = last_response.headers['Location']
|
29
|
+
code = code_url.gsub(/.*\//, '')
|
30
|
+
|
31
|
+
get "/#{code}"
|
32
|
+
assert_equal 302, last_response.status
|
33
|
+
assert_equal url, last_response.headers['Location']
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_adding_a_link_with_query_params_returns_code
|
37
|
+
with_service :strip_query => false do
|
38
|
+
url = 'http://github.com?a=1'
|
39
|
+
post '/', :url => url
|
40
|
+
assert_equal 201, last_response.status
|
41
|
+
assert code_url = last_response.headers['Location']
|
42
|
+
code = code_url.gsub(/.*\//, '')
|
43
|
+
|
44
|
+
get "/#{code}"
|
45
|
+
assert_equal 302, last_response.status
|
46
|
+
assert_equal url, last_response.headers['Location']
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def test_adding_a_link_with_anchor_strips_anchor
|
51
|
+
query_url = 'http://github.com?a=1#a'
|
52
|
+
url = 'http://github.com'
|
53
|
+
post '/', :url => query_url
|
54
|
+
assert_equal 201, last_response.status
|
55
|
+
assert code_url = last_response.headers['Location']
|
56
|
+
code = code_url.gsub(/.*\//, '')
|
57
|
+
|
58
|
+
get "/#{code}"
|
59
|
+
assert_equal 302, last_response.status
|
60
|
+
assert_equal url, last_response.headers['Location']
|
61
|
+
end
|
62
|
+
|
63
|
+
def test_adding_a_link_with_anchor_params_returns_code
|
64
|
+
with_service :strip_anchor => false do
|
65
|
+
url = 'http://github.com#a'
|
66
|
+
post '/', :url => url
|
67
|
+
assert_equal 201, last_response.status
|
68
|
+
assert code_url = last_response.headers['Location']
|
69
|
+
code = code_url.gsub(/.*\//, '')
|
70
|
+
|
71
|
+
get "/#{code}"
|
72
|
+
assert_equal 302, last_response.status
|
73
|
+
assert_equal url, last_response.headers['Location']
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
23
77
|
def test_adding_duplicate_link_returns_same_code
|
24
78
|
url = 'http://github.com'
|
25
79
|
code = ADAPTER.add url
|
@@ -101,23 +155,41 @@ module Guillotine
|
|
101
155
|
end
|
102
156
|
|
103
157
|
def test_reject_shortened_url_from_other_domain_by_regex
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
158
|
+
with_service /abc\.com$/ do
|
159
|
+
post '/', :url => 'http://github.com'
|
160
|
+
assert_equal 422, last_response.status
|
161
|
+
assert_match /must match \/abc\\.com/, last_response.body
|
108
162
|
|
109
|
-
|
110
|
-
|
163
|
+
post '/', :url => 'http://abc.com/def'
|
164
|
+
assert_equal 201, last_response.status
|
111
165
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
166
|
+
post '/', :url => 'http://www.abc.com/def'
|
167
|
+
assert_equal 201, last_response.status
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def test_get_without_code_returns_default_url
|
172
|
+
with_service :default_url => 'http://google.com' do
|
173
|
+
get '/'
|
174
|
+
assert_equal "http://google.com", last_response.headers['location']
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def test_get_without_code_no_default_url
|
179
|
+
get '/'
|
180
|
+
assert_equal nil, last_response.headers['location']
|
116
181
|
end
|
117
182
|
|
118
183
|
def app
|
119
184
|
App
|
120
185
|
end
|
186
|
+
|
187
|
+
def with_service(options)
|
188
|
+
App.set :service, Service.new(ADAPTER, options)
|
189
|
+
yield
|
190
|
+
ensure
|
191
|
+
App.set :service, SERVICE
|
192
|
+
end
|
121
193
|
end
|
122
194
|
end
|
123
195
|
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require File.expand_path('../helper', __FILE__)
|
2
|
+
|
3
|
+
begin
|
4
|
+
require "rubygems"
|
5
|
+
require "cassandra"
|
6
|
+
require 'cassandra/mock'
|
7
|
+
|
8
|
+
class CassandraAdapterTest < Guillotine::TestCase
|
9
|
+
@test_schema = JSON.parse(File.read(File.join(File.expand_path(File.dirname(__FILE__)), '..','config', 'cassandra_config.json')))
|
10
|
+
@cassandra_mock = Cassandra::Mock.new('url_shortener', @test_schema)
|
11
|
+
@cassandra_mock.clear_keyspace!
|
12
|
+
ADAPTER = Guillotine::CassandraAdapter.new @cassandra_mock
|
13
|
+
|
14
|
+
def setup
|
15
|
+
@db = ADAPTER
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_adding_a_link_returns_code
|
19
|
+
code = @db.add 'abc'
|
20
|
+
assert_equal 'abc', @db.find(code)
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_adding_duplicate_link_returns_same_code
|
24
|
+
code = @db.add 'abc'
|
25
|
+
assert_equal code, @db.add('abc')
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_adds_url_with_custom_code
|
29
|
+
assert_equal 'code', @db.add('def', 'code')
|
30
|
+
assert_equal 'def', @db.find('code')
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_clashing_urls_raises_error
|
34
|
+
code = @db.add 'abc'
|
35
|
+
assert_raise Guillotine::DuplicateCodeError do
|
36
|
+
@db.add 'ghi', code
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_missing_code
|
41
|
+
assert_nil @db.find('missing')
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_gets_code_for_url
|
45
|
+
code = @db.add 'abc'
|
46
|
+
assert_equal code, @db.code_for('abc')
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_clears_code_for_url
|
50
|
+
code = @db.add 'abc'
|
51
|
+
assert_equal 'abc', @db.find(code)
|
52
|
+
|
53
|
+
@db.clear 'abc'
|
54
|
+
|
55
|
+
assert_nil @db.find(code)
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_read_only
|
59
|
+
Guillotine::CassandraAdapter.new @cassandra_mock, true
|
60
|
+
code = @db.add 'abc'
|
61
|
+
assert_equal nil, code
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
rescue LoadError
|
66
|
+
puts "Skipping Cassandra tests: #{$!}"
|
67
|
+
end
|
data/test/helper.rb
CHANGED
@@ -0,0 +1,59 @@
|
|
1
|
+
require File.expand_path('../helper', __FILE__)
|
2
|
+
|
3
|
+
module Guillotine
|
4
|
+
class OptionsTest < TestCase
|
5
|
+
def test_parses_from_options
|
6
|
+
options = Service::Options.new
|
7
|
+
assert_equal options.object_id, Service::Options.from(options).object_id
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_parses_from_string
|
11
|
+
options = Service::Options.from('abc')
|
12
|
+
assert_equal 'abc', options.required_host
|
13
|
+
assert options.strip_query?
|
14
|
+
assert options.strip_anchor?
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_parses_from_regex
|
18
|
+
options = Service::Options.from(/abc/)
|
19
|
+
assert_equal /abc/, options.required_host
|
20
|
+
assert options.strip_query?
|
21
|
+
assert options.strip_anchor?
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_parses_from_hash
|
25
|
+
options = Service::Options.from(:strip_query => true,
|
26
|
+
:strip_anchor => false)
|
27
|
+
assert_nil options.required_host
|
28
|
+
assert options.strip_query?
|
29
|
+
assert !options.strip_anchor?
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_parses_from_bad_hash
|
33
|
+
assert_raises NameError do
|
34
|
+
Service::Options.from :foo => 1
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_parses_from_unknown
|
39
|
+
assert_raises ArgumentError do
|
40
|
+
Service::Options.from 123
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_parses_from_empty_string
|
45
|
+
options = Service::Options.from('')
|
46
|
+
assert_nil options.required_host
|
47
|
+
assert options.strip_query?
|
48
|
+
assert options.strip_anchor?
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_parses_from_nil
|
52
|
+
options = Service::Options.from(nil)
|
53
|
+
assert_nil options.required_host
|
54
|
+
assert options.strip_query?
|
55
|
+
assert options.strip_anchor?
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
data/test/service_test.rb
CHANGED
@@ -9,7 +9,19 @@ module Guillotine
|
|
9
9
|
|
10
10
|
def test_adding_a_link_returns_code
|
11
11
|
url = 'http://github.com'
|
12
|
-
status, head, body = @service.create(url
|
12
|
+
status, head, body = @service.create(url)
|
13
|
+
assert_equal 201, status
|
14
|
+
assert code_url = head['Location']
|
15
|
+
code = code_url.gsub(/.*\//, '')
|
16
|
+
|
17
|
+
status, head, body = @service.get(code)
|
18
|
+
assert_equal 302, status
|
19
|
+
assert_equal url, head['Location']
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_adding_a_link_with_query_param_returns_code
|
23
|
+
url = 'http://github.com?a=1'
|
24
|
+
status, head, body = @service.create(url)
|
13
25
|
assert_equal 201, status
|
14
26
|
assert code_url = head['Location']
|
15
27
|
code = code_url.gsub(/.*\//, '')
|
@@ -110,6 +122,23 @@ module Guillotine
|
|
110
122
|
status, head, body = service.create('http://www.abc.com/def')
|
111
123
|
assert_equal 201, status
|
112
124
|
end
|
125
|
+
|
126
|
+
def test_fixed_charset_code
|
127
|
+
@db = MemoryAdapter.new
|
128
|
+
length = 4
|
129
|
+
char_set = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']
|
130
|
+
@service = Service.new @db, :length => length, :charset => char_set
|
131
|
+
|
132
|
+
url = 'http://github.com'
|
133
|
+
status, head, body = @service.create(url)
|
134
|
+
assert_equal 201, status
|
135
|
+
assert code_url = head['Location']
|
136
|
+
|
137
|
+
assert_equal 4, code_url.length
|
138
|
+
code_url.each_char do |c|
|
139
|
+
assert char_set.include?(c)
|
140
|
+
end
|
141
|
+
end
|
113
142
|
end
|
114
143
|
end
|
115
144
|
|
metadata
CHANGED
@@ -1,64 +1,82 @@
|
|
1
|
-
--- !ruby/object:Gem::Specification
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
2
|
name: guillotine
|
3
|
-
version: !ruby/object:Gem::Version
|
4
|
-
|
5
|
-
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 1
|
7
|
+
- 3
|
8
|
+
- 0
|
9
|
+
version: 1.3.0
|
6
10
|
platform: ruby
|
7
|
-
authors:
|
11
|
+
authors:
|
8
12
|
- Rick Olson
|
9
13
|
autorequire:
|
10
14
|
bindir: bin
|
11
15
|
cert_chain: []
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
16
|
+
|
17
|
+
date: 2012-08-19 00:00:00 -07:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
type: :runtime
|
22
|
+
version_requirements: &id001 !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
19
24
|
- - ~>
|
20
|
-
- !ruby/object:Gem::Version
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
segments:
|
27
|
+
- 1
|
28
|
+
- 2
|
29
|
+
- 6
|
21
30
|
version: 1.2.6
|
22
|
-
|
31
|
+
name: sinatra
|
32
|
+
requirement: *id001
|
23
33
|
prerelease: false
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
none: false
|
29
|
-
requirements:
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
type: :runtime
|
36
|
+
version_requirements: &id002 !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
30
38
|
- - ~>
|
31
|
-
- !ruby/object:Gem::Version
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
segments:
|
41
|
+
- 2
|
42
|
+
- 2
|
43
|
+
- 6
|
32
44
|
version: 2.2.6
|
33
|
-
|
45
|
+
name: addressable
|
46
|
+
requirement: *id002
|
34
47
|
prerelease: false
|
35
|
-
|
36
|
-
- !ruby/object:Gem::Dependency
|
37
|
-
name: rack-test
|
38
|
-
requirement: &70118903830520 !ruby/object:Gem::Requirement
|
39
|
-
none: false
|
40
|
-
requirements:
|
41
|
-
- - ! '>='
|
42
|
-
- !ruby/object:Gem::Version
|
43
|
-
version: '0'
|
48
|
+
- !ruby/object:Gem::Dependency
|
44
49
|
type: :development
|
50
|
+
version_requirements: &id003 !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
segments:
|
55
|
+
- 0
|
56
|
+
version: "0"
|
57
|
+
name: rack-test
|
58
|
+
requirement: *id003
|
45
59
|
prerelease: false
|
46
|
-
version_requirements: *70118903830520
|
47
60
|
description: Adaptable private URL shortener
|
48
61
|
email: technoweenie@gmail.com
|
49
62
|
executables: []
|
63
|
+
|
50
64
|
extensions: []
|
65
|
+
|
51
66
|
extra_rdoc_files: []
|
52
|
-
|
67
|
+
|
68
|
+
files:
|
53
69
|
- CHANGELOG.md
|
54
70
|
- Gemfile
|
55
71
|
- LICENSE
|
56
72
|
- README.md
|
57
73
|
- Rakefile
|
74
|
+
- config/cassandra_config.json
|
58
75
|
- config/haproxy.riak.cfg
|
59
76
|
- guillotine.gemspec
|
60
77
|
- lib/guillotine.rb
|
61
78
|
- lib/guillotine/adapters/active_record_adapter.rb
|
79
|
+
- lib/guillotine/adapters/cassandra_adapter.rb
|
62
80
|
- lib/guillotine/adapters/memory_adapter.rb
|
63
81
|
- lib/guillotine/adapters/mongo_adapter.rb
|
64
82
|
- lib/guillotine/adapters/redis_adapter.rb
|
@@ -70,44 +88,54 @@ files:
|
|
70
88
|
- script/cibuild
|
71
89
|
- test/active_record_adapter_test.rb
|
72
90
|
- test/app_test.rb
|
91
|
+
- test/cassandra_adapter_test.rb
|
73
92
|
- test/helper.rb
|
74
93
|
- test/host_checker_test.rb
|
75
94
|
- test/memory_adapter_test.rb
|
76
95
|
- test/mongo_adapter_test.rb
|
96
|
+
- test/options_test.rb
|
77
97
|
- test/redis_adapter_test.rb
|
78
98
|
- test/riak_adapter_test.rb
|
79
99
|
- test/sequel_adapter_test.rb
|
80
100
|
- test/service_test.rb
|
101
|
+
has_rdoc: true
|
81
102
|
homepage: https://github.com/technoweenie/gitio
|
82
103
|
licenses: []
|
104
|
+
|
83
105
|
post_install_message:
|
84
106
|
rdoc_options: []
|
85
|
-
|
107
|
+
|
108
|
+
require_paths:
|
86
109
|
- lib
|
87
|
-
required_ruby_version: !ruby/object:Gem::Requirement
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
requirements:
|
96
|
-
- -
|
97
|
-
- !ruby/object:Gem::Version
|
98
|
-
|
110
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
111
|
+
requirements:
|
112
|
+
- - ">="
|
113
|
+
- !ruby/object:Gem::Version
|
114
|
+
segments:
|
115
|
+
- 0
|
116
|
+
version: "0"
|
117
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
118
|
+
requirements:
|
119
|
+
- - ">="
|
120
|
+
- !ruby/object:Gem::Version
|
121
|
+
segments:
|
122
|
+
- 0
|
123
|
+
version: "0"
|
99
124
|
requirements: []
|
125
|
+
|
100
126
|
rubyforge_project: guillotine
|
101
|
-
rubygems_version: 1.
|
127
|
+
rubygems_version: 1.3.6
|
102
128
|
signing_key:
|
103
129
|
specification_version: 2
|
104
130
|
summary: Adaptable private URL shortener
|
105
|
-
test_files:
|
131
|
+
test_files:
|
106
132
|
- test/active_record_adapter_test.rb
|
107
133
|
- test/app_test.rb
|
134
|
+
- test/cassandra_adapter_test.rb
|
108
135
|
- test/host_checker_test.rb
|
109
136
|
- test/memory_adapter_test.rb
|
110
137
|
- test/mongo_adapter_test.rb
|
138
|
+
- test/options_test.rb
|
111
139
|
- test/redis_adapter_test.rb
|
112
140
|
- test/riak_adapter_test.rb
|
113
141
|
- test/sequel_adapter_test.rb
|