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 +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