guillotine 1.1.0 → 1.2.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 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