Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Merge pull request #10 from technoweenie/rajeshucsb-master

Rajeshucsb master
  • Loading branch information...
commit 1ae168f53ec66fda6f2f780391aacdf31ca21cac 2 parents 6572b92 + 8b28a22
risk danger olson authored
37 README.md
Source Rendered
@@ -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.
10 config/cassandra_config.json
... ... @@ -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 +}
4 guillotine.gemspec
@@ -14,7 +14,7 @@ Gem::Specification.new do |s|
14 14 ## the sub! line in the Rakefile
15 15 s.name = 'guillotine'
16 16 s.version = '1.2.1'
17   - s.date = '2012-06-04'
  17 + s.date = '2012-07-02'
18 18 s.rubyforge_project = 'guillotine'
19 19
20 20 ## Make sure your summary is short. The description may be as long
@@ -56,6 +56,7 @@ Gem::Specification.new do |s|
56 56 guillotine.gemspec
57 57 lib/guillotine.rb
58 58 lib/guillotine/adapters/active_record_adapter.rb
  59 + lib/guillotine/adapters/cassandra_adapter.rb
59 60 lib/guillotine/adapters/memory_adapter.rb
60 61 lib/guillotine/adapters/mongo_adapter.rb
61 62 lib/guillotine/adapters/redis_adapter.rb
@@ -67,6 +68,7 @@ Gem::Specification.new do |s|
67 68 script/cibuild
68 69 test/active_record_adapter_test.rb
69 70 test/app_test.rb
  71 + test/cassandra_adapter_test.rb
70 72 test/helper.rb
71 73 test/host_checker_test.rb
72 74 test/memory_adapter_test.rb
42 lib/guillotine.rb
@@ -23,7 +23,7 @@ def initialize(existing_url, new_url, code)
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,6 +37,28 @@ def shorten(url)
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 64 # url - A String URL.
@@ -49,6 +71,23 @@ def parse_url(url, options)
49 71 url.gsub!(/\#.*/, '') if options.strip_anchor?
50 72 Addressable::URI.parse(url)
51 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 +
52 91 end
53 92
54 93 dir = File.expand_path '../guillotine/adapters', __FILE__
@@ -58,6 +97,7 @@ def parse_url(url, options)
58 97 autoload :ActiveRecordAdapter, dir + "/active_record_adapter"
59 98 autoload :RedisAdapter, dir + "/redis_adapter"
60 99 autoload :MongoAdapter, dir + "/mongo_adapter"
  100 + autoload :CassandraAdapter, dir + "/cassandra_adapter"
61 101
62 102 dir = File.expand_path '../guillotine', __FILE__
63 103 autoload :App, "#{dir}/app"
10 lib/guillotine/adapters/active_record_adapter.rb
@@ -10,16 +10,18 @@ def initialize(config)
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
67 lib/guillotine/adapters/cassandra_adapter.rb
... ... @@ -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
11 lib/guillotine/adapters/memory_adapter.rb
@@ -8,16 +8,18 @@ def initialize
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 @@ def reset
62 64 end
63 65 end
64 66 end
  67 +
14 lib/guillotine/adapters/mongo_adapter.rb
@@ -15,13 +15,14 @@ def initialize(collection)
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 @@ def add(url, code = nil)
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 @@ def find(code)
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 @@ def insert(url, code)
66 67 end
67 68 end
68 69 end
  70 +
10 lib/guillotine/adapters/redis_adapter.rb
@@ -9,16 +9,18 @@ def initialize(redis)
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
11 lib/guillotine/adapters/riak_adapter.rb
@@ -17,13 +17,14 @@ def initialize(code_bucket, url_bucket = nil)
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 @@ def add(url, code = nil)
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
9 lib/guillotine/adapters/sequel_adapter.rb
@@ -7,16 +7,17 @@ def initialize(db)
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
7 lib/guillotine/app.rb
@@ -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)
18 lib/guillotine/service.rb
@@ -3,7 +3,11 @@ class Service
3 3 # Deprecated until v2
4 4 NullChecker = Guillotine::HostChecker
5 5
6   - class Options < Struct.new(:required_host, :strip_query, :strip_anchor)
  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)
7 11 def self.from(value)
8 12 case value
9 13 when nil, "" then new
@@ -28,6 +32,10 @@ def strip_anchor?
28 32 strip_anchor != false
29 33 end
30 34
  35 + def with_charset?
  36 + !(length.nil? || charset.nil?)
  37 + end
  38 +
31 39 def host_checker
32 40 @host_checker ||= HostChecker.matching(required_host)
33 41 end
@@ -76,7 +84,7 @@ def create(url, code = nil)
76 84 end
77 85
78 86 begin
79   - if code = @db.add(url.to_s, code)
  87 + if code = @db.add(url.to_s, code, @options)
80 88 [201, {"Location" => code}]
81 89 else
82 90 [422, {}, "Unable to shorten #{url}"]
@@ -113,8 +121,14 @@ def ensure_url(str)
113 121 end
114 122 end
115 123
  124 + # Internal
116 125 def parse_url(url)
117 126 @db.parse_url(url, @options)
118 127 end
  128 +
  129 + # Public
  130 + def default_url
  131 + @options.default_url
  132 + end
119 133 end
120 134 end
12 test/app_test.rb
@@ -168,6 +168,18 @@ def test_reject_shortened_url_from_other_domain_by_regex
168 168 end
169 169 end
170 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']
  181 + end
  182 +
171 183 def app
172 184 App
173 185 end
67 test/cassandra_adapter_test.rb
... ... @@ -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
17 test/service_test.rb
@@ -122,6 +122,23 @@ def test_reject_shortened_url_from_other_domain_by_regex
122 122 status, head, body = service.create('http://www.abc.com/def')
123 123 assert_equal 201, status
124 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
125 142 end
126 143 end
127 144

0 comments on commit 1ae168f

Please sign in to comment.
Something went wrong with that request. Please try again.