Permalink
Browse files

Merge pull request #10 from technoweenie/rajeshucsb-master

Rajeshucsb master
  • Loading branch information...
2 parents 6572b92 + 8b28a22 commit 1ae168f53ec66fda6f2f780391aacdf31ca21cac @technoweenie committed Aug 19, 2012
View
37 README.md
@@ -50,7 +50,7 @@ require 'guillotine'
require 'sequel'
module MyApp
class App < Guillotine::App
- db = Sequel.sqlite
+ db = Sequel.sqlite
adapter = Guillotine::Adapters::SequelAdapter.new(db)
set :service => Guillotine::Service.new(adapter)
end
@@ -106,6 +106,41 @@ module MyApp
end
```
+## Cassandra
+
+you can use Cassandra!
+
+```ruby
+require 'guillotine'
+require 'cassandra'
+
+module MyApp
+ class App < Guillotine::App
+ cassandra = Cassandra.new('url_shortener', '127.0.0.1:9160')
+ adapter = Guillotine::Adapters::CassandraAdapter.new(cassandra)
+
+ set :service => Guillotine::Service.new(adapter)
+ end
+end
+```
+
+You need to create keyspace and column families as below
+
+```sql
+CREATE KEYSPACE url_shortener;
+USE url_shortener;
+
+CREATE COLUMN FAMILY urls
+WITH comparator = UTF8Type
+AND key_validation_class=UTF8Type
+AND column_metadata = [{column_name: code, validation_class: UTF8Type}];
+
+CREATE COLUMN FAMILY codes
+WITH comparator = UTF8Type
+AND key_validation_class=UTF8Type
+AND column_metadata = [{column_name: url, validation_class: UTF8Type}];
+```
+
## Domain Restriction
You can restrict what domains that Guillotine will shorten.
View
10 config/cassandra_config.json
@@ -0,0 +1,10 @@
+{
+ "url_shortener":{
+ "urls":{
+ "comparator_type":"org.apache.cassandra.db.marshal.UTF8Type",
+ "column_type":"Standard"},
+ "codes":{
+ "comparator_type":"org.apache.cassandra.db.marshal.UTF8Type",
+ "column_type":"Standard"}
+ }
+}
View
4 guillotine.gemspec
@@ -14,7 +14,7 @@ Gem::Specification.new do |s|
## the sub! line in the Rakefile
s.name = 'guillotine'
s.version = '1.2.1'
- s.date = '2012-06-04'
+ s.date = '2012-07-02'
s.rubyforge_project = 'guillotine'
## Make sure your summary is short. The description may be as long
@@ -56,6 +56,7 @@ Gem::Specification.new do |s|
guillotine.gemspec
lib/guillotine.rb
lib/guillotine/adapters/active_record_adapter.rb
+ lib/guillotine/adapters/cassandra_adapter.rb
lib/guillotine/adapters/memory_adapter.rb
lib/guillotine/adapters/mongo_adapter.rb
lib/guillotine/adapters/redis_adapter.rb
@@ -67,6 +68,7 @@ Gem::Specification.new do |s|
script/cibuild
test/active_record_adapter_test.rb
test/app_test.rb
+ test/cassandra_adapter_test.rb
test/helper.rb
test/host_checker_test.rb
test/memory_adapter_test.rb
View
42 lib/guillotine.rb
@@ -23,7 +23,7 @@ def initialize(existing_url, new_url, code)
# use whatever you want, as long as it implements the #add and #find
# methods. See MemoryAdapter for a simple solution.
class Adapter
- # Public: Shortens a given URL to a short code.
+ # Internal: Shortens a given URL to a short code.
#
# 1) MD5 hash the URL to the hexdigest
# 2) Convert it to a Bignum
@@ -37,6 +37,28 @@ def shorten(url)
Base64.urlsafe_encode64([Digest::MD5.hexdigest(url).to_i(16)].pack("N")).sub(/==\n?$/, '')
end
+ # Internal: Shortens a URL with a specific character set at a certain
+ # length.
+ #
+ # url - String URL to shorten.
+ # length - Optional Integer maximum length of the short code desired.
+ # charset - Optional Array of String characters which will be present in
+ # short code. eg. ['a', 'b', 'c', 'd', 'e', 'f']
+ #
+ # Returns an encoded String code for the URL.
+ def shorten_fixed_charset(url, length, char_set)
+ number = (Digest::MD5.hexdigest(url).to_i(16) % (char_set.size**length))
+
+ code = ""
+
+ while (number > 0)
+ code = code + char_set[number % char_set.size]
+ number /= char_set.size
+ end
+
+ code
+ end
+
# Parses and sanitizes a URL.
#
# url - A String URL.
@@ -49,6 +71,23 @@ def parse_url(url, options)
url.gsub!(/\#.*/, '') if options.strip_anchor?
Addressable::URI.parse(url)
end
+
+ # Internal: Shortens a URL with the given options.
+ #
+ # url - A String URL.
+ # code - Optional String code.
+ # options - Optional Guillotine::Service::Options to specify how the code
+ # is generated.
+ #
+ # returns a String code.
+ def get_code(url, code = nil, options = nil)
+ code ||= if options && options.with_charset?
+ shorten_fixed_charset(url, options.length, options.charset)
+ else
+ shorten(url)
+ end
+ end
+
end
dir = File.expand_path '../guillotine/adapters', __FILE__
@@ -58,6 +97,7 @@ def parse_url(url, options)
autoload :ActiveRecordAdapter, dir + "/active_record_adapter"
autoload :RedisAdapter, dir + "/redis_adapter"
autoload :MongoAdapter, dir + "/mongo_adapter"
+ autoload :CassandraAdapter, dir + "/cassandra_adapter"
dir = File.expand_path '../guillotine', __FILE__
autoload :App, "#{dir}/app"
View
10 lib/guillotine/adapters/active_record_adapter.rb
@@ -10,16 +10,18 @@ def initialize(config)
# Public: Stores the shortened version of a URL.
#
- # url - The String URL to shorten and store.
- # code - Optional String code for the URL.
+ # url - The String URL to shorten and store.
+ # code - Optional String code for the URL.
+ # options - Optional Guillotine::Service::Options
#
# Returns the unique String code for the URL. If the URL is added
# multiple times, this should return the same code.
- def add(url, code = nil)
+ def add(url, code = nil, options = nil)
if row = Url.select(:code).where(:url => url).first
row[:code]
else
- code ||= shorten url
+ code = get_code(url, code, options)
+
begin
Url.create :url => url, :code => code
rescue ActiveRecord::RecordNotUnique, ActiveRecord::StatementInvalid
View
67 lib/guillotine/adapters/cassandra_adapter.rb
@@ -0,0 +1,67 @@
+module Guillotine
+ class CassandraAdapter < Adapter
+ # Public: Initialise the adapter with a Redis instance.
+ #
+ # cassandra - A Cassandra instance to persist urls and codes to.
+ def initialize(cassandra, read_only = false)
+ @cassandra = cassandra
+ @read_only = read_only
+ end
+
+ # Public: Stores the shortened version of a URL.
+ #
+ # url - The String URL to shorten and store.
+ # code - Optional String code for the URL.
+ # options - Optional Guillotine::Service::Options
+ #
+ # Returns the unique String code for the URL. If the URL is added
+ # multiple times, this should return the same code.
+ def add(url, code = nil, options = nil)
+ return if @read_only
+ if existing_code = code_for(url)
+ existing_code
+ else
+ code = get_code(url, code, options)
+
+ if existing_url = find(code)
+ raise DuplicateCodeError.new(existing_url, url, code) if url != existing_url
+ end
+ @cassandra.insert("codes", code, 'url' => url)
+ @cassandra.insert("urls", url, 'code' => code)
+ code
+ end
+ end
+
+ # Public: Retrieves a URL from the code.
+ #
+ # code - The String code to lookup the URL.
+ #
+ # Returns the String URL, or nil if none is found.
+ def find(code)
+ obj = @cassandra.get("codes", code)
+ obj.nil? ? nil : obj["url"]
+ end
+
+ # Public: Retrieves the code for a given URL.
+ #
+ # url - The String URL to lookup.
+ #
+ # Returns the String code, or nil if none is found.
+ def code_for(url)
+ obj = @cassandra.get("urls", url)
+ obj.nil? ? nil : obj["code"]
+ end
+
+ # Public: Removes the assigned short code for a URL.
+ #
+ # url - The String URL to remove.
+ #
+ # Returns nothing.
+ def clear(url)
+ if code = code_for(url)
+ @cassandra.remove("urls", url)
+ @cassandra.remove("codes", code)
+ end
+ end
+ end
+end
View
11 lib/guillotine/adapters/memory_adapter.rb
@@ -8,16 +8,18 @@ def initialize
# Public: Stores the shortened version of a URL.
#
- # url - The String URL to shorten and store.
- # code - Optional String code for the URL.
+ # url - The String URL to shorten and store.
+ # code - Optional String code for the URL.
+ # options - Optional Guillotine::Service::Options
#
# Returns the unique String code for the URL. If the URL is added
# multiple times, this should return the same code.
- def add(url, code = nil)
+ def add(url, code = nil, options = nil)
if existing_code = @urls[url]
existing_code
else
- code ||= shorten(url)
+ code = get_code(url, code, options)
+
if existing_url = @hash[code]
raise DuplicateCodeError.new(existing_url, url, code) if url != existing_url
end
@@ -62,3 +64,4 @@ def reset
end
end
end
+
View
14 lib/guillotine/adapters/mongo_adapter.rb
@@ -15,13 +15,14 @@ def initialize(collection)
# Public: Stores the shortened version of a URL.
#
- # url - The String URL to shorten and store.
- # code - Optional String code for the URL.
+ # url - The String URL to shorten and store.
+ # code - Optional String code for the URL.
+ # options - Optional Guillotine::Service::Options
#
# Returns the unique String code for the URL. If the URL is added
# multiple times, this should return the same code.
- def add(url, code = nil)
- code_for(url) || insert(url, code || shorten(url))
+ def add(url, code = nil, options = nil)
+ code_for(url) || insert(url, get_code(url, code, options))
end
@@ -31,7 +32,7 @@ def add(url, code = nil)
#
# Returns the String URL, or nil if none is found.
def find(code)
- select :url, :_id => code
+ select(:url, :_id => code)
end
# Public: Retrieves the code for a given URL.
@@ -40,7 +41,7 @@ def find(code)
#
# Returns the String code, or nil if none is found.
def code_for(url)
- select :code, :url => url
+ select(:code, :url => url)
end
# Public: Removes the assigned short code for a URL.
@@ -66,3 +67,4 @@ def insert(url, code)
end
end
end
+
View
10 lib/guillotine/adapters/redis_adapter.rb
@@ -9,16 +9,18 @@ def initialize(redis)
# Public: Stores the shortened version of a URL.
#
- # url - The String URL to shorten and store.
- # code - Optional String code for the URL.
+ # url - The String URL to shorten and store.
+ # code - Optional String code for the URL.
+ # options - Optional Guillotine::Service::Options
#
# Returns the unique String code for the URL. If the URL is added
# multiple times, this should return the same code.
- def add(url, code = nil)
+ def add(url, code = nil, options = nil)
if existing_code = @redis.get(url_key(url))
existing_code
else
- code ||= shorten(url)
+ code = get_code(url, code, options)
+
if existing_url = @redis.get(code_key(code))
raise DuplicateCodeError.new(existing_url, url, code) if url != existing_url
end
View
11 lib/guillotine/adapters/riak_adapter.rb
@@ -17,21 +17,22 @@ def initialize(code_bucket, url_bucket = nil)
end
# Public: Stores the shortened version of a URL.
- #
- # url - The String URL to shorten and store.
- # code - Optional String code for the URL.
+ #
+ # url - The String URL to shorten and store.
+ # code - Optional String code for the URL.
+ # options - Optional Guillotine::Service::Options
#
# Returns the unique String code for the URL. If the URL is added
# multiple times, this should return the same code.
- def add(url, code = nil)
+ def add(url, code = nil, options = nil)
sha = url_key url
url_obj = @url_bucket.get_or_new sha, :r => 1
if url_obj.raw_data
fix_url_object(url_obj)
code = url_obj.data
end
- code ||= shorten url
+ code = get_code(url, code, options)
code_obj = @code_bucket.get_or_new code
code_obj.content_type = url_obj.content_type = PLAIN
View
9 lib/guillotine/adapters/sequel_adapter.rb
@@ -7,16 +7,17 @@ def initialize(db)
# Public: Stores the shortened version of a URL.
#
- # url - The String URL to shorten and store.
- # code - Optional String code for the URL.
+ # url - The String URL to shorten and store.
+ # code - Optional String code for the URL.
+ # options - Optional Guillotine::Service::Options
#
# Returns the unique String code for the URL. If the URL is added
# multiple times, this should return the same code.
- def add(url, code = nil)
+ def add(url, code = nil, options = nil)
if existing = code_for(url)
existing
else
- code ||= shorten url
+ code = get_code(url, code, options)
begin
@table << {:url => url, :code => code}
rescue Sequel::DatabaseError
View
7 lib/guillotine/app.rb
@@ -6,6 +6,13 @@ module Guillotine
class App < Sinatra::Base
set :service, nil
+ get "/" do
+ if params[:code].nil?
+ default_url = settings.service.default_url
+ redirect default_url if !default_url.nil?
+ end
+ end
+
get "/:code" do
escaped = Addressable::URI.escape(params[:code])
status, head, body = settings.service.get(escaped)
View
18 lib/guillotine/service.rb
@@ -3,7 +3,11 @@ class Service
# Deprecated until v2
NullChecker = Guillotine::HostChecker
- class Options < Struct.new(:required_host, :strip_query, :strip_anchor)
+ # length - Optional Integer maximum length of the short code desired.
+ # charset - Optional Array of String characters which will be present in
+ # short code. eg. ['a', 'b', 'c', 'd', 'e', 'f']
+ class Options < Struct.new(:required_host, :strip_query, :strip_anchor,
+ :length, :charset, :default_url)
def self.from(value)
case value
when nil, "" then new
@@ -28,6 +32,10 @@ def strip_anchor?
strip_anchor != false
end
+ def with_charset?
+ !(length.nil? || charset.nil?)
+ end
+
def host_checker
@host_checker ||= HostChecker.matching(required_host)
end
@@ -76,7 +84,7 @@ def create(url, code = nil)
end
begin
- if code = @db.add(url.to_s, code)
+ if code = @db.add(url.to_s, code, @options)
[201, {"Location" => code}]
else
[422, {}, "Unable to shorten #{url}"]
@@ -113,8 +121,14 @@ def ensure_url(str)
end
end
+ # Internal
def parse_url(url)
@db.parse_url(url, @options)
end
+
+ # Public
+ def default_url
+ @options.default_url
+ end
end
end
View
12 test/app_test.rb
@@ -168,6 +168,18 @@ def test_reject_shortened_url_from_other_domain_by_regex
end
end
+ def test_get_without_code_returns_default_url
+ with_service :default_url => 'http://google.com' do
+ get '/'
+ assert_equal "http://google.com", last_response.headers['location']
+ end
+ end
+
+ def test_get_without_code_no_default_url
+ get '/'
+ assert_equal nil, last_response.headers['location']
+ end
+
def app
App
end
View
67 test/cassandra_adapter_test.rb
@@ -0,0 +1,67 @@
+require File.expand_path('../helper', __FILE__)
+
+begin
+ require "rubygems"
+ require "cassandra"
+ require 'cassandra/mock'
+
+ class CassandraAdapterTest < Guillotine::TestCase
+ @test_schema = JSON.parse(File.read(File.join(File.expand_path(File.dirname(__FILE__)), '..','config', 'cassandra_config.json')))
+ @cassandra_mock = Cassandra::Mock.new('url_shortener', @test_schema)
+ @cassandra_mock.clear_keyspace!
+ ADAPTER = Guillotine::CassandraAdapter.new @cassandra_mock
+
+ def setup
+ @db = ADAPTER
+ end
+
+ def test_adding_a_link_returns_code
+ code = @db.add 'abc'
+ assert_equal 'abc', @db.find(code)
+ end
+
+ def test_adding_duplicate_link_returns_same_code
+ code = @db.add 'abc'
+ assert_equal code, @db.add('abc')
+ end
+
+ def test_adds_url_with_custom_code
+ assert_equal 'code', @db.add('def', 'code')
+ assert_equal 'def', @db.find('code')
+ end
+
+ def test_clashing_urls_raises_error
+ code = @db.add 'abc'
+ assert_raise Guillotine::DuplicateCodeError do
+ @db.add 'ghi', code
+ end
+ end
+
+ def test_missing_code
+ assert_nil @db.find('missing')
+ end
+
+ def test_gets_code_for_url
+ code = @db.add 'abc'
+ assert_equal code, @db.code_for('abc')
+ end
+
+ def test_clears_code_for_url
+ code = @db.add 'abc'
+ assert_equal 'abc', @db.find(code)
+
+ @db.clear 'abc'
+
+ assert_nil @db.find(code)
+ end
+
+ def test_read_only
+ Guillotine::CassandraAdapter.new @cassandra_mock, true
+ code = @db.add 'abc'
+ assert_equal nil, code
+ end
+ end
+
+rescue LoadError
+ puts "Skipping Cassandra tests: #{$!}"
+end
View
17 test/service_test.rb
@@ -122,6 +122,23 @@ def test_reject_shortened_url_from_other_domain_by_regex
status, head, body = service.create('http://www.abc.com/def')
assert_equal 201, status
end
+
+ def test_fixed_charset_code
+ @db = MemoryAdapter.new
+ length = 4
+ 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']
+ @service = Service.new @db, :length => length, :charset => char_set
+
+ url = 'http://github.com'
+ status, head, body = @service.create(url)
+ assert_equal 201, status
+ assert code_url = head['Location']
+
+ assert_equal 4, code_url.length
+ code_url.each_char do |c|
+ assert char_set.include?(c)
+ end
+ end
end
end

0 comments on commit 1ae168f

Please sign in to comment.