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 +12 -0
- data/README.md +51 -7
- data/guillotine.gemspec +5 -2
- data/lib/guillotine.rb +55 -40
- data/lib/guillotine/adapters/active_record_adapter.rb +62 -64
- data/lib/guillotine/adapters/memory_adapter.rb +53 -51
- data/lib/guillotine/adapters/mongo_adapter.rb +56 -58
- data/lib/guillotine/adapters/redis_adapter.rb +58 -52
- data/lib/guillotine/adapters/riak_adapter.rb +107 -109
- data/lib/guillotine/adapters/sequel_adapter.rb +61 -63
- data/lib/guillotine/host_checkers.rb +87 -0
- data/lib/guillotine/service.rb +6 -51
- data/test/active_record_adapter_test.rb +1 -1
- data/test/app_test.rb +1 -1
- data/test/host_checker_test.rb +114 -0
- data/test/memory_adapter_test.rb +1 -1
- data/test/mongo_adapter_test.rb +1 -1
- data/test/redis_adapter_test.rb +1 -1
- data/test/riak_adapter_test.rb +3 -3
- data/test/sequel_adapter_test.rb +1 -1
- data/test/service_test.rb +1 -1
- metadata +12 -11
@@ -1,77 +1,75 @@
|
|
1
1
|
module Guillotine
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
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
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
60
|
+
def setup
|
61
|
+
@db.create_table :urls do
|
62
|
+
String :url
|
63
|
+
String :code
|
65
64
|
|
66
|
-
|
67
|
-
|
68
|
-
end
|
65
|
+
unique :url
|
66
|
+
unique :code
|
69
67
|
end
|
68
|
+
end
|
70
69
|
|
71
|
-
|
72
|
-
|
73
|
-
|
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
|
+
|
data/lib/guillotine/service.rb
CHANGED
@@ -1,13 +1,11 @@
|
|
1
1
|
module Guillotine
|
2
|
-
class Service
|
3
|
-
|
4
|
-
|
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
|
-
|
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::
|
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
@@ -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
|
+
|
data/test/memory_adapter_test.rb
CHANGED