guillotine 1.2.1 → 1.3.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.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