horza 0.0.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3b297b82ecba5dd4f72a6974c8018d2895f80dbc
4
- data.tar.gz: c2403238d237160290cd7c68a37cda4f106b78b9
3
+ metadata.gz: aa0c514a09b3c9cc5c381187de5123e83d6bbc20
4
+ data.tar.gz: d6fb63bc973ed1cca3b703d8eceb5390163a5a0e
5
5
  SHA512:
6
- metadata.gz: 5b6cabd74b00d0d5954bd030d2854f1ed30feabb540dbdb4f53b46b79485f5b02cddc8f266b9230884de78b37fd241af2c59370ea278b7aa6d6db4714b6bffdb
7
- data.tar.gz: 2a0fbb2d13b0e524642c66ddf7a37a0fb00b94f67c13d0f54621a159e779b4b235feabd910cb7b4c9082f661b3d2ebc54d11f61f4676efafa52b7985252d0d3b
6
+ metadata.gz: d59d0f2c0ab53696247e2144f34541a3a60074731e1431cea21ea73d3da6beab250a2cd0a57f55a52f75d695e0d49ccc3d22c8f570d0c9d86787e87073203371
7
+ data.tar.gz: 8970bb56079f409f998e36a5d97192d5cec75f04a0ee05ac87611def55a51458b4c92b1c0901e64c25a746249f8b16dfb72777150e145e6f03db49676428349b
data/README.md CHANGED
@@ -1,333 +1,73 @@
1
- # Get
1
+ # Horza
2
2
 
3
- Dynamically generate classes to encapsulate common database queries in Rails.
3
+ Horza is a library for decoupling your application from the ORM you have implemented.
4
4
 
5
- ## Why is this necessary?
5
+ ## Inputs
6
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:
7
+ Horza uses ORM-specific adapters to decouple Ruby apps from their ORMS.
19
8
 
9
+ **Configure Adapter**
20
10
  ```ruby
21
- def find_two_blakes
22
- User.where(name: 'blake').order('updated_at DESC').limit(2)
11
+ Horza.configure do |config|
12
+ config.adapter = :active_record
23
13
  end
24
14
  ```
25
15
 
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
16
+ **Get Adapter for your ORM Object**
100
17
  ```ruby
101
- Get::UserById.run!(123)
102
- ```
18
+ # ActiveRecord Example
19
+ user = User.create(user_params)
20
+ horza_user = Horza.adapter.new(user)
103
21
 
104
- Slightly more flexible model:
105
- ```ruby
106
- Get::UserBy.run(id: 123, employer_id: 88)
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
- #### Plural Queries - Return a collection of records
31
+ ## Outputs
110
32
 
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.
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
- Get::EmployerFromUser.run(user)
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
- 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.
42
+ result['id'] # => 1
43
+ result.id # => 1
44
+ result.id? # => true
144
45
 
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.
46
+ # Collection Entity
47
+ result = horza_user.find_all(last_name: 'Turner')
48
+ result.class.name # => "Horza::Entities::Collection"
148
49
 
149
- ```ruby
150
- >> result = Get::UserById.run(user.id)
151
- >> result.class.name
152
- >> "Get::Entities::GetUser"
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
- 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
- ```
59
+ ## Custom Entities
180
60
 
181
- The above methods do the exact same thing. Cool, let's test them:
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
- # 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
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
- _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)
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
- ```
@@ -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.descendants.reduce({}) { |hash, (klass)| hash.merge(klass.name.split('::').last.underscore.to_sym => klass) }
16
+ @map ||= ::Horza.descendants_map(::ActiveRecord::Base)
15
17
  end
16
18
  end
17
19
 
18
- def get!(options = {})
19
- @context = @context.find(options[:id])
20
+ def get!(id)
21
+ entity_class(@context.find(id).attributes)
20
22
  end
21
23
 
22
- def find_first(options = {})
23
- @context = find_all(options).limit(1).first!
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
- @context = @context.where(options[:conditions]).order('ID DESC')
29
+ entity_class(base_query(options))
28
30
  end
29
31
 
30
32
  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.')
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 walk_family_tree(options)
44
- options[:via].push(options[:result_key]).each { |relation| @context = @context.send(relation) }
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
@@ -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 ||= ::Horza::Adapters::AbstractAdapter.descendants.reduce({}) { |hash, (klass)| hash.merge(klass.name.split('::').last.underscore.to_sym => klass) }
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
 
@@ -1,9 +1,20 @@
1
1
  module Horza
2
2
  module Entities
3
3
  class << self
4
- def const_missing(name)
5
- parent_klass = name.to_s.plural? ? Horza::Entities::Collection : Horza::Entities::Single
6
- Horza::Entities.const_set(name, Class.new(parent_klass))
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
- singular_entity_class.new(Horza.adapter.new(record).to_hash)
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::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
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
@@ -8,5 +8,11 @@ module Horza
8
8
 
9
9
  class InvalidAncestry < StandardError
10
10
  end
11
+
12
+ class QueryNotYetPerformed < StandardError
13
+ end
14
+
15
+ class CannotGetHashFromCollection < StandardError
16
+ end
11
17
  end
12
18
  end
@@ -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
@@ -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
@@ -1,6 +1,7 @@
1
1
  require 'rubygems'
2
2
  require 'byebug'
3
3
  require 'rspec'
4
+ require 'active_record'
4
5
  require 'bundler/setup'
5
6
  Bundler.setup
6
7
 
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.0.1
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-22 00:00:00.000000000 Z
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: