lookup_by 0.9.1 → 0.10.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/.travis.yml +5 -4
- data/.yardopts +1 -0
- data/Appraisals +1 -5
- data/Gemfile +1 -1
- data/README.md +39 -8
- data/gemfiles/rails_4.0.gemfile +1 -1
- data/gemfiles/rails_4.1.gemfile +1 -1
- data/gemfiles/rails_4.2.gemfile +2 -2
- data/lib/lookup_by/association.rb +16 -7
- data/lib/lookup_by/cache.rb +64 -25
- data/lib/lookup_by/lookup.rb +29 -5
- data/lib/lookup_by/version.rb +1 -1
- data/lib/lookup_by.rb +34 -0
- data/lookup_by.gemspec +2 -2
- data/spec/association_spec.rb +19 -2
- data/spec/dummy/app/models/account.rb +3 -1
- data/spec/dummy/app/models/country.rb +1 -0
- data/spec/dummy/app/models/phone_number.rb +5 -0
- data/spec/dummy/config/application.rb +0 -2
- data/spec/dummy/config/database.yml +0 -8
- data/spec/dummy/db/migrate/20121019040009_create_tables.rb +8 -2
- data/spec/dummy/db/structure.sql +61 -1
- data/spec/lookup_by_spec.rb +75 -10
- data/spec/rails_helper.rb +0 -2
- data/spec/spec_helper.rb +30 -19
- data/spec/support/shared_examples_for_a_lookup.rb +1 -1
- metadata +12 -10
- data/gemfiles/rails_3.2.gemfile +0 -22
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b7b482a9755de8d24837b3555d53cc416b77ac87
|
4
|
+
data.tar.gz: 087b776e7ad8c89b4fcb90a63769820c6279d81e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2d728f55c1da30eb53de8d33397bb4b6fbe1c1d1b9003d03f11db296d34455c65d5a0f16058f904b298b9050cc5b86ee69d88223cb942c6ff9419dadf34e2501
|
7
|
+
data.tar.gz: 62479abc739d5125a2feffa8589236f4e1f83fd9a391b343bb5f49ac3510dd05293c221cb4b3521362a659804a021ce7a114556d045fc2644a223faa88a7afd9
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
ruby-2.
|
1
|
+
ruby-2.2.0
|
data/.travis.yml
CHANGED
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--markup markdown
|
data/Appraisals
CHANGED
@@ -1,7 +1,3 @@
|
|
1
|
-
appraise 'rails-3.2' do
|
2
|
-
gem 'rails', '~> 3.2.0'
|
3
|
-
end
|
4
|
-
|
5
1
|
appraise 'rails-4.0' do
|
6
2
|
gem 'rails', '~> 4.0.0'
|
7
3
|
end
|
@@ -11,5 +7,5 @@ appraise 'rails-4.1' do
|
|
11
7
|
end
|
12
8
|
|
13
9
|
appraise 'rails-4.2' do
|
14
|
-
gem 'rails', '~> 4.2.0
|
10
|
+
gem 'rails', '~> 4.2.0'
|
15
11
|
end
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -25,7 +25,8 @@ LookupBy is a thread-safe lookup table cache for ActiveRecord that reduces norma
|
|
25
25
|
|
26
26
|
### Dependencies
|
27
27
|
|
28
|
-
* Rails
|
28
|
+
* Rails 4.0+, _tested on Rails 4.0, 4.1, and 4.2_
|
29
|
+
* Ruby 1.9.3+, _tested on Ruby 1.9.3, 2.0, 2.1, 2.2 and Rubinius 1.5.2_
|
29
30
|
* PostgreSQL
|
30
31
|
|
31
32
|
### Development
|
@@ -64,12 +65,16 @@ Or install it manually:
|
|
64
65
|
LookupBy adds two "macro" methods to `ActiveRecord::Base`
|
65
66
|
|
66
67
|
```ruby
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
68
|
+
class ExampleLookup < ActiveRecord::Base
|
69
|
+
lookup_by :column_name
|
70
|
+
# Defines .[], .lookup, .is_a_lookup?, and .seed class methods.
|
71
|
+
end
|
72
|
+
|
73
|
+
class ExampleObject < ActiveRecord::Base
|
74
|
+
lookup_for :status
|
75
|
+
# Defines #status and #status= instance methods that transparently reference the lookup table.
|
76
|
+
# Defines .with_status(*names) and .without_status(*names) scopes on the model.
|
77
|
+
end
|
73
78
|
```
|
74
79
|
|
75
80
|
### Define the lookup model
|
@@ -98,6 +103,9 @@ class Status < ActiveRecord::Base
|
|
98
103
|
lookup_by :status
|
99
104
|
end
|
100
105
|
|
106
|
+
# Seed some values
|
107
|
+
Status.seed *%w[unpaid paid shipped]
|
108
|
+
|
101
109
|
# Aliases :name to the lookup attribute
|
102
110
|
Status.new(name: "paid")
|
103
111
|
```
|
@@ -140,6 +148,29 @@ Order.column_names
|
|
140
148
|
=> ["order_id", "status_id"]
|
141
149
|
```
|
142
150
|
|
151
|
+
### Seed the lookup table
|
152
|
+
|
153
|
+
```ruby
|
154
|
+
# Find or create each argument
|
155
|
+
Status.seed *%w[unpaid paid shipped returned]
|
156
|
+
```
|
157
|
+
|
158
|
+
### Manage lookups globally
|
159
|
+
|
160
|
+
```ruby
|
161
|
+
# Clear all caches
|
162
|
+
LookupBy.clear
|
163
|
+
|
164
|
+
# Disable all
|
165
|
+
LookupBy.disable
|
166
|
+
|
167
|
+
# Enable all, this will reload the caches
|
168
|
+
LookupBy.enable
|
169
|
+
|
170
|
+
# Reload all caches
|
171
|
+
LookupBy.reload
|
172
|
+
```
|
173
|
+
|
143
174
|
# Configuration
|
144
175
|
|
145
176
|
### Symbolize
|
@@ -239,7 +270,7 @@ lookup_by :column_name
|
|
239
270
|
lookup_by :column_name, cache: 20, find_or_create: true
|
240
271
|
```
|
241
272
|
|
242
|
-
### Raise on
|
273
|
+
### Raise on miss
|
243
274
|
|
244
275
|
Configure cache misses to raise a `LookupBy::RecordNotFound` error.
|
245
276
|
|
data/gemfiles/rails_4.0.gemfile
CHANGED
data/gemfiles/rails_4.1.gemfile
CHANGED
data/gemfiles/rails_4.2.gemfile
CHANGED
@@ -2,10 +2,10 @@
|
|
2
2
|
|
3
3
|
source "https://rubygems.org"
|
4
4
|
|
5
|
-
gem "rails", "~> 4.2.0
|
5
|
+
gem "rails", "~> 4.2.0"
|
6
6
|
|
7
7
|
group :development, :test do
|
8
|
-
gem "appraisal", "~> 1.0", :require => false
|
8
|
+
gem "appraisal", "~> 1.0.0", :require => false
|
9
9
|
gem "rspec"
|
10
10
|
gem "rspec-its"
|
11
11
|
gem "rspec-rails"
|
@@ -13,6 +13,7 @@
|
|
13
13
|
module LookupBy
|
14
14
|
module Association
|
15
15
|
module MacroMethods
|
16
|
+
# @see https://practicingruby.com/articles/closures-are-complicated
|
16
17
|
def lookup_for field, options = {}
|
17
18
|
begin
|
18
19
|
return unless table_exists?
|
@@ -116,24 +117,32 @@ module LookupBy
|
|
116
117
|
end
|
117
118
|
|
118
119
|
def #{field}=(arg)
|
119
|
-
|
120
|
+
result = case arg
|
120
121
|
when nil
|
121
122
|
nil
|
122
123
|
when String, Integer, IPAddr
|
123
|
-
#{class_name}[arg]
|
124
|
+
#{class_name}[arg]
|
124
125
|
when Symbol
|
125
126
|
#{%Q(raise ArgumentError, "#{foreign_key}=(Symbol): use `lookup_for :column, symbolize: true` to allow symbols") unless options[:symbolize]}
|
126
|
-
#{class_name}[arg]
|
127
|
+
#{class_name}[arg]
|
127
128
|
when #{class_name}
|
128
|
-
raise ArgumentError, "self.#{foreign_key}=(#{class_name}): must be saved" unless arg.
|
129
|
-
arg
|
129
|
+
raise ArgumentError, "self.#{foreign_key}=(#{class_name}): must be saved" unless arg.persisted?
|
130
|
+
arg
|
130
131
|
else
|
131
132
|
raise TypeError, "#{foreign_key}=(arg): arg must be a String, Symbol, Integer, IPAddr, nil, or #{class_name}"
|
132
133
|
end
|
133
134
|
|
134
|
-
#{%Q(raise LookupBy::Error, "\#{arg.inspect} is not in the <#{class_name}> lookup cache" if arg.present? &&
|
135
|
+
#{ %Q(raise LookupBy::Error, "\#{arg.inspect} is not in the <#{class_name}> lookup cache" if arg.present? && result.nil?) if strict }
|
135
136
|
|
136
|
-
|
137
|
+
if result.blank?
|
138
|
+
self.#{foreign_key} = nil
|
139
|
+
elsif result.persisted?
|
140
|
+
self.#{foreign_key} = result.id
|
141
|
+
elsif lookup_errors = result.errors[:#{lookup_field}]
|
142
|
+
lookup_errors.each do |msg|
|
143
|
+
errors.add :#{field}, msg
|
144
|
+
end
|
145
|
+
end
|
137
146
|
end
|
138
147
|
METHODS
|
139
148
|
end
|
data/lib/lookup_by/cache.rb
CHANGED
@@ -9,6 +9,7 @@ module LookupBy
|
|
9
9
|
@primary_key_type = klass.columns_hash[@primary_key].type
|
10
10
|
@field = options[:field].to_sym
|
11
11
|
@cache = {}
|
12
|
+
@reverse = {}
|
12
13
|
@order = options[:order] || @field
|
13
14
|
@read = options[:find_or_create] || options[:find]
|
14
15
|
@write = options[:find_or_create]
|
@@ -18,11 +19,13 @@ module LookupBy
|
|
18
19
|
@testing = false
|
19
20
|
@enabled = true
|
20
21
|
@safe = options[:safe] || concurrent?
|
22
|
+
@mutex = Mutex.new if @safe
|
21
23
|
|
22
24
|
@stats = { db: Hash.new(0), cache: Hash.new(0) }
|
23
25
|
|
24
26
|
raise ArgumentError, %Q(unknown attribute "#{@field}" for <#{klass}>) unless klass.column_names.include?(@field.to_s)
|
25
27
|
|
28
|
+
# Order matters here, some instance variables depend on prior assignments.
|
26
29
|
case options[:cache]
|
27
30
|
when true
|
28
31
|
@type = :all
|
@@ -35,6 +38,7 @@ module LookupBy
|
|
35
38
|
@type = :lru
|
36
39
|
@limit = options[:cache]
|
37
40
|
@cache = @safe ? Caching::SafeLRU.new(@limit) : Caching::LRU.new(@limit)
|
41
|
+
@reverse = @safe ? Caching::SafeLRU.new(@limit) : Caching::LRU.new(@limit)
|
38
42
|
@read = true
|
39
43
|
@write ||= false
|
40
44
|
@testing = true if Rails.env.test? && @write
|
@@ -53,8 +57,8 @@ module LookupBy
|
|
53
57
|
clear
|
54
58
|
|
55
59
|
::ActiveRecord::Base.connection.send :log, "", "#{@klass.name} Load Cache All" do
|
56
|
-
@klass.order(@order).each do |
|
57
|
-
|
60
|
+
@klass.order(@order).each do |object|
|
61
|
+
cache_write(object)
|
58
62
|
end
|
59
63
|
end
|
60
64
|
end
|
@@ -65,19 +69,23 @@ module LookupBy
|
|
65
69
|
|
66
70
|
def create(*args, &block)
|
67
71
|
created = @klass.create(*args, &block)
|
68
|
-
|
72
|
+
|
73
|
+
cache_write(created) if cache?
|
74
|
+
|
69
75
|
created
|
70
76
|
end
|
71
77
|
|
72
78
|
def create!(*args, &block)
|
73
79
|
created = @klass.create!(*args, &block)
|
74
|
-
|
80
|
+
|
81
|
+
cache_write(created) if cache?
|
82
|
+
|
75
83
|
created
|
76
84
|
end
|
77
85
|
|
78
86
|
def seed(*values)
|
79
87
|
@klass.transaction(requires_new: true) do
|
80
|
-
values.
|
88
|
+
values.map { |value| @klass.where(@field => value).first_or_create! }
|
81
89
|
end
|
82
90
|
end
|
83
91
|
|
@@ -89,7 +97,7 @@ module LookupBy
|
|
89
97
|
found = cache_read(value) if cache?
|
90
98
|
found ||= db_read(value) if @read || !@enabled
|
91
99
|
|
92
|
-
|
100
|
+
cache_write(found) if cache?
|
93
101
|
|
94
102
|
found ||= db_write(value) if @write
|
95
103
|
|
@@ -132,13 +140,9 @@ module LookupBy
|
|
132
140
|
|
133
141
|
private
|
134
142
|
|
143
|
+
# RAILS_ENV=test will not use the SafeLRU
|
135
144
|
def concurrent?
|
136
|
-
|
137
|
-
when 4 then Rails.configuration.cache_classes && Rails.configuration.eager_load
|
138
|
-
when 3 then Rails.configuration.allow_concurrency
|
139
|
-
else
|
140
|
-
true
|
141
|
-
end
|
145
|
+
Rails.configuration.cache_classes && Rails.configuration.eager_load
|
142
146
|
end
|
143
147
|
|
144
148
|
def primary_key?(value)
|
@@ -154,26 +158,61 @@ module LookupBy
|
|
154
158
|
@klass.new(@field => value).send(@field)
|
155
159
|
end
|
156
160
|
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
161
|
+
|
162
|
+
if Rails.env.production?
|
163
|
+
def cache_read(value)
|
164
|
+
if primary_key?(value)
|
165
|
+
@cache[value]
|
166
|
+
else
|
167
|
+
@reverse[value]
|
168
|
+
end
|
162
169
|
end
|
170
|
+
else
|
171
|
+
def cache_read(value)
|
172
|
+
found = if primary_key?(value)
|
173
|
+
@cache[value]
|
174
|
+
else
|
175
|
+
@reverse[value]
|
176
|
+
end
|
163
177
|
|
164
|
-
|
178
|
+
increment :cache, found ? :hit : :miss
|
165
179
|
|
166
|
-
|
180
|
+
found
|
181
|
+
end
|
167
182
|
end
|
168
183
|
|
169
|
-
|
170
|
-
|
184
|
+
if Rails.env.production?
|
185
|
+
def db_read(value)
|
186
|
+
@klass.where(column_for(value) => value).first
|
187
|
+
end
|
188
|
+
else
|
189
|
+
def db_read(value)
|
190
|
+
increment :db, :get
|
171
191
|
|
172
|
-
|
192
|
+
found = @klass.where(column_for(value) => value).first
|
173
193
|
|
174
|
-
|
194
|
+
increment :db, found ? :hit : :miss
|
175
195
|
|
176
|
-
|
196
|
+
found
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
if @safe
|
201
|
+
def cache_write(object)
|
202
|
+
return unless object
|
203
|
+
|
204
|
+
@mutex.synchronize do
|
205
|
+
@cache[object.id] = object
|
206
|
+
@reverse[object.send(@field)] = object
|
207
|
+
end
|
208
|
+
end
|
209
|
+
else
|
210
|
+
def cache_write(object)
|
211
|
+
return unless object
|
212
|
+
|
213
|
+
@cache[object.id] = object
|
214
|
+
@reverse[object.send(@field)] = object
|
215
|
+
end
|
177
216
|
end
|
178
217
|
|
179
218
|
def db_write(value)
|
@@ -182,7 +221,7 @@ module LookupBy
|
|
182
221
|
return if column == @primary_key
|
183
222
|
|
184
223
|
@klass.transaction(requires_new: true) do
|
185
|
-
@klass.create
|
224
|
+
@klass.create(column => value)
|
186
225
|
end
|
187
226
|
rescue ActiveRecord::RecordNotUnique
|
188
227
|
db_read(value)
|
data/lib/lookup_by/lookup.rb
CHANGED
@@ -32,8 +32,8 @@ module LookupBy
|
|
32
32
|
options.assert_valid_keys :allow_blank, :order, :cache, :normalize, :find, :find_or_create, :raise, :safe
|
33
33
|
|
34
34
|
raise "#{self} already called lookup_by" if is_a? Lookup::ClassMethods
|
35
|
-
raise "#{self} responds_to
|
36
|
-
raise "#{self} responds_to
|
35
|
+
raise "#{self} responds_to .[], needed for lookup_by" if respond_to? :[]
|
36
|
+
raise "#{self} responds_to .lookup, needed for lookup_by" if respond_to? :lookup
|
37
37
|
|
38
38
|
extend ClassMethods
|
39
39
|
|
@@ -57,13 +57,15 @@ module LookupBy
|
|
57
57
|
@lookup = Cache.new(self, options.merge(field: field))
|
58
58
|
@lookup.reload
|
59
59
|
end
|
60
|
+
|
61
|
+
LookupBy.register self
|
60
62
|
end
|
61
63
|
end
|
62
64
|
|
63
65
|
module ClassMethods
|
64
66
|
# TODO: Rails 4 needs to return a proxy object here
|
65
67
|
def all(*args)
|
66
|
-
return super if Rails::VERSION::MAJOR
|
68
|
+
return super if Rails::VERSION::MAJOR >= 4
|
67
69
|
return super if @lookup.read_through?
|
68
70
|
return super if args.any?
|
69
71
|
|
@@ -77,10 +79,11 @@ module LookupBy
|
|
77
79
|
@lookup.cache.size
|
78
80
|
end
|
79
81
|
|
80
|
-
def pluck(
|
82
|
+
def pluck(*column_names)
|
81
83
|
return super if @lookup.read_through?
|
84
|
+
return super if column_names.size > 1
|
82
85
|
|
83
|
-
@lookup.cache.values.map { |o| o.send(
|
86
|
+
@lookup.cache.values.map { |o| o.send(column_names.first) }
|
84
87
|
end
|
85
88
|
|
86
89
|
def [](*args)
|
@@ -100,6 +103,12 @@ module LookupBy
|
|
100
103
|
else return args.map { |arg| self[arg] }
|
101
104
|
end
|
102
105
|
end
|
106
|
+
|
107
|
+
def seed(*args)
|
108
|
+
super if defined?(super)
|
109
|
+
|
110
|
+
@lookup.seed *args
|
111
|
+
end
|
103
112
|
end
|
104
113
|
|
105
114
|
module InstanceMethods
|
@@ -116,6 +125,21 @@ module LookupBy
|
|
116
125
|
end
|
117
126
|
|
118
127
|
module SchemaMethods
|
128
|
+
# Create a lookup table.
|
129
|
+
#
|
130
|
+
# @example
|
131
|
+
# create_lookup_table :statuses, schema: "custom", small: true
|
132
|
+
#
|
133
|
+
# create_lookup_table :companies do |t|
|
134
|
+
# t.string :short_name
|
135
|
+
# end
|
136
|
+
#
|
137
|
+
# @param [Symbol] name
|
138
|
+
# @param [Hash] options
|
139
|
+
# @option options [Symbol] lookup_column Name of the lookup column.
|
140
|
+
# @option options [Symbol] lookup_type Type of the lookup column, _e.g. :text, :uuid, or :inet_.
|
141
|
+
# @option options [String] primary_key Name of the primary key.
|
142
|
+
# @option options [String] schema
|
119
143
|
def create_lookup_table(name, options = {})
|
120
144
|
options.symbolize_keys!
|
121
145
|
|
data/lib/lookup_by/version.rb
CHANGED
data/lib/lookup_by.rb
CHANGED
@@ -4,6 +4,12 @@ require "lookup_by/railtie" if defined? Rails
|
|
4
4
|
module LookupBy
|
5
5
|
class Error < StandardError; end
|
6
6
|
|
7
|
+
mattr_accessor :classes
|
8
|
+
self.classes = []
|
9
|
+
|
10
|
+
mattr_accessor :mutex
|
11
|
+
self.mutex = Mutex.new
|
12
|
+
|
7
13
|
UUID_REGEX = /\A\h{8}-\h{4}-\h{4}-\h{4}-\h{12}\Z/
|
8
14
|
UUID_REGEX_V4 = /\A\h{8}-\h{4}-4\h{3}-[89aAbB]\h{3}-\h{12}\Z/
|
9
15
|
|
@@ -16,6 +22,34 @@ module LookupBy
|
|
16
22
|
autoload :LRU, "lookup_by/caching/lru"
|
17
23
|
autoload :SafeLRU, "lookup_by/caching/safe_lru"
|
18
24
|
end
|
25
|
+
|
26
|
+
class << self
|
27
|
+
def register(klass)
|
28
|
+
mutex.synchronize do
|
29
|
+
self.classes << klass unless classes.include?(klass)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def lookups
|
34
|
+
classes.map { |klass| klass.lookup }
|
35
|
+
end
|
36
|
+
|
37
|
+
def clear
|
38
|
+
lookups.each { |lookup| lookup.clear }
|
39
|
+
end
|
40
|
+
|
41
|
+
def disable
|
42
|
+
lookups.each { |lookup| lookup.disable! }
|
43
|
+
end
|
44
|
+
|
45
|
+
def enable
|
46
|
+
lookups.each { |lookup| lookup.enable! }
|
47
|
+
end
|
48
|
+
|
49
|
+
def reload
|
50
|
+
lookups.each { |lookup| lookup.reload }
|
51
|
+
end
|
52
|
+
end
|
19
53
|
end
|
20
54
|
|
21
55
|
begin
|
data/lookup_by.gemspec
CHANGED
@@ -22,8 +22,8 @@ Gem::Specification.new do |gem|
|
|
22
22
|
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
23
23
|
gem.require_paths = ["lib"]
|
24
24
|
|
25
|
-
gem.add_dependency "rails", ">=
|
25
|
+
gem.add_dependency "rails", ">= 4.0.0"
|
26
26
|
|
27
|
-
gem.add_development_dependency "bundler", "
|
27
|
+
gem.add_development_dependency "bundler", ">= 1.7.0"
|
28
28
|
gem.add_development_dependency "rake"
|
29
29
|
end
|
data/spec/association_spec.rb
CHANGED
@@ -62,12 +62,12 @@ describe LookupBy::Association do
|
|
62
62
|
it_behaves_like "a lookup for", :city
|
63
63
|
|
64
64
|
it "accepts Integers" do
|
65
|
-
subject.city = City
|
65
|
+
subject.city = City["New York"].id
|
66
66
|
expect(subject.city).to eq "New York"
|
67
67
|
end
|
68
68
|
|
69
69
|
it "rejects symbols" do
|
70
|
-
expect { subject.city = :
|
70
|
+
expect { subject.city = :invalid }.to raise_error ArgumentError
|
71
71
|
end
|
72
72
|
|
73
73
|
it "returns strings" do
|
@@ -100,6 +100,8 @@ describe LookupBy::Association do
|
|
100
100
|
end
|
101
101
|
|
102
102
|
context "Address.lookup_for :street" do
|
103
|
+
it_behaves_like "a lookup for", :street
|
104
|
+
|
103
105
|
it "accepts write-through values" do
|
104
106
|
expect { subject.street = "Dearborn Street" }.to change(Street, :count)
|
105
107
|
end
|
@@ -192,3 +194,18 @@ describe LookupBy::Association, 'scopes' do
|
|
192
194
|
end
|
193
195
|
end
|
194
196
|
end
|
197
|
+
|
198
|
+
context 'validation' do
|
199
|
+
subject { Account.new(phone_number: "invalid") }
|
200
|
+
|
201
|
+
# it { is_expected.to have(2).errors_on(:phone_number) }
|
202
|
+
# it { expect(subject).to have(2).errors_on(:phone_number) }
|
203
|
+
|
204
|
+
# it 'bubbles errors' do
|
205
|
+
# expect(subject).to have(2).errors_on(:phone_number)
|
206
|
+
# end
|
207
|
+
|
208
|
+
it 'bubbles errors' do
|
209
|
+
expect(subject.errors[:phone_number].size).to eq(2)
|
210
|
+
end
|
211
|
+
end
|
@@ -5,11 +5,13 @@ class CreateTables < ActiveRecord::Migration
|
|
5
5
|
create_lookup_table :user_agents
|
6
6
|
create_lookup_table :email_addresses
|
7
7
|
|
8
|
-
|
8
|
+
|
9
9
|
create_lookup_table :statuses, small: true
|
10
10
|
|
11
11
|
create_lookup_table :ip_addresses, lookup_type: :inet
|
12
12
|
|
13
|
+
create_lookup_table :phone_numbers
|
14
|
+
|
13
15
|
create_lookup_table :uncacheables
|
14
16
|
create_lookup_table :unfindables
|
15
17
|
|
@@ -24,7 +26,11 @@ class CreateTables < ActiveRecord::Migration
|
|
24
26
|
|
25
27
|
create_lookup_table :paths, schema: 'traffic', id: :uuid
|
26
28
|
|
27
|
-
|
29
|
+
create_lookup_table :accounts do |t|
|
30
|
+
t.belongs_to :phone_number
|
31
|
+
end
|
32
|
+
|
33
|
+
create_lookup_table :addresses do |t|
|
28
34
|
t.belongs_to :city
|
29
35
|
t.belongs_to :state
|
30
36
|
t.belongs_to :postal_code
|
data/spec/dummy/db/structure.sql
CHANGED
@@ -56,7 +56,8 @@ SET default_with_oids = false;
|
|
56
56
|
|
57
57
|
CREATE TABLE accounts (
|
58
58
|
account_id integer NOT NULL,
|
59
|
-
account text NOT NULL
|
59
|
+
account text NOT NULL,
|
60
|
+
phone_number_id integer
|
60
61
|
);
|
61
62
|
|
62
63
|
|
@@ -85,6 +86,7 @@ ALTER SEQUENCE accounts_account_id_seq OWNED BY accounts.account_id;
|
|
85
86
|
|
86
87
|
CREATE TABLE addresses (
|
87
88
|
address_id integer NOT NULL,
|
89
|
+
address text NOT NULL,
|
88
90
|
city_id integer,
|
89
91
|
state_id integer,
|
90
92
|
postal_code_id integer,
|
@@ -228,6 +230,35 @@ CREATE SEQUENCE ip_addresses_ip_address_id_seq
|
|
228
230
|
ALTER SEQUENCE ip_addresses_ip_address_id_seq OWNED BY ip_addresses.ip_address_id;
|
229
231
|
|
230
232
|
|
233
|
+
--
|
234
|
+
-- Name: phone_numbers; Type: TABLE; Schema: public; Owner: -; Tablespace:
|
235
|
+
--
|
236
|
+
|
237
|
+
CREATE TABLE phone_numbers (
|
238
|
+
phone_number_id integer NOT NULL,
|
239
|
+
phone_number text NOT NULL
|
240
|
+
);
|
241
|
+
|
242
|
+
|
243
|
+
--
|
244
|
+
-- Name: phone_numbers_phone_number_id_seq; Type: SEQUENCE; Schema: public; Owner: -
|
245
|
+
--
|
246
|
+
|
247
|
+
CREATE SEQUENCE phone_numbers_phone_number_id_seq
|
248
|
+
START WITH 1
|
249
|
+
INCREMENT BY 1
|
250
|
+
NO MINVALUE
|
251
|
+
NO MAXVALUE
|
252
|
+
CACHE 1;
|
253
|
+
|
254
|
+
|
255
|
+
--
|
256
|
+
-- Name: phone_numbers_phone_number_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
|
257
|
+
--
|
258
|
+
|
259
|
+
ALTER SEQUENCE phone_numbers_phone_number_id_seq OWNED BY phone_numbers.phone_number_id;
|
260
|
+
|
261
|
+
|
231
262
|
--
|
232
263
|
-- Name: postal_codes; Type: TABLE; Schema: public; Owner: -; Tablespace:
|
233
264
|
--
|
@@ -583,6 +614,13 @@ ALTER TABLE ONLY email_addresses ALTER COLUMN email_address_id SET DEFAULT nextv
|
|
583
614
|
ALTER TABLE ONLY ip_addresses ALTER COLUMN ip_address_id SET DEFAULT nextval('ip_addresses_ip_address_id_seq'::regclass);
|
584
615
|
|
585
616
|
|
617
|
+
--
|
618
|
+
-- Name: phone_number_id; Type: DEFAULT; Schema: public; Owner: -
|
619
|
+
--
|
620
|
+
|
621
|
+
ALTER TABLE ONLY phone_numbers ALTER COLUMN phone_number_id SET DEFAULT nextval('phone_numbers_phone_number_id_seq'::regclass);
|
622
|
+
|
623
|
+
|
586
624
|
--
|
587
625
|
-- Name: postal_code_id; Type: DEFAULT; Schema: public; Owner: -
|
588
626
|
--
|
@@ -701,6 +739,14 @@ ALTER TABLE ONLY ip_addresses
|
|
701
739
|
ADD CONSTRAINT ip_addresses_pkey PRIMARY KEY (ip_address_id);
|
702
740
|
|
703
741
|
|
742
|
+
--
|
743
|
+
-- Name: phone_numbers_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace:
|
744
|
+
--
|
745
|
+
|
746
|
+
ALTER TABLE ONLY phone_numbers
|
747
|
+
ADD CONSTRAINT phone_numbers_pkey PRIMARY KEY (phone_number_id);
|
748
|
+
|
749
|
+
|
704
750
|
--
|
705
751
|
-- Name: postal_codes_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace:
|
706
752
|
--
|
@@ -800,6 +846,13 @@ SET search_path = public, pg_catalog;
|
|
800
846
|
CREATE UNIQUE INDEX accounts__u_account ON accounts USING btree (account);
|
801
847
|
|
802
848
|
|
849
|
+
--
|
850
|
+
-- Name: addresses__u_address; Type: INDEX; Schema: public; Owner: -; Tablespace:
|
851
|
+
--
|
852
|
+
|
853
|
+
CREATE UNIQUE INDEX addresses__u_address ON addresses USING btree (address);
|
854
|
+
|
855
|
+
|
803
856
|
--
|
804
857
|
-- Name: cities__u_city; Type: INDEX; Schema: public; Owner: -; Tablespace:
|
805
858
|
--
|
@@ -828,6 +881,13 @@ CREATE UNIQUE INDEX email_addresses__u_email_address ON email_addresses USING bt
|
|
828
881
|
CREATE UNIQUE INDEX ip_addresses__u_ip_address ON ip_addresses USING btree (ip_address);
|
829
882
|
|
830
883
|
|
884
|
+
--
|
885
|
+
-- Name: phone_numbers__u_phone_number; Type: INDEX; Schema: public; Owner: -; Tablespace:
|
886
|
+
--
|
887
|
+
|
888
|
+
CREATE UNIQUE INDEX phone_numbers__u_phone_number ON phone_numbers USING btree (phone_number);
|
889
|
+
|
890
|
+
|
831
891
|
--
|
832
892
|
-- Name: postal_codes__u_postal_code; Type: INDEX; Schema: public; Owner: -; Tablespace:
|
833
893
|
--
|
data/spec/lookup_by_spec.rb
CHANGED
@@ -1,6 +1,42 @@
|
|
1
1
|
require "rails_helper"
|
2
2
|
require "lookup_by"
|
3
3
|
|
4
|
+
describe LookupBy do
|
5
|
+
describe ".register" do
|
6
|
+
it "adds its argument to .lookups" do
|
7
|
+
LookupBy.register(Array)
|
8
|
+
expect(LookupBy.classes).to include(Array)
|
9
|
+
LookupBy.classes.delete(Array)
|
10
|
+
end
|
11
|
+
|
12
|
+
it "doesn't register classes twice" do
|
13
|
+
LookupBy.register(Array)
|
14
|
+
LookupBy.register(Array)
|
15
|
+
|
16
|
+
expect(LookupBy.classes.select { |l| l == Array }.size).to eq(1)
|
17
|
+
|
18
|
+
LookupBy.classes.delete(Array)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
describe ".clear" do
|
23
|
+
it "clears all lookup caches" do
|
24
|
+
Path.lookup.cache[1] = "remove-this"
|
25
|
+
expect { LookupBy.clear }.to change { Path.lookup.cache.size }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe ".disable and .enable" do
|
30
|
+
it "affects all lookups" do
|
31
|
+
expect(City.lookup.enabled?).to eq(true)
|
32
|
+
LookupBy.disable
|
33
|
+
expect(City.lookup.enabled?).to eq(false)
|
34
|
+
LookupBy.enable
|
35
|
+
expect(City.lookup.enabled?).to eq(true)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
4
40
|
describe ::ActiveRecord::Base do
|
5
41
|
describe "macro methods" do
|
6
42
|
subject { described_class }
|
@@ -9,26 +45,50 @@ describe ::ActiveRecord::Base do
|
|
9
45
|
it { is_expected.to respond_to :is_a_lookup? }
|
10
46
|
end
|
11
47
|
|
48
|
+
describe ".lookup_by" do
|
49
|
+
class CityTest < ActiveRecord::Base
|
50
|
+
self.table_name = "cities"
|
51
|
+
lookup_by :city
|
52
|
+
end
|
53
|
+
|
54
|
+
it "registers lookup models" do
|
55
|
+
expect(LookupBy.classes).to include(CityTest)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
12
59
|
describe "instance methods" do
|
13
60
|
subject { Status.new }
|
14
61
|
|
15
62
|
it { is_expected.to respond_to :name }
|
16
63
|
end
|
17
|
-
|
18
64
|
end
|
19
65
|
|
20
|
-
describe LookupBy::Lookup do
|
21
|
-
describe "
|
22
|
-
it "
|
23
|
-
City.
|
24
|
-
City.
|
66
|
+
describe LookupBy::Lookup::ClassMethods do
|
67
|
+
describe "#seed" do
|
68
|
+
it "accepts multiple values" do
|
69
|
+
City.seed 'Boston'
|
70
|
+
City.seed 'Chicago', 'New York City'
|
25
71
|
|
26
|
-
|
27
|
-
|
28
|
-
|
72
|
+
expect(City.pluck(:city).sort).to eq(['Boston', 'Chicago', 'New York City'])
|
73
|
+
end
|
74
|
+
|
75
|
+
it "accepts duplicates" do
|
76
|
+
expect { City.seed 'Chicago', 'Chicago' }.not_to raise_error
|
77
|
+
|
78
|
+
if Rails::VERSION::MAJOR == 4 && Rails::VERSION::MINOR == 0
|
79
|
+
expect(City.pluck(:city)).to eq(['Chicago'])
|
80
|
+
else
|
81
|
+
expect(City.pluck(:name)).to eq(['Chicago'])
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
it "returns objects" do
|
86
|
+
expect(City.seed 'Chicago').to eq(City.all)
|
29
87
|
end
|
30
88
|
end
|
89
|
+
end
|
31
90
|
|
91
|
+
describe LookupBy::Lookup do
|
32
92
|
context "Uncacheable.lookup_by :column, cache: true, find_or_create: true" do
|
33
93
|
it "fails when trying to cache and write-through" do
|
34
94
|
expect { Uncacheable }.to raise_error
|
@@ -83,7 +143,12 @@ describe LookupBy::Lookup do
|
|
83
143
|
it_behaves_like "a strict cache"
|
84
144
|
|
85
145
|
it "preloads the cache" do
|
86
|
-
|
146
|
+
class StateTest < ActiveRecord::Base
|
147
|
+
self.table_name = "states"
|
148
|
+
lookup_by :state, cache: true
|
149
|
+
end
|
150
|
+
|
151
|
+
expect(StateTest.lookup.cache).not_to be_empty
|
87
152
|
end
|
88
153
|
end
|
89
154
|
|
data/spec/rails_helper.rb
CHANGED
@@ -39,8 +39,6 @@ Dir[Rails.root.join( "../support/**/*.rb")].each { |f| require f }
|
|
39
39
|
# If you are not using ActiveRecord, you can remove this line.
|
40
40
|
ActiveRecord::Migration.maintain_test_schema! if ActiveRecord::Migration.respond_to?(:maintain_test_schema!)
|
41
41
|
|
42
|
-
# ActiveRecord::Migration.maintain_test_schema! if defined?(ActiveRecord::Migration) && ActiveRecord::Migration.respond_to?(:maintain_test_schema!)
|
43
|
-
|
44
42
|
RSpec.configure do |config|
|
45
43
|
config.use_transactional_fixtures = true
|
46
44
|
|
data/spec/spec_helper.rb
CHANGED
@@ -15,6 +15,32 @@
|
|
15
15
|
#
|
16
16
|
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
17
17
|
RSpec.configure do |config|
|
18
|
+
# rspec-expectations config goes here. You can use an alternate
|
19
|
+
# assertion/expectation library such as wrong or the stdlib/minitest
|
20
|
+
# assertions if you prefer.
|
21
|
+
config.expect_with :rspec do |expectations|
|
22
|
+
# Enable only the newer, non-monkey-patching expect syntax.
|
23
|
+
# For more details, see:
|
24
|
+
# - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
|
25
|
+
expectations.syntax = :expect
|
26
|
+
end
|
27
|
+
|
28
|
+
# rspec-mocks config goes here. You can use an alternate test double
|
29
|
+
# library (such as bogus or mocha) by changing the `mock_with` option here.
|
30
|
+
config.mock_with :rspec do |mocks|
|
31
|
+
# Enable only the newer, non-monkey-patching expect syntax.
|
32
|
+
# For more details, see:
|
33
|
+
# - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
|
34
|
+
mocks.syntax = :expect
|
35
|
+
|
36
|
+
# Prevents you from mocking or stubbing a method that does not exist on
|
37
|
+
# a real object. This is generally recommended.
|
38
|
+
mocks.verify_partial_doubles = true
|
39
|
+
end
|
40
|
+
|
41
|
+
# The settings below are suggested to provide a good initial experience
|
42
|
+
# with RSpec, but feel free to customize to your heart's content.
|
43
|
+
|
18
44
|
# These two settings work together to allow you to limit a spec run
|
19
45
|
# to individual examples or groups you care about by tagging them with
|
20
46
|
# `:focus` metadata. When nothing is tagged with `:focus`, all examples
|
@@ -49,26 +75,11 @@ RSpec.configure do |config|
|
|
49
75
|
# as the one that triggered the failure.
|
50
76
|
Kernel.srand config.seed
|
51
77
|
|
52
|
-
|
53
|
-
|
54
|
-
# assertions if you prefer.
|
55
|
-
config.expect_with :rspec do |expectations|
|
56
|
-
# Enable only the newer, non-monkey-patching expect syntax.
|
57
|
-
# For more details, see:
|
58
|
-
# - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
|
59
|
-
expectations.syntax = :expect
|
78
|
+
config.before(:each) do
|
79
|
+
LookupBy.reload
|
60
80
|
end
|
61
81
|
|
62
|
-
|
63
|
-
|
64
|
-
config.mock_with :rspec do |mocks|
|
65
|
-
# Enable only the newer, non-monkey-patching expect syntax.
|
66
|
-
# For more details, see:
|
67
|
-
# - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
|
68
|
-
mocks.syntax = :expect
|
69
|
-
|
70
|
-
# Prevents you from mocking or stubbing a method that does not exist on
|
71
|
-
# a real object. This is generally recommended.
|
72
|
-
mocks.verify_partial_doubles = true
|
82
|
+
config.after(:each) do
|
83
|
+
LookupBy.clear
|
73
84
|
end
|
74
85
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: lookup_by
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.10.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Erik Peterson
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2015-02-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -16,28 +16,28 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
19
|
+
version: 4.0.0
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
26
|
+
version: 4.0.0
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: bundler
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - "
|
31
|
+
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
33
|
+
version: 1.7.0
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- - "
|
38
|
+
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version:
|
40
|
+
version: 1.7.0
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: rake
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -62,13 +62,13 @@ files:
|
|
62
62
|
- ".gitignore"
|
63
63
|
- ".ruby-version"
|
64
64
|
- ".travis.yml"
|
65
|
+
- ".yardopts"
|
65
66
|
- Appraisals
|
66
67
|
- Gemfile
|
67
68
|
- MIT-LICENSE
|
68
69
|
- README.md
|
69
70
|
- Rakefile
|
70
71
|
- TODO.md
|
71
|
-
- gemfiles/rails_3.2.gemfile
|
72
72
|
- gemfiles/rails_4.0.gemfile
|
73
73
|
- gemfiles/rails_4.1.gemfile
|
74
74
|
- gemfiles/rails_4.2.gemfile
|
@@ -96,6 +96,7 @@ files:
|
|
96
96
|
- spec/dummy/app/models/email_address.rb
|
97
97
|
- spec/dummy/app/models/ip_address.rb
|
98
98
|
- spec/dummy/app/models/path.rb
|
99
|
+
- spec/dummy/app/models/phone_number.rb
|
99
100
|
- spec/dummy/app/models/postal_code.rb
|
100
101
|
- spec/dummy/app/models/raisin.rb
|
101
102
|
- spec/dummy/app/models/read_through_raisin.rb
|
@@ -151,7 +152,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
151
152
|
version: '0'
|
152
153
|
requirements: []
|
153
154
|
rubyforge_project:
|
154
|
-
rubygems_version: 2.4.
|
155
|
+
rubygems_version: 2.4.5
|
155
156
|
signing_key:
|
156
157
|
specification_version: 4
|
157
158
|
summary: A thread-safe lookup table cache for ActiveRecord
|
@@ -167,6 +168,7 @@ test_files:
|
|
167
168
|
- spec/dummy/app/models/email_address.rb
|
168
169
|
- spec/dummy/app/models/ip_address.rb
|
169
170
|
- spec/dummy/app/models/path.rb
|
171
|
+
- spec/dummy/app/models/phone_number.rb
|
170
172
|
- spec/dummy/app/models/postal_code.rb
|
171
173
|
- spec/dummy/app/models/raisin.rb
|
172
174
|
- spec/dummy/app/models/read_through_raisin.rb
|
data/gemfiles/rails_3.2.gemfile
DELETED
@@ -1,22 +0,0 @@
|
|
1
|
-
# This file was generated by Appraisal
|
2
|
-
|
3
|
-
source "https://rubygems.org"
|
4
|
-
|
5
|
-
gem "rails", "~> 3.2.0"
|
6
|
-
|
7
|
-
group :development, :test do
|
8
|
-
gem "appraisal", "~> 1.0", :require => false
|
9
|
-
gem "rspec"
|
10
|
-
gem "rspec-its"
|
11
|
-
gem "rspec-rails"
|
12
|
-
gem "pg", :platform => :ruby
|
13
|
-
gem "activerecord-jdbcpostgresql-adapter", :platform => :jruby
|
14
|
-
gem "simplecov", :require => false
|
15
|
-
gem "coveralls", :require => false
|
16
|
-
gem "pry", :require => false
|
17
|
-
gem "colored", :require => false
|
18
|
-
gem "racc"
|
19
|
-
gem "json"
|
20
|
-
end
|
21
|
-
|
22
|
-
gemspec :path => "../"
|