dbhijacker 0.9.2 → 0.10.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.
- checksums.yaml +4 -4
- data/Gemfile.lock +162 -33
- data/Rakefile +1 -1
- data/dbhijacker.gemspec +10 -6
- data/example_root_schema.rb +2 -2
- data/lib/hijacker.rb +17 -12
- data/lib/hijacker/controller_methods.rb +6 -18
- data/lib/hijacker/database.rb +2 -2
- data/lib/hijacker/middleware.rb +35 -7
- data/spec/hijacker/database_spec.rb +6 -6
- data/spec/hijacker/host_spec.rb +2 -2
- data/spec/hijacker/middleware_spec.rb +117 -5
- data/spec/hijacker_spec.rb +5 -5
- data/spec/spec_helper.rb +1 -1
- metadata +85 -23
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 534c79458c53e9f6cd0a858a830ca956aa4589de
|
|
4
|
+
data.tar.gz: 1a99b7b0f639f24b99a834b390031ee15c3d2508
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9185d392c8d2d3451fa3a693cac9e44a53dbffbecf67b2b57cba763f5894c6ab68463cc87a04a15120f339dc4d41b04610d95dd4c0725d8c0dff70876b592ce5
|
|
7
|
+
data.tar.gz: 2e4e921fdcd69cf7d058664b0cf553138e6557a409c3619e234c0c58045d1278b111f5f94827383368ec6638332ea5eb5e99ce21520869a5e226b209d02aca72
|
data/Gemfile.lock
CHANGED
|
@@ -1,52 +1,181 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
dbhijacker (0.
|
|
5
|
-
rails (>= 2.3.14)
|
|
4
|
+
dbhijacker (0.10.0)
|
|
5
|
+
rails (>= 2.3.14, < 4.0)
|
|
6
6
|
|
|
7
7
|
GEM
|
|
8
8
|
remote: https://rubygems.org/
|
|
9
9
|
specs:
|
|
10
|
-
actionmailer (2.
|
|
11
|
-
actionpack (= 2.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
10
|
+
actionmailer (3.2.18)
|
|
11
|
+
actionpack (= 3.2.18)
|
|
12
|
+
mail (~> 2.5.4)
|
|
13
|
+
actionpack (3.2.18)
|
|
14
|
+
activemodel (= 3.2.18)
|
|
15
|
+
activesupport (= 3.2.18)
|
|
16
|
+
builder (~> 3.0.0)
|
|
17
|
+
erubis (~> 2.7.0)
|
|
18
|
+
journey (~> 1.0.4)
|
|
19
|
+
rack (~> 1.4.5)
|
|
20
|
+
rack-cache (~> 1.2)
|
|
21
|
+
rack-test (~> 0.6.1)
|
|
22
|
+
sprockets (~> 2.2.1)
|
|
23
|
+
activemodel (3.2.18)
|
|
24
|
+
activesupport (= 3.2.18)
|
|
25
|
+
builder (~> 3.0.0)
|
|
26
|
+
activerecord (3.2.18)
|
|
27
|
+
activemodel (= 3.2.18)
|
|
28
|
+
activesupport (= 3.2.18)
|
|
29
|
+
arel (~> 3.0.2)
|
|
30
|
+
tzinfo (~> 0.3.29)
|
|
31
|
+
activeresource (3.2.18)
|
|
32
|
+
activemodel (= 3.2.18)
|
|
33
|
+
activesupport (= 3.2.18)
|
|
34
|
+
activesupport (3.2.18)
|
|
35
|
+
i18n (~> 0.6, >= 0.6.4)
|
|
36
|
+
multi_json (~> 1.0)
|
|
37
|
+
addressable (2.3.6)
|
|
38
|
+
arel (3.0.3)
|
|
39
|
+
builder (3.0.4)
|
|
40
|
+
celluloid (0.15.2)
|
|
41
|
+
timers (~> 1.1.0)
|
|
42
|
+
coderay (1.1.0)
|
|
43
|
+
descendants_tracker (0.0.4)
|
|
44
|
+
thread_safe (~> 0.3, >= 0.3.1)
|
|
45
|
+
diff-lcs (1.2.5)
|
|
46
|
+
erubis (2.7.0)
|
|
47
|
+
faraday (0.9.0)
|
|
48
|
+
multipart-post (>= 1.2, < 3)
|
|
49
|
+
ffi (1.9.3)
|
|
50
|
+
formatador (0.2.5)
|
|
51
|
+
git (1.2.7)
|
|
52
|
+
github_api (0.11.3)
|
|
53
|
+
addressable (~> 2.3)
|
|
54
|
+
descendants_tracker (~> 0.0.1)
|
|
55
|
+
faraday (~> 0.8, < 0.10)
|
|
56
|
+
hashie (>= 1.2)
|
|
57
|
+
multi_json (>= 1.7.5, < 2.0)
|
|
58
|
+
nokogiri (~> 1.6.0)
|
|
59
|
+
oauth2
|
|
60
|
+
guard (2.6.1)
|
|
61
|
+
formatador (>= 0.2.4)
|
|
62
|
+
listen (~> 2.7)
|
|
63
|
+
lumberjack (~> 1.0)
|
|
64
|
+
pry (>= 0.9.12)
|
|
65
|
+
thor (>= 0.18.1)
|
|
66
|
+
guard-bundler (2.0.0)
|
|
67
|
+
bundler (~> 1.0)
|
|
68
|
+
guard (~> 2.2)
|
|
69
|
+
guard-rspec (4.2.10)
|
|
70
|
+
guard (~> 2.1)
|
|
71
|
+
rspec (>= 2.14, < 4.0)
|
|
72
|
+
hashie (3.0.0)
|
|
73
|
+
highline (1.6.21)
|
|
74
|
+
hike (1.2.3)
|
|
75
|
+
i18n (0.6.9)
|
|
76
|
+
jeweler (2.0.1)
|
|
77
|
+
builder
|
|
78
|
+
bundler (>= 1.0)
|
|
79
|
+
git (>= 1.2.5)
|
|
80
|
+
github_api
|
|
81
|
+
highline (>= 1.6.15)
|
|
82
|
+
nokogiri (>= 1.5.10)
|
|
83
|
+
rake
|
|
84
|
+
rdoc
|
|
85
|
+
journey (1.0.4)
|
|
86
|
+
json (1.8.1)
|
|
87
|
+
jwt (1.0.0)
|
|
88
|
+
listen (2.7.8)
|
|
89
|
+
celluloid (>= 0.15.2)
|
|
90
|
+
rb-fsevent (>= 0.9.3)
|
|
91
|
+
rb-inotify (>= 0.9)
|
|
92
|
+
lumberjack (1.0.6)
|
|
93
|
+
mail (2.5.4)
|
|
94
|
+
mime-types (~> 1.16)
|
|
95
|
+
treetop (~> 1.4.8)
|
|
96
|
+
method_source (0.8.2)
|
|
97
|
+
mime-types (1.25.1)
|
|
98
|
+
mini_portile (0.6.0)
|
|
99
|
+
multi_json (1.10.1)
|
|
100
|
+
multi_xml (0.5.5)
|
|
101
|
+
multipart-post (2.0.0)
|
|
102
|
+
nokogiri (1.6.2.1)
|
|
103
|
+
mini_portile (= 0.6.0)
|
|
104
|
+
oauth2 (0.9.4)
|
|
105
|
+
faraday (>= 0.8, < 0.10)
|
|
106
|
+
jwt (~> 1.0)
|
|
107
|
+
multi_json (~> 1.3)
|
|
108
|
+
multi_xml (~> 0.5)
|
|
109
|
+
rack (~> 1.2)
|
|
110
|
+
polyglot (0.3.5)
|
|
111
|
+
pry (0.10.0)
|
|
112
|
+
coderay (~> 1.1.0)
|
|
113
|
+
method_source (~> 0.8.1)
|
|
114
|
+
slop (~> 3.4)
|
|
115
|
+
rack (1.4.5)
|
|
116
|
+
rack-cache (1.2)
|
|
117
|
+
rack (>= 0.4)
|
|
118
|
+
rack-ssl (1.3.4)
|
|
119
|
+
rack
|
|
22
120
|
rack-test (0.6.2)
|
|
23
121
|
rack (>= 1.0)
|
|
24
|
-
rails (2.
|
|
25
|
-
actionmailer (= 2.
|
|
26
|
-
actionpack (= 2.
|
|
27
|
-
activerecord (= 2.
|
|
28
|
-
activeresource (= 2.
|
|
29
|
-
activesupport (= 2.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
122
|
+
rails (3.2.18)
|
|
123
|
+
actionmailer (= 3.2.18)
|
|
124
|
+
actionpack (= 3.2.18)
|
|
125
|
+
activerecord (= 3.2.18)
|
|
126
|
+
activeresource (= 3.2.18)
|
|
127
|
+
activesupport (= 3.2.18)
|
|
128
|
+
bundler (~> 1.0)
|
|
129
|
+
railties (= 3.2.18)
|
|
130
|
+
railties (3.2.18)
|
|
131
|
+
actionpack (= 3.2.18)
|
|
132
|
+
activesupport (= 3.2.18)
|
|
133
|
+
rack-ssl (~> 1.3.2)
|
|
134
|
+
rake (>= 0.8.7)
|
|
135
|
+
rdoc (~> 3.4)
|
|
136
|
+
thor (>= 0.14.6, < 2.0)
|
|
137
|
+
rake (10.3.2)
|
|
138
|
+
rb-fsevent (0.9.4)
|
|
139
|
+
rb-inotify (0.9.5)
|
|
140
|
+
ffi (>= 0.5.0)
|
|
141
|
+
rdoc (3.12.2)
|
|
142
|
+
json (~> 1.4)
|
|
143
|
+
rspec (2.99.0)
|
|
144
|
+
rspec-core (~> 2.99.0)
|
|
145
|
+
rspec-expectations (~> 2.99.0)
|
|
146
|
+
rspec-mocks (~> 2.99.0)
|
|
147
|
+
rspec-core (2.99.0)
|
|
148
|
+
rspec-expectations (2.99.0)
|
|
38
149
|
diff-lcs (>= 1.1.3, < 2.0)
|
|
39
|
-
rspec-mocks (2.
|
|
40
|
-
|
|
150
|
+
rspec-mocks (2.99.1)
|
|
151
|
+
slop (3.5.0)
|
|
152
|
+
sprockets (2.2.2)
|
|
153
|
+
hike (~> 1.2)
|
|
154
|
+
multi_json (~> 1.0)
|
|
155
|
+
rack (~> 1.0)
|
|
156
|
+
tilt (~> 1.1, != 1.3.0)
|
|
157
|
+
sqlite3 (1.3.9)
|
|
158
|
+
thor (0.19.1)
|
|
159
|
+
thread_safe (0.3.4)
|
|
160
|
+
tilt (1.4.1)
|
|
161
|
+
timers (1.1.0)
|
|
162
|
+
treetop (1.4.15)
|
|
163
|
+
polyglot
|
|
164
|
+
polyglot (>= 0.3.1)
|
|
165
|
+
tzinfo (0.3.39)
|
|
41
166
|
|
|
42
167
|
PLATFORMS
|
|
43
168
|
ruby
|
|
44
169
|
|
|
45
170
|
DEPENDENCIES
|
|
46
|
-
bundler
|
|
171
|
+
bundler
|
|
47
172
|
dbhijacker!
|
|
173
|
+
guard
|
|
174
|
+
guard-bundler
|
|
175
|
+
guard-rspec
|
|
176
|
+
jeweler
|
|
48
177
|
rack (>= 1.1.0)
|
|
49
178
|
rack-test (>= 0.6.1)
|
|
50
179
|
rake (>= 0.9.2)
|
|
51
|
-
rspec (
|
|
52
|
-
sqlite3 (
|
|
180
|
+
rspec (~> 2.8)
|
|
181
|
+
sqlite3 (~> 1.3.5)
|
data/Rakefile
CHANGED
data/dbhijacker.gemspec
CHANGED
|
@@ -3,23 +3,27 @@
|
|
|
3
3
|
Gem::Specification.new do |s|
|
|
4
4
|
s.name = %q{dbhijacker}
|
|
5
5
|
s.homepage = "https://github.com/crystalcommerce/hijacker"
|
|
6
|
-
s.version = "0.
|
|
6
|
+
s.version = "0.10.0"
|
|
7
7
|
|
|
8
8
|
s.license = "MIT"
|
|
9
9
|
|
|
10
10
|
s.authors = ["Michael Xavier", "Donald Plummer", "Woody Peterson"]
|
|
11
|
-
s.date = %q{
|
|
11
|
+
s.date = %q{2014-06-18}
|
|
12
12
|
s.description = %q{Allows a single Rails appliation to access many different databases}
|
|
13
13
|
s.email = %q{developers@crystalcommerce.com}
|
|
14
14
|
|
|
15
|
-
s.add_dependency("rails", ">= 2.3.14")
|
|
15
|
+
s.add_dependency("rails", ">= 2.3.14", "< 4.0")
|
|
16
16
|
|
|
17
|
-
s.add_development_dependency "
|
|
17
|
+
s.add_development_dependency "jeweler"
|
|
18
|
+
s.add_development_dependency "bundler"
|
|
19
|
+
s.add_development_dependency "guard"
|
|
20
|
+
s.add_development_dependency "guard-rspec"
|
|
21
|
+
s.add_development_dependency "guard-bundler"
|
|
18
22
|
s.add_development_dependency "rake", ">= 0.9.2"
|
|
19
23
|
s.add_development_dependency "rack-test", ">= 0.6.1"
|
|
20
24
|
s.add_development_dependency "rack", ">= 1.1.0"
|
|
21
|
-
s.add_development_dependency "rspec", "
|
|
22
|
-
s.add_development_dependency "sqlite3", "
|
|
25
|
+
s.add_development_dependency "rspec", "~> 2.8"
|
|
26
|
+
s.add_development_dependency "sqlite3", "~> 1.3.5"
|
|
23
27
|
|
|
24
28
|
s.extra_rdoc_files = [
|
|
25
29
|
"README.rdoc"
|
data/example_root_schema.rb
CHANGED
|
@@ -5,7 +5,7 @@ ActiveRecord::Schema.define(:version => 2) do
|
|
|
5
5
|
t.integer "host_id"
|
|
6
6
|
end
|
|
7
7
|
|
|
8
|
-
add_index "databases", "database"
|
|
8
|
+
add_index "databases", "database", :unique => true
|
|
9
9
|
add_index "databases", "master_id"
|
|
10
10
|
|
|
11
11
|
create_table "aliases", :force => true do |t|
|
|
@@ -13,7 +13,7 @@ ActiveRecord::Schema.define(:version => 2) do
|
|
|
13
13
|
t.string "name"
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
add_index "aliases", "name"
|
|
16
|
+
add_index "aliases", "name", :unique => true
|
|
17
17
|
|
|
18
18
|
create_table "hosts", :force => true do |t|
|
|
19
19
|
t.string "hostname"
|
data/lib/hijacker.rb
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
require 'hijacker/active_record_ext'
|
|
2
|
+
require 'hijacker/request_parser'
|
|
2
3
|
require 'active_record'
|
|
3
4
|
require 'action_controller'
|
|
4
5
|
require 'set'
|
|
5
6
|
|
|
6
7
|
module Hijacker
|
|
7
8
|
class UnparseableURL < StandardError;end
|
|
8
|
-
class InvalidDatabase < StandardError
|
|
9
|
+
class InvalidDatabase < StandardError
|
|
10
|
+
attr_reader :database
|
|
11
|
+
def initialize(database, message = "Database #{database} not found")
|
|
12
|
+
@database = database
|
|
13
|
+
super(message)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
9
16
|
|
|
10
17
|
class << self
|
|
11
18
|
attr_accessor :config, :master, :sister
|
|
@@ -21,21 +28,21 @@ module Hijacker
|
|
|
21
28
|
end
|
|
22
29
|
|
|
23
30
|
# Manually establishes a new connection to the database.
|
|
24
|
-
#
|
|
31
|
+
#
|
|
25
32
|
# Background: every time rails gets information
|
|
26
33
|
# from the database, it uses the last established connection. So,
|
|
27
34
|
# although we've already established a connection to a root db
|
|
28
35
|
# ("crystal", in this case), if we establish a new connection, all
|
|
29
36
|
# subsequent database calls will use these settings instead (well,
|
|
30
37
|
# until it's called again when it gets another request).
|
|
31
|
-
#
|
|
38
|
+
#
|
|
32
39
|
# Note that you can manually call this from script/console (or wherever)
|
|
33
40
|
# to connect to the database you want, ex Hijacker.connect("database")
|
|
34
41
|
def self.connect(target_name, sister_name = nil, options = {})
|
|
35
42
|
original_database = Hijacker::Database.current
|
|
36
43
|
|
|
37
44
|
begin
|
|
38
|
-
raise InvalidDatabase, 'master cannot be nil' if target_name.nil?
|
|
45
|
+
raise InvalidDatabase.new(nil, 'master cannot be nil') if target_name.nil?
|
|
39
46
|
|
|
40
47
|
target_name = target_name.downcase
|
|
41
48
|
sister_name = sister_name.downcase unless sister_name.nil?
|
|
@@ -102,7 +109,7 @@ module Hijacker
|
|
|
102
109
|
klass.connection
|
|
103
110
|
rescue
|
|
104
111
|
klass.establish_connection(root_config)
|
|
105
|
-
raise Hijacker::InvalidDatabase
|
|
112
|
+
raise Hijacker::InvalidDatabase.new(database.name)
|
|
106
113
|
end
|
|
107
114
|
master_db_connection_pool = klass.connection_pool
|
|
108
115
|
else
|
|
@@ -123,9 +130,7 @@ module Hijacker
|
|
|
123
130
|
|
|
124
131
|
result
|
|
125
132
|
end
|
|
126
|
-
|
|
127
|
-
# maintains and returns a connection to the root database.
|
|
128
|
-
#
|
|
133
|
+
|
|
129
134
|
# The advantage of using this over just calling
|
|
130
135
|
# ActiveRecord::Base.establish_connection (without arguments) to reconnect
|
|
131
136
|
# to the root database is that reusing the same connection greatly reduces
|
|
@@ -133,7 +138,7 @@ module Hijacker
|
|
|
133
138
|
# the database. It may seem trivial, but it actually seems to speed things
|
|
134
139
|
# up by ~ 1/3 for already fast requests (probably less noticeable on slower
|
|
135
140
|
# pages).
|
|
136
|
-
#
|
|
141
|
+
#
|
|
137
142
|
# Note: does not hijack, just returns the root connection (i.e. AR::Base will
|
|
138
143
|
# maintain its connection)
|
|
139
144
|
def self.root_connection
|
|
@@ -180,7 +185,7 @@ module Hijacker
|
|
|
180
185
|
|
|
181
186
|
# just calling establish_connection doesn't actually check to see if
|
|
182
187
|
# we've established a VALID connection. a call to connection will check
|
|
183
|
-
# this, and throw an error if the connection's invalid. It is important
|
|
188
|
+
# this, and throw an error if the connection's invalid. It is important
|
|
184
189
|
# to catch the error and reconnect to a known valid database or rails
|
|
185
190
|
# will get stuck. This is because once we establish a connection to an
|
|
186
191
|
# invalid database, the next request will do a courteousy touch to the
|
|
@@ -200,14 +205,14 @@ private
|
|
|
200
205
|
def self.determine_database(target_name, sister_name)
|
|
201
206
|
if sister_name
|
|
202
207
|
database = Hijacker::Database.find_by_name(sister_name)
|
|
203
|
-
raise(Hijacker::InvalidDatabase
|
|
208
|
+
raise(Hijacker::InvalidDatabase.new(sister_name)) if database.nil?
|
|
204
209
|
database
|
|
205
210
|
elsif valid_routes[target_name]
|
|
206
211
|
valid_routes[target_name] # cached valid database
|
|
207
212
|
else
|
|
208
213
|
database = Hijacker::Alias.find_by_name(target_name).try(:database) ||
|
|
209
214
|
Hijacker::Database.find_by_name(target_name)
|
|
210
|
-
raise(Hijacker::InvalidDatabase
|
|
215
|
+
raise(Hijacker::InvalidDatabase.new(target_name)) if database.nil?
|
|
211
216
|
database
|
|
212
217
|
end
|
|
213
218
|
end
|
|
@@ -1,16 +1,15 @@
|
|
|
1
|
+
# DEPRECATED: use middleware instead
|
|
1
2
|
module Hijacker::ControllerMethods
|
|
2
3
|
module Instance
|
|
3
4
|
def hijack_connection
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
master, sister = determine_databases(host)
|
|
5
|
+
master, sister = determine_databases
|
|
7
6
|
|
|
8
7
|
Hijacker.connect(master, sister)
|
|
9
|
-
|
|
8
|
+
|
|
10
9
|
return true
|
|
11
10
|
rescue Hijacker::InvalidDatabase => e
|
|
12
11
|
render_invalid_db
|
|
13
|
-
|
|
12
|
+
|
|
14
13
|
# If we've encountered a bad database connection, we don't want
|
|
15
14
|
# to continue rendering the rest of the before_filters on this, which it will
|
|
16
15
|
# try to do even when just rendering the bit of text above. If any filters
|
|
@@ -20,19 +19,8 @@ module Hijacker::ControllerMethods
|
|
|
20
19
|
|
|
21
20
|
# Returns 2-member array of the main database to connect to, and the sister
|
|
22
21
|
# (sister will be nil if no master is found, which means we are on the master).
|
|
23
|
-
def determine_databases
|
|
24
|
-
|
|
25
|
-
Hijacker.config[:domain_patterns].find {|pattern| host =~ pattern}
|
|
26
|
-
client = $1
|
|
27
|
-
else # development, test, etc
|
|
28
|
-
client = ActiveRecord::Base.configurations[Rails.env]['database']
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
raise Hijacker::UnparseableURL, "cannot parse '#{host}'" if client.nil?
|
|
32
|
-
|
|
33
|
-
master, sister = Hijacker::Database.find_master_and_sister_for(client)
|
|
34
|
-
|
|
35
|
-
return [master, sister]
|
|
22
|
+
def determine_databases
|
|
23
|
+
Hijacker::RequestParser.new(request.env).determine_databases
|
|
36
24
|
end
|
|
37
25
|
|
|
38
26
|
def render_invalid_db
|
data/lib/hijacker/database.rb
CHANGED
|
@@ -51,7 +51,7 @@ class Hijacker::Database < ActiveRecord::Base
|
|
|
51
51
|
def self.current
|
|
52
52
|
find(:first, :conditions => {:database => Hijacker.current_client})
|
|
53
53
|
end
|
|
54
|
-
|
|
54
|
+
|
|
55
55
|
# returns a string or nil
|
|
56
56
|
def self.find_master_for(client)
|
|
57
57
|
@masters ||= {}
|
|
@@ -85,7 +85,7 @@ class Hijacker::Database < ActiveRecord::Base
|
|
|
85
85
|
sites.delete(Hijacker.current_client)
|
|
86
86
|
connect_each(sites, &block)
|
|
87
87
|
end
|
|
88
|
-
|
|
88
|
+
|
|
89
89
|
def self.find_shared_sites_for(client)
|
|
90
90
|
@shared_sites ||= {}
|
|
91
91
|
return @shared_sites[client] if @shared_sites[client].present?
|
data/lib/hijacker/middleware.rb
CHANGED
|
@@ -1,18 +1,46 @@
|
|
|
1
1
|
module Hijacker
|
|
2
2
|
class Middleware
|
|
3
|
-
|
|
3
|
+
HEADER_KEY = "HTTP_X_HIJACKER_DB".freeze
|
|
4
|
+
DEFAULT_NOT_FOUND = ->(database, env) {
|
|
5
|
+
[404, {}, ["Database #{database} not found"]]
|
|
6
|
+
}
|
|
7
|
+
DEFAULT_BAD_URL = ->(message, env) {
|
|
8
|
+
[404, {}, [message]]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
attr_reader :not_found, :bad_url
|
|
12
|
+
|
|
13
|
+
def initialize(app, options = {})
|
|
14
|
+
options = options.dup
|
|
4
15
|
@app = app
|
|
16
|
+
@not_found = options.delete(:not_found) || DEFAULT_NOT_FOUND
|
|
17
|
+
@bad_url = options.delete(:bad_url) || DEFAULT_BAD_URL
|
|
18
|
+
|
|
19
|
+
unless options.blank?
|
|
20
|
+
raise "Unknown Hijacker::Middleware options #{options.keys.join(",")}"
|
|
21
|
+
end
|
|
5
22
|
end
|
|
6
23
|
|
|
7
24
|
def call(env)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
25
|
+
begin
|
|
26
|
+
Hijacker.connect(*determine_databases(env))
|
|
27
|
+
rescue Hijacker::InvalidDatabase => e
|
|
28
|
+
return not_found.call(e.database, env)
|
|
29
|
+
rescue Hijacker::UnparseableURL => e
|
|
30
|
+
return bad_url.call(e.message, env)
|
|
14
31
|
end
|
|
32
|
+
|
|
15
33
|
@app.call(env)
|
|
16
34
|
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def determine_databases(env)
|
|
39
|
+
if client = env[HEADER_KEY]
|
|
40
|
+
Hijacker::Database.find_master_and_sister_for(client)
|
|
41
|
+
else
|
|
42
|
+
RequestParser.new(env).determine_databases
|
|
43
|
+
end
|
|
44
|
+
end
|
|
17
45
|
end
|
|
18
46
|
end
|
|
@@ -18,7 +18,7 @@ module Hijacker
|
|
|
18
18
|
it "requires a host" do
|
|
19
19
|
subject.host = nil
|
|
20
20
|
subject.should_not be_valid
|
|
21
|
-
subject.errors
|
|
21
|
+
subject.errors[:host_id].should == ["can't be blank"]
|
|
22
22
|
|
|
23
23
|
subject.host = host
|
|
24
24
|
subject.should be_valid
|
|
@@ -38,12 +38,12 @@ module Hijacker
|
|
|
38
38
|
|
|
39
39
|
describe "#connect_each" do
|
|
40
40
|
def db(name)
|
|
41
|
-
|
|
41
|
+
double("#{name}_db", :database => name)
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
before (:each) do
|
|
45
|
-
Database.stub
|
|
46
|
-
Hijacker.stub
|
|
45
|
+
Database.stub(:all).and_return([ db("one"), db("two"), db("three") ])
|
|
46
|
+
Hijacker.stub(:connect)
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
it "Calls the block once for each database" do
|
|
@@ -71,10 +71,10 @@ module Hijacker
|
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
it "eats invalid database errors" do
|
|
74
|
-
Hijacker.stub(:connect).and_raise(Hijacker::InvalidDatabase)
|
|
74
|
+
Hijacker.stub(:connect).and_raise(Hijacker::InvalidDatabase.new("doesntmatter"))
|
|
75
75
|
expect { Database.connect_each {|db| } }.not_to raise_error
|
|
76
76
|
end
|
|
77
|
-
|
|
77
|
+
|
|
78
78
|
it "eats mysql-specific errors for missing databases" do
|
|
79
79
|
[Mysql::Error, Mysql2::Error].each do |klass|
|
|
80
80
|
exception = klass.new("Unknown database 'fake'")
|
data/spec/hijacker/host_spec.rb
CHANGED
|
@@ -4,11 +4,11 @@ describe Hijacker::Host do
|
|
|
4
4
|
it "validates the format of the hostname" do
|
|
5
5
|
subject.hostname = "lol nope"
|
|
6
6
|
subject.should_not be_valid
|
|
7
|
-
subject.errors
|
|
7
|
+
subject.errors[:hostname].should == ["is invalid"]
|
|
8
8
|
|
|
9
9
|
subject.hostname = nil
|
|
10
10
|
subject.should_not be_valid
|
|
11
|
-
subject.errors
|
|
11
|
+
subject.errors[:hostname].should == ["is invalid"]
|
|
12
12
|
|
|
13
13
|
subject.hostname = "db-01.example.com"
|
|
14
14
|
subject.should be_valid
|
|
@@ -14,18 +14,130 @@ module Hijacker
|
|
|
14
14
|
end
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
+
let!(:host) { Hijacker::Host.create!(:hostname => "localhost") }
|
|
18
|
+
let!(:master) { Hijacker::Database.create!(:database => "sample-db", :host => host) }
|
|
19
|
+
let!(:foo) { Hijacker::Database.create!(:database => "foo", :host => host) }
|
|
20
|
+
|
|
21
|
+
before(:each) do
|
|
22
|
+
Hijacker.config = {
|
|
23
|
+
:hosted_environments => %w[test],
|
|
24
|
+
:domain_patterns => [
|
|
25
|
+
/^(.+)-admin\.crystalcommerce\.com/
|
|
26
|
+
],
|
|
27
|
+
:sister_site_models => []
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
ActiveRecord::Base.stub(:establish_connection)
|
|
31
|
+
end
|
|
32
|
+
|
|
17
33
|
describe "#call" do
|
|
34
|
+
let(:request_env) {{ 'HTTP_X_HIJACKER_DB' => 'sample-db' }}
|
|
35
|
+
|
|
36
|
+
def make_request
|
|
37
|
+
get '/', {}, request_env
|
|
38
|
+
end
|
|
39
|
+
|
|
18
40
|
context "When the 'X-Hijacker-DB' header is set" do
|
|
19
41
|
it "connects to the database from the header" do
|
|
20
|
-
|
|
21
|
-
|
|
42
|
+
make_request
|
|
43
|
+
expect(Hijacker.current_client).to eq('sample-db')
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it "passes through" do
|
|
47
|
+
resp = make_request
|
|
48
|
+
expect(resp.status).to eq(200)
|
|
49
|
+
expect(resp.headers['blah']).to eq("blah")
|
|
50
|
+
expect(resp.body).to eq("success")
|
|
22
51
|
end
|
|
23
52
|
end
|
|
24
53
|
|
|
25
54
|
context "When the 'X-Hijacker-DB' header is not set" do
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
55
|
+
let(:request_env) do
|
|
56
|
+
{
|
|
57
|
+
"HTTP_HOST" => "foo-admin.crystalcommerce.com"
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it "parses the host from the request and connects" do
|
|
62
|
+
make_request
|
|
63
|
+
expect(Hijacker.current_client).to eq("foo")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it "passes through" do
|
|
67
|
+
resp = make_request
|
|
68
|
+
expect(resp.status).to eq(200)
|
|
69
|
+
expect(resp.headers['blah']).to eq("blah")
|
|
70
|
+
expect(resp.body).to eq("success")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
context "databases could not be determined" do
|
|
75
|
+
let(:request_env) do
|
|
76
|
+
{
|
|
77
|
+
"HTTP_HOST" => "bogus-admin.crystalcommerce.com"
|
|
78
|
+
}
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it "returns a 404" do
|
|
82
|
+
resp = make_request
|
|
83
|
+
expect(resp.status).to eq(404)
|
|
84
|
+
expect(resp.body).to eq("Database bogus not found")
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
context "unparseable URL" do
|
|
90
|
+
let(:request_env) do
|
|
91
|
+
{
|
|
92
|
+
"HTTP_HOST" => "(>'-'>)"
|
|
93
|
+
}
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it "returns a 404" do
|
|
97
|
+
resp = make_request
|
|
98
|
+
expect(resp.status).to eq(404)
|
|
99
|
+
expect(resp.body).to eq("cannot parse '(>'-'>)'")
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
context "custom bad url handler" do
|
|
103
|
+
def app
|
|
104
|
+
Rack::Builder.new do
|
|
105
|
+
use Hijacker::Middleware, :bad_url => ->(message, env) { [404, {}, "You done goofed, #{message}"]}
|
|
106
|
+
run lambda { |env| [200, { 'blah' => 'blah' }, ["success"]] }
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it "uses the custom not found handler" do
|
|
111
|
+
resp = make_request
|
|
112
|
+
expect(resp.status).to eq(404)
|
|
113
|
+
expect(resp.body).to eq("You done goofed, cannot parse '(>'-'>)'")
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
context "database not found" do
|
|
119
|
+
let(:request_env) {{ 'HTTP_X_HIJACKER_DB' => 'bogus' }}
|
|
120
|
+
|
|
121
|
+
it "returns a 404" do
|
|
122
|
+
resp = make_request
|
|
123
|
+
expect(resp.status).to eq(404)
|
|
124
|
+
expect(resp.body).to eq("Database bogus not found")
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
context "custom missing database handler" do
|
|
129
|
+
def app
|
|
130
|
+
Rack::Builder.new do
|
|
131
|
+
use Hijacker::Middleware, :not_found => ->(database, env) { [404, {}, "You done goofed, #{database}"]}
|
|
132
|
+
run lambda { |env| [200, { 'blah' => 'blah' }, ["success"]] }
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
it "uses the custom not found handler" do
|
|
137
|
+
resp = make_request
|
|
138
|
+
expect(resp.status).to eq(404)
|
|
139
|
+
expect(resp.body).to eq("You done goofed, bogus")
|
|
140
|
+
end
|
|
29
141
|
end
|
|
30
142
|
end
|
|
31
143
|
end
|
data/spec/hijacker_spec.rb
CHANGED
|
@@ -6,7 +6,7 @@ describe Hijacker do
|
|
|
6
6
|
before(:each) do
|
|
7
7
|
Hijacker.config = {
|
|
8
8
|
:hosted_environments => hosted_environments
|
|
9
|
-
}
|
|
9
|
+
}
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
let!(:host) { Hijacker::Host.create!(:hostname => "localhost") }
|
|
@@ -32,7 +32,7 @@ describe Hijacker do
|
|
|
32
32
|
end
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
-
describe "class methods" do
|
|
35
|
+
describe "class methods" do
|
|
36
36
|
subject { Hijacker }
|
|
37
37
|
|
|
38
38
|
describe ".connect" do
|
|
@@ -43,7 +43,7 @@ describe Hijacker do
|
|
|
43
43
|
subject.sister = nil
|
|
44
44
|
subject.valid_routes = {}
|
|
45
45
|
ActiveRecord::Base.stub(:establish_connection)
|
|
46
|
-
subject.stub(:root_connection).and_return(
|
|
46
|
+
subject.stub(:root_connection).and_return(double(:config => {}))
|
|
47
47
|
subject.stub(:connect_sister_site_models)
|
|
48
48
|
Hijacker.stub(:do_hijacking?).and_return(true)
|
|
49
49
|
::ActionController::Base.stub(:perform_caching).
|
|
@@ -149,7 +149,7 @@ describe Hijacker do
|
|
|
149
149
|
|
|
150
150
|
it "enables the query cache on ActiveRecord::Base" do
|
|
151
151
|
subject.connect('master_db')
|
|
152
|
-
::ActiveRecord::Base.connection.query_cache_enabled.should
|
|
152
|
+
::ActiveRecord::Base.connection.query_cache_enabled.should eq(true)
|
|
153
153
|
end
|
|
154
154
|
|
|
155
155
|
it "calls cache on the connection" do
|
|
@@ -159,7 +159,7 @@ describe Hijacker do
|
|
|
159
159
|
end
|
|
160
160
|
|
|
161
161
|
context "after_hijack call specified" do
|
|
162
|
-
let(:spy) {
|
|
162
|
+
let(:spy) { double.as_null_object }
|
|
163
163
|
before(:each) do
|
|
164
164
|
Hijacker.config.merge!(:after_hijack => spy)
|
|
165
165
|
end
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dbhijacker
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.10.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michael Xavier
|
|
@@ -10,104 +10,166 @@ authors:
|
|
|
10
10
|
autorequire:
|
|
11
11
|
bindir: bin
|
|
12
12
|
cert_chain: []
|
|
13
|
-
date:
|
|
13
|
+
date: 2014-06-18 00:00:00.000000000 Z
|
|
14
14
|
dependencies:
|
|
15
15
|
- !ruby/object:Gem::Dependency
|
|
16
16
|
name: rails
|
|
17
17
|
requirement: !ruby/object:Gem::Requirement
|
|
18
18
|
requirements:
|
|
19
|
-
- -
|
|
19
|
+
- - ">="
|
|
20
20
|
- !ruby/object:Gem::Version
|
|
21
21
|
version: 2.3.14
|
|
22
|
+
- - "<"
|
|
23
|
+
- !ruby/object:Gem::Version
|
|
24
|
+
version: '4.0'
|
|
22
25
|
type: :runtime
|
|
23
26
|
prerelease: false
|
|
24
27
|
version_requirements: !ruby/object:Gem::Requirement
|
|
25
28
|
requirements:
|
|
26
|
-
- -
|
|
29
|
+
- - ">="
|
|
27
30
|
- !ruby/object:Gem::Version
|
|
28
31
|
version: 2.3.14
|
|
32
|
+
- - "<"
|
|
33
|
+
- !ruby/object:Gem::Version
|
|
34
|
+
version: '4.0'
|
|
35
|
+
- !ruby/object:Gem::Dependency
|
|
36
|
+
name: jeweler
|
|
37
|
+
requirement: !ruby/object:Gem::Requirement
|
|
38
|
+
requirements:
|
|
39
|
+
- - ">="
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: '0'
|
|
42
|
+
type: :development
|
|
43
|
+
prerelease: false
|
|
44
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - ">="
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '0'
|
|
29
49
|
- !ruby/object:Gem::Dependency
|
|
30
50
|
name: bundler
|
|
31
51
|
requirement: !ruby/object:Gem::Requirement
|
|
32
52
|
requirements:
|
|
33
|
-
- -
|
|
53
|
+
- - ">="
|
|
54
|
+
- !ruby/object:Gem::Version
|
|
55
|
+
version: '0'
|
|
56
|
+
type: :development
|
|
57
|
+
prerelease: false
|
|
58
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
59
|
+
requirements:
|
|
60
|
+
- - ">="
|
|
61
|
+
- !ruby/object:Gem::Version
|
|
62
|
+
version: '0'
|
|
63
|
+
- !ruby/object:Gem::Dependency
|
|
64
|
+
name: guard
|
|
65
|
+
requirement: !ruby/object:Gem::Requirement
|
|
66
|
+
requirements:
|
|
67
|
+
- - ">="
|
|
34
68
|
- !ruby/object:Gem::Version
|
|
35
|
-
version: '
|
|
69
|
+
version: '0'
|
|
36
70
|
type: :development
|
|
37
71
|
prerelease: false
|
|
38
72
|
version_requirements: !ruby/object:Gem::Requirement
|
|
39
73
|
requirements:
|
|
40
|
-
- -
|
|
74
|
+
- - ">="
|
|
41
75
|
- !ruby/object:Gem::Version
|
|
42
|
-
version: '
|
|
76
|
+
version: '0'
|
|
77
|
+
- !ruby/object:Gem::Dependency
|
|
78
|
+
name: guard-rspec
|
|
79
|
+
requirement: !ruby/object:Gem::Requirement
|
|
80
|
+
requirements:
|
|
81
|
+
- - ">="
|
|
82
|
+
- !ruby/object:Gem::Version
|
|
83
|
+
version: '0'
|
|
84
|
+
type: :development
|
|
85
|
+
prerelease: false
|
|
86
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
87
|
+
requirements:
|
|
88
|
+
- - ">="
|
|
89
|
+
- !ruby/object:Gem::Version
|
|
90
|
+
version: '0'
|
|
91
|
+
- !ruby/object:Gem::Dependency
|
|
92
|
+
name: guard-bundler
|
|
93
|
+
requirement: !ruby/object:Gem::Requirement
|
|
94
|
+
requirements:
|
|
95
|
+
- - ">="
|
|
96
|
+
- !ruby/object:Gem::Version
|
|
97
|
+
version: '0'
|
|
98
|
+
type: :development
|
|
99
|
+
prerelease: false
|
|
100
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
101
|
+
requirements:
|
|
102
|
+
- - ">="
|
|
103
|
+
- !ruby/object:Gem::Version
|
|
104
|
+
version: '0'
|
|
43
105
|
- !ruby/object:Gem::Dependency
|
|
44
106
|
name: rake
|
|
45
107
|
requirement: !ruby/object:Gem::Requirement
|
|
46
108
|
requirements:
|
|
47
|
-
- -
|
|
109
|
+
- - ">="
|
|
48
110
|
- !ruby/object:Gem::Version
|
|
49
111
|
version: 0.9.2
|
|
50
112
|
type: :development
|
|
51
113
|
prerelease: false
|
|
52
114
|
version_requirements: !ruby/object:Gem::Requirement
|
|
53
115
|
requirements:
|
|
54
|
-
- -
|
|
116
|
+
- - ">="
|
|
55
117
|
- !ruby/object:Gem::Version
|
|
56
118
|
version: 0.9.2
|
|
57
119
|
- !ruby/object:Gem::Dependency
|
|
58
120
|
name: rack-test
|
|
59
121
|
requirement: !ruby/object:Gem::Requirement
|
|
60
122
|
requirements:
|
|
61
|
-
- -
|
|
123
|
+
- - ">="
|
|
62
124
|
- !ruby/object:Gem::Version
|
|
63
125
|
version: 0.6.1
|
|
64
126
|
type: :development
|
|
65
127
|
prerelease: false
|
|
66
128
|
version_requirements: !ruby/object:Gem::Requirement
|
|
67
129
|
requirements:
|
|
68
|
-
- -
|
|
130
|
+
- - ">="
|
|
69
131
|
- !ruby/object:Gem::Version
|
|
70
132
|
version: 0.6.1
|
|
71
133
|
- !ruby/object:Gem::Dependency
|
|
72
134
|
name: rack
|
|
73
135
|
requirement: !ruby/object:Gem::Requirement
|
|
74
136
|
requirements:
|
|
75
|
-
- -
|
|
137
|
+
- - ">="
|
|
76
138
|
- !ruby/object:Gem::Version
|
|
77
139
|
version: 1.1.0
|
|
78
140
|
type: :development
|
|
79
141
|
prerelease: false
|
|
80
142
|
version_requirements: !ruby/object:Gem::Requirement
|
|
81
143
|
requirements:
|
|
82
|
-
- -
|
|
144
|
+
- - ">="
|
|
83
145
|
- !ruby/object:Gem::Version
|
|
84
146
|
version: 1.1.0
|
|
85
147
|
- !ruby/object:Gem::Dependency
|
|
86
148
|
name: rspec
|
|
87
149
|
requirement: !ruby/object:Gem::Requirement
|
|
88
150
|
requirements:
|
|
89
|
-
- -
|
|
151
|
+
- - "~>"
|
|
90
152
|
- !ruby/object:Gem::Version
|
|
91
153
|
version: '2.8'
|
|
92
154
|
type: :development
|
|
93
155
|
prerelease: false
|
|
94
156
|
version_requirements: !ruby/object:Gem::Requirement
|
|
95
157
|
requirements:
|
|
96
|
-
- -
|
|
158
|
+
- - "~>"
|
|
97
159
|
- !ruby/object:Gem::Version
|
|
98
160
|
version: '2.8'
|
|
99
161
|
- !ruby/object:Gem::Dependency
|
|
100
162
|
name: sqlite3
|
|
101
163
|
requirement: !ruby/object:Gem::Requirement
|
|
102
164
|
requirements:
|
|
103
|
-
- -
|
|
165
|
+
- - "~>"
|
|
104
166
|
- !ruby/object:Gem::Version
|
|
105
167
|
version: 1.3.5
|
|
106
168
|
type: :development
|
|
107
169
|
prerelease: false
|
|
108
170
|
version_requirements: !ruby/object:Gem::Requirement
|
|
109
171
|
requirements:
|
|
110
|
-
- -
|
|
172
|
+
- - "~>"
|
|
111
173
|
- !ruby/object:Gem::Version
|
|
112
174
|
version: 1.3.5
|
|
113
175
|
description: Allows a single Rails appliation to access many different databases
|
|
@@ -122,8 +184,8 @@ files:
|
|
|
122
184
|
- MIT-LICENSE
|
|
123
185
|
- README.rdoc
|
|
124
186
|
- Rakefile
|
|
125
|
-
- example_root_schema.rb
|
|
126
187
|
- dbhijacker.gemspec
|
|
188
|
+
- example_root_schema.rb
|
|
127
189
|
- lib/dbhijacker.rb
|
|
128
190
|
- lib/hijacker.rb
|
|
129
191
|
- lib/hijacker/active_record_ext.rb
|
|
@@ -145,22 +207,22 @@ licenses:
|
|
|
145
207
|
metadata: {}
|
|
146
208
|
post_install_message:
|
|
147
209
|
rdoc_options:
|
|
148
|
-
- --charset=UTF-8
|
|
210
|
+
- "--charset=UTF-8"
|
|
149
211
|
require_paths:
|
|
150
212
|
- lib
|
|
151
213
|
required_ruby_version: !ruby/object:Gem::Requirement
|
|
152
214
|
requirements:
|
|
153
|
-
- -
|
|
215
|
+
- - ">="
|
|
154
216
|
- !ruby/object:Gem::Version
|
|
155
217
|
version: '0'
|
|
156
218
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
157
219
|
requirements:
|
|
158
|
-
- -
|
|
220
|
+
- - ">="
|
|
159
221
|
- !ruby/object:Gem::Version
|
|
160
222
|
version: '0'
|
|
161
223
|
requirements: []
|
|
162
224
|
rubyforge_project:
|
|
163
|
-
rubygems_version: 2.
|
|
225
|
+
rubygems_version: 2.2.2
|
|
164
226
|
signing_key:
|
|
165
227
|
specification_version: 4
|
|
166
228
|
summary: One application, multiple client databases
|