horza 0.0.1 → 0.1.2
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 +4 -4
- data/README.md +46 -306
- data/lib/horza.rb +6 -0
- data/lib/horza/adapters/abstract_adapter.rb +19 -8
- data/lib/horza/adapters/active_record.rb +29 -12
- data/lib/horza/configuration.rb +11 -2
- data/lib/horza/entities.rb +14 -3
- data/lib/horza/entities/collection.rb +6 -7
- data/lib/horza/errors.rb +6 -0
- data/spec/active_record_spec.rb +227 -0
- data/spec/horza_spec.rb +2 -37
- data/spec/spec_helper.rb +1 -0
- metadata +32 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: aa0c514a09b3c9cc5c381187de5123e83d6bbc20
|
4
|
+
data.tar.gz: d6fb63bc973ed1cca3b703d8eceb5390163a5a0e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d59d0f2c0ab53696247e2144f34541a3a60074731e1431cea21ea73d3da6beab250a2cd0a57f55a52f75d695e0d49ccc3d22c8f570d0c9d86787e87073203371
|
7
|
+
data.tar.gz: 8970bb56079f409f998e36a5d97192d5cec75f04a0ee05ac87611def55a51458b4c92b1c0901e64c25a746249f8b16dfb72777150e145e6f03db49676428349b
|
data/README.md
CHANGED
@@ -1,333 +1,73 @@
|
|
1
|
-
#
|
1
|
+
# Horza
|
2
2
|
|
3
|
-
|
3
|
+
Horza is a library for decoupling your application from the ORM you have implemented.
|
4
4
|
|
5
|
-
##
|
5
|
+
## Inputs
|
6
6
|
|
7
|
-
|
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:
|
7
|
+
Horza uses ORM-specific adapters to decouple Ruby apps from their ORMS.
|
19
8
|
|
9
|
+
**Configure Adapter**
|
20
10
|
```ruby
|
21
|
-
|
22
|
-
|
11
|
+
Horza.configure do |config|
|
12
|
+
config.adapter = :active_record
|
23
13
|
end
|
24
14
|
```
|
25
15
|
|
26
|
-
|
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
|
16
|
+
**Get Adapter for your ORM Object**
|
100
17
|
```ruby
|
101
|
-
|
102
|
-
|
18
|
+
# ActiveRecord Example
|
19
|
+
user = User.create(user_params)
|
20
|
+
horza_user = Horza.adapter.new(user)
|
103
21
|
|
104
|
-
|
105
|
-
|
106
|
-
|
22
|
+
# Examples
|
23
|
+
horza_user.get(id) # Find by id - Return nil on fail
|
24
|
+
horza_user.get!(id) # Find by id - Error on fail
|
25
|
+
horza_user.find_first(params) # Find 1 user - Orders by id desc by default - Return nil on fail
|
26
|
+
horza_user.find_first!(params) # Find 1 user - Orders by id desc by default - Error nil on fail
|
27
|
+
horza_user.find_all(params) # Find all users that match parameters
|
28
|
+
horza_user.ancestors(target: :employer, via: []) # Traverse relations
|
107
29
|
```
|
108
30
|
|
109
|
-
|
31
|
+
## Outputs
|
110
32
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
```
|
115
|
-
|
116
|
-
#### Associations
|
117
|
-
|
118
|
-
Associations use 'From', and are sugar for the chains we so often write in rails.
|
33
|
+
Horza queries return very dumb vanilla entities instead of ORM response objects.
|
34
|
+
Singular entities are simply hashes that allow both hash and dot notation, and binary helpers.
|
35
|
+
Collection entities behave like arrays.
|
119
36
|
|
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
37
|
```ruby
|
124
|
-
|
125
|
-
|
38
|
+
# Singular Entity
|
39
|
+
result = horza_user.find_first(first_name: 'Blake') # => {"id"=>1, "first_name"=>"Blake", "last_name"=>"Turner", "employer_id"=>nil}
|
40
|
+
result.class.name # => "Horza::Entities::Single"
|
126
41
|
|
127
|
-
|
128
|
-
|
129
|
-
|
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.
|
42
|
+
result['id'] # => 1
|
43
|
+
result.id # => 1
|
44
|
+
result.id? # => true
|
144
45
|
|
145
|
-
|
146
|
-
|
147
|
-
|
46
|
+
# Collection Entity
|
47
|
+
result = horza_user.find_all(last_name: 'Turner')
|
48
|
+
result.class.name # => "Horza::Entities::Collection"
|
148
49
|
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
50
|
+
result.length # => 1
|
51
|
+
result.size # => 1
|
52
|
+
result.empty? # => false
|
53
|
+
result.present? # => true
|
54
|
+
result.first # => {"id"=>1, "first_name"=>"Blake", "last_name"=>"Turner", "employer_id"=>nil}
|
55
|
+
result.last # => {"id"=>1, "first_name"=>"Blake", "last_name"=>"Turner", "employer_id"=>nil}
|
56
|
+
result[0] # => {"id"=>1, "first_name"=>"Blake", "last_name"=>"Turner", "employer_id"=>nil}
|
153
57
|
```
|
154
58
|
|
155
|
-
|
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
|
-
```
|
59
|
+
## Custom Entities
|
180
60
|
|
181
|
-
|
61
|
+
You can define your own entities by making them subclasses of [Horza entities](https://github.com/onfido/horza/tree/master/lib/horza/entities). Just make sure they have the same class name as your ORM classes. Horza will automatically detect custom entities and use them for output.
|
182
62
|
|
183
63
|
```ruby
|
184
|
-
|
185
|
-
|
186
|
-
|
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
|
64
|
+
module CustomEntities
|
65
|
+
# Collection Entity
|
66
|
+
class Users < Horza::Entities::Collection
|
217
67
|
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
68
|
|
228
|
-
|
229
|
-
|
230
|
-
|
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)
|
69
|
+
# Singular Entity
|
70
|
+
class User < Horza::Entities::Single
|
71
|
+
end
|
247
72
|
end
|
248
73
|
```
|
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
|
-
```
|
data/lib/horza.rb
CHANGED
@@ -10,4 +10,10 @@ require 'active_support/inflector'
|
|
10
10
|
|
11
11
|
module Horza
|
12
12
|
extend Configuration
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def descendants_map(klass)
|
16
|
+
klass.descendants.reduce({}) { |hash, (klass)| hash.merge(klass.name.split('::').last.underscore.to_sym => klass) }
|
17
|
+
end
|
18
|
+
end
|
13
19
|
end
|
@@ -19,25 +19,27 @@ module Horza
|
|
19
19
|
def not_implemented_error
|
20
20
|
raise ::Horza::Errors::MethodNotImplemented, 'You must implement this method in your adapter.'
|
21
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
22
|
end
|
31
23
|
|
32
24
|
def initialize(context)
|
33
25
|
@context = context
|
34
26
|
end
|
35
27
|
|
28
|
+
def get(options = {})
|
29
|
+
get!(options = {})
|
30
|
+
rescue *self.class.expected_errors
|
31
|
+
end
|
32
|
+
|
36
33
|
def get!(options = {})
|
37
34
|
not_implemented_error
|
38
35
|
end
|
39
36
|
|
40
37
|
def find_first(options = {})
|
38
|
+
find_first!(options = {})
|
39
|
+
rescue *self.class.expected_errors
|
40
|
+
end
|
41
|
+
|
42
|
+
def find_first!(options = {})
|
41
43
|
not_implemented_error
|
42
44
|
end
|
43
45
|
|
@@ -57,11 +59,20 @@ module Horza
|
|
57
59
|
not_implemented_error
|
58
60
|
end
|
59
61
|
|
62
|
+
def entity_class(res = @context)
|
63
|
+
collection?(res) ? ::Horza::Entities.collection_entity_for(entity_symbol).new(res) : ::Horza::Entities.single_entity_for(entity_symbol).new(res)
|
64
|
+
end
|
65
|
+
|
60
66
|
private
|
61
67
|
|
62
68
|
def not_implemented_error
|
63
69
|
self.class.not_implemented_error
|
64
70
|
end
|
71
|
+
|
72
|
+
def entity_symbol
|
73
|
+
klass = @context.name.split('::').last
|
74
|
+
collection? ? klass.pluralize.symbolize : klass.symbolize
|
75
|
+
end
|
65
76
|
end
|
66
77
|
end
|
67
78
|
end
|
@@ -1,6 +1,8 @@
|
|
1
1
|
module Horza
|
2
2
|
module Adapters
|
3
3
|
class ActiveRecord < AbstractAdapter
|
4
|
+
INVALID_ANCESTRY_MSG = 'Invalid relation. Ensure that the plurality of your associations is correct.'
|
5
|
+
|
4
6
|
class << self
|
5
7
|
def expected_errors
|
6
8
|
[::ActiveRecord::RecordNotFound]
|
@@ -11,37 +13,52 @@ module Horza
|
|
11
13
|
end
|
12
14
|
|
13
15
|
def entity_context_map
|
14
|
-
@map ||= ::ActiveRecord::Base
|
16
|
+
@map ||= ::Horza.descendants_map(::ActiveRecord::Base)
|
15
17
|
end
|
16
18
|
end
|
17
19
|
|
18
|
-
def get!(
|
19
|
-
@context
|
20
|
+
def get!(id)
|
21
|
+
entity_class(@context.find(id).attributes)
|
20
22
|
end
|
21
23
|
|
22
|
-
def find_first(options = {})
|
23
|
-
|
24
|
+
def find_first!(options = {})
|
25
|
+
entity_class(base_query(options).first!.attributes)
|
24
26
|
end
|
25
27
|
|
26
28
|
def find_all(options = {})
|
27
|
-
|
29
|
+
entity_class(base_query(options))
|
28
30
|
end
|
29
31
|
|
30
32
|
def ancestors(options = {})
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
33
|
+
result = walk_family_tree(@context.find(options[:id]), options)
|
34
|
+
|
35
|
+
return nil unless result
|
36
|
+
|
37
|
+
collection?(result) ? entity_class(result) : entity_class(result.attributes)
|
35
38
|
end
|
36
39
|
|
37
40
|
def to_hash
|
41
|
+
raise ::Horza::Errors::CannotGetHashFromCollection.new if collection?
|
42
|
+
raise ::Horza::Errors::QueryNotYetPerformed.new unless @context.respond_to?(:attributes)
|
38
43
|
@context.attributes
|
39
44
|
end
|
40
45
|
|
41
46
|
private
|
42
47
|
|
43
|
-
def
|
44
|
-
|
48
|
+
def base_query(options)
|
49
|
+
@context.where(options).order('ID DESC')
|
50
|
+
end
|
51
|
+
|
52
|
+
def collection?(subject = @context)
|
53
|
+
subject.is_a?(::ActiveRecord::Relation) || subject.is_a?(Array)
|
54
|
+
end
|
55
|
+
|
56
|
+
def walk_family_tree(object, options)
|
57
|
+
via = options[:via] || []
|
58
|
+
via.push(options[:target]).reduce(object) do |object, relation|
|
59
|
+
raise ::Horza::Errors::InvalidAncestry.new(INVALID_ANCESTRY_MSG) unless object.respond_to? relation
|
60
|
+
object.send(relation)
|
61
|
+
end
|
45
62
|
end
|
46
63
|
end
|
47
64
|
end
|
data/lib/horza/configuration.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
module Horza
|
2
2
|
module Configuration
|
3
3
|
String.send(:include, ::Horza::CoreExtensions::String)
|
4
|
-
|
4
|
+
|
5
5
|
def configuration
|
6
6
|
@configuration ||= Config.new
|
7
7
|
end
|
@@ -21,7 +21,16 @@ module Horza
|
|
21
21
|
end
|
22
22
|
|
23
23
|
def adapter_map
|
24
|
-
@adapter_map ||=
|
24
|
+
@adapter_map ||= generate_map
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def generate_map
|
30
|
+
::Horza::Adapters::AbstractAdapter.descendants.reduce({}) do |hash, (klass)|
|
31
|
+
return hash unless klass.name
|
32
|
+
hash.merge(klass.name.split('::').last.underscore.to_sym => klass)
|
33
|
+
end
|
25
34
|
end
|
26
35
|
end
|
27
36
|
|
data/lib/horza/entities.rb
CHANGED
@@ -1,9 +1,20 @@
|
|
1
1
|
module Horza
|
2
2
|
module Entities
|
3
3
|
class << self
|
4
|
-
def
|
5
|
-
|
6
|
-
|
4
|
+
def single_entity_for(entity_symbol)
|
5
|
+
single_entities[entity_symbol] || ::Horza::Entities::Single
|
6
|
+
end
|
7
|
+
|
8
|
+
def single_entities
|
9
|
+
@singles ||= ::Horza.descendants_map(::Horza::Entities::Single)
|
10
|
+
end
|
11
|
+
|
12
|
+
def collection_entity_for(entity_symbol)
|
13
|
+
collection_entities[entity_symbol] || ::Horza::Entities::Collection
|
14
|
+
end
|
15
|
+
|
16
|
+
def collection_entities
|
17
|
+
@singles ||= ::Horza.descendants_map(::Horza::Entities::Collection)
|
7
18
|
end
|
8
19
|
end
|
9
20
|
end
|
@@ -26,15 +26,14 @@ module Horza
|
|
26
26
|
end
|
27
27
|
|
28
28
|
def singular_entity(record)
|
29
|
-
|
29
|
+
adapter = Horza.adapter.new(record)
|
30
|
+
singular_entity_class(record).new(adapter.to_hash)
|
30
31
|
end
|
31
32
|
|
32
|
-
# Collection classes have the form Horza::Entities::
|
33
|
-
# Single output requires the form Horza::Entities::
|
34
|
-
def singular_entity_class
|
35
|
-
@singular_entity ||=
|
36
|
-
rescue NameError
|
37
|
-
@singular_entity = ::Horza::Entities::Single
|
33
|
+
# Collection classes have the form Horza::Entities::Users
|
34
|
+
# Single output requires the form Horza::Entities::User
|
35
|
+
def singular_entity_class(record)
|
36
|
+
@singular_entity ||= ::Horza::Entities::single_entity_for(record.class.name.split('::').last.symbolize)
|
38
37
|
end
|
39
38
|
end
|
40
39
|
end
|
data/lib/horza/errors.rb
CHANGED
@@ -0,0 +1,227 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
if !defined?(ActiveRecord::Base)
|
4
|
+
puts "** require 'active_record' to run the specs in #{__FILE__}"
|
5
|
+
else
|
6
|
+
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
|
7
|
+
|
8
|
+
ActiveRecord::Migration.suppress_messages do
|
9
|
+
ActiveRecord::Schema.define(:version => 0) do
|
10
|
+
create_table(:employers, force: true) {|t| t.string :name }
|
11
|
+
create_table(:users, force: true) {|t| t.string :first_name; t.string :last_name; t.references :employer; }
|
12
|
+
create_table(:sports_cars, force: true) {|t| t.string :make; t.references :employer; }
|
13
|
+
create_table(:dummy_models, force: true) {|t| t.string :key }
|
14
|
+
create_table(:other_dummy_models, force: true) {|t| t.string :key }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module HorzaSpec
|
19
|
+
class Employer < ActiveRecord::Base
|
20
|
+
has_many :users
|
21
|
+
has_many :sports_cars
|
22
|
+
end
|
23
|
+
|
24
|
+
class User < ActiveRecord::Base
|
25
|
+
belongs_to :employer
|
26
|
+
end
|
27
|
+
|
28
|
+
class SportsCar < ActiveRecord::Base
|
29
|
+
belongs_to :employer
|
30
|
+
end
|
31
|
+
|
32
|
+
class DummyModel < ActiveRecord::Base
|
33
|
+
belongs_to :employer
|
34
|
+
end
|
35
|
+
|
36
|
+
class OtherDummyModel < ActiveRecord::Base
|
37
|
+
belongs_to :employer
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe Horza do
|
43
|
+
let(:last_name) { 'Turner' }
|
44
|
+
let(:adapter) { :active_record }
|
45
|
+
let(:user_adapter) { Horza.adapter.new(HorzaSpec::User) }
|
46
|
+
let(:employer_adapter) { Horza.adapter.new(HorzaSpec::Employer) }
|
47
|
+
let(:sports_car_adapter) { Horza.adapter.new(HorzaSpec::SportsCar) }
|
48
|
+
|
49
|
+
# Reset base config with each iteration
|
50
|
+
before { Horza.configure { |config| config.adapter = adapter } }
|
51
|
+
after do
|
52
|
+
HorzaSpec::User.delete_all
|
53
|
+
HorzaSpec::Employer.delete_all
|
54
|
+
end
|
55
|
+
|
56
|
+
describe '#adapter' do
|
57
|
+
let(:user) { HorzaSpec::User.create }
|
58
|
+
|
59
|
+
describe '#get!' do
|
60
|
+
context 'when user exists' do
|
61
|
+
it 'returns Single entity' do
|
62
|
+
expect(user_adapter.get!(user.id).is_a? Horza::Entities::Single).to be true
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
context 'when user exists' do
|
67
|
+
it 'returns user' do
|
68
|
+
expect(user_adapter.get!(user.id).to_h).to eq user.attributes
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
context 'when user does not exist' do
|
73
|
+
it 'throws error' do
|
74
|
+
expect { user_adapter.get!(999) }.to raise_error ActiveRecord::RecordNotFound
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
describe '#get' do
|
80
|
+
context 'when user does not exist' do
|
81
|
+
it 'returns nil' do
|
82
|
+
expect(user_adapter.get(999)).to be nil
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
describe '#find_first!' do
|
88
|
+
context 'when users exist' do
|
89
|
+
before do
|
90
|
+
3.times { HorzaSpec::User.create(last_name: last_name) }
|
91
|
+
2.times { HorzaSpec::User.create(last_name: 'OTHER') }
|
92
|
+
end
|
93
|
+
it 'returns single Entity' do
|
94
|
+
expect(user_adapter.find_first(last_name: last_name).is_a? Horza::Entities::Single).to be true
|
95
|
+
end
|
96
|
+
|
97
|
+
it 'returns user' do
|
98
|
+
expect(user_adapter.find_first!(last_name: last_name).to_h).to eq HorzaSpec::User.where(last_name: last_name).order('id DESC').first.attributes
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
context 'when user does not exist' do
|
103
|
+
it 'throws error' do
|
104
|
+
expect { user_adapter.find_first!(last_name: last_name) }.to raise_error ActiveRecord::RecordNotFound
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
describe '#find_first' do
|
110
|
+
context 'when user does not exist' do
|
111
|
+
it 'returns nil' do
|
112
|
+
expect(user_adapter.find_first(last_name: last_name)).to be nil
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
describe '#find_all' do
|
118
|
+
context 'when users exist' do
|
119
|
+
before do
|
120
|
+
3.times { HorzaSpec::User.create(last_name: last_name) }
|
121
|
+
2.times { HorzaSpec::User.create(last_name: 'OTHER') }
|
122
|
+
end
|
123
|
+
it 'returns user' do
|
124
|
+
expect(user_adapter.find_all(last_name: last_name).length).to eq 3
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
context 'when user does not exist' do
|
129
|
+
it 'throws error' do
|
130
|
+
expect(user_adapter.find_all(last_name: last_name).empty?).to be true
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
context '#ancestors' do
|
136
|
+
context 'direct relation' do
|
137
|
+
let(:employer) { HorzaSpec::Employer.create }
|
138
|
+
let!(:user1) { HorzaSpec::User.create(employer: employer) }
|
139
|
+
let!(:user2) { HorzaSpec::User.create(employer: employer) }
|
140
|
+
|
141
|
+
context 'parent' do
|
142
|
+
it 'returns parent' do
|
143
|
+
expect(user_adapter.ancestors(id: user1.id, target: :employer).to_h).to eq employer.attributes
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
context 'children' do
|
148
|
+
it 'returns children' do
|
149
|
+
result = employer_adapter.ancestors(id: employer.id, target: :users)
|
150
|
+
expect(result.length).to eq 2
|
151
|
+
expect(result.first.is_a? Horza::Entities::Single).to be true
|
152
|
+
expect(result.first.to_hash).to eq HorzaSpec::User.order('id DESC').last.attributes
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
context 'invalid ancestry' do
|
157
|
+
it 'throws error' do
|
158
|
+
expect { employer_adapter.ancestors(id: employer.id, target: :user) }.to raise_error Horza::Errors::InvalidAncestry
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
context 'valid ancestry with no saved childred' do
|
163
|
+
let(:employer2) { HorzaSpec::Employer.create }
|
164
|
+
it 'returns empty collection error' do
|
165
|
+
expect(employer_adapter.ancestors(id: employer2.id, target: :users).empty?).to be true
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
context 'valid ancestry with no saved parent' do
|
170
|
+
let(:user2) { HorzaSpec::User.create }
|
171
|
+
it 'returns nil' do
|
172
|
+
expect(user_adapter.ancestors(id: user2.id, target: :employer)).to be nil
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
context 'using via' do
|
178
|
+
let(:employer) { HorzaSpec::Employer.create }
|
179
|
+
let(:user) { HorzaSpec::User.create(employer: employer) }
|
180
|
+
let(:sportscar) { HorzaSpec::SportsCar.create(employer: employer) }
|
181
|
+
|
182
|
+
before do
|
183
|
+
employer.sports_cars << sportscar
|
184
|
+
end
|
185
|
+
|
186
|
+
it 'returns the correct ancestor' do
|
187
|
+
expect(user_adapter.ancestors(id: user.id, target: :sports_cars, via: [:employer]).first).to eq sportscar.attributes
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
describe 'Entities' do
|
194
|
+
describe 'Collection' do
|
195
|
+
context '#singular_entity_class' do
|
196
|
+
context 'when singular entity class does not exist' do
|
197
|
+
let(:dummy_model) { HorzaSpec::DummyModel.create }
|
198
|
+
|
199
|
+
module TestNamespace
|
200
|
+
class DummyModels < Horza::Entities::Collection
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
it 'returns Horza::Collection::Single' do
|
205
|
+
expect(TestNamespace::DummyModels.new([]).send(:singular_entity_class, dummy_model)).to eq Horza::Entities::Single
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
context 'when singular entity class exists' do
|
210
|
+
let(:other_dummy_model) { HorzaSpec::OtherDummyModel.create }
|
211
|
+
|
212
|
+
module TestNamespace
|
213
|
+
class OtherDummyModels < Horza::Entities::Collection
|
214
|
+
end
|
215
|
+
|
216
|
+
class OtherDummyModel < Horza::Entities::Single
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
it 'returns the existing singular class' do
|
221
|
+
expect(TestNamespace::OtherDummyModels.new([]).send(:singular_entity_class, other_dummy_model)).to eq TestNamespace::OtherDummyModel
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
data/spec/horza_spec.rb
CHANGED
@@ -3,6 +3,8 @@ require 'spec_helper'
|
|
3
3
|
describe Horza do
|
4
4
|
context '#adapter' do
|
5
5
|
context 'when adapter is not configured' do
|
6
|
+
before { Horza.reset }
|
7
|
+
after { Horza.reset }
|
6
8
|
it 'throws error' do
|
7
9
|
expect { Horza.adapter }.to raise_error(Horza::Errors::AdapterNotConfigured)
|
8
10
|
end
|
@@ -16,41 +18,4 @@ describe Horza do
|
|
16
18
|
end
|
17
19
|
end
|
18
20
|
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
21
|
end
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: horza
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Blake Turner
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-05-
|
11
|
+
date: 2015-05-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: hashie
|
@@ -66,6 +66,34 @@ dependencies:
|
|
66
66
|
- - ">="
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '4.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: activerecord
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 3.2.15
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 3.2.15
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: sqlite3
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
69
97
|
description: Horza is a shapeshifter that provides common inputs and outputs for your
|
70
98
|
ORM
|
71
99
|
email: mail@blakewilliamturner.com
|
@@ -84,6 +112,7 @@ files:
|
|
84
112
|
- lib/horza/entities/collection.rb
|
85
113
|
- lib/horza/entities/single.rb
|
86
114
|
- lib/horza/errors.rb
|
115
|
+
- spec/active_record_spec.rb
|
87
116
|
- spec/horza_spec.rb
|
88
117
|
- spec/spec_helper.rb
|
89
118
|
homepage: https://github.com/onfido/horza
|
@@ -111,6 +140,7 @@ signing_key:
|
|
111
140
|
specification_version: 4
|
112
141
|
summary: Keep your app ORM-agnostic
|
113
142
|
test_files:
|
143
|
+
- spec/active_record_spec.rb
|
114
144
|
- spec/horza_spec.rb
|
115
145
|
- spec/spec_helper.rb
|
116
146
|
has_rdoc:
|