lookup_by 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +11 -0
- data/.rvmrc +1 -0
- data/.travis.yml +9 -0
- data/Gemfile +16 -0
- data/MIT-LICENSE +20 -0
- data/README.md +230 -0
- data/Rakefile +14 -0
- data/TODO.md +16 -0
- data/lib/lookup_by/association.rb +89 -0
- data/lib/lookup_by/cache.rb +145 -0
- data/lib/lookup_by/caching/lru.rb +57 -0
- data/lib/lookup_by/caching/safe_lru.rb +30 -0
- data/lib/lookup_by/cucumber.rb +7 -0
- data/lib/lookup_by/hooks/cucumber.rb +9 -0
- data/lib/lookup_by/hooks/formtastic.rb +27 -0
- data/lib/lookup_by/hooks/simple_form.rb +27 -0
- data/lib/lookup_by/lookup.rb +113 -0
- data/lib/lookup_by/railtie.rb +20 -0
- data/lib/lookup_by/version.rb +3 -0
- data/lib/lookup_by.rb +27 -0
- data/lookup_by.gemspec +23 -0
- data/spec/association_spec.rb +102 -0
- data/spec/caching/lru_spec.rb +72 -0
- data/spec/dummy/.rspec +1 -0
- data/spec/dummy/Rakefile +14 -0
- data/spec/dummy/app/models/.gitkeep +0 -0
- data/spec/dummy/app/models/account.rb +5 -0
- data/spec/dummy/app/models/address.rb +6 -0
- data/spec/dummy/app/models/city.rb +5 -0
- data/spec/dummy/app/models/email_address.rb +5 -0
- data/spec/dummy/app/models/ip_address.rb +5 -0
- data/spec/dummy/app/models/postal_code.rb +5 -0
- data/spec/dummy/app/models/state.rb +5 -0
- data/spec/dummy/app/models/status.rb +9 -0
- data/spec/dummy/app/models/street.rb +5 -0
- data/spec/dummy/config/application.rb +20 -0
- data/spec/dummy/config/boot.rb +10 -0
- data/spec/dummy/config/database.yml +52 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +15 -0
- data/spec/dummy/config/environments/test.rb +16 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/db/migrate/20121019040009_create_tables.rb +23 -0
- data/spec/dummy/db/schema.rb +71 -0
- data/spec/dummy/lib/missing.rb +3 -0
- data/spec/dummy/log/.gitkeep +0 -0
- data/spec/dummy/script/rails +6 -0
- data/spec/lookup_by_spec.rb +100 -0
- data/spec/spec_helper.rb +43 -0
- data/spec/support/shared_examples_for_a_lookup.rb +163 -0
- metadata +140 -0
data/.gitignore
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm use 1.9.3-p194@lookup_by --create
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
|
3
|
+
# Declare your gem's dependencies in lookup_by.gemspec.
|
4
|
+
# Bundler will treat runtime dependencies like base dependencies, and
|
5
|
+
# development dependencies will be added by default to the :development group.
|
6
|
+
gemspec
|
7
|
+
|
8
|
+
group :development, :test do
|
9
|
+
gem "pry"
|
10
|
+
gem "rake"
|
11
|
+
gem "simplecov", require: false
|
12
|
+
gem "rspec-rails", "~> 2.11.0"
|
13
|
+
|
14
|
+
gem "pg", platform: :ruby
|
15
|
+
gem "activerecord-jdbcpostgresql-adapter", platform: :jruby
|
16
|
+
end
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright © 2012 Erik Peterson, Enova
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,230 @@
|
|
1
|
+
# LookupBy
|
2
|
+
|
3
|
+
[](http://travis-ci.org/companygardener/lookup_by)
|
4
|
+
[](https://codeclimate.com/github/companygardener/lookup_by)
|
5
|
+
|
6
|
+
### Description
|
7
|
+
|
8
|
+
LookupBy is a thread-safe lookup table cache for ActiveRecord. It
|
9
|
+
reduces normalization pains.
|
10
|
+
|
11
|
+
LookupBy adds two macro methods to ActiveRecord:
|
12
|
+
|
13
|
+
`lookup_by :column` — defines `.[]`, `.lookup`, and `.is_a_lookup?`
|
14
|
+
methods on the class.
|
15
|
+
|
16
|
+
`lookup_for :column` — defines `column` and `column=` accessors that
|
17
|
+
transparently reference the lookup table.
|
18
|
+
|
19
|
+
### Features
|
20
|
+
|
21
|
+
* Thread-safety
|
22
|
+
* Configurable lookup column
|
23
|
+
* Caching (read-through, write-through, Least Recently Used (LRU))
|
24
|
+
|
25
|
+
### Compatibility
|
26
|
+
|
27
|
+
* PostgreSQL
|
28
|
+
|
29
|
+
### Development
|
30
|
+
|
31
|
+
* [github.com/companygardener/lookup_by][development]
|
32
|
+
|
33
|
+
### Source
|
34
|
+
|
35
|
+
* git clone git://github.com/companygardener/lookup_by.git
|
36
|
+
|
37
|
+
### Issues
|
38
|
+
|
39
|
+
Please submit issues to this Github project in the [Issues
|
40
|
+
tab][issues]. _Provide a failing rspec test that works with the
|
41
|
+
existing test suite_.
|
42
|
+
|
43
|
+
Installation
|
44
|
+
------------
|
45
|
+
|
46
|
+
Add this line to your application's Gemfile:
|
47
|
+
|
48
|
+
gem "lookup_by"
|
49
|
+
|
50
|
+
And then execute:
|
51
|
+
|
52
|
+
$ bundle
|
53
|
+
|
54
|
+
Or install it yourself:
|
55
|
+
|
56
|
+
$ gem install lookup_by
|
57
|
+
|
58
|
+
Usage / Configuration
|
59
|
+
=====================
|
60
|
+
|
61
|
+
### Define the lookup model
|
62
|
+
|
63
|
+
class Status < ActiveRecord::Base
|
64
|
+
lookup_by :column
|
65
|
+
end
|
66
|
+
|
67
|
+
# Aliases the `:column` attribute to `:name`.
|
68
|
+
Status.new(name: "paid")
|
69
|
+
|
70
|
+
### Associations / Foreign Keys
|
71
|
+
|
72
|
+
class Order < ActiveRecord::Base
|
73
|
+
lookup_for :status
|
74
|
+
end
|
75
|
+
|
76
|
+
Creates accessors to use the `status` attribute transparently:
|
77
|
+
|
78
|
+
order = Order.new(status: "paid")
|
79
|
+
|
80
|
+
order.status
|
81
|
+
=> "paid"
|
82
|
+
|
83
|
+
order.raw_status
|
84
|
+
=> <#Status id: 1, status: "paid">
|
85
|
+
|
86
|
+
# Access to the lookup value before type casting
|
87
|
+
order.status_before_type_cast
|
88
|
+
=> "paid"
|
89
|
+
|
90
|
+
### Symbolize
|
91
|
+
|
92
|
+
Casts the attribute to a symbol. Enables the setter to take a symbol.
|
93
|
+
|
94
|
+
_This is a bad idea if the set of lookup values is large. Symbols are
|
95
|
+
never garbage collected._
|
96
|
+
|
97
|
+
class Order < ActiveRecord::Base
|
98
|
+
lookup_for :status, symbolize: true
|
99
|
+
end
|
100
|
+
|
101
|
+
order = Order.new(status: "paid")
|
102
|
+
|
103
|
+
order.status
|
104
|
+
=> :paid
|
105
|
+
|
106
|
+
order.status = :shipped
|
107
|
+
=> :shipped
|
108
|
+
|
109
|
+
### Strict
|
110
|
+
|
111
|
+
# Raise
|
112
|
+
# Default
|
113
|
+
lookup_for :status
|
114
|
+
|
115
|
+
# this will raise a LookupBy::Error
|
116
|
+
Order.status = "non-existent status"
|
117
|
+
|
118
|
+
# Error
|
119
|
+
lookup_for :status, strict: false
|
120
|
+
|
121
|
+
### Caching
|
122
|
+
|
123
|
+
# No caching - Not very useful
|
124
|
+
# Default
|
125
|
+
lookup_by :column_name
|
126
|
+
|
127
|
+
# Cache all
|
128
|
+
# Use for a small finite list (e.g. status codes, US states)
|
129
|
+
#
|
130
|
+
# find: false DEFAULT
|
131
|
+
lookup_by :column_name, cache: true
|
132
|
+
|
133
|
+
# Cache N (with LRU eviction)
|
134
|
+
# Use for a large list with uneven distribution (e.g. email domain, city)
|
135
|
+
#
|
136
|
+
# find: true DEFAULT and REQUIRED
|
137
|
+
lookup_by :column_name, cache: 50
|
138
|
+
|
139
|
+
### Configure cache misses
|
140
|
+
|
141
|
+
# Return nil
|
142
|
+
# Default when caching all records
|
143
|
+
#
|
144
|
+
# Skips the database for these methods:
|
145
|
+
# .all, .count, .pluck
|
146
|
+
lookup_by :column_name, cache: true
|
147
|
+
|
148
|
+
# Find (read-through)
|
149
|
+
# Required when caching N records
|
150
|
+
lookup_by :column_name, cache: 10
|
151
|
+
lookup_by :column_name, cache: true, find: true
|
152
|
+
|
153
|
+
### Configure database misses
|
154
|
+
|
155
|
+
# Return nil
|
156
|
+
# Default
|
157
|
+
lookup_by :column_name
|
158
|
+
|
159
|
+
# Find or create
|
160
|
+
# Useful for user-submitted fields that grow over time
|
161
|
+
# e.g. user_agents, ip_addresses
|
162
|
+
#
|
163
|
+
# Note: Only works if its attributes are nullable
|
164
|
+
lookup_by :column_name, cache: 20, find_or_create: true
|
165
|
+
|
166
|
+
### Normalizing values
|
167
|
+
|
168
|
+
# Normalize
|
169
|
+
# Run through the your attribute's setter
|
170
|
+
lookup_by :column_name, normalize: true
|
171
|
+
|
172
|
+
Integration
|
173
|
+
===========
|
174
|
+
|
175
|
+
### Cucumber
|
176
|
+
|
177
|
+
LookupBy comes with a few cucumber steps. To use, `require` them
|
178
|
+
from one of the ruby files under `features/support` (e.g. `env.rb`)
|
179
|
+
|
180
|
+
require 'lookup_by/cucumber'
|
181
|
+
|
182
|
+
This provides `Given I reload the cache for $plural_class_name`.
|
183
|
+
|
184
|
+
### SimpleForm
|
185
|
+
|
186
|
+
= simple_form_for @order do |f|
|
187
|
+
= f.input :status
|
188
|
+
= f.input :status, :as => :radio_buttons
|
189
|
+
|
190
|
+
### Formtastic
|
191
|
+
|
192
|
+
= semantic_form_for @order do |f|
|
193
|
+
= f.input :status
|
194
|
+
= f.input :status, :as => :radio
|
195
|
+
|
196
|
+
Testing
|
197
|
+
-------
|
198
|
+
|
199
|
+
This plugin uses rspec and pry for testing. Make sure you have them
|
200
|
+
installed:
|
201
|
+
|
202
|
+
bundle
|
203
|
+
|
204
|
+
To run the test suite:
|
205
|
+
|
206
|
+
rake
|
207
|
+
|
208
|
+
Giving Back
|
209
|
+
===========
|
210
|
+
|
211
|
+
### Contributing
|
212
|
+
|
213
|
+
1. Fork
|
214
|
+
2. Create a feature branch `git checkout -b new-hotness`
|
215
|
+
3. Commit your changes `git commit -am 'Added some feature'`
|
216
|
+
4. Push to the branch `git push origin new-hotness`
|
217
|
+
5. Create a Pull Request
|
218
|
+
|
219
|
+
### Attribution
|
220
|
+
|
221
|
+
A list of authors can be found on the [LookupBy Contributors page][contributors].
|
222
|
+
|
223
|
+
Copyright © 2012 Erik Peterson, Enova
|
224
|
+
|
225
|
+
Released under the MIT License. See [MIT-LICENSE][license] file for more details.
|
226
|
+
|
227
|
+
[development]: http://www.github.com/companygardener/lookup_by "LookupBy Development"
|
228
|
+
[issues]: http://www.github.com/companygardener/lookup_by/issues "LookupBy Issues"
|
229
|
+
[license]: http://www.github.com/companygardener/lookup_by/blob/master/MIT-LICENSE "LookupBy License"
|
230
|
+
[contributors]: http://github.com/companygardener/lookup_by/graphs/contributors "LookupBy Contributors"
|
data/Rakefile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
begin
|
3
|
+
require 'bundler/setup'
|
4
|
+
rescue LoadError
|
5
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
6
|
+
end
|
7
|
+
|
8
|
+
Bundler::GemHelper.install_tasks
|
9
|
+
|
10
|
+
require "rspec/core/rake_task"
|
11
|
+
RSpec::Core::RakeTask.new(:spec)
|
12
|
+
|
13
|
+
task :default => :spec
|
14
|
+
|
data/TODO.md
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
Roadmap
|
2
|
+
=======
|
3
|
+
|
4
|
+
* Pluggable backend (memcache, redis, etc.) to reduce memory usage, allow
|
5
|
+
larger LRUs.
|
6
|
+
* Additional database support / tests
|
7
|
+
* Improve LRU algorithm
|
8
|
+
* Lookup by multiple fields, multi-column unique constraint
|
9
|
+
|
10
|
+
Soon
|
11
|
+
====
|
12
|
+
* Travis CI
|
13
|
+
* Gemnasium
|
14
|
+
* Require validations / constraints on lookup field (presence, uniqueness)
|
15
|
+
* Trap signal to output stats
|
16
|
+
* Populate LRU with using the counts of an association
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# TODO: play nicely with belongs_to
|
2
|
+
# TODO: has_many association
|
3
|
+
#
|
4
|
+
# class Decision
|
5
|
+
# lookup_for :reasons
|
6
|
+
# end
|
7
|
+
#
|
8
|
+
# Decision.first.reasons
|
9
|
+
# => ["employment", "income"]
|
10
|
+
#
|
11
|
+
# Decision.new.reasons = %w(employment income)
|
12
|
+
|
13
|
+
module LookupBy
|
14
|
+
module Association
|
15
|
+
module MacroMethods
|
16
|
+
def lookup_for field, options = {}
|
17
|
+
return unless table_exists?
|
18
|
+
|
19
|
+
field = field.to_sym
|
20
|
+
|
21
|
+
%W(#{field} raw_#{field} #{field}= #{field}_before_type_cast).map(&:to_sym).each do |method|
|
22
|
+
raise Error, "method `#{method}` already exists on #{self.inspect}" if instance_methods.include? method
|
23
|
+
end
|
24
|
+
|
25
|
+
options.symbolize_keys!
|
26
|
+
options.assert_valid_keys(:class_name, :foreign_key, :symbolize, :strict)
|
27
|
+
|
28
|
+
class_name = options[:class_name] || field
|
29
|
+
class_name = class_name.to_s.camelize
|
30
|
+
|
31
|
+
foreign_key = options[:foreign_key] || "#{field}_id"
|
32
|
+
foreign_key = foreign_key.to_sym
|
33
|
+
|
34
|
+
strict = options[:strict]
|
35
|
+
strict = true if strict.nil?
|
36
|
+
|
37
|
+
raise Error, "foreign key `#{foreign_key}` is required on #{self}" unless attribute_names.include?(foreign_key.to_s)
|
38
|
+
|
39
|
+
lookup_field = class_name.constantize.lookup.field
|
40
|
+
|
41
|
+
cast = options[:symbolize] ? ".to_sym" : ""
|
42
|
+
|
43
|
+
lookup_object = "#{class_name}[#{foreign_key}]"
|
44
|
+
|
45
|
+
class << self; attr_reader :lookups; end
|
46
|
+
|
47
|
+
@lookups ||= []
|
48
|
+
@lookups << field
|
49
|
+
|
50
|
+
class_eval <<-METHODS, __FILE__, __LINE__.next
|
51
|
+
def raw_#{field}
|
52
|
+
#{lookup_object}
|
53
|
+
end
|
54
|
+
|
55
|
+
def #{field}
|
56
|
+
value = #{lookup_object}
|
57
|
+
value ? value.#{lookup_field}#{cast} : nil
|
58
|
+
end
|
59
|
+
|
60
|
+
def #{field}_before_type_cast
|
61
|
+
value = #{lookup_object}
|
62
|
+
value.#{lookup_field}_before_type_cast
|
63
|
+
end
|
64
|
+
|
65
|
+
def #{field}=(arg)
|
66
|
+
value = case arg
|
67
|
+
when "", nil
|
68
|
+
nil
|
69
|
+
when String, Fixnum
|
70
|
+
#{class_name}[arg].try(:id)
|
71
|
+
when Symbol
|
72
|
+
#{%Q(raise ArgumentError, "#{foreign_key}=(Symbol): use `lookup_for :column, symbolize: true` to allow symbols") unless options[:symbolize]}
|
73
|
+
#{class_name}[arg].try(:id)
|
74
|
+
when #{class_name}
|
75
|
+
raise ArgumentError, "self.#{foreign_key}=(#{class_name}): must be saved" unless arg.id
|
76
|
+
arg.id
|
77
|
+
else
|
78
|
+
raise TypeError, "#{foreign_key}=(arg): arg must be a String, Symbol, Fixnum, nil, or #{class_name}"
|
79
|
+
end
|
80
|
+
|
81
|
+
#{%Q(raise LookupBy::Error, "\#{arg.inspect} is not in the <#{class_name}> lookup cache" if arg.present? && value.nil?) if strict}
|
82
|
+
|
83
|
+
self.#{foreign_key} = value
|
84
|
+
end
|
85
|
+
METHODS
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
module LookupBy
|
2
|
+
class Cache
|
3
|
+
attr_reader :klass, :primary_key
|
4
|
+
attr_reader :cache, :stats
|
5
|
+
attr_reader :field, :order, :type, :limit, :find, :write, :normalize
|
6
|
+
|
7
|
+
attr_accessor :enabled
|
8
|
+
|
9
|
+
def initialize(klass, options = {})
|
10
|
+
@klass = klass
|
11
|
+
@primary_key = klass.primary_key
|
12
|
+
@field = options[:field].to_sym
|
13
|
+
@cache = {}
|
14
|
+
@order = options[:order] || field
|
15
|
+
@read = options[:find]
|
16
|
+
@write = options[:find_or_create]
|
17
|
+
@normalize = options[:normalize]
|
18
|
+
@enabled = true
|
19
|
+
|
20
|
+
@stats = { db: Hash.new(0), cache: Hash.new(0) }
|
21
|
+
|
22
|
+
raise ArgumentError, %Q(unknown attribute "#{field}" for <#{klass}>) unless klass.column_names.include?(field.to_s)
|
23
|
+
|
24
|
+
case options[:cache]
|
25
|
+
when true
|
26
|
+
@type = :all
|
27
|
+
@read ||= false
|
28
|
+
when ::Fixnum
|
29
|
+
raise ArgumentError, "`#{@klass}.lookup_by :#{@field}` options[:find] must be true when caching N" if @read == false
|
30
|
+
|
31
|
+
@type = :lru
|
32
|
+
@limit = options[:cache]
|
33
|
+
@cache = Rails.configuration.allow_concurrency ? Caching::SafeLRU.new(@limit) : Caching::LRU.new(@limit)
|
34
|
+
@read = true
|
35
|
+
@write ||= false
|
36
|
+
@enabled = false if Rails.env.test? && write?
|
37
|
+
else
|
38
|
+
@read = true
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def reload
|
43
|
+
return unless cache_all?
|
44
|
+
|
45
|
+
cache.clear
|
46
|
+
|
47
|
+
::ActiveRecord::Base.connection.send :log, "", "#{klass.name} Load Cache All" do
|
48
|
+
klass.order(order).each do |i|
|
49
|
+
cache[i.id] = i
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def create!(*args, &block)
|
55
|
+
created = klass.create!(*args, &block)
|
56
|
+
cache[created.id] = created if cache?
|
57
|
+
created
|
58
|
+
end
|
59
|
+
|
60
|
+
def fetch(value)
|
61
|
+
increment :cache, :get
|
62
|
+
|
63
|
+
value = clean(value) if normalize?
|
64
|
+
|
65
|
+
found = cache_read(value) if cache?
|
66
|
+
found ||= db_read(value) if read_through?
|
67
|
+
|
68
|
+
cache[found.id] = found if found && cache?
|
69
|
+
|
70
|
+
found ||= db_write(value) if write?
|
71
|
+
|
72
|
+
found
|
73
|
+
end
|
74
|
+
|
75
|
+
def read_through?
|
76
|
+
@read
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def clean(value)
|
82
|
+
return value if value.is_a? Fixnum
|
83
|
+
|
84
|
+
klass.new(field => value).send(field)
|
85
|
+
end
|
86
|
+
|
87
|
+
def cache_read(value)
|
88
|
+
if value.is_a? Fixnum
|
89
|
+
found = cache[value]
|
90
|
+
else
|
91
|
+
found = cache.values.detect { |o| o.send(field) == value }
|
92
|
+
end
|
93
|
+
|
94
|
+
increment :cache, found ? :hit : :miss
|
95
|
+
|
96
|
+
found
|
97
|
+
end
|
98
|
+
|
99
|
+
def db_read(value)
|
100
|
+
increment :db, :get
|
101
|
+
|
102
|
+
found = klass.where(column_for(value) => value).first
|
103
|
+
|
104
|
+
increment :db, found ? :hit : :miss
|
105
|
+
|
106
|
+
found
|
107
|
+
end
|
108
|
+
|
109
|
+
# TODO: Handle race condition on create! failure
|
110
|
+
def db_write(value)
|
111
|
+
column = column_for(value)
|
112
|
+
|
113
|
+
found = klass.create!(column => value) if column != primary_key
|
114
|
+
found
|
115
|
+
end
|
116
|
+
|
117
|
+
def column_for(value)
|
118
|
+
value.is_a?(Fixnum) ? primary_key : field
|
119
|
+
end
|
120
|
+
|
121
|
+
def cache?
|
122
|
+
!!type && enabled?
|
123
|
+
end
|
124
|
+
|
125
|
+
def enabled?
|
126
|
+
enabled
|
127
|
+
end
|
128
|
+
|
129
|
+
def cache_all?
|
130
|
+
type == :all
|
131
|
+
end
|
132
|
+
|
133
|
+
def write?
|
134
|
+
!!write
|
135
|
+
end
|
136
|
+
|
137
|
+
def normalize?
|
138
|
+
!!normalize
|
139
|
+
end
|
140
|
+
|
141
|
+
def increment(type, stat)
|
142
|
+
@stats[type][stat] += 1
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module LookupBy
|
2
|
+
module Caching
|
3
|
+
class LRU < ::Hash
|
4
|
+
attr_reader :lru
|
5
|
+
|
6
|
+
def initialize(maxsize)
|
7
|
+
super()
|
8
|
+
|
9
|
+
@maxsize = maxsize
|
10
|
+
@lru = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def clear
|
14
|
+
@lru.clear
|
15
|
+
|
16
|
+
super
|
17
|
+
end
|
18
|
+
|
19
|
+
def [](key)
|
20
|
+
return nil unless has_key?(key)
|
21
|
+
touch(key)
|
22
|
+
super
|
23
|
+
end
|
24
|
+
|
25
|
+
def []=(key, value)
|
26
|
+
touch(key)
|
27
|
+
super
|
28
|
+
prune
|
29
|
+
end
|
30
|
+
|
31
|
+
def merge!(hash)
|
32
|
+
hash.each { |k, v| self[k] = v }
|
33
|
+
end
|
34
|
+
|
35
|
+
def delete(key)
|
36
|
+
@lru.delete(key)
|
37
|
+
|
38
|
+
super
|
39
|
+
end
|
40
|
+
|
41
|
+
def to_h
|
42
|
+
{}.merge!(self)
|
43
|
+
end
|
44
|
+
|
45
|
+
protected
|
46
|
+
|
47
|
+
def touch(key)
|
48
|
+
@lru.delete(key)
|
49
|
+
@lru << key
|
50
|
+
end
|
51
|
+
|
52
|
+
def prune
|
53
|
+
delete(@lru.shift) while size > @maxsize
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module LookupBy
|
2
|
+
module Caching
|
3
|
+
class SafeLRU < LRU
|
4
|
+
def initialize(maxsize = nil)
|
5
|
+
@mutex = Mutex.new
|
6
|
+
super
|
7
|
+
end
|
8
|
+
|
9
|
+
def clear
|
10
|
+
@mutex.synchronize { super }
|
11
|
+
end
|
12
|
+
|
13
|
+
def [](key)
|
14
|
+
@mutex.synchronize { super }
|
15
|
+
end
|
16
|
+
|
17
|
+
def []=(key, value)
|
18
|
+
@mutex.synchronize { super }
|
19
|
+
end
|
20
|
+
|
21
|
+
def merge!(hash)
|
22
|
+
@mutex.synchronize { super }
|
23
|
+
end
|
24
|
+
|
25
|
+
def delete(key)
|
26
|
+
@mutex.synchronize { super }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require "active_support/concern"
|
2
|
+
|
3
|
+
module LookupBy
|
4
|
+
module Hooks
|
5
|
+
module Formtastic
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
alias_method_chain :input, :lookup
|
10
|
+
end
|
11
|
+
|
12
|
+
def input_with_lookup(method, options = {})
|
13
|
+
klass = object.class
|
14
|
+
|
15
|
+
if klass.respond_to?(:lookups) && klass.lookups.include?(method.to_sym)
|
16
|
+
target = method.to_s.classify.constantize
|
17
|
+
|
18
|
+
options[:collection] ||= target.pluck(target.lookup.field) if target.lookup.cache_all?
|
19
|
+
end
|
20
|
+
|
21
|
+
input_without_lookup(method, options)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
::Formtastic::FormBuilder.send :include, LookupBy::Hooks::Formtastic
|