api_hammer 0.2.2 → 0.3.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: 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