lookup_by 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. data/.gitignore +11 -0
  2. data/.rvmrc +1 -0
  3. data/.travis.yml +9 -0
  4. data/Gemfile +16 -0
  5. data/MIT-LICENSE +20 -0
  6. data/README.md +230 -0
  7. data/Rakefile +14 -0
  8. data/TODO.md +16 -0
  9. data/lib/lookup_by/association.rb +89 -0
  10. data/lib/lookup_by/cache.rb +145 -0
  11. data/lib/lookup_by/caching/lru.rb +57 -0
  12. data/lib/lookup_by/caching/safe_lru.rb +30 -0
  13. data/lib/lookup_by/cucumber.rb +7 -0
  14. data/lib/lookup_by/hooks/cucumber.rb +9 -0
  15. data/lib/lookup_by/hooks/formtastic.rb +27 -0
  16. data/lib/lookup_by/hooks/simple_form.rb +27 -0
  17. data/lib/lookup_by/lookup.rb +113 -0
  18. data/lib/lookup_by/railtie.rb +20 -0
  19. data/lib/lookup_by/version.rb +3 -0
  20. data/lib/lookup_by.rb +27 -0
  21. data/lookup_by.gemspec +23 -0
  22. data/spec/association_spec.rb +102 -0
  23. data/spec/caching/lru_spec.rb +72 -0
  24. data/spec/dummy/.rspec +1 -0
  25. data/spec/dummy/Rakefile +14 -0
  26. data/spec/dummy/app/models/.gitkeep +0 -0
  27. data/spec/dummy/app/models/account.rb +5 -0
  28. data/spec/dummy/app/models/address.rb +6 -0
  29. data/spec/dummy/app/models/city.rb +5 -0
  30. data/spec/dummy/app/models/email_address.rb +5 -0
  31. data/spec/dummy/app/models/ip_address.rb +5 -0
  32. data/spec/dummy/app/models/postal_code.rb +5 -0
  33. data/spec/dummy/app/models/state.rb +5 -0
  34. data/spec/dummy/app/models/status.rb +9 -0
  35. data/spec/dummy/app/models/street.rb +5 -0
  36. data/spec/dummy/config/application.rb +20 -0
  37. data/spec/dummy/config/boot.rb +10 -0
  38. data/spec/dummy/config/database.yml +52 -0
  39. data/spec/dummy/config/environment.rb +5 -0
  40. data/spec/dummy/config/environments/development.rb +15 -0
  41. data/spec/dummy/config/environments/test.rb +16 -0
  42. data/spec/dummy/config.ru +4 -0
  43. data/spec/dummy/db/migrate/20121019040009_create_tables.rb +23 -0
  44. data/spec/dummy/db/schema.rb +71 -0
  45. data/spec/dummy/lib/missing.rb +3 -0
  46. data/spec/dummy/log/.gitkeep +0 -0
  47. data/spec/dummy/script/rails +6 -0
  48. data/spec/lookup_by_spec.rb +100 -0
  49. data/spec/spec_helper.rb +43 -0
  50. data/spec/support/shared_examples_for_a_lookup.rb +163 -0
  51. metadata +140 -0
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ .DS_Store
2
+ .bundle/
3
+ .rbx/
4
+ log/*.log
5
+ pkg/
6
+ spec/dummy/db/*.sqlite3
7
+ spec/dummy/log/*.log
8
+ spec/dummy/tmp/
9
+ spec/dummy/.sass-cache
10
+ coverage/
11
+ Gemfile.lock
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use 1.9.3-p194@lookup_by --create
data/.travis.yml ADDED
@@ -0,0 +1,9 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - rbx-19mode
5
+ - ruby-head
6
+ services:
7
+ - postgresql
8
+ before_script:
9
+ - sh -c "cd spec/dummy && RAILS_ENV=test bundle exec rake db:migrate:reset"
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
+ [![Build Status](https://secure.travis-ci.org/companygardener/lookup_by.png)](http://travis-ci.org/companygardener/lookup_by)
4
+ [![Code Climate](https://codeclimate.com/badge.png)](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,7 @@
1
+ require 'lookup_by/hooks/cucumber'
2
+
3
+ World(LookupBy::Hooks::Cucumber)
4
+
5
+ Given "I reload the cache for $name" do |name|
6
+ reload_cache_for(name)
7
+ end
@@ -0,0 +1,9 @@
1
+ module LookupBy
2
+ module Hooks
3
+ module Cucumber
4
+ def reload_cache_for(name)
5
+ name.classify.constantize.lookup.reload
6
+ end
7
+ end
8
+ end
9
+ 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