api_hammer 0.2.2 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 96ebda010e8ade01533266d0f06a7e13026fd746
4
- data.tar.gz: a5e217afbf69ec2d4c0fb2635cce63ba4baf2c9a
3
+ metadata.gz: 9927a5896d97412a1668d60c7f341649cca1232c
4
+ data.tar.gz: 0d65a4cf1399613c3f1433f0a456f88ae1cf9008
5
5
  SHA512:
6
- metadata.gz: 8f7a065899bd65bc13b9dee409796d9778c3104a7bb2addc7a65e33409e3f94de247fa3d4ae2a3dd826d5ee630b9beb1af32a0f96a410c65dd629b616e88716d
7
- data.tar.gz: f4c8bd00c8c74a02517594fd55e45ba08f46ebb44ad87104ddc87d13e59c75acbfc48a04dc13a48a048a9add4676c5ce39c7a1383c3a50777e5a02b5d1c238c5
6
+ metadata.gz: 1f1fdf86e116a112eee8a09420b62d356ce66ac8522e41ef30470862ed7c29491e0cbe7fa02e5fe7b63bb834887d572a68feeeb5fc9ab5eede0db6ddebe3e95c
7
+ data.tar.gz: 51a41edd33b376e0d2cc9e49ff86fa0d60afa7cb8dbda2d4ebac12b10f64c300002813db33f79d7f914d52a9f9636f87c11bea584aa06fcf081705b81e96ecb6
data/CHANGELOG.md CHANGED
@@ -1,3 +1,6 @@
1
+ # 0.3.0
2
+ - ActiveRecord::Base.cache_find_by
3
+
1
4
  # 0.2.2
2
5
  - RequestLogger, in addition to logging response bodies on error, logs id/uuid fields from request body and
3
6
  response body if there's no error
data/Rakefile.rb CHANGED
@@ -4,7 +4,8 @@ Rake::TestTask.new do |t|
4
4
  t.test_files = FileList['test/**/*_test.rb']
5
5
  t.verbose = true
6
6
  end
7
- task 'default' => 'test'
7
+ require 'wwtd/tasks'
8
+ task 'default' => 'wwtd'
8
9
 
9
10
  require 'yard'
10
11
  YARD::Rake::YardocTask.new do |t|
@@ -0,0 +1,135 @@
1
+ require 'active_record'
2
+
3
+ module ActiveRecord
4
+ class Relation
5
+ if !method_defined?(:first_without_caching)
6
+ alias_method :first_without_caching, :first
7
+ def first(*args)
8
+ one_record_with_caching(args.empty?) { first_without_caching(*args) }
9
+ end
10
+ end
11
+ if !method_defined?(:take_without_caching) && method_defined?(:take)
12
+ alias_method :take_without_caching, :take
13
+ def take(*args)
14
+ one_record_with_caching(args.empty?) { take_without_caching(*args) }
15
+ end
16
+ end
17
+
18
+ # retrieves one record, hitting the cache if appropriate. the argument may bypass caching
19
+ # (the caller could elect to just not call this method if caching is to be avoided, but since this
20
+ # method already builds in opting whether or not to hit cache, the code is simpler just passing that in).
21
+ #
22
+ # requires a block which returns the record
23
+ def one_record_with_caching(can_cache = true)
24
+ actual_right = proc do |where_value|
25
+ if where_value.right.is_a?(Arel::Nodes::BindParam)
26
+ column, value = bind_values.detect { |(column, value)| column.name == where_value.left.name }
27
+ value
28
+ else
29
+ where_value.right
30
+ end
31
+ end
32
+ cache_find_bys = klass.send(:cache_find_bys)
33
+ can_cache &&= cache_find_bys &&
34
+ !loaded? && # if it's loaded no need to hit cache
35
+ where_values.all? { |wv| wv.is_a?(Arel::Nodes::Equality) } && # no inequality or that sort of thing
36
+ cache_find_bys.include?(where_values.map { |wv| wv.left.name }.sort) && # any of the set of where-values to cache match this relation
37
+ where_values.map(&actual_right).all? { |r| r.is_a?(String) || r.is_a?(Numeric) } && # check all right side values are simple types, number or string
38
+ offset_value.nil? &&
39
+ joins_values.blank? &&
40
+ order_values.blank? &&
41
+ !reverse_order_value &&
42
+ includes_values.blank? &&
43
+ preload_values.blank? &&
44
+ select_values.blank? &&
45
+ group_values.blank? &&
46
+ from_value.nil? &&
47
+ lock_value.nil?
48
+
49
+ if can_cache
50
+ cache_key = klass.send(:cache_key_for, where_values.map { |wv| [wv.left.name, actual_right.call(wv)] })
51
+ klass.finder_cache.fetch(cache_key) do
52
+ yield
53
+ end
54
+ else
55
+ yield
56
+ end
57
+ end
58
+ end
59
+
60
+ class Base
61
+ class << self
62
+ def finder_cache=(val)
63
+ define_singleton_method(:finder_cache) { val }
64
+ end
65
+
66
+ # the cache. should be an instance of some sort of ActiveSupport::Cache::Store.
67
+ # by default uses Rails.cache if that exists, or creates a ActiveSupport::Cache::MemoryStore to use.
68
+ # set this per-model or on ActiveRecord::Base, as needed; it is inherited.
69
+ def finder_cache
70
+ # dummy; this gets set below
71
+ end
72
+
73
+ # causes requests to retrieve a record by the given attributes (all of them) to be cached.
74
+ # this is for single records only. it is unsafe to use with a set of attributes whose values
75
+ # (in conjunction) may be associated with multiple records.
76
+ #
77
+ # see .finder_cache and .find_cache= for where it is cached.
78
+ #
79
+ # #flush_find_cache is defined on the instance. it is called on save to clear an updated record from
80
+ # the cache. it may also be called explicitly to clear a record from the cache.
81
+ #
82
+ # beware of multiple application servers with different caches - a record cached in multiple will not
83
+ # be invalidated in all when it is saved in one.
84
+ def cache_find_by(*attribute_names)
85
+ unless cache_find_bys
86
+ # initial setup
87
+ self.cache_find_bys = Set.new
88
+ after_update :flush_find_cache
89
+ before_destroy :flush_find_cache
90
+ end
91
+
92
+ find_by = attribute_names.map do |name|
93
+ raise(ArgumentError) unless name.is_a?(Symbol) || name.is_a?(String)
94
+ name.to_s.dup.freeze
95
+ end.sort.freeze
96
+
97
+ self.cache_find_bys = (cache_find_bys | [find_by]).freeze
98
+ end
99
+
100
+ private
101
+ def cache_find_bys=(val)
102
+ define_singleton_method(:cache_find_bys) { val }
103
+ singleton_class.send(:private, :cache_find_bys)
104
+ end
105
+
106
+ def cache_find_bys
107
+ nil
108
+ end
109
+
110
+ def cache_key_for(find_attributes)
111
+ attrs = find_attributes.map { |k,v| [k.to_s, v.to_s] }.sort_by(&:first).inject([], &:+)
112
+ cache_key_prefix = ['cache_find_by', table_name]
113
+ @parser ||= URI.const_defined?(:Parser) ? URI::Parser.new : URI
114
+ cache_key = (cache_key_prefix + attrs).map do |part|
115
+ @parser.escape(part, /[^a-z0-9\-\.\_\~]/i)
116
+ end.join('/')
117
+ end
118
+ end
119
+
120
+ # the above dummy method has no content because we want to evaluate this now, not in the method, to
121
+ # avoid instantiating duplicate MemoryStores.
122
+ self.finder_cache = (Object.const_defined?(:Rails) && ::Rails.cache) || ::ActiveSupport::Cache::MemoryStore.new
123
+
124
+ # clears this record from the cache used by cache_find_by
125
+ def flush_find_cache
126
+ self.class.send(:cache_find_bys).each do |attribute_names|
127
+ find_attributes = attribute_names.map { |attr_name| [attr_name, attribute_was(attr_name)] }
128
+ self.class.instance_exec(find_attributes) do |find_attributes|
129
+ finder_cache.delete(cache_key_for(find_attributes))
130
+ end
131
+ end
132
+ nil
133
+ end
134
+ end
135
+ end
@@ -1,3 +1,3 @@
1
1
  module ApiHammer
2
- VERSION = "0.2.2"
2
+ VERSION = "0.3.0"
3
3
  end
@@ -0,0 +1,201 @@
1
+ proc { |p| $:.unshift(p) unless $:.any? { |lp| File.expand_path(lp) == p } }.call(File.expand_path('.', File.dirname(__FILE__)))
2
+ require 'helper'
3
+
4
+ require 'active_support/cache'
5
+ require 'active_record'
6
+
7
+ ActiveRecord::Base.establish_connection(
8
+ :adapter => "sqlite3",
9
+ :database => ":memory:"
10
+ )
11
+
12
+ module Rails
13
+ class << self
14
+ def cache
15
+ @cache ||= ActiveSupport::Cache::MemoryStore.new
16
+ end
17
+ end
18
+ end
19
+
20
+ require 'api_hammer/active_record_cache_find_by'
21
+
22
+ ActiveRecord::Schema.define do
23
+ create_table :albums do |table|
24
+ table.column :title, :string
25
+ table.column :performer, :string
26
+ table.column :tracks, :integer
27
+ end
28
+ end
29
+
30
+ class Album < ActiveRecord::Base
31
+ cache_find_by(:id)
32
+ cache_find_by(:performer)
33
+ cache_find_by(:title, :performer)
34
+ cache_find_by(:tracks)
35
+ end
36
+
37
+ class VinylAlbum < Album
38
+ self.finder_cache = ActiveSupport::Cache::MemoryStore.new
39
+ end
40
+
41
+ describe 'ActiveRecord::Base.cache_find_by' do
42
+ def assert_caches(key, cache = Rails.cache)
43
+ assert !cache.read(key), "cache already contains a key #{key}: #{cache.read(key)}"
44
+ yield
45
+ ensure
46
+ assert cache.read(key), "key #{key} was not cached"
47
+ end
48
+
49
+ def assert_not_caches(key, cache = Rails.cache)
50
+ assert !cache.read(key), "cache already contains a key #{key}: #{cache.read(key)}"
51
+ yield
52
+ ensure
53
+ assert !cache.read(key), "key was incorrectly cached - #{key}: #{cache.read(key)}"
54
+ end
55
+
56
+ after do
57
+ Album.all.each(&:destroy)
58
+ end
59
+
60
+ it('caches #find by primary key') do
61
+ id = Album.create!.id
62
+ assert_caches("cache_find_by/albums/id/#{id}") { assert Album.find(id) }
63
+ end
64
+
65
+ it('caches #find_by_id') do
66
+ id = Album.create!.id
67
+ assert_caches("cache_find_by/albums/id/#{id}") { assert Album.find_by_id(id) }
68
+ end
69
+
70
+ it('caches #where.first with primary key') do
71
+ id = Album.create!.id
72
+ assert_caches("cache_find_by/albums/id/#{id}") { assert Album.where(:id => id).first }
73
+ end
74
+
75
+ it('caches find_by_x with one attribute') do
76
+ Album.create!(:performer => 'x')
77
+ assert_caches("cache_find_by/albums/performer/x") { assert Album.find_by_performer('x') }
78
+ end
79
+
80
+ it('caches find_by_x! with one attribute') do
81
+ Album.create!(:performer => 'x')
82
+ assert_caches("cache_find_by/albums/performer/x") { assert Album.find_by_performer!('x') }
83
+ end
84
+
85
+ it('caches where.first with one attribute') do
86
+ Album.create!(:performer => 'x')
87
+ assert_caches("cache_find_by/albums/performer/x") { assert Album.where(:performer => 'x').first }
88
+ end
89
+
90
+ it('caches where.first! with one attribute') do
91
+ Album.create!(:performer => 'x')
92
+ assert_caches("cache_find_by/albums/performer/x") { assert Album.where(:performer => 'x').first! }
93
+ end
94
+
95
+ it('caches #where.first with integer attribute') do
96
+ id = Album.create!(:tracks => 3).id
97
+ assert_caches("cache_find_by/albums/tracks/3") { assert Album.where(:tracks => 3).first }
98
+ end
99
+
100
+ it('does not cache #where.first with inequality of integer attribute') do
101
+ id = Album.create!(:tracks => 3).id
102
+ assert_not_caches("cache_find_by/albums/tracks/3") { assert Album.where(Album.arel_table['tracks'].gteq(3)).first }
103
+ end
104
+
105
+ if ActiveRecord::Relation.method_defined?(:take)
106
+ it('caches where.take with one attribute') do
107
+ Album.create!(:performer => 'x')
108
+ assert_caches("cache_find_by/albums/performer/x") { assert Album.where(:performer => 'x').take }
109
+ end
110
+ end
111
+
112
+ it('does not cache where.last with one attribute') do
113
+ Album.create!(:performer => 'x')
114
+ assert_not_caches("cache_find_by/albums/performer/x") { assert Album.where(:performer => 'x').last }
115
+ end
116
+
117
+ it('does not cache find with array') do
118
+ ids = [Album.create!.id, Album.create!.id]
119
+ assert_not_caches("cache_find_by/albums/id/#{ids.first}") { assert Album.find(ids) }
120
+ end
121
+
122
+ it('does not cache find_by_x with array') do
123
+ ids = [Album.create!.id, Album.create!.id]
124
+ assert_not_caches("cache_find_by/albums/id/#{ids.first}") { assert Album.find_by_id(ids) }
125
+ end
126
+
127
+ it('does not cache where.first with array') do
128
+ ids = [Album.create!.id, Album.create!.id]
129
+ assert_not_caches("cache_find_by/albums/id/#{ids.first}") { assert Album.where(:id => ids).first }
130
+ end
131
+
132
+ it('does not cache find_by_x with one attribute') do
133
+ Album.create!(:title => 'x')
134
+ assert_not_caches("cache_find_by/albums/title/x") { assert Album.find_by_title('x') }
135
+ end
136
+
137
+ it('does not cache where.first with one attribute') do
138
+ Album.create!(:title => 'x')
139
+ assert_not_caches("cache_find_by/albums/title/x") { assert Album.where(:title => 'x').first }
140
+ end
141
+
142
+ it('caches find_by_x with two attributes') do
143
+ Album.create!(:title => 'x', :performer => 'y')
144
+ assert_caches("cache_find_by/albums/performer/y/title/x") { assert Album.find_by_title_and_performer('x', 'y') }
145
+ end
146
+
147
+ it('caches where.first with two attributes') do
148
+ Album.create!(:title => 'x', :performer => 'y')
149
+ assert_caches("cache_find_by/albums/performer/y/title/x") { assert Album.where(:title => 'x', :performer => 'y').first }
150
+ end
151
+
152
+ it('flushes cache on save') do
153
+ album = Album.create!(:title => 'x', :performer => 'y')
154
+ assert_caches(key1 = "cache_find_by/albums/performer/y/title/x") { assert Album.find_by_title_and_performer('x', 'y') }
155
+ assert_caches(key2 = "cache_find_by/albums/performer/y") { assert Album.find_by_performer('y') }
156
+ album.update_attributes!(:performer => 'z')
157
+ assert !Rails.cache.read(key1), Rails.cache.instance_eval { @data }.inspect
158
+ assert !Rails.cache.read(key2), Rails.cache.instance_eval { @data }.inspect
159
+ end
160
+
161
+ it('flushes cache on destroy') do
162
+ album = Album.create!(:title => 'x', :performer => 'y')
163
+ assert_caches(key1 = "cache_find_by/albums/performer/y/title/x") { assert Album.find_by_title_and_performer('x', 'y') }
164
+ assert_caches(key2 = "cache_find_by/albums/performer/y") { assert Album.find_by_performer('y') }
165
+ album.destroy
166
+ assert !Rails.cache.read(key1), Rails.cache.instance_eval { @data }.inspect
167
+ assert !Rails.cache.read(key2), Rails.cache.instance_eval { @data }.inspect
168
+ end
169
+
170
+ it 'inherits cache_find_bys' do
171
+ assert VinylAlbum.send(:cache_find_bys).any? { |f| f == ['id'] }
172
+ end
173
+
174
+ it 'uses a different cache when specified' do
175
+ assert Album.finder_cache != VinylAlbum.finder_cache
176
+
177
+ id = Album.create!.id
178
+ key = "cache_find_by/albums/id/#{id}"
179
+ assert_caches(key) do
180
+ assert_not_caches(key, VinylAlbum.finder_cache) do
181
+ assert Album.find(id)
182
+ end
183
+ end
184
+
185
+ id = VinylAlbum.create!.id
186
+ key = "cache_find_by/albums/id/#{id}"
187
+ assert_caches(key, VinylAlbum.finder_cache) do
188
+ assert_not_caches(key) do
189
+ assert VinylAlbum.find(id)
190
+ end
191
+ end
192
+ end
193
+
194
+ it 'does not get confused by values with slashes' do
195
+ Album.create!(:title => 'z', :performer => 'y/title/x')
196
+ Album.create!(:title => 'x', :performer => 'y')
197
+
198
+ Album.where(:performer => 'y', :title => 'x').first
199
+ assert_equal 'z', Album.where(:performer => 'y/title/x').first.title
200
+ end
201
+ end
data/test/halt_test.rb CHANGED
@@ -36,10 +36,8 @@ describe 'ApiHammer::Rails#halt' do
36
36
  it 'returns a record if it exists' do
37
37
  record = Object.new
38
38
  model = Class.new do
39
- (class << self; self; end).class_eval do
40
- define_method(:where) { |attrs| [record] }
41
- define_method(:table_name) { 'records' }
42
- end
39
+ define_singleton_method(:where) { |attrs| [record] }
40
+ define_singleton_method(:table_name) { 'records' }
43
41
  end
44
42
  assert_equal record, FakeController.new.find_or_halt(model, {:id => 'anid'})
45
43
  end
data/test/helper.rb CHANGED
@@ -1,6 +1,10 @@
1
1
  proc { |p| $:.unshift(p) unless $:.any? { |lp| File.expand_path(lp) == p } }.call(File.expand_path('../lib', File.dirname(__FILE__)))
2
2
 
3
+ require 'bundler'
4
+ Bundler.setup
5
+
3
6
  require 'simplecov'
7
+ require 'byebug'
4
8
 
5
9
  # NO EXPECTATIONS
6
10
  ENV["MT_NO_EXPECTATIONS"] = ''
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: api_hammer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ethan
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-06-04 00:00:00.000000000 Z
11
+ date: 2014-06-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -164,6 +164,48 @@ dependencies:
164
164
  - - ">="
165
165
  - !ruby/object:Gem::Version
166
166
  version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: activesupport
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ - !ruby/object:Gem::Dependency
182
+ name: activerecord
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ">="
186
+ - !ruby/object:Gem::Version
187
+ version: '0'
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - ">="
193
+ - !ruby/object:Gem::Version
194
+ version: '0'
195
+ - !ruby/object:Gem::Dependency
196
+ name: sqlite3
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - ">="
200
+ - !ruby/object:Gem::Version
201
+ version: '0'
202
+ type: :development
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - ">="
207
+ - !ruby/object:Gem::Version
208
+ version: '0'
167
209
  description: actually a set of small API-related tools. very much unlike a hammer
168
210
  at all, which is one large tool.
169
211
  email:
@@ -181,6 +223,7 @@ files:
181
223
  - Rakefile.rb
182
224
  - bin/hc
183
225
  - lib/api_hammer.rb
226
+ - lib/api_hammer/active_record_cache_find_by.rb
184
227
  - lib/api_hammer/check_required_params.rb
185
228
  - lib/api_hammer/faraday/outputter.rb
186
229
  - lib/api_hammer/halt.rb
@@ -196,6 +239,7 @@ files:
196
239
  - lib/api_hammer/unmunged_request_params.rb
197
240
  - lib/api_hammer/version.rb
198
241
  - lib/api_hammer/weblink.rb
242
+ - test/active_record_cache_find_by_test.rb
199
243
  - test/check_required_params_test.rb
200
244
  - test/halt_test.rb
201
245
  - test/helper.rb
@@ -229,6 +273,7 @@ signing_key:
229
273
  specification_version: 4
230
274
  summary: an API tool
231
275
  test_files:
276
+ - test/active_record_cache_find_by_test.rb
232
277
  - test/check_required_params_test.rb
233
278
  - test/halt_test.rb
234
279
  - test/helper.rb