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.
@@ -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