lookup_by 0.9.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f743786ef23e1404d94ab4e182d9fc4e56ebf02b
4
- data.tar.gz: 544619c07c7172702cba5729e8a8c1bb80783ddf
3
+ metadata.gz: b7b482a9755de8d24837b3555d53cc416b77ac87
4
+ data.tar.gz: 087b776e7ad8c89b4fcb90a63769820c6279d81e
5
5
  SHA512:
6
- metadata.gz: c5e44713f89aaca0651af41331620efec4fd7e42a9dbf2d72b2617ddb95ee77d96b8f1bcb17832a31d65365aa62af4820ed29c6b23fc84e99c262446c875af59
7
- data.tar.gz: 46803218755acd5241351f1ae535a6a71b41f8adfbd0fe9d53897ea677cd11358318a134c14a5ddf61487c4f823cd9eab7a6123d36c6a5ee3b2d1e211ad7577c
6
+ metadata.gz: 2d728f55c1da30eb53de8d33397bb4b6fbe1c1d1b9003d03f11db296d34455c65d5a0f16058f904b298b9050cc5b86ee69d88223cb942c6ff9419dadf34e2501
7
+ data.tar.gz: 62479abc739d5125a2feffa8589236f4e1f83fd9a391b343bb5f49ac3510dd05293c221cb4b3521362a659804a021ce7a114556d045fc2644a223faa88a7afd9
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- ruby-2.1.2
1
+ ruby-2.2.0
data/.travis.yml CHANGED
@@ -1,9 +1,10 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 1.9.3
4
- - 2.0.0
5
- - 2.1.2
6
- - rbx-2.2.10
3
+ - 1.9.3-p551
4
+ - 2.0.0-p598
5
+ - 2.1.5
6
+ - 2.2.0
7
+ - rbx-2.5.2
7
8
  services:
8
9
  - postgresql
9
10
  before_script:
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.beta1'
10
+ gem 'rails', '~> 4.2.0'
15
11
  end
data/Gemfile CHANGED
@@ -6,7 +6,7 @@ source "https://rubygems.org"
6
6
  gemspec
7
7
 
8
8
  group :development, :test do
9
- gem 'appraisal', '~> 1.0', require: false
9
+ gem 'appraisal', '~> 1.0.0', require: false
10
10
 
11
11
  gem 'rspec'
12
12
  gem 'rspec-its'
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 3.2+, _tested on Rails 3.2, 4.0, 4.1, and 4.2_
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
- lookup_by :column_name
68
- # Defines .[], .lookup, and .is_a_lookup? class methods.
69
-
70
- lookup_for :status
71
- # Defines #status and #status= instance methods that transparently reference the lookup table.
72
- # Defines .with_status(*names) and .without_status(*names) scopes on the model.
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 Miss
273
+ ### Raise on miss
243
274
 
244
275
  Configure cache misses to raise a `LookupBy::RecordNotFound` error.
245
276
 
@@ -5,7 +5,7 @@ source "https://rubygems.org"
5
5
  gem "rails", "~> 4.0.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"
@@ -5,7 +5,7 @@ source "https://rubygems.org"
5
5
  gem "rails", "~> 4.1.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"
@@ -2,10 +2,10 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "rails", "~> 4.2.0.beta1"
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
- value = case arg
120
+ result = case arg
120
121
  when nil
121
122
  nil
122
123
  when String, Integer, IPAddr
123
- #{class_name}[arg].try(:id)
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].try(:id)
127
+ #{class_name}[arg]
127
128
  when #{class_name}
128
- raise ArgumentError, "self.#{foreign_key}=(#{class_name}): must be saved" unless arg.id
129
- arg.id
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? && value.nil?) if strict}
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
- self.#{foreign_key} = value
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
@@ -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 |i|
57
- @cache[i.id] = i
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
- @cache[created.id] = created if created && cache?
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
- @cache[created.id] = created if cache?
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.each { |value| create!(@field => value) }
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
- @cache[found.id] = found if found && cache?
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
- case Rails::VERSION::MAJOR
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
- def cache_read(value)
158
- if primary_key?(value)
159
- found = @cache[value]
160
- else
161
- found = @cache.values.detect { |o| o.send(@field) == value }
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
- increment :cache, found ? :hit : :miss
178
+ increment :cache, found ? :hit : :miss
165
179
 
166
- found
180
+ found
181
+ end
167
182
  end
168
183
 
169
- def db_read(value)
170
- increment :db, :get
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
- found = @klass.where(column_for(value) => value).first
192
+ found = @klass.where(column_for(value) => value).first
173
193
 
174
- increment :db, found ? :hit : :miss
194
+ increment :db, found ? :hit : :miss
175
195
 
176
- found
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!(column => value)
224
+ @klass.create(column => value)
186
225
  end
187
226
  rescue ActiveRecord::RecordNotUnique
188
227
  db_read(value)
@@ -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 :[], needed for lookup_by" if respond_to? :[]
36
- raise "#{self} responds_to :lookup, needed for lookup_by" if respond_to? :lookup
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 > 3
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(column_name)
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(column_name) }
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
 
@@ -1,3 +1,3 @@
1
1
  module LookupBy
2
- VERSION = "0.9.1"
2
+ VERSION = "0.10.0"
3
3
  end
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", ">= 3.2.0"
25
+ gem.add_dependency "rails", ">= 4.0.0"
26
26
 
27
- gem.add_development_dependency "bundler", "~> 1.3"
27
+ gem.add_development_dependency "bundler", ">= 1.7.0"
28
28
  gem.add_development_dependency "rake"
29
29
  end
@@ -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.where(city: "New York").first.id
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 = :'New York' }.to raise_error ArgumentError
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
@@ -1,3 +1,5 @@
1
1
  class Account < ActiveRecord::Base
2
- lookup_by :account, cache: true, find: true
2
+ lookup_by :account, cache: true, find: true
3
+
4
+ lookup_for :phone_number
3
5
  end
@@ -1,2 +1,3 @@
1
1
  class Country < ActiveRecord::Base
2
+ # Empty intentionally
2
3
  end
@@ -0,0 +1,5 @@
1
+ class PhoneNumber < ActiveRecord::Base
2
+ lookup_by :phone_number, find_or_create: true, allow_blank: false
3
+
4
+ validates :phone_number, format: /\d{3}-\d{3}-\d{4}/, length: 10..11
5
+ end
@@ -1,7 +1,5 @@
1
1
  require File.expand_path('../boot', __FILE__)
2
2
 
3
- # require "rails/all"
4
-
5
3
  require 'active_record/railtie'
6
4
 
7
5
  # Require the gems listed in Gemfile, including any gems
@@ -42,11 +42,3 @@ test:
42
42
  adapter: postgresql
43
43
  database: lookup_by_test
44
44
  username: postgres
45
-
46
- production:
47
- adapter: postgresql
48
- encoding: unicode
49
- database: dummy_production
50
- pool: 5
51
- username: dummy
52
- password:
@@ -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
- create_lookup_table :accounts
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
- create_table :addresses, primary_key: "address_id" do |t|
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
@@ -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
  --
@@ -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 "cache" do
22
- it "seeds values" do
23
- City.lookup.seed 'Boston'
24
- City.lookup.seed 'Chicago', 'New York 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
- names = City.all.map(&:name).sort
27
- expect(names.sort).to eq(['Boston', 'Chicago', 'New York City'])
28
- City.lookup.clear
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
- expect(subject.lookup.cache).not_to be_empty
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
- # rspec-expectations config goes here. You can use an alternate
53
- # assertion/expectation library such as wrong or the stdlib/minitest
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
- # rspec-mocks config goes here. You can use an alternate test double
63
- # library (such as bogus or mocha) by changing the `mock_with` option here.
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
@@ -204,7 +204,7 @@ shared_examples "a lookup for" do |field|
204
204
  end
205
205
 
206
206
  it "converts empty strings to nil" do
207
- subject.send "#{field}=", ""
207
+ subject.send "#{field}=", ""
208
208
  expect(subject.send(field)).to be_nil
209
209
  end
210
210
 
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.9.1
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: 2014-08-21 00:00:00.000000000 Z
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: 3.2.0
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: 3.2.0
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: '1.3'
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: '1.3'
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.1
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
@@ -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 => "../"