guillotine 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,77 +1,75 @@
1
1
  module Guillotine
2
- module Adapters
3
- class SequelAdapter < Adapter
4
- def initialize(db)
5
- @db = db
6
- @table = @db[:urls]
7
- end
8
-
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_for(url)
18
- existing
19
- else
20
- code ||= shorten url
21
- begin
22
- @table << {:url => url, :code => code}
23
- rescue Sequel::DatabaseError
24
- if existing_url = @table.select(:url).where(:code => code).first
25
- raise DuplicateCodeError.new(existing_url, url, code)
26
- else
27
- raise
28
- end
2
+ class SequelAdapter < Adapter
3
+ def initialize(db)
4
+ @db = db
5
+ @table = @db[:urls]
6
+ end
7
+
8
+ # Public: Stores the shortened version of a URL.
9
+ #
10
+ # url - The String URL to shorten and store.
11
+ # code - Optional String code for the URL.
12
+ #
13
+ # Returns the unique String code for the URL. If the URL is added
14
+ # multiple times, this should return the same code.
15
+ def add(url, code = nil)
16
+ if existing = code_for(url)
17
+ existing
18
+ else
19
+ code ||= shorten url
20
+ begin
21
+ @table << {:url => url, :code => code}
22
+ rescue Sequel::DatabaseError
23
+ if existing_url = @table.select(:url).where(:code => code).first
24
+ raise DuplicateCodeError.new(existing_url, url, code)
25
+ else
26
+ raise
29
27
  end
30
- code
31
28
  end
29
+ code
32
30
  end
31
+ end
33
32
 
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
33
+ # Public: Retrieves a URL from the code.
34
+ #
35
+ # code - The String code to lookup the URL.
36
+ #
37
+ # Returns the String URL.
38
+ def find(code)
39
+ select :url, :code => code
40
+ end
42
41
 
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
42
+ # Public: Retrieves the code for a given URL.
43
+ #
44
+ # url - The String URL to lookup.
45
+ #
46
+ # Returns the String code, or nil if none is found.
47
+ def code_for(url)
48
+ select :code, :url => url
49
+ end
51
50
 
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
- @table.where(:url => url).delete
59
- end
51
+ # Public: Removes the assigned short code for a URL.
52
+ #
53
+ # url - The String URL to remove.
54
+ #
55
+ # Returns nothing.
56
+ def clear(url)
57
+ @table.where(:url => url).delete
58
+ end
60
59
 
61
- def setup
62
- @db.create_table :urls do
63
- String :url
64
- String :code
60
+ def setup
61
+ @db.create_table :urls do
62
+ String :url
63
+ String :code
65
64
 
66
- unique :url
67
- unique :code
68
- end
65
+ unique :url
66
+ unique :code
69
67
  end
68
+ end
70
69
 
71
- def select(field, query)
72
- if row = @table.select(field).where(query).first
73
- row[field]
74
- end
70
+ def select(field, query)
71
+ if row = @table.select(field).where(query).first
72
+ row[field]
75
73
  end
76
74
  end
77
75
  end
@@ -0,0 +1,87 @@
1
+ module Guillotine
2
+ class HostChecker
3
+ def self.matching(arg)
4
+ case arg
5
+ when HostChecker then arg
6
+ else (all.detect { |ch| ch.match?(arg) } || self).new(arg)
7
+ end
8
+ end
9
+
10
+ def self.all
11
+ @all ||= []
12
+ end
13
+
14
+ attr_reader :error, :error_response
15
+
16
+ def initialize(arg = nil)
17
+ @error_response = [422, {}, @error]
18
+ end
19
+
20
+ def valid?(url)
21
+ true
22
+ end
23
+
24
+ def call(url)
25
+ @error_response unless valid?(url)
26
+ end
27
+ end
28
+
29
+ class RegexHostChecker < HostChecker
30
+ def self.match?(arg)
31
+ arg.is_a?(Regexp)
32
+ end
33
+
34
+ attr_reader :regex
35
+
36
+ def initialize(regex)
37
+ @error = "URL must match #{regex.inspect}"
38
+ @regex = regex
39
+ super
40
+ end
41
+
42
+ def valid?(url)
43
+ url.host.to_s =~ @regex
44
+ end
45
+ end
46
+
47
+ class StringHostChecker < HostChecker
48
+ def self.match?(arg)
49
+ arg.is_a?(String)
50
+ end
51
+
52
+ attr_reader :host
53
+
54
+ def initialize(host)
55
+ @error = "URL must be from #{host}"
56
+ @host = host
57
+ super
58
+ end
59
+
60
+ def valid?(url)
61
+ url.host == @host
62
+ end
63
+ end
64
+
65
+ class WildcardHostChecker < RegexHostChecker
66
+ def self.match?(arg)
67
+ arg.to_s =~ /^\*\.([\w\.]+)$/ && $1
68
+ end
69
+
70
+ class << self
71
+ alias host_from match?
72
+ end
73
+
74
+ attr_reader :pattern, :host
75
+
76
+ def initialize(pattern)
77
+ @pattern = pattern
78
+ @host = self.class.host_from(pattern)
79
+ @regex = %r{(^|\.)#{Regexp.escape @host}$}
80
+ super(@regex)
81
+ @error = "URL must be from #{@host}"
82
+ end
83
+ end
84
+
85
+ HostChecker.all << RegexHostChecker << WildcardHostChecker << StringHostChecker
86
+ end
87
+
@@ -1,13 +1,11 @@
1
1
  module Guillotine
2
- class Service
3
- class NullChecker
4
- def call(url)
5
- end
6
- end
2
+ class Service
3
+ # Deprecated until v2
4
+ NullChecker = Guillotine::HostChecker
7
5
 
8
6
  # This is the public API to the Guillotine service. Wire this up to Sinatra
9
7
  # or whatever. Every public method should return a compatible Rack Response:
10
- # [Integer Status, Hash headers, String body].
8
+ # [Integer Status, Hash headers, String body].
11
9
  #
12
10
  # db - A Guillotine::Adapter instance.
13
11
  # required_host - Either a String or Regex limiting which domains the
@@ -15,7 +13,7 @@ module Guillotine
15
13
  #
16
14
  def initialize(db, required_host = nil)
17
15
  @db = db
18
- build_host_check(required_host)
16
+ @host_check = HostChecker.matching(required_host)
19
17
  end
20
18
 
21
19
  # Public: Gets the full URL for a shortened code.
@@ -44,7 +42,7 @@ module Guillotine
44
42
  if resp = check_host(url)
45
43
  return resp
46
44
  end
47
-
45
+
48
46
  begin
49
47
  if code = @db.add(url.to_s, code)
50
48
  [201, {"Location" => code}]
@@ -70,49 +68,6 @@ module Guillotine
70
68
  end
71
69
  end
72
70
 
73
- # Converts the `required_host` argument to a lambda for #check_host.
74
- #
75
- # host_check - Either a String or Regex limiting which domains the
76
- # shortened URLs can come from.
77
- #
78
- # Returns nothing.
79
- def build_host_check(host_check)
80
- case host_check
81
- when nil
82
- @host_check = NullChecker.new
83
- when Regexp
84
- build_host_regex_check(host_check)
85
- else
86
- build_host_string_check(host_check.to_s)
87
- end
88
- end
89
-
90
- # Builds the host check lambda for regexes.
91
- #
92
- # regex - The Regexp that limits allowable URL hosts.
93
- #
94
- # Returns a Lambda that verifies an Addressible::URI.
95
- def build_host_regex_check(regex)
96
- @host_check = lambda do |url|
97
- if url.host.to_s !~ regex
98
- [422, {}, "URL must match #{regex.inspect}"]
99
- end
100
- end
101
- end
102
-
103
- # Builds the host check lambda for Strings.
104
- #
105
- # hostname - The String that limits allowable URL hosts.
106
- #
107
- # Returns a Lambda that verifies an Addressible::URI.
108
- def build_host_string_check(hostname)
109
- @host_check = lambda do |url|
110
- if url.host != hostname
111
- [422, {}, "URL must be from #{hostname}"]
112
- end
113
- end
114
- end
115
-
116
71
  # Ensures that the argument is an Addressable::URI.
117
72
  #
118
73
  # str - A String URL or an Addressable::URI.
@@ -3,7 +3,7 @@ require File.expand_path('../helper', __FILE__)
3
3
  begin
4
4
  require 'active_record'
5
5
  class ActiveRecordAdapterTest < Guillotine::TestCase
6
- ADAPTER = Guillotine::Adapters::ActiveRecordAdapter.new :adapter => 'sqlite3', :database => ':memory:'
6
+ ADAPTER = Guillotine::ActiveRecordAdapter.new :adapter => 'sqlite3', :database => ':memory:'
7
7
  ADAPTER.setup
8
8
 
9
9
  def setup
data/test/app_test.rb CHANGED
@@ -2,7 +2,7 @@ require File.expand_path('../helper', __FILE__)
2
2
 
3
3
  module Guillotine
4
4
  class AppTest < TestCase
5
- ADAPTER = Adapters::MemoryAdapter.new
5
+ ADAPTER = MemoryAdapter.new
6
6
  SERVICE = Service.new(ADAPTER)
7
7
  App.set :service, SERVICE
8
8
 
@@ -0,0 +1,114 @@
1
+ require File.expand_path('../helper', __FILE__)
2
+
3
+ module Guillotine
4
+ class MatchingCheckerTest < TestCase
5
+ def test_regex_checker_is_matched
6
+ checker = HostChecker.matching(/a/)
7
+ assert_kind_of RegexHostChecker, checker
8
+ assert_equal /a/, checker.regex
9
+ end
10
+
11
+ def test_wildcard_checker_is_matched
12
+ checker = HostChecker.matching('*.foo.com')
13
+ assert_kind_of WildcardHostChecker, checker
14
+ assert_equal 'foo.com', checker.host
15
+ end
16
+
17
+ def test_string_checker_is_matched
18
+ checker = HostChecker.matching('foo.com')
19
+ assert_kind_of StringHostChecker, checker
20
+ assert_equal 'foo.com', checker.host
21
+ end
22
+
23
+ def test_hostchecker_matches_self
24
+ checker = HostChecker.new
25
+ assert_equal checker, HostChecker.matching(checker)
26
+ end
27
+ end
28
+
29
+ class CheckerTest < TestCase
30
+ def test_allows_urls
31
+ allowed_urls.each do |url|
32
+ assert_nil checker.call(uri(url)), url
33
+ end
34
+ end
35
+
36
+ def test_rejects_urls
37
+ rejected_urls.each do |url|
38
+ assert res = checker.call(uri(url)), "#{checker.inspect} matched #{url.inspect}"
39
+ assert_equal 422, res.first, res.inspect
40
+ end
41
+ end
42
+
43
+ def allowed_urls
44
+ ['abc']
45
+ end
46
+
47
+ def rejected_urls
48
+ []
49
+ end
50
+
51
+ def checker
52
+ @checker ||= build_checker
53
+ end
54
+
55
+ def build_checker
56
+ HostChecker.new
57
+ end
58
+
59
+ def uri(url)
60
+ Addressable::URI.parse "http://#{url}"
61
+ end
62
+ end
63
+
64
+ class WildcardHostCheckerTest < CheckerTest
65
+ def build_checker
66
+ WildcardHostChecker.new '*.foo.com'
67
+ end
68
+
69
+ def allowed_urls
70
+ %w(foo.com foo.com/a abc.foo.com/a)
71
+ end
72
+
73
+ def rejected_urls
74
+ %w(bar.com foo.com.uk foobcom)
75
+ end
76
+
77
+ def test_parses_host
78
+ assert_equal 'foo.com', checker.host
79
+ end
80
+
81
+ def test_builds_custom_error
82
+ assert_match /must be from foo\.com/, checker.error
83
+ end
84
+ end
85
+
86
+ class StringHostCheckerTest < CheckerTest
87
+ def build_checker
88
+ StringHostChecker.new('foo.com')
89
+ end
90
+
91
+ def allowed_urls
92
+ %w(foo.com foo.com/a)
93
+ end
94
+
95
+ def rejected_urls
96
+ %w(bar.com)
97
+ end
98
+ end
99
+
100
+ class RegexHostCheckerTest < CheckerTest
101
+ def build_checker
102
+ RegexHostChecker.new(/a/)
103
+ end
104
+
105
+ def allowed_urls
106
+ %w(abc.com aaa.com)
107
+ end
108
+
109
+ def rejected_urls
110
+ %w(b.com)
111
+ end
112
+ end
113
+ end
114
+
@@ -2,7 +2,7 @@ require File.expand_path('../helper', __FILE__)
2
2
 
3
3
  class MemoryAdapterTest < Guillotine::TestCase
4
4
  def setup
5
- @db = Guillotine::Adapters::MemoryAdapter.new
5
+ @db = Guillotine::MemoryAdapter.new
6
6
  end
7
7
 
8
8
  def test_adding_a_link_returns_code