guillotine 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # Changelog
2
+
3
+ ## master
4
+
5
+ * Add a simple wildcard host checker.
6
+
7
+ Guillotine::Service.new(adapter, '*.foo.com')
8
+
9
+ * Guillotine::Adapters has been deprecated until v2. Adapters are now in the
10
+ top level namespace.
11
+ * Memory Adapter can be reset in tests
12
+
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Guillotine
2
2
 
3
- Simple URL Shortener hobby kit.
3
+ Simple URL Shortener hobby kit. Currently used to shorten URLs at GitHub.com, and also available as a an [installable Heroku app](https://github.com/mrtazz/katana).
4
4
 
5
5
  ## USAGE
6
6
 
@@ -11,7 +11,8 @@ The easiest way to use it is with the built-in memory adapter.
11
11
  require 'guillotine'
12
12
  module MyApp
13
13
  class App < Guillotine::App
14
- set :db => Guillotine::Adapters::MemoryAdapter.new
14
+ adapter = Guillotine::Adapters::MemoryAdapter.new
15
+ set :service => Guillotine::Service.new(adapter)
15
16
 
16
17
  get '/' do
17
18
  redirect 'https://homepage.com'
@@ -40,14 +41,49 @@ You can specify your own code too:
40
41
 
41
42
  ## Sequel
42
43
 
43
- The memory adapter sucks though. You probably want to use a DB.
44
+ The memory adapter sucks though. You probably want to use a DB. Check
45
+ out the [Sequel gem](http://sequel.rubyforge.org/) for more examples.
46
+ It'll support SQLite, MySQL, PostgreSQL, and a bunch of other databases.
44
47
 
45
48
  ```ruby
46
49
  require 'guillotine'
47
50
  require 'sequel'
48
51
  module MyApp
49
52
  class App < Guillotine::App
50
- set :db => Guillotine::Adapters::SequelAdapter.new(Sequel.sqlite)
53
+ db = Sequel.sqlite
54
+ adapter = Guillotine::Adapters::SequelAdapter.new(db)
55
+ set :service => Guillotine::Service.new(adapter)
56
+ end
57
+ end
58
+ ```
59
+
60
+ You'll need to initialize the DB schema with something like this
61
+ (depending on which DB you use):
62
+
63
+ ```
64
+ CREATE TABLE IF NOT EXISTS `urls` (
65
+ `url` varchar(255) DEFAULT NULL,
66
+ `code` varchar(255) DEFAULT NULL,
67
+ UNIQUE KEY `url` (`url`),
68
+ UNIQUE KEY `code` (`code`)
69
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
70
+ ```
71
+
72
+ ## Redis
73
+
74
+ Redis works well, too. The sample below is [adapted](https://github.com/mrtazz/katana/blob/master/app.rb) from [Katana](https://github.com/mrtazz/katana), a hosted wrapper around Guillotine designed for Heroku.
75
+
76
+ ```ruby
77
+ require 'guillotine'
78
+ require 'redis'
79
+
80
+ module MyApp
81
+ class App < Guillotine::App
82
+ # use redis adapter with redistogo on Heroku
83
+ uri = URI.parse(ENV["REDISTOGO_URL"])
84
+ redis = Redis.new(:host => uri.host, :port => uri.port, :password => uri.password)
85
+ adapter = Guillotine::Adapters::RedisAdapter.new(redis)
86
+ set :service => Guillotine::Service.new(adapter)
51
87
  end
52
88
  end
53
89
  ```
@@ -64,7 +100,8 @@ module MyApp
64
100
  class App < Guillotine::App
65
101
  client = Riak::Client.new :protocol => 'pbc', :pb_port => 8087
66
102
  bucket = client['guillotine']
67
- set :db => Guillotine::Adapters::RiakAdapter.new(bucket)
103
+ adapter = Guillotine::Adapters::RiakAdapter.new(bucket)
104
+ set :service => Guillotine::Service.new(adapter)
68
105
  end
69
106
  end
70
107
  ```
@@ -77,11 +114,18 @@ You can restrict what domains that Guillotine will shorten.
77
114
  require 'guillotine'
78
115
  module MyApp
79
116
  class App < Guillotine::App
117
+ adapter = Guillotine::Adapters::MemoryAdapter.new
80
118
  # only this domain
81
- set :required_host, 'github.com'
119
+ set :service => Guillotine::Service.new(adapter,
120
+ 'github.com')
82
121
 
83
122
  # or, any *.github.com domain
84
- set :required_host, /(^|\.)github\.com$/
123
+ set :service => Guillotine::Service.new(adapter,
124
+ /(^|\.)github\.com$/)
125
+
126
+ # or set a simple wildcard
127
+ set :service => Guillotine::Servicew.new(adapter,
128
+ '*.github.com')
85
129
  end
86
130
  end
87
131
  ```
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.1.0'
17
- s.date = '2011-11-26'
16
+ s.version = '1.2.0'
17
+ s.date = '2012-06-04'
18
18
  s.rubyforge_project = 'guillotine'
19
19
 
20
20
  ## Make sure your summary is short. The description may be as long
@@ -47,6 +47,7 @@ Gem::Specification.new do |s|
47
47
  ## THE MANIFEST COMMENTS, they are used as delimiters by the task.
48
48
  # = MANIFEST =
49
49
  s.files = %w[
50
+ CHANGELOG.md
50
51
  Gemfile
51
52
  LICENSE
52
53
  README.md
@@ -61,11 +62,13 @@ Gem::Specification.new do |s|
61
62
  lib/guillotine/adapters/riak_adapter.rb
62
63
  lib/guillotine/adapters/sequel_adapter.rb
63
64
  lib/guillotine/app.rb
65
+ lib/guillotine/host_checkers.rb
64
66
  lib/guillotine/service.rb
65
67
  script/cibuild
66
68
  test/active_record_adapter_test.rb
67
69
  test/app_test.rb
68
70
  test/helper.rb
71
+ test/host_checker_test.rb
69
72
  test/memory_adapter_test.rb
70
73
  test/mongo_adapter_test.rb
71
74
  test/redis_adapter_test.rb
data/lib/guillotine.rb CHANGED
@@ -3,13 +3,12 @@ require 'digest/md5'
3
3
  require 'addressable/uri'
4
4
 
5
5
  module Guillotine
6
- VERSION = "1.1.0"
6
+ VERSION = "1.2.0"
7
7
 
8
- dir = File.expand_path '../guillotine', __FILE__
9
- require "#{dir}/service"
10
- autoload :App, "#{dir}/app"
8
+ class Error < StandardError
9
+ end
11
10
 
12
- class DuplicateCodeError < StandardError
11
+ class DuplicateCodeError < Error
13
12
  attr_reader :existing_url, :new_url, :code
14
13
 
15
14
  def initialize(existing_url, new_url, code)
@@ -20,43 +19,59 @@ module Guillotine
20
19
  end
21
20
  end
22
21
 
23
- module Adapters
24
- dir = File.expand_path '../guillotine/adapters', __FILE__
25
- autoload :MemoryAdapter, "#{dir}/memory_adapter"
26
- autoload :SequelAdapter, "#{dir}/sequel_adapter"
27
- autoload :RiakAdapter, "#{dir}/riak_adapter"
28
- autoload :ActiveRecordAdapter, "#{dir}/active_record_adapter"
29
- autoload :RedisAdapter, "#{dir}/redis_adapter"
30
- autoload :MongoAdapter, "#{dir}/mongo_adapter"
31
-
32
- # Adapters handle the storage and retrieval of URLs in the system. You can
33
- # use whatever you want, as long as it implements the #add and #find
34
- # methods. See MemoryAdapter for a simple solution.
35
- class Adapter
36
- # Public: Shortens a given URL to a short code.
37
- #
38
- # 1) MD5 hash the URL to the hexdigest
39
- # 2) Convert it to a Bignum
40
- # 3) Pack it into a bitstring as a big-endian int
41
- # 4) base64-encode the bitstring, remove the trailing junk
42
- #
43
- # url - String URL to shorten.
44
- #
45
- # Returns a unique String code for the URL.
46
- def shorten(url)
47
- Base64.urlsafe_encode64([Digest::MD5.hexdigest(url).to_i(16)].pack("N")).sub(/==\n?$/, '')
48
- end
22
+ # Adapters handle the storage and retrieval of URLs in the system. You can
23
+ # use whatever you want, as long as it implements the #add and #find
24
+ # methods. See MemoryAdapter for a simple solution.
25
+ class Adapter
26
+ # Public: Shortens a given URL to a short code.
27
+ #
28
+ # 1) MD5 hash the URL to the hexdigest
29
+ # 2) Convert it to a Bignum
30
+ # 3) Pack it into a bitstring as a big-endian int
31
+ # 4) base64-encode the bitstring, remove the trailing junk
32
+ #
33
+ # url - String URL to shorten.
34
+ #
35
+ # Returns a unique String code for the URL.
36
+ def shorten(url)
37
+ Base64.urlsafe_encode64([Digest::MD5.hexdigest(url).to_i(16)].pack("N")).sub(/==\n?$/, '')
38
+ end
49
39
 
50
- # Parses and sanitizes a URL.
51
- #
52
- # url - A String URL.
53
- #
54
- # Returns an Addressable::URI.
55
- def parse_url(url)
56
- url.gsub! /\s/, ''
57
- url.gsub! /(\#|\?).*/, ''
58
- Addressable::URI.parse url
40
+ # Parses and sanitizes a URL.
41
+ #
42
+ # url - A String URL.
43
+ #
44
+ # Returns an Addressable::URI.
45
+ def parse_url(url)
46
+ url.gsub! /\s/, ''
47
+ url.gsub! /(\#|\?).*/, ''
48
+ Addressable::URI.parse url
49
+ end
50
+ end
51
+
52
+ dir = File.expand_path '../guillotine/adapters', __FILE__
53
+ autoload :MemoryAdapter, dir + "/memory_adapter"
54
+ autoload :SequelAdapter, dir + "/sequel_adapter"
55
+ autoload :RiakAdapter, dir + "/riak_adapter"
56
+ autoload :ActiveRecordAdapter, dir + "/active_record_adapter"
57
+ autoload :RedisAdapter, dir + "/redis_adapter"
58
+ autoload :MongoAdapter, dir + "/mongo_adapter"
59
+
60
+ dir = File.expand_path '../guillotine', __FILE__
61
+ autoload :App, "#{dir}/app"
62
+
63
+ require "#{dir}/host_checkers"
64
+ require "#{dir}/service"
65
+
66
+ module Adapters
67
+ @@warned = false
68
+ def self.const_missing(*args)
69
+ unless @@warned
70
+ puts "Guillotine::Adapters has been deprecated until v2."
71
+ @@warned = true
59
72
  end
73
+ puts "Change Guillotine::Adapters::#{args.first} => Guillotine::#{args.first}"
74
+ ::Guillotine.const_get(args.first)
60
75
  end
61
76
  end
62
77
  end
@@ -1,79 +1,77 @@
1
1
  require 'active_record'
2
2
 
3
3
  module Guillotine
4
- module Adapters
5
- class ActiveRecordAdapter < Adapter
6
- class Url < ActiveRecord::Base; end
4
+ class ActiveRecordAdapter < Adapter
5
+ class Url < ActiveRecord::Base; end
7
6
 
8
- def initialize(config)
9
- Url.establish_connection config
10
- end
11
-
12
- # Public: Stores the shortened version of a URL.
13
- #
14
- # url - The String URL to shorten and store.
15
- # code - Optional String code for the URL.
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)
20
- if row = Url.select(:code).where(:url => url).first
21
- row[:code]
22
- else
23
- code ||= shorten url
24
- begin
25
- Url.create :url => url, :code => code
26
- rescue ActiveRecord::RecordNotUnique, ActiveRecord::StatementInvalid
27
- row = Url.select(:url).where(:code => code).first
28
- existing_url = row && row[:url]
29
- raise DuplicateCodeError.new(existing_url, url, code)
30
- end
31
- code
32
- end
33
- end
7
+ def initialize(config)
8
+ Url.establish_connection config
9
+ end
34
10
 
35
- # Public: Retrieves a URL from the code.
36
- #
37
- # code - The String code to lookup the URL.
38
- #
39
- # Returns the String URL.
40
- def find(code)
41
- select :url, :code => code
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
+ #
16
+ # Returns the unique String code for the URL. If the URL is added
17
+ # multiple times, this should return the same code.
18
+ def add(url, code = nil)
19
+ if row = Url.select(:code).where(:url => url).first
20
+ row[:code]
21
+ else
22
+ code ||= shorten url
23
+ begin
24
+ Url.create :url => url, :code => code
25
+ rescue ActiveRecord::RecordNotUnique, ActiveRecord::StatementInvalid
26
+ row = Url.select(:url).where(:code => code).first
27
+ existing_url = row && row[:url]
28
+ raise DuplicateCodeError.new(existing_url, url, code)
29
+ end
30
+ code
42
31
  end
32
+ end
43
33
 
44
- # Public: Retrieves the code for a given URL.
45
- #
46
- # url - The String URL to lookup.
47
- #
48
- # Returns the String code, or nil if none is found.
49
- def code_for(url)
50
- select :code, :url => url
51
- end
34
+ # Public: Retrieves a URL from the code.
35
+ #
36
+ # code - The String code to lookup the URL.
37
+ #
38
+ # Returns the String URL.
39
+ def find(code)
40
+ select :url, :code => code
41
+ end
52
42
 
53
- # Public: Removes the assigned short code for a URL.
54
- #
55
- # url - The String URL to remove.
56
- #
57
- # Returns nothing.
58
- def clear(url)
59
- Url.where(:url => url).delete_all
60
- end
43
+ # Public: Retrieves the code for a given URL.
44
+ #
45
+ # url - The String URL to lookup.
46
+ #
47
+ # Returns the String code, or nil if none is found.
48
+ def code_for(url)
49
+ select :code, :url => url
50
+ end
61
51
 
62
- def setup
63
- conn = Url.connection
64
- conn.create_table :urls do |t|
65
- t.string :url
66
- t.string :code
67
- end
52
+ # Public: Removes the assigned short code for a URL.
53
+ #
54
+ # url - The String URL to remove.
55
+ #
56
+ # Returns nothing.
57
+ def clear(url)
58
+ Url.where(:url => url).delete_all
59
+ end
68
60
 
69
- conn.add_index :urls, :url, :unique => true
70
- conn.add_index :urls, :code, :unique => true
61
+ def setup
62
+ conn = Url.connection
63
+ conn.create_table :urls do |t|
64
+ t.string :url
65
+ t.string :code
71
66
  end
72
67
 
73
- def select(field, query)
74
- if row = Url.select(field).where(query).first
75
- row[field]
76
- end
68
+ conn.add_index :urls, :url, :unique => true
69
+ conn.add_index :urls, :code, :unique => true
70
+ end
71
+
72
+ def select(field, query)
73
+ if row = Url.select(field).where(query).first
74
+ row[field]
77
75
  end
78
76
  end
79
77
  end
@@ -1,62 +1,64 @@
1
1
  module Guillotine
2
- module Adapters
3
- # Stores shortened URLs in memory. Totally scales.
4
- class MemoryAdapter < Adapter
5
- attr_reader :hash, :urls
6
- def initialize
7
- @hash = {}
8
- @urls = {}
9
- end
2
+ # Stores shortened URLs in memory. Totally scales.
3
+ class MemoryAdapter < Adapter
4
+ attr_reader :hash, :urls
5
+ def initialize
6
+ reset
7
+ end
10
8
 
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
- #
16
- # Returns the unique String code for the URL. If the URL is added
17
- # multiple times, this should return the same code.
18
- def add(url, code = nil)
19
- if existing_code = @urls[url]
20
- existing_code
21
- else
22
- code ||= shorten(url)
23
- if existing_url = @hash[code]
24
- raise DuplicateCodeError.new(existing_url, url, code) if url != existing_url
25
- end
26
- @hash[code] = url
27
- @urls[url] = code
28
- code
9
+ # Public: Stores the shortened version of a URL.
10
+ #
11
+ # url - The String URL to shorten and store.
12
+ # code - Optional String code for the URL.
13
+ #
14
+ # Returns the unique String code for the URL. If the URL is added
15
+ # multiple times, this should return the same code.
16
+ def add(url, code = nil)
17
+ if existing_code = @urls[url]
18
+ existing_code
19
+ else
20
+ code ||= shorten(url)
21
+ if existing_url = @hash[code]
22
+ raise DuplicateCodeError.new(existing_url, url, code) if url != existing_url
29
23
  end
24
+ @hash[code] = url
25
+ @urls[url] = code
26
+ code
30
27
  end
28
+ end
31
29
 
32
- # Public: Retrieves a URL from the code.
33
- #
34
- # code - The String code to lookup the URL.
35
- #
36
- # Returns the String URL, or nil if none is found.
37
- def find(code)
38
- @hash[code]
39
- end
30
+ # Public: Retrieves a URL from the code.
31
+ #
32
+ # code - The String code to lookup the URL.
33
+ #
34
+ # Returns the String URL, or nil if none is found.
35
+ def find(code)
36
+ @hash[code]
37
+ end
40
38
 
41
- # Public: Retrieves the code for a given URL.
42
- #
43
- # url - The String URL to lookup.
44
- #
45
- # Returns the String code, or nil if none is found.
46
- def code_for(url)
47
- @urls[url]
48
- end
39
+ # Public: Retrieves the code for a given URL.
40
+ #
41
+ # url - The String URL to lookup.
42
+ #
43
+ # Returns the String code, or nil if none is found.
44
+ def code_for(url)
45
+ @urls[url]
46
+ end
49
47
 
50
- # Public: Removes the assigned short code for a URL.
51
- #
52
- # url - The String URL to remove.
53
- #
54
- # Returns nothing.
55
- def clear(url)
56
- if code = @urls.delete(url)
57
- @hash.delete code
58
- end
48
+ # Public: Removes the assigned short code for a URL.
49
+ #
50
+ # url - The String URL to remove.
51
+ #
52
+ # Returns nothing.
53
+ def clear(url)
54
+ if code = @urls.delete(url)
55
+ @hash.delete code
59
56
  end
60
57
  end
58
+
59
+ def reset
60
+ @hash = {}
61
+ @urls = {}
62
+ end
61
63
  end
62
64
  end