kashmir 0.1

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.
@@ -0,0 +1,54 @@
1
+ require 'ar_test_helper'
2
+
3
+ describe 'ActiveRecord performance tricks' do
4
+
5
+ before(:all) do
6
+ TestData.create_tom
7
+ TestData.create_netto
8
+ end
9
+
10
+ describe "Query preload to avoid N+1 queries" do
11
+
12
+ it 'tries to preload records whenever possible' do
13
+ selects = track_queries do
14
+ AR::Chef.all.each do |chef|
15
+ chef.recipes.to_a
16
+ end
17
+ end
18
+ # SELECT * FROM chefs
19
+ # SELECT "recipes".* FROM "recipes" WHERE "recipes"."chef_id" = ?
20
+ # SELECT "recipes".* FROM "recipes" WHERE "recipes"."chef_id" = ?
21
+ assert_equal 3, selects.size
22
+
23
+ selects = track_queries do
24
+ AR::Chef.all.represent([:recipes])
25
+ end
26
+ # SELECT "chefs".* FROM "chefs"
27
+ # SELECT "recipes".* FROM "recipes" WHERE "recipes"."chef_id" IN (1, 2)
28
+ assert_equal 2, selects.size
29
+ end
30
+
31
+ it 'preloads queries per each level in the tree' do
32
+ selects = track_queries do
33
+ AR::Chef.all.each do |chef|
34
+ chef.recipes.each do |recipe|
35
+ recipe.ingredients.to_a
36
+ end
37
+ end
38
+ end
39
+ # SELECT "chefs".* FROM "chefs"
40
+ # (2x) SELECT "recipes".* FROM "recipes" WHERE "recipes"."chef_id" = ?
41
+ # (4x) SELECT "ingredients".* FROM "ingredients" INNER JOIN "recipes_ingredients" ...
42
+ assert_equal 7, selects.size
43
+
44
+ selects = track_queries do
45
+ AR::Chef.all.represent([ :recipes => [ :ingredients => [:name] ] ])
46
+ end
47
+ # SELECT "chefs".* FROM "chefs"
48
+ # SELECT "recipes".* FROM "recipes" WHERE "recipes"."chef_id" IN (1, 2)
49
+ # SELECT "recipes_ingredients".* FROM "recipes_ingredients" WHERE "recipes_ingredients"."recipe_id" IN (1, 2, 3, 4)
50
+ # SELECT "ingredients".* FROM "ingredients" WHERE "ingredients"."id" IN (1, 2, 3, 4, 5, 6, 7, 8)
51
+ assert_equal 4, selects.size
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,34 @@
1
+ require 'test_helper'
2
+ require 'minitest/around'
3
+
4
+ require 'active_record'
5
+
6
+ ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
7
+
8
+ require 'support/schema'
9
+ require 'support/ar_models'
10
+ require 'support/factories'
11
+
12
+
13
+ class Minitest::Test
14
+ def around(&block)
15
+ ActiveRecord::Base.transaction do
16
+ block.call
17
+ raise ActiveRecord::Rollback
18
+ end
19
+ end
20
+ end
21
+
22
+ def track_queries
23
+ selects = []
24
+ queries_collector = lambda do |name, start, finish, id, payload|
25
+ selects << payload
26
+ end
27
+
28
+ ActiveRecord::Base.connection.clear_query_cache
29
+ ActiveSupport::Notifications.subscribed(queries_collector, 'sql.active_record') do
30
+ yield
31
+ end
32
+
33
+ selects.map { |sel| sel[:sql] }
34
+ end
@@ -0,0 +1,197 @@
1
+ require 'ar_test_helper'
2
+
3
+ describe 'Caching' do
4
+
5
+ before(:all) do
6
+
7
+ Kashmir.init(
8
+ cache_client: Kashmir::Caching::Memory.new
9
+ )
10
+
11
+ TestData.create_tom
12
+ @restaurant = AR::Restaurant.find_by_name('Chef Tom Belly Burgers')
13
+ end
14
+
15
+ before(:each) do
16
+ Kashmir.caching.flush!
17
+ end
18
+
19
+ def from_cache(definition, model)
20
+ Kashmir.caching.from_cache(definition, model)
21
+ end
22
+
23
+ describe 'flat data' do
24
+ it 'loads the same exact data from cache' do
25
+ representation = @restaurant.represent([:name])
26
+ @restaurant.reload
27
+ cached_representation = @restaurant.represent([:name])
28
+ assert_equal representation, cached_representation
29
+ end
30
+
31
+ it 'stores the data in cache' do
32
+ representation = @restaurant.represent([:name])
33
+ cached_representation = from_cache([:name], @restaurant)
34
+ assert_equal representation, cached_representation
35
+ end
36
+ end
37
+
38
+ describe 'references to other representations' do
39
+ it 'does not perform the same query twice' do
40
+ selects = track_queries do
41
+ @restaurant.represent([:name, :owner])
42
+ end
43
+ assert_equal 1, selects.size
44
+
45
+ # clear active record cache for this instance
46
+ @restaurant.reload
47
+ selects = track_queries do
48
+ @restaurant.represent([:name, :owner])
49
+ end
50
+ assert_equal 0, selects.size
51
+ end
52
+
53
+ it 'loads the same exact data from cache' do
54
+ representation = @restaurant.represent([:name, :owner])
55
+ @restaurant.reload
56
+ cached_representation = @restaurant.represent([:name, :owner])
57
+ assert_equal representation, cached_representation
58
+ end
59
+
60
+ it 'stores the data in cache at every level' do
61
+ representation = @restaurant.represent([:name, owner: [:name] ])
62
+ cached_restaurant_with_chef = from_cache([:name, owner: [ :name ]], @restaurant)
63
+
64
+ assert_equal representation, cached_restaurant_with_chef
65
+
66
+ chef = @restaurant.owner
67
+ cached_chef = from_cache([:name], chef)
68
+
69
+ assert_equal cached_chef, representation[:owner]
70
+ end
71
+ end
72
+
73
+ describe 'nesting' do
74
+ before(:all) do
75
+ @chef = @restaurant.owner
76
+ @chef.reload
77
+ end
78
+
79
+ it 'caches at every level' do
80
+ representation = @chef.represent([:name, :restaurant =>[ :name, :rating =>[ :value ]]])
81
+ fully_cached_chef = from_cache([:name, :restaurant =>[ :name, :rating =>[ :value ]]], @chef)
82
+ assert_equal representation, fully_cached_chef
83
+
84
+ fully_cached_restaurant = from_cache([ :name, :rating => [:value] ], @restaurant)
85
+ assert_equal representation[:restaurant], fully_cached_restaurant
86
+
87
+ cached_rating = from_cache([:value], @restaurant.rating)
88
+ assert_equal representation[:restaurant][:rating], cached_rating
89
+
90
+ assert_equal 3, Kashmir.caching.keys.size
91
+ end
92
+
93
+ it 'tries to hit the cache at every level' do
94
+ selects = track_queries do
95
+ representation = @chef.represent([:name, :restaurant =>[ :name, :rating =>[ :value ]]])
96
+ end
97
+ # SELECT "restaurants".* FROM "restaurants" WHERE "restaurants"."owner_id" = ? LIMIT 1
98
+ # SELECT "ratings".* FROM "ratings" WHERE "ratings"."restaurant_id" = ? LIMIT 1
99
+ assert_equal selects.size, 2
100
+
101
+ @chef.reload
102
+ selects = track_queries do
103
+ representation = @chef.represent([:name, :restaurant =>[ :name, :rating =>[ :value ]]])
104
+ end
105
+ assert_equal selects.size, 0
106
+ end
107
+
108
+ it 'tries to fill holes in the cache graph' do
109
+ definition = [:name, :restaurant =>[ :name, :rating =>[ :value ]]]
110
+ representation = @chef.represent(definition)
111
+ Kashmir.caching.clear(definition, @chef)
112
+
113
+ assert_equal 2, Kashmir.caching.keys.size
114
+
115
+ @chef.reload
116
+ selects = track_queries do
117
+ @chef.represent(definition)
118
+ end
119
+ # ratings is still cached
120
+ # SELECT "restaurants".* FROM "restaurants" WHERE "restaurants"."owner_id" = ? LIMIT 1
121
+ assert_equal selects.size, 1
122
+ end
123
+ end
124
+
125
+ describe 'collections' do
126
+ it 'caches every item' do
127
+ presented_recipes = AR::Recipe.all.represent([:title])
128
+
129
+ cached_keys = %w(
130
+ presenter:AR::Recipe:1:[:title]
131
+ presenter:AR::Recipe:2:[:title]
132
+ )
133
+
134
+ assert_equal cached_keys, Kashmir.caching.keys
135
+ end
136
+
137
+ it 'presents from cache' do
138
+ selects = track_queries do
139
+ AR::Recipe.all.represent([:title, :ingredients => [:name]])
140
+ end
141
+ # SELECT "recipes_ingredients".* FROM "recipes_ingredients" WHERE "recipes_ingredients"."recipe_id" IN (1, 2)
142
+ # SELECT "ingredients".* FROM "ingredients" WHERE "ingredients"."id" IN (1, 2, 3, 4)
143
+ assert_equal 3, selects.size
144
+
145
+ selects = track_queries do
146
+ AR::Recipe.all.represent([:title, :ingredients => [:name]])
147
+ end
148
+ # SELECT "recipes".* FROM "recipes"
149
+ assert_equal 1, selects.size
150
+
151
+ cache_keys = [
152
+ "presenter:AR::Ingredient:1:[:name]",
153
+ "presenter:AR::Ingredient:2:[:name]",
154
+ "presenter:AR::Recipe:1:[:title, {:ingredients=>[:name]}]",
155
+ "presenter:AR::Ingredient:3:[:name]",
156
+ "presenter:AR::Ingredient:4:[:name]",
157
+ "presenter:AR::Recipe:2:[:title, {:ingredients=>[:name]}]"
158
+ ]
159
+
160
+ assert_equal cache_keys, Kashmir.caching.keys
161
+ end
162
+ end
163
+
164
+ describe 'misc' do
165
+
166
+ it 'does not cache already cached results' do
167
+ @restaurant.represent([:name])
168
+ assert_equal 1, Kashmir.caching.keys.size
169
+
170
+ Kashmir.caching.expects(:store_presenter).never
171
+ @restaurant.represent([:name])
172
+ end
173
+
174
+ describe 'skipping cache' do
175
+ # see current_customer_count in test/support/ar_models.rb
176
+
177
+ before(:each) do
178
+ @restaurant.update_attributes(current_customer_count: 10)
179
+ @representation = @restaurant.represent([:name, :current_customer_count])
180
+ end
181
+
182
+ it 'still renders everything normally' do
183
+ assert_equal @representation, { name: 'Chef Tom Belly Burgers', current_customer_count: 10 }
184
+ end
185
+
186
+ it 'does not include that field in the key' do
187
+ assert_equal %w(presenter:AR::Restaurant:1:[:name]), Kashmir.caching.keys
188
+ end
189
+
190
+ it 'does not insert value in the cached results' do
191
+ assert_nil Kashmir.caching.from_cache([:name, :current_customar_count], @restaurant)
192
+ assert_equal Kashmir.caching.from_cache([:name], @restaurant), { name: 'Chef Tom Belly Burgers' }
193
+ end
194
+ end
195
+
196
+ end
197
+ end
data/test/dsl_test.rb ADDED
@@ -0,0 +1,162 @@
1
+ require 'test_helper'
2
+
3
+ describe Kashmir do
4
+
5
+ module DSLTesting
6
+ class Recipe < OpenStruct
7
+ include Kashmir
8
+
9
+ representations do
10
+ rep(:num_steps)
11
+ rep(:title)
12
+ rep(:chef)
13
+ rep(:ingredients)
14
+ end
15
+ end
16
+
17
+ class Chef < OpenStruct
18
+ include Kashmir
19
+
20
+ representations do
21
+ rep(:full_name)
22
+ end
23
+
24
+ def full_name
25
+ "#{first_name} #{last_name}"
26
+ end
27
+ end
28
+
29
+ class SimpleRecipeRepresenter
30
+ include Kashmir::Dsl
31
+
32
+ prop :num_steps
33
+ prop :title
34
+ end
35
+
36
+ class ChefRepresenter
37
+ include Kashmir::Dsl
38
+
39
+ prop :full_name
40
+ end
41
+
42
+ class RecipeWithChefRepresenter
43
+ include Kashmir::Dsl
44
+
45
+ prop :title
46
+ embed :chef, ChefRepresenter
47
+ end
48
+ end
49
+
50
+ before do
51
+ @chef = DSLTesting::Chef.new(first_name: 'Netto', last_name: 'Farah')
52
+ @brisket = DSLTesting::Recipe.new(title: 'BBQ Brisket', num_steps: 2, chef: @chef)
53
+ end
54
+
55
+ it 'translates to representation definitions' do
56
+ definitions = DSLTesting::SimpleRecipeRepresenter.definitions
57
+ assert_equal definitions, [:num_steps, :title]
58
+ end
59
+
60
+ it 'generates the same representations as hardcoded definitions' do
61
+ cooked_brisket = @brisket.represent([:num_steps, :title])
62
+ assert_equal cooked_brisket, { title: 'BBQ Brisket', num_steps: 2 }
63
+ assert_equal cooked_brisket, @brisket.represent(DSLTesting::SimpleRecipeRepresenter.definitions)
64
+ end
65
+
66
+ it 'generates nested representations' do
67
+ brisket_with_chef = @brisket.represent([:title, { :chef => [:full_name] }])
68
+ assert_equal brisket_with_chef, {
69
+ title: 'BBQ Brisket',
70
+ chef: {
71
+ full_name: 'Netto Farah'
72
+ }
73
+ }
74
+
75
+ assert_equal brisket_with_chef, @brisket.represent(DSLTesting::RecipeWithChefRepresenter.definitions)
76
+ end
77
+
78
+ describe 'Nested inline representations' do
79
+
80
+ module DSLTesting
81
+ class RecipeWithInlineChefRepresenter
82
+ include Kashmir::Dsl
83
+
84
+ prop :title
85
+
86
+ inline :chef do
87
+ prop :full_name
88
+ end
89
+ end
90
+
91
+ class Ingredient < OpenStruct
92
+ include Kashmir
93
+
94
+ representations do
95
+ rep(:name)
96
+ rep(:quantity)
97
+ end
98
+ end
99
+ end
100
+
101
+ it 'generates nested representations' do
102
+ brisket_with_chef = @brisket.represent([:title, { :chef => [:full_name] }])
103
+ assert_equal brisket_with_chef, {
104
+ title: 'BBQ Brisket',
105
+ chef: {
106
+ full_name: 'Netto Farah'
107
+ }
108
+ }
109
+
110
+ assert_equal brisket_with_chef, @brisket.represent(DSLTesting::RecipeWithInlineChefRepresenter.definitions)
111
+ end
112
+ end
113
+
114
+ describe 'Collections' do
115
+ module DSLTesting
116
+ class RecipeWithIngredientsRepresenter
117
+ include Kashmir::Dsl
118
+
119
+ prop :title
120
+
121
+ inline :ingredients do
122
+ prop :name
123
+ prop :quantity
124
+ end
125
+ end
126
+ end
127
+
128
+ it 'generates nested collections' do
129
+ @brisket.ingredients = [
130
+ DSLTesting::Ingredient.new(name: 'Beef', quantity: '8oz'),
131
+ DSLTesting::Ingredient.new(name: 'BBQ Sauce', quantity: 'plenty!'),
132
+ ]
133
+
134
+ cooked_brisket = {
135
+ title: 'BBQ Brisket',
136
+ ingredients: [
137
+ { name: 'Beef', quantity: '8oz' },
138
+ { name: 'BBQ Sauce', quantity: 'plenty!' }
139
+ ]
140
+ }
141
+
142
+ assert_equal cooked_brisket, @brisket.represent(DSLTesting::RecipeWithIngredientsRepresenter.definitions)
143
+ end
144
+ end
145
+
146
+ describe "Kashmir.props" do
147
+
148
+ module DSLTesting
149
+
150
+ class MultiPropRecipeRepresenter
151
+ include Kashmir::Dsl
152
+
153
+ props :num_steps, :title
154
+ end
155
+ end
156
+
157
+ it 'allows the client to specify many props at once' do
158
+ definitions = DSLTesting::MultiPropRecipeRepresenter.definitions
159
+ assert_equal definitions, [:num_steps, :title]
160
+ end
161
+ end
162
+ end