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 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
  ```
@@ -0,0 +1,10 @@
1
+ {
2
+ "url_shortener":{
3
+ "urls":{
4
+ "comparator_type":"org.apache.cassandra.db.marshal.UTF8Type",
5
+ "column_type":"Standard"},
6
+ "codes":{
7
+ "comparator_type":"org.apache.cassandra.db.marshal.UTF8Type",
8
+ "column_type":"Standard"}
9
+ }
10
+ }
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.2.1'
17
- s.date = '2012-06-04'
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.2.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
- # Public: Shortens a given URL to a short code.
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 - A String 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! /\s/, ''
47
- url.gsub! /(\#|\?).*/, ''
48
- Addressable::URI.parse url
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 - The String URL to shorten and store.
14
- # code - Optional String code for the URL.
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 ||= shorten url
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 - The String URL to shorten and store.
12
- # code - Optional String code for the URL.
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 ||= shorten(url)
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 - The String URL to shorten and store.
19
- # code - Optional String code for the URL.
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 || shorten(url))
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 :url, :_id => code
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 :code, :url => url
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 - The String URL to shorten and store.
13
- # code - Optional String code for the URL.
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 ||= shorten(url)
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 - The String URL to shorten and store.
22
- # code - Optional String code for the URL.
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 ||= shorten url
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 - The String URL to shorten and store.
11
- # code - Optional String code for the URL.
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 ||= shorten url
20
+ code = get_code(url, code, options)
20
21
  begin
21
22
  @table << {:url => url, :code => code}
22
23
  rescue Sequel::DatabaseError
@@ -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)
@@ -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, required_host = nil)
54
+ def initialize(db, value = nil)
15
55
  @db = db
16
- @host_check = HostChecker.matching(required_host)
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" => @db.parse_url(url).to_s}]
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
- @host_check.call url
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 = str.to_s
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 + '?a=1'
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
- App.set :service, Service.new(ADAPTER, /abc\.com$/)
105
- post '/', :url => 'http://github.com'
106
- assert_equal 422, last_response.status
107
- assert_match /must match \/abc\\.com/, last_response.body
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
- post '/', :url => 'http://abc.com/def'
110
- assert_equal 201, last_response.status
163
+ post '/', :url => 'http://abc.com/def'
164
+ assert_equal 201, last_response.status
111
165
 
112
- post '/', :url => 'http://www.abc.com/def'
113
- assert_equal 201, last_response.status
114
- ensure
115
- App.set :service, SERVICE
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
@@ -3,4 +3,7 @@ require File.expand_path('../../lib/guillotine', __FILE__)
3
3
  require 'rack/test'
4
4
 
5
5
  class Guillotine::TestCase < Test::Unit::TestCase
6
+ def default_test
7
+ end
6
8
  end
9
+
@@ -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 + '?a=1')
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
- version: 1.2.1
5
- prerelease:
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
- date: 2012-06-04 00:00:00.000000000 Z
13
- dependencies:
14
- - !ruby/object:Gem::Dependency
15
- name: sinatra
16
- requirement: &70118903831360 !ruby/object:Gem::Requirement
17
- none: false
18
- requirements:
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
- type: :runtime
31
+ name: sinatra
32
+ requirement: *id001
23
33
  prerelease: false
24
- version_requirements: *70118903831360
25
- - !ruby/object:Gem::Dependency
26
- name: addressable
27
- requirement: &70118903830900 !ruby/object:Gem::Requirement
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
- type: :runtime
45
+ name: addressable
46
+ requirement: *id002
34
47
  prerelease: false
35
- version_requirements: *70118903830900
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
- files:
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
- require_paths:
107
+
108
+ require_paths:
86
109
  - lib
87
- required_ruby_version: !ruby/object:Gem::Requirement
88
- none: false
89
- requirements:
90
- - - ! '>='
91
- - !ruby/object:Gem::Version
92
- version: '0'
93
- required_rubygems_version: !ruby/object:Gem::Requirement
94
- none: false
95
- requirements:
96
- - - ! '>='
97
- - !ruby/object:Gem::Version
98
- version: '0'
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.8.11
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