get 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 585aedf15abe17bcbfca617a1eee4f249ac8822b
4
+ data.tar.gz: 29146be9ca581eb442991aaa34f78d4310c113d0
5
+ SHA512:
6
+ metadata.gz: 61e0565290f33c8cf0a74115cb1c541d7c466376494df93c3f727edb3256001064afc2e761b4fc8aad4a3b7e05bf3163e826583d95a6245b64c127abe85c3f17
7
+ data.tar.gz: a3a1b97257304d0531e9b6f299ef59f6ed737b8099341c595b30e832d9b186007902030c00e1c9edb1948a9fffcafcf8ef9700b254e332596dd86c76431c7134
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2015 Blake Turner
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,333 @@
1
+ # Get
2
+
3
+ Dynamically generate classes to encapsulate common database queries in Rails.
4
+
5
+ ## Why is this necessary?
6
+
7
+ #### Problem 1: Encapsulation
8
+
9
+ ORMs like ActiveRecord make querying the database incredible easy, but with power comes responsibility, and there's a lot of irresponsible code out there.
10
+
11
+ Consider:
12
+
13
+ ```ruby
14
+ User.where(name: 'blake').order('updated_at DESC').limit(2)
15
+ ```
16
+
17
+ This query is easy to read, and it works. Unfortunately, anything that uses it is tough to test, and any other implementation has to repeat this same cumbersome method chain.
18
+ Sure, you can wrap it in a method:
19
+
20
+ ```ruby
21
+ def find_two_blakes
22
+ User.where(name: 'blake').order('updated_at DESC').limit(2)
23
+ end
24
+ ```
25
+
26
+ But where does it live? Scope methods on models are (IMHO) hideous, so maybe a Helper? A Service? A private method in a class that I inherit from? The options aren't great.
27
+
28
+ #### Problem 2: Associations
29
+
30
+ ORMs like ActiveRecord also makes querying associations incredible easy. Consider:
31
+
32
+ ```html+ruby
33
+ <div>
34
+ <ul>
35
+ <% current_user.employer.sportscars.each do |car| %>
36
+ <li><%= car.cost %></li>
37
+ <% end >
38
+ </ul>
39
+ </div>
40
+ ```
41
+
42
+ The above is a great example of query pollution in the view layer. It's quick-to-build, tough-to-test, and very common in Rails.
43
+ A spec for a view like this would need to either create/stub each of the records with the proper associations, or stub the entire method chain.
44
+
45
+ If you move the query to the controller, it's a bit better:
46
+
47
+ ```ruby
48
+ # controller
49
+ def index
50
+ @employer = current_user.employer
51
+ @sportscars = @employer.sportscars
52
+ end
53
+ ```
54
+
55
+ ```html+ruby
56
+ #view
57
+ <div>
58
+ <ul>
59
+ <% @sportscars.each do |car| %>
60
+ <li><%= car.cost %></li>
61
+ <% end >
62
+ </ul>
63
+ </div>
64
+ ```
65
+
66
+ But that's just lipstick on a pig. We've simply shifted the testing burden to the controller; the dependencies and mocking complexity remain the same.
67
+
68
+ #### Problem 3: Self-Documenting code
69
+
70
+ Consider:
71
+
72
+ ```ruby
73
+ User.where(last_name: 'Turner').order('id DESC').limit(1)
74
+ ```
75
+
76
+ Most programmers familiar with Rails will be able to understand the above immediately, but only because they've written similar chains a hundred times.
77
+
78
+ ## Solution
79
+
80
+ The Get library tries to solve the above problems by dynamically generating classes that perform common database queries.
81
+ Get identifies four themes in common queries:
82
+
83
+ - **Singular**: Queries that expect a single record in response
84
+ - **Plural**: Queries that expect a collection of records in response
85
+ - **Query**: Query is performed on the given model
86
+ - **Association**: Query traverses the associations of the given model and returns a different model
87
+
88
+ These themes are not mutually exclusive; **Query** and **Association** can be either **Singular** or **Plural**.
89
+
90
+ ## Usage
91
+
92
+ #### Singular Queries - Return a single record
93
+
94
+ With field being queried in the class name
95
+ ```ruby
96
+ Get::UserById.run(123)
97
+ ```
98
+
99
+ Fail loudly
100
+ ```ruby
101
+ Get::UserById.run!(123)
102
+ ```
103
+
104
+ Slightly more flexible model:
105
+ ```ruby
106
+ Get::UserBy.run(id: 123, employer_id: 88)
107
+ ```
108
+
109
+ #### Plural Queries - Return a collection of records
110
+
111
+ _Note the plurality of 'Users'_
112
+ ```ruby
113
+ Get::UsersByLastName.run('Turner')
114
+ ```
115
+
116
+ #### Associations
117
+
118
+ Associations use 'From', and are sugar for the chains we so often write in rails.
119
+
120
+ _You can pass either an entity or an id, the only requirement is that it responds to #id_
121
+
122
+ Parent relationship (user.employer):
123
+ ```ruby
124
+ Get::EmployerFromUser.run(user)
125
+ ```
126
+
127
+ Child relationship (employer.users):
128
+ ```ruby
129
+ Get::UsersFromEmployer.run(employer_id)
130
+ ```
131
+
132
+ Complex relationship (user.employer.sportscars)
133
+ ```ruby
134
+ Get::SportscarsFromUser.run(user, via: :employer)
135
+ ```
136
+
137
+ Keep the plurality of associations in mind. If an Employer has many Users, UsersFromEmployer works,
138
+ but UserFromEmployer will throw `Get::Errors::InvalidAncestry`.
139
+
140
+ ## Entities
141
+
142
+ Ironically, one of the "features" of Get is its removal of the ability to query associations from the query response object.
143
+ This choice was made to combat query pollution throughout the app, particularly in the view layer.
144
+
145
+ To achieve this, Get returns **entities** instead of ORM objects (`ActiveRecord::Base`, etc.).
146
+ These entity classes are generated at runtime with names appropriate to their contents.
147
+ You can also register your own entities in the Get config.
148
+
149
+ ```ruby
150
+ >> result = Get::UserById.run(user.id)
151
+ >> result.class.name
152
+ >> "Get::Entities::GetUser"
153
+ ```
154
+
155
+ Individual entities will have all attributes accessible via dot notation and hash notation, but attempts to get associations will fail.
156
+ Collections have all of the common enumerator methods: `first`, `last`, `each`, and `[]`.
157
+
158
+ Dynamically generated Get::Entities are prefixed with `Get` to avoid colliding with your ORM objects.
159
+
160
+ ## Testing
161
+
162
+ A big motivation for this library is to make testing database queries easier.
163
+ Get accomplishes this by making class-level mocking/stubbing very easy.
164
+
165
+ Consider:
166
+
167
+ ```ruby
168
+ # sportscars_controller.rb
169
+
170
+ # ActiveRecord
171
+ def index
172
+ @sportscars = current_user.employer.sportscars
173
+ end
174
+
175
+ # Get
176
+ def index
177
+ @sportscars = Get::SportscarsFromUser.run(current_user, via: employer)
178
+ end
179
+ ```
180
+
181
+ The above methods do the exact same thing. Cool, let's test them:
182
+
183
+ ```ruby
184
+ # sportscars_controller.rb
185
+ describe SportscarsController, type: :controller do
186
+ context '#index' do
187
+ context 'ActiveRecord' do
188
+ let(:user) { FactoryGirl.build_stubbed(:user, employer: employer) }
189
+ let(:employer) { FactoryGirl.build_stubbed(:employer) }
190
+ let(:sportscars) { 3.times { FactoryGirl.build_stubbed(:sportscars) } }
191
+
192
+ before do
193
+ employer.sportscars << sportscars
194
+ sign_in(user)
195
+ get :index
196
+ end
197
+
198
+ it 'assigns sportscars' do
199
+ expect(assigns(:sportscars)).to eq(sportscars)
200
+ end
201
+ end
202
+
203
+ context 'Get' do
204
+ let(:user) { FactoryGirl.build_stubbed(:user, employer: employer) }
205
+ let(:sportscars) { 3.times { FactoryGirl.build_stubbed(:sportscars) } }
206
+
207
+ before do
208
+ allow(Get::SportscarsFromUser).to receive(:run).and_return(sportscars)
209
+ sign_in(user)
210
+ get :index
211
+ end
212
+
213
+ it 'assigns sportscars' do
214
+ expect(assigns(:sportscars)).to eq(sportscars)
215
+ end
216
+ end
217
+ end
218
+ end
219
+ ```
220
+
221
+ By encapsulating the query in a class, we're able to stub it at the class level, which eliminates then need to create any dependencies.
222
+ This will speed up tests (a little), but more importantly it makes them easier to read and write.
223
+
224
+ ## Config
225
+
226
+ **Define your adapter**
227
+
228
+ _config/initializers/ask.rb_
229
+ ```ruby
230
+ Get.configure { |config| config.adapter = :active_record }
231
+ ```
232
+
233
+ **Configure custom entities**
234
+
235
+ The code below will cause Get classes that begin with _Users_ (ie. `UsersByLastName`) to return a MyCustomEntity instead of the default `Get::Entities::User`.
236
+
237
+ _config/initializers/ask.rb_
238
+ ```ruby
239
+ class MyCustomEntity < Get::Entities::Collection
240
+ def east_london_length
241
+ "#{length}, bruv"
242
+ end
243
+ end
244
+
245
+ Get.config do |config|
246
+ config.register_entity(:users_by_last_name, MyCustomEntity)
247
+ end
248
+ ```
249
+
250
+ You can reset the config at any time using `Get.reset`.
251
+
252
+ ## Adapters
253
+
254
+ Get currently works with ActiveRecord.
255
+
256
+ ## Benchmarking
257
+
258
+ Get requests generally run < 1ms slower than ActiveRecord requests.
259
+
260
+ ```
261
+ GETTING BY ID, SAMPLE_SIZE: 400
262
+
263
+
264
+ >>> ActiveRecord
265
+ user system total real
266
+ Clients::User.find 0.170000 0.020000 0.190000 ( 0.224373)
267
+ Clients::User.find_by_id 0.240000 0.010000 0.250000 ( 0.342278)
268
+
269
+ >>> Get
270
+ user system total real
271
+ Get::UserById 0.300000 0.020000 0.320000 ( 0.402454)
272
+ Get::UserBy 0.260000 0.010000 0.270000 ( 0.350982)
273
+
274
+
275
+ GETTING SINGLE RECORD BY LAST NAME, SAMPLE_SIZE: 400
276
+
277
+
278
+ >>> ActiveRecord
279
+ user system total real
280
+ Clients::User.where 0.190000 0.020000 0.210000 ( 0.292516)
281
+ Clients::User.find_by_last_name 0.180000 0.010000 0.190000 ( 0.270033)
282
+
283
+ >>> Get
284
+ user system total real
285
+ Get::UserByLastName 0.240000 0.010000 0.250000 ( 0.337908)
286
+ Get::UserBy 0.310000 0.020000 0.330000 ( 0.415142)
287
+
288
+
289
+ GETTING MULTIPLE RECORDS BY LAST NAME, SAMPLE_SIZE: 400
290
+
291
+
292
+ >>> ActiveRecord
293
+ user system total real
294
+ Clients::User.where 0.020000 0.000000 0.020000 ( 0.012604)
295
+
296
+ >>> Get
297
+ user system total real
298
+ Get::UsersByLastName 0.100000 0.000000 0.100000 ( 0.105822)
299
+ Get::UsersBy 0.100000 0.010000 0.110000 ( 0.106406)
300
+
301
+
302
+ GETTING PARENT FROM CHILD, SAMPLE_SIZE: 400
303
+
304
+
305
+ >>> ActiveRecord
306
+ user system total real
307
+ Clients::User.find(:id).employer 0.440000 0.030000 0.470000 ( 0.580800)
308
+
309
+ >>> Get
310
+ user system total real
311
+ Get::EmployerFromUser 0.500000 0.020000 0.520000 ( 0.643316)
312
+
313
+
314
+ GETTING CHILDREN FROM PARENT, SAMPLE_SIZE: 400
315
+
316
+
317
+ >>> ActiveRecord
318
+ user system total real
319
+ Clients::Employer.find[:id].users 0.160000 0.020000 0.180000 ( 0.218710)
320
+
321
+ >>> Get
322
+ user system total real
323
+ Get::UsersFromEmployer 0.230000 0.010000 0.240000 ( 0.293037)
324
+
325
+
326
+ STATS
327
+
328
+ AVERAGE DIFF FOR BY ID: 0.000233s
329
+ AVERAGE DIFF FOR BY LAST NAME: 0.000238s
330
+ AVERAGE DIFF FOR BY LAST NAME (MULTIPLE): 0.000234s
331
+ AVERAGE DIFF FOR PARENT FROM CHILD: 0.000156s
332
+ AVERAGE DIFF FOR CHILDREN FROM PARENT: -0.000186s
333
+ ```
@@ -0,0 +1,67 @@
1
+ module Get
2
+ module Adapters
3
+ class AbstractAdapter
4
+ attr_reader :context
5
+
6
+ class << self
7
+ def expected_errors
8
+ not_implemented_error
9
+ end
10
+
11
+ def context_for_entity(entity)
12
+ not_implemented_error
13
+ end
14
+
15
+ def entity_context_map
16
+ not_implemented_error
17
+ end
18
+
19
+ def not_implemented_error
20
+ raise ::Get::Errors::MethodNotImplemented, 'You must implement this method in your adapter.'
21
+ end
22
+
23
+ def descendants
24
+ descendants = []
25
+ ObjectSpace.each_object(singleton_class) do |k|
26
+ descendants.unshift k unless k == self
27
+ end
28
+ descendants
29
+ end
30
+ end
31
+
32
+ def initialize(context)
33
+ @context = context
34
+ end
35
+
36
+ def get!(options = {})
37
+ not_implemented_error
38
+ end
39
+
40
+ def find_first(options = {})
41
+ not_implemented_error
42
+ end
43
+
44
+ def find_all(options = {})
45
+ not_implemented_error
46
+ end
47
+
48
+ def ancestors(options = {})
49
+ not_implemented_error
50
+ end
51
+
52
+ def eager_load(options = {})
53
+ not_implemented_error
54
+ end
55
+
56
+ def to_hash
57
+ not_implemented_error
58
+ end
59
+
60
+ private
61
+
62
+ def not_implemented_error
63
+ self.class.not_implemented_error
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,48 @@
1
+ module Get
2
+ module Adapters
3
+ class ActiveRecord < AbstractAdapter
4
+ class << self
5
+ def expected_errors
6
+ [::ActiveRecord::RecordNotFound]
7
+ end
8
+
9
+ def context_for_entity(entity)
10
+ entity_context_map[entity]
11
+ end
12
+
13
+ def entity_context_map
14
+ @map ||= ::ActiveRecord::Base.descendants.reduce({}) { |hash, (klass)| hash.merge(klass.name.split('::').last.underscore.to_sym => klass) }
15
+ end
16
+ end
17
+
18
+ def get!(options = {})
19
+ @context = @context.find(options[:id])
20
+ end
21
+
22
+ def find_first(options = {})
23
+ @context = find_all(options).limit(1).first!
24
+ end
25
+
26
+ def find_all(options = {})
27
+ @context = @context.where(options[:conditions]).order('ID DESC')
28
+ end
29
+
30
+ def ancestors(options = {})
31
+ get!(options)
32
+ walk_family_tree(options)
33
+ rescue NoMethodError
34
+ raise ::Get::Errors::InvalidAncestry.new('Invalid relation. Ensure that the plurality of your associations is correct.')
35
+ end
36
+
37
+ def to_hash
38
+ @context.attributes
39
+ end
40
+
41
+ private
42
+
43
+ def walk_family_tree(options)
44
+ options[:via].push(options[:result_key]).each { |relation| @context = @context.send(relation) }
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,57 @@
1
+ module Get
2
+ module Builders
3
+ class AncestryBuilder < BaseBuilder
4
+ private
5
+
6
+ def class_args
7
+ {
8
+ key: @key,
9
+ collection: @result_entity.plural?,
10
+ result_entity: @result_entity.symbolize,
11
+ store: Get.adapter.context_for_entity(@key.to_s.singularize.symbolize)
12
+ }
13
+ end
14
+
15
+ def template_class(args)
16
+ Class.new(::Get::Db) do
17
+ include Get
18
+
19
+ class << self
20
+ attr_reader :result_key
21
+ end
22
+
23
+ @entity, @result_key, @collection, @store = args[:key], args[:result_entity], args[:collection], args[:store]
24
+
25
+ def initialize(model, options = {})
26
+ @model, @options = model, options
27
+ super(query_params)
28
+ end
29
+
30
+ private
31
+
32
+ def id
33
+ return @model.id if @model.respond_to? :id
34
+ @model
35
+ end
36
+
37
+ def query_params
38
+ options = @options.except(:via) || {}
39
+ { ancestors: ancestor_params }.merge(options)
40
+ end
41
+
42
+ def ancestor_params
43
+ {
44
+ id: id,
45
+ via: via,
46
+ result_key: self.class.result_key
47
+ }
48
+ end
49
+
50
+ def via
51
+ @options[:via] || []
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,20 @@
1
+ module Get
2
+ module Builders
3
+ class BaseBuilder
4
+ def initialize(class_name)
5
+ parse_class_name(class_name)
6
+ end
7
+
8
+ def class
9
+ template_class(class_args)
10
+ end
11
+
12
+ private
13
+
14
+ def parse_class_name(class_name)
15
+ @result_entity, key = class_name.to_s.match(::Get::ASK_CLASS_REGEX).values_at(1, 3)
16
+ @key = key.present? ? key.symbolize : nil
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,54 @@
1
+ module Get
2
+ module Builders
3
+ class QueryBuilder < BaseBuilder
4
+ private
5
+
6
+ def class_args
7
+ {
8
+ key: @key,
9
+ collection: @result_entity.plural?,
10
+ result_entity: @result_entity.singularize.symbolize,
11
+ store: Get.adapter.context_for_entity(@result_entity.singularize.symbolize)
12
+ }
13
+ end
14
+
15
+ def template_class(args)
16
+ Class.new(::Get::Db) do
17
+ include Get
18
+
19
+ class << self
20
+ attr_reader :field
21
+ end
22
+
23
+ @field, @entity, @collection, @store = args[:key], args[:result_entity], args[:collection], args[:store]
24
+
25
+ def initialize(params)
26
+ @params = params
27
+ super(query_params)
28
+ end
29
+
30
+ private
31
+
32
+ def query_params
33
+ { query_action => { conditions: conditions } }
34
+ end
35
+
36
+ def query_action
37
+ self.class.collection ? :find_all : :find_first
38
+ end
39
+
40
+ # find_first
41
+ def conditions
42
+ return @params unless self.class.field
43
+ { self.class.field => @params }
44
+ end
45
+
46
+ def single_params
47
+ return {} if self.class.collection
48
+ { limit: 1, first: true }
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,21 @@
1
+ module Get
2
+ module Builders
3
+ String.include ::Get::CoreExtensions::String
4
+
5
+ class << self
6
+ def generate_class(name)
7
+ method = name.to_s.match(ASK_CLASS_REGEX)[2]
8
+ Get.const_set(name, builder_for_method(method).new(name).class)
9
+ end
10
+
11
+ def builder_for_method(method)
12
+ case method
13
+ when 'By'
14
+ QueryBuilder
15
+ when 'From'
16
+ AncestryBuilder
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,45 @@
1
+ module Get
2
+ module Configuration
3
+ def configuration
4
+ @configuration ||= Config.new
5
+ end
6
+
7
+ def reset
8
+ @configuration = Config.new
9
+ @adapter, @adapter_map = nil, nil # Class-level cache clear
10
+ end
11
+
12
+ def configure
13
+ yield(configuration)
14
+ end
15
+
16
+ def adapter
17
+ raise ::Get::Errors::Base.new('Adapter has not been configured') unless configuration.adapter
18
+ @adapter ||= adapter_map[configuration.adapter]
19
+ end
20
+
21
+ def entity_for(model)
22
+ configuration.entity_for(model)
23
+ end
24
+
25
+ def adapter_map
26
+ @adapter_map ||= ::Get::Adapters::AbstractAdapter.descendants.reduce({}) { |hash, (klass)| hash.merge(klass.name.split('::').last.underscore.to_sym => klass) }
27
+ end
28
+ end
29
+
30
+ class Config
31
+ attr_accessor :adapter
32
+
33
+ def initialize
34
+ @registered_entities = {}
35
+ end
36
+
37
+ def entity_for(model)
38
+ @registered_entities[model]
39
+ end
40
+
41
+ def register_entity(model, klass)
42
+ @registered_entities[model] = klass
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,17 @@
1
+ module Get
2
+ module CoreExtensions
3
+ module String
4
+ def singular?
5
+ singularize == self
6
+ end
7
+
8
+ def plural?
9
+ pluralize == self
10
+ end
11
+
12
+ def symbolize
13
+ underscore.to_sym
14
+ end
15
+ end
16
+ end
17
+ end