horza 0.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.
- checksums.yaml +7 -0
- data/LICENSE.txt +20 -0
- data/README.md +333 -0
- data/lib/horza/adapters/abstract_adapter.rb +67 -0
- data/lib/horza/adapters/active_record.rb +48 -0
- data/lib/horza/configuration.rb +31 -0
- data/lib/horza/core_extensions/string.rb +17 -0
- data/lib/horza/entities/collection.rb +41 -0
- data/lib/horza/entities/single.rb +13 -0
- data/lib/horza/entities.rb +10 -0
- data/lib/horza/errors.rb +12 -0
- data/lib/horza.rb +13 -0
- data/spec/horza_spec.rb +56 -0
- data/spec/spec_helper.rb +7 -0
- metadata +116 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 3b297b82ecba5dd4f72a6974c8018d2895f80dbc
|
4
|
+
data.tar.gz: c2403238d237160290cd7c68a37cda4f106b78b9
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5b6cabd74b00d0d5954bd030d2854f1ed30feabb540dbdb4f53b46b79485f5b02cddc8f266b9230884de78b37fd241af2c59370ea278b7aa6d6db4714b6bffdb
|
7
|
+
data.tar.gz: 2a0fbb2d13b0e524642c66ddf7a37a0fb00b94f67c13d0f54621a159e779b4b235feabd910cb7b4c9082f661b3d2ebc54d11f61f4676efafa52b7985252d0d3b
|
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 Horza
|
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 ::Horza::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 Horza
|
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 ::Horza::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,31 @@
|
|
1
|
+
module Horza
|
2
|
+
module Configuration
|
3
|
+
String.send(:include, ::Horza::CoreExtensions::String)
|
4
|
+
|
5
|
+
def configuration
|
6
|
+
@configuration ||= Config.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def reset
|
10
|
+
@configuration = Config.new
|
11
|
+
@adapter, @adapter_map = nil, nil # Class-level cache clear
|
12
|
+
end
|
13
|
+
|
14
|
+
def configure
|
15
|
+
yield(configuration)
|
16
|
+
end
|
17
|
+
|
18
|
+
def adapter
|
19
|
+
raise ::Horza::Errors::AdapterNotConfigured.new unless configuration.adapter
|
20
|
+
@adapter ||= adapter_map[configuration.adapter]
|
21
|
+
end
|
22
|
+
|
23
|
+
def adapter_map
|
24
|
+
@adapter_map ||= ::Horza::Adapters::AbstractAdapter.descendants.reduce({}) { |hash, (klass)| hash.merge(klass.name.split('::').last.underscore.to_sym => klass) }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class Config
|
29
|
+
attr_accessor :adapter
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Horza
|
2
|
+
module Entities
|
3
|
+
class Collection
|
4
|
+
def initialize(collection)
|
5
|
+
@collection = collection
|
6
|
+
end
|
7
|
+
|
8
|
+
def each
|
9
|
+
@collection.each do |result|
|
10
|
+
yield singular_entity(result)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def [](index)
|
15
|
+
singular_entity(@collection[index])
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def method_missing(method)
|
21
|
+
if [:length, :size, :empty?, :present?].include? method
|
22
|
+
@collection.send(method)
|
23
|
+
elsif [:first, :last].include? method
|
24
|
+
singular_entity(@collection.send(method))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def singular_entity(record)
|
29
|
+
singular_entity_class.new(Horza.adapter.new(record).to_hash)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Collection classes have the form Horza::Entities::TypesMapper
|
33
|
+
# Single output requires the form Horza::Entities::TypeMapper
|
34
|
+
def singular_entity_class
|
35
|
+
@singular_entity ||= Kernel.const_get(self.class.name.deconstantize).const_get(self.class.name.demodulize.singularize)
|
36
|
+
rescue NameError
|
37
|
+
@singular_entity = ::Horza::Entities::Single
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/horza/errors.rb
ADDED
data/lib/horza.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'horza/adapters/abstract_adapter'
|
2
|
+
require 'horza/adapters/active_record'
|
3
|
+
require 'horza/core_extensions/string'
|
4
|
+
require 'horza/entities/single'
|
5
|
+
require 'horza/entities/collection'
|
6
|
+
require 'horza/entities'
|
7
|
+
require 'horza/configuration'
|
8
|
+
require 'horza/errors'
|
9
|
+
require 'active_support/inflector'
|
10
|
+
|
11
|
+
module Horza
|
12
|
+
extend Configuration
|
13
|
+
end
|
data/spec/horza_spec.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Horza do
|
4
|
+
context '#adapter' do
|
5
|
+
context 'when adapter is not configured' do
|
6
|
+
it 'throws error' do
|
7
|
+
expect { Horza.adapter }.to raise_error(Horza::Errors::AdapterNotConfigured)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
context 'when adapter is configured' do
|
12
|
+
before { Horza.configure { |config| config.adapter = :active_record } }
|
13
|
+
after { Horza.reset }
|
14
|
+
it 'returns appropriate class' do
|
15
|
+
expect(Horza.adapter).to eq Horza::Adapters::ActiveRecord
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe 'Entities' do
|
21
|
+
context '#const_missing' do
|
22
|
+
it 'dynamically defines classes' do
|
23
|
+
expect { Horza::Entities.const_get('NewClass') }.to_not raise_error
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe 'Collection' do
|
28
|
+
context '#singular_entity_class' do
|
29
|
+
context 'when singular entity class does not exist' do
|
30
|
+
module TestNamespace
|
31
|
+
class GetUsers < Horza::Entities::Collection
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'returns Horza::Collection::Single' do
|
36
|
+
expect(TestNamespace::GetUsers.new([]).send(:singular_entity_class)).to eq Horza::Entities::Single
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
context 'when singular entity class exists' do
|
41
|
+
module TestNamespace
|
42
|
+
class GetEmployers < Horza::Entities::Collection
|
43
|
+
end
|
44
|
+
|
45
|
+
class GetEmployer < Horza::Entities::Single
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'returns the existing singular class' do
|
50
|
+
expect(TestNamespace::GetEmployers.new([]).send(:singular_entity_class)).to eq TestNamespace::GetEmployer
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: horza
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Blake Turner
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-05-22 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: hashie
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 3.4.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 3.4.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activesupport
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 2.3.11
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 2.3.11
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 2.4.0
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 2.4.0
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: byebug
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '4.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '4.0'
|
69
|
+
description: Horza is a shapeshifter that provides common inputs and outputs for your
|
70
|
+
ORM
|
71
|
+
email: mail@blakewilliamturner.com
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- LICENSE.txt
|
77
|
+
- README.md
|
78
|
+
- lib/horza.rb
|
79
|
+
- lib/horza/adapters/abstract_adapter.rb
|
80
|
+
- lib/horza/adapters/active_record.rb
|
81
|
+
- lib/horza/configuration.rb
|
82
|
+
- lib/horza/core_extensions/string.rb
|
83
|
+
- lib/horza/entities.rb
|
84
|
+
- lib/horza/entities/collection.rb
|
85
|
+
- lib/horza/entities/single.rb
|
86
|
+
- lib/horza/errors.rb
|
87
|
+
- spec/horza_spec.rb
|
88
|
+
- spec/spec_helper.rb
|
89
|
+
homepage: https://github.com/onfido/horza
|
90
|
+
licenses:
|
91
|
+
- MIT
|
92
|
+
metadata: {}
|
93
|
+
post_install_message:
|
94
|
+
rdoc_options: []
|
95
|
+
require_paths:
|
96
|
+
- lib
|
97
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
98
|
+
requirements:
|
99
|
+
- - ">="
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
103
|
+
requirements:
|
104
|
+
- - ">="
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: '0'
|
107
|
+
requirements: []
|
108
|
+
rubyforge_project:
|
109
|
+
rubygems_version: 2.2.2
|
110
|
+
signing_key:
|
111
|
+
specification_version: 4
|
112
|
+
summary: Keep your app ORM-agnostic
|
113
|
+
test_files:
|
114
|
+
- spec/horza_spec.rb
|
115
|
+
- spec/spec_helper.rb
|
116
|
+
has_rdoc:
|