hoodoo 2.0.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,54 @@
1
+ ########################################################################
2
+ # File:: active_record_manually_dated_finder_additions.rb
3
+ # (C):: Loyalty New Zealand 2017
4
+ #
5
+ # Purpose:: Extend
6
+ # Hoodoo::ActiveRecord::Finder::ClassMethods#acquire_in! so
7
+ # that it adds error +generic.contemporary_exists+ to the
8
+ # provided +context+ if a dated instance is absent.
9
+ # ----------------------------------------------------------------------
10
+ # 01-Nov-2017 (ADH): Created.
11
+ ########################################################################
12
+
13
+ module Hoodoo
14
+ module Monkey
15
+ module Patch
16
+
17
+ # Extend Hoodoo::ActiveRecord::Finder::ClassMethods#acquire_in! so
18
+ # that it adds error +generic.contemporary_exists+ to the provided
19
+ # +context+ if a dated instance is absent.
20
+ #
21
+ module ActiveRecordManuallyDatedFinderAdditions
22
+
23
+ # Class methods to patch over an ActiveRecord::Base subclass
24
+ # which includes Hoodoo::ActiveRecord::Finder and
25
+ # Hoodoo::ActiveRecord::ManuallyDated.
26
+ #
27
+ module ClassExtensions
28
+
29
+ # See Hoodoo::ActiveRecord::Finder::ClassMethods#acquire_in! for
30
+ # details. Calls that method then, upon error, checks to see if a
31
+ # contemporary version of the resource exists and adds error
32
+ # +generic.contemporary_exists+ to the given +context+ if so.
33
+ #
34
+ def acquire_in!( context )
35
+ result = super( context )
36
+
37
+ if result.nil?
38
+ ident = context.request.ident
39
+ contemporary_result = scoped_undated_in( context ).
40
+ manually_dated_contemporary().
41
+ acquire( ident )
42
+
43
+ context.response.contemporary_exists( ident ) if contemporary_result.present?
44
+ end
45
+
46
+ return result
47
+ end
48
+
49
+ end
50
+ end
51
+
52
+ end # module Patch
53
+ end # module Monkey
54
+ end # module Hoodoo
@@ -8,7 +8,7 @@
8
8
  #
9
9
  # See Hoodoo::Monkey::Patch::DatadogTracedAMQP for more.
10
10
  # ----------------------------------------------------------------------
11
- # 22-June-2017 (JRW): Created.
11
+ # 22-Jun-2017 (JRW): Created.
12
12
  ########################################################################
13
13
 
14
14
  module Hoodoo
@@ -299,20 +299,81 @@ module Hoodoo; module Services
299
299
  return @errors.merge!( errors_object )
300
300
  end
301
301
 
302
- # Set the standard not found error message (generic.not_found), to
303
- # be used durning a 'show' call when the requested resource does not
304
- # exist.
302
+ # Add the standard error message '+generic.not_found+' to this response.
303
+ # Used during a 'show' call when the requested resource does not exist.
305
304
  #
306
- # +ident+:: The identifier of the resource which was not found
305
+ # +ident+:: The identifier of the resource which was not found
307
306
  #
308
- # Example:
307
+ # Low level example:
309
308
  #
310
309
  # return response.not_found( ident ) if resource.nil?
311
310
  #
311
+ # High level example for resource implementations with a +context+
312
+ # available, through which this method is called automatically:
313
+ #
314
+ # resource = SomeModel.acquire_in!( context )
315
+ # return if context.response.halt_processing?
316
+ #
317
+ # See also:
318
+ #
319
+ # * #contemporary_exists
320
+ # * Hoodoo::ActiveRecord::Finder::ClassMethods#acquire_in
321
+ #
312
322
  def not_found( ident )
313
323
  @errors.add_error( 'generic.not_found', :reference => { :ident => ident } )
314
324
  end
315
325
 
326
+ # Add the standard error message '+generic.contemporary_exists+' to this
327
+ # response. Optionally used during a 'show' call for a historical
328
+ # dating-aware resource, when an instance does not exist at a requested
329
+ # historic date/time, but a contemporary version is present.
330
+ #
331
+ # +ident+:: The identifier of the resource which was not found
332
+ #
333
+ # Example for resource implementations with a +context+ available:
334
+ #
335
+ # resource = SomeModel.acquire_in( context )
336
+ #
337
+ # if resource.nil?
338
+ # ident = context.request.ident
339
+ # context.response.not_found( ident )
340
+ #
341
+ # # You'd omit "#manually_dated_contemporary" if using automatic
342
+ # # dating (but manual dating is recommended over automatic for
343
+ # # performance reasons).
344
+ # #
345
+ # contemporary_resource = SomeModel.
346
+ # scoped_undated_in( context ).
347
+ # manually_dated_contemporary().
348
+ # acquire( ident )
349
+ #
350
+ # # Use of ActiveRecord means some ActiveSupport extensions
351
+ # # such as "#present?" will be available.
352
+ # #
353
+ # context.response.contemporary_exists( ident ) if contemporary_resource.present?
354
+ # end
355
+ #
356
+ # An even higher level approach through +context+, through which this
357
+ # method is called automatically:
358
+ #
359
+ # resource = SomeModel.acquire_in!( context )
360
+ # return if context.response.halt_processing?
361
+ #
362
+ # This frees application authors of the burden of constructing an
363
+ # appropriately secure and "now-dated" scope for the contemporary resource
364
+ # lookup.
365
+ #
366
+ # See also:
367
+ #
368
+ # * #not_found
369
+ # * Hoodoo::ActiveRecord::Finder::ClassMethods#acquire_in
370
+ # * Hoodoo::ActiveRecord::Finder::ClassMethods#scoped_undated_in
371
+ # * Hoodoo::ActiveRecord::ManuallyDated::ClassMethods#manually_dated_contemporary
372
+ #
373
+ def contemporary_exists( ident )
374
+ @errors.add_error( 'generic.contemporary_exists', :reference => { :ident => ident } )
375
+ end
376
+
316
377
  # Convert the internal response data into something that Rack expects.
317
378
  # The return value of this method can be passed back to Rack from Rack
318
379
  # middleware or applications. Usually, this is only called directly by
@@ -12,11 +12,11 @@ module Hoodoo
12
12
  # The Hoodoo gem version. If this changes, be sure to re-run
13
13
  # <tt>bundle install</tt> or <tt>bundle update</tt>.
14
14
  #
15
- VERSION = '2.0.0'
15
+ VERSION = '2.1.1'
16
16
 
17
17
  # The Hoodoo gem date. If this changes, be sure to re-run
18
18
  # <tt>bundle install</tt> or <tt>bundle update</tt>.
19
19
  #
20
- DATE = '2017-10-13'
20
+ DATE = '2017-11-03'
21
21
 
22
22
  end
@@ -6,11 +6,10 @@ describe Hoodoo::ActiveRecord::Base do
6
6
  spec_helper_silence_stdout() do
7
7
  tblname = :r_spec_model_base_tests
8
8
 
9
- ActiveRecord::Migration.create_table( tblname, :id => false ) do | t |
10
- t.string( :id, :limit => 32, :null => false )
9
+ ActiveRecord::Migration.create_table( tblname, :id => :string ) do | t |
11
10
  end
12
11
 
13
- ActiveRecord::Migration.add_index( tblname, :id, :unique => true )
12
+ ActiveRecord::Migration.change_column( tblname, :id, :string, :limit => 32 )
14
13
 
15
14
  class RSpecModelBaseTest < Hoodoo::ActiveRecord::Base
16
15
  end
@@ -5,44 +5,47 @@ describe Hoodoo::ActiveRecord::Dated do
5
5
 
6
6
  before :all do
7
7
  spec_helper_silence_stdout() do
8
-
9
- ActiveRecord::Migration.create_table( :r_spec_model_effective_date_tests, :id => false ) do | t |
10
- t.text :id, :null => false
8
+ ActiveRecord::Migration.create_table( :r_spec_model_effective_date_tests, :id => :string ) do | t |
11
9
  t.text :data
12
10
  t.timestamps :null => true
13
11
  end
14
12
 
15
- ActiveRecord::Migration.create_table( :r_spec_model_effective_date_tests_history_entries, :id => false ) do | t |
16
- t.text :id, :null => false
13
+ ActiveRecord::Migration.change_column( :r_spec_model_effective_date_tests, :id, :string, :limit => 32 )
14
+
15
+ ActiveRecord::Migration.create_table( :r_spec_model_effective_date_tests_history_entries, :id => :string ) do | t |
17
16
  t.text :uuid, :null => false
18
17
  t.text :data
19
18
  t.datetime :effective_start, :null => false
20
19
  t.datetime :effective_end, :null => false
20
+
21
21
  t.timestamps :null => true
22
22
  end
23
23
 
24
24
  class RSpecModelEffectiveDateTest < ActiveRecord::Base
25
25
  include Hoodoo::ActiveRecord::Dated
26
+
26
27
  dating_enabled()
27
28
  end
28
29
 
29
- ActiveRecord::Migration.create_table( :r_spec_model_effective_date_test_overrides, :id => false ) do | t |
30
- t.text :id, :null => false
30
+ ActiveRecord::Migration.create_table( :r_spec_model_effective_date_test_overrides, :id => :string ) do | t |
31
31
  t.text :data
32
32
  t.timestamps :null => true
33
33
  end
34
34
 
35
- ActiveRecord::Migration.create_table( :r_spec_model_effective_date_history_entries, :id => false ) do | t |
36
- t.text :id, :null => false
35
+ ActiveRecord::Migration.change_column( :r_spec_model_effective_date_test_overrides, :id, :string, :limit => 32 )
36
+
37
+ ActiveRecord::Migration.create_table( :r_spec_model_effective_date_history_entries, :id => :string ) do | t |
37
38
  t.text :uuid, :null => false
38
39
  t.text :data
39
40
  t.datetime :effective_start, :null => false
40
41
  t.datetime :effective_end, :null => false
42
+
41
43
  t.timestamps :null => true
42
44
  end
43
45
 
44
46
  class RSpecModelEffectiveDateTestOverride < ActiveRecord::Base
45
47
  include Hoodoo::ActiveRecord::Dated
48
+ include Hoodoo::ActiveRecord::Finder
46
49
 
47
50
  dating_enabled( :history_table_name => :r_spec_model_effective_date_history_entries )
48
51
  end
@@ -51,7 +54,16 @@ describe Hoodoo::ActiveRecord::Dated do
51
54
 
52
55
  shared_examples Hoodoo::ActiveRecord::Dated do
53
56
 
54
- before( :all ) do
57
+ before( :each ) do
58
+ interaction = Hoodoo::Services::Middleware::Interaction.new( {}, nil )
59
+ interaction.context = Hoodoo::Services::Context.new(
60
+ Hoodoo::Services::Session.new,
61
+ interaction.context.request,
62
+ interaction.context.response,
63
+ interaction
64
+ )
65
+
66
+ @context = interaction.context
55
67
 
56
68
  # Create some example data for finding. The data has two different UUIDs
57
69
  # which I'll referer to as A and B. The following tables contain the
@@ -169,26 +181,16 @@ describe Hoodoo::ActiveRecord::Dated do
169
181
 
170
182
  context '#dated' do
171
183
  it 'returns counts correctly' do
172
- # The contents of the Context are irrelevant aside from the fact that it
173
- # needs a request to store the dated_at value.
174
- request = Hoodoo::Services::Request.new
175
- context = Hoodoo::Services::Context.new( nil, request, nil, nil )
184
+ @context.request.dated_at = @now - 10.hours
185
+ expect( model_klass.dated( @context ).count ).to be 0
176
186
 
177
- context.request.dated_at = @now - 10.hours
178
- expect( model_klass.dated( context ).count ).to be 0
179
-
180
- context.request.dated_at = @now
181
- expect( model_klass.dated( context ).count ).to be 2
187
+ @context.request.dated_at = @now
188
+ expect( model_klass.dated( @context ).count ).to be 2
182
189
  end
183
190
 
184
191
  def test_expectation( time, expected_data )
185
- # The contents of the Context are irrelevant aside from the fact that it
186
- # needs a request to store the dated_at value.
187
- request = Hoodoo::Services::Request.new
188
- context = Hoodoo::Services::Context.new( nil, request, nil, nil )
189
- context.request.dated_at = time
190
-
191
- expect( model_klass.dated( context ).pluck( :data ) ).to match_array( expected_data )
192
+ @context.request.dated_at = time
193
+ expect( model_klass.dated( @context ).pluck( :data ) ).to match_array( expected_data )
192
194
  end
193
195
 
194
196
  it 'returns no records before any were effective' do
@@ -208,27 +210,14 @@ describe Hoodoo::ActiveRecord::Dated do
208
210
  end
209
211
 
210
212
  it 'works with further filtering' do
211
-
212
- # The contents of the Context are irrelevant aside from the fact that it
213
- # needs a request to store the dated_at value.
214
- request = Hoodoo::Services::Request.new
215
- context = Hoodoo::Services::Context.new( nil, request, nil, nil )
216
- context.request.dated_at = @now
217
-
218
- expect( model_klass.dated( context ).where( :id => @uuid_a ).pluck( :data ) ).to eq( [ "six" ] )
213
+ @context.request.dated_at = @now
214
+ expect( model_klass.dated( @context ).where( :id => @uuid_a ).pluck( :data ) ).to eq( [ "six" ] )
219
215
  end
220
216
 
221
217
  it 'works with dating last' do
222
-
223
- # The contents of the Context are irrelevant aside from the fact that it
224
- # needs a request to store the dated_at value.
225
- request = Hoodoo::Services::Request.new
226
- context = Hoodoo::Services::Context.new( nil, request, nil, nil )
227
- context.request.dated_at = @now
228
-
229
- expect( model_klass.where( :id => @uuid_a ).dated( context ).pluck( :data ) ).to eq( [ "six" ] )
218
+ @context.request.dated_at = @now
219
+ expect( model_klass.where( :id => @uuid_a ).dated( @context ).pluck( :data ) ).to eq( [ "six" ] )
230
220
  end
231
-
232
221
  end
233
222
 
234
223
  context '#dated_historical_and_current' do
@@ -268,7 +257,171 @@ describe Hoodoo::ActiveRecord::Dated do
268
257
  end
269
258
  end
270
259
 
271
- end
260
+ # These appear in the shared examples to take advantage of the test
261
+ # data that's set up, but only work when "model_klass" is the
262
+ # Finder-extended "RSpecModelEffectiveDateTestOverride".
263
+ #
264
+ # Hence "next unless..." in each example (using "skip" in a "before"
265
+ # filter would result in RSpect listing pending tests; they are not
266
+ # pending at all).
267
+
268
+ context 'with Hoodoo::ActiveRecord::Finder support' do
269
+ context '#scoped_in' do
270
+ it 'generates appropriate scope' do
271
+ next unless model_klass == RSpecModelEffectiveDateTestOverride
272
+
273
+ expect( Hoodoo::ActiveRecord::Support ).to(
274
+ receive( :full_scope_for ).once().with(
275
+ RSpecModelEffectiveDateTestOverride, @context
276
+ ).and_call_original()
277
+ )
278
+
279
+ sql = RSpecModelEffectiveDateTestOverride.scoped_in( @context ).to_sql
280
+
281
+ expect( sql ).to include( "UNION ALL" )
282
+ expect( sql ).to include( "' AND (\"effective_end\" > '" )
283
+ end
284
+
285
+ it 'generates appropriate undated scope' do
286
+ next unless model_klass == RSpecModelEffectiveDateTestOverride
287
+
288
+ expect( Hoodoo::ActiveRecord::Support ).to(
289
+ receive( :add_undated_scope_to ).once().with(
290
+ kind_of( ActiveRecord::Relation ), RSpecModelEffectiveDateTestOverride, @context
291
+ ).and_call_original()
292
+ )
293
+
294
+ sql = RSpecModelEffectiveDateTestOverride.scoped_undated_in( @context ).to_sql
295
+
296
+ expect( sql ).to eq( "SELECT \"r_spec_model_effective_date_test_overrides\".* FROM \"r_spec_model_effective_date_test_overrides\"" )
297
+ end
298
+ end
299
+
300
+ context '#acquire_in' do
301
+ context 'with contemporary' do
302
+ it 'finds the contemporary record' do
303
+ next unless model_klass == RSpecModelEffectiveDateTestOverride
304
+
305
+ @context.request.dated_at = nil
306
+ @context.request.uri_path_components = [ @uuid_a ]
307
+
308
+ result = RSpecModelEffectiveDateTestOverride.acquire_in!( @context )
309
+ expect( result ).to_not be_nil
310
+
311
+ expect( @context.response.halt_processing? ).to eq( false )
312
+ expect( result.data ).to eq( 'six' )
313
+ end
314
+
315
+ it 'finds a historic record' do
316
+ next unless model_klass == RSpecModelEffectiveDateTestOverride
317
+
318
+ @context.request.dated_at = @now - 4.hours
319
+ @context.request.uri_path_components = [ @uuid_a ]
320
+
321
+ result = RSpecModelEffectiveDateTestOverride.acquire_in!( @context )
322
+ expect( result ).to_not be_nil
323
+
324
+ expect( @context.response.halt_processing? ).to eq( false )
325
+ expect( result.data ).to eq( 'one' )
326
+ end
327
+
328
+ it 'indicates correctly that a contemporary exists during a far-backdated lookup' do
329
+ next unless model_klass == RSpecModelEffectiveDateTestOverride
330
+
331
+ @context.request.dated_at = @now - 1.year
332
+ @context.request.uri_path_components = [ @uuid_a ]
333
+
334
+ result = RSpecModelEffectiveDateTestOverride.acquire_in!( @context )
335
+ expect( result ).to be_nil
336
+
337
+ expect( @context.response.halt_processing? ).to eq( true )
338
+ expect( @context.response.errors.errors.count ).to eq( 2 )
339
+
340
+ expect( @context.response.errors.errors[ 0 ][ 'code' ] ).to eq( 'generic.not_found' )
341
+ expect( @context.response.errors.errors[ 1 ][ 'code' ] ).to eq( 'generic.contemporary_exists' )
342
+ expect( @context.response.errors.errors[ 0 ][ 'reference' ] ).to eq( @uuid_a )
343
+ expect( @context.response.errors.errors[ 1 ][ 'reference' ] ).to eq( @uuid_a )
344
+ end
345
+ end
346
+
347
+ context 'with a UUID that matches no existing record' do
348
+ it 'indicates correctly that no record exists and does not say a contemporary is present' do
349
+ next unless model_klass == RSpecModelEffectiveDateTestOverride
350
+
351
+ alt_uuid = Hoodoo::UUID.generate
352
+ @context.request.dated_at = @now - 5.seconds
353
+ @context.request.uri_path_components = [ alt_uuid ]
354
+
355
+ result = RSpecModelEffectiveDateTestOverride.acquire_in!( @context )
356
+ expect( result ).to be_nil
357
+
358
+ expect( @context.response.halt_processing? ).to eq( true )
359
+ expect( @context.response.errors.errors.count ).to eq( 1 )
360
+
361
+ expect( @context.response.errors.errors[ 0 ][ 'code' ] ).to eq( 'generic.not_found' )
362
+ expect( @context.response.errors.errors[ 0 ][ 'reference' ] ).to eq( alt_uuid )
363
+ end
364
+ end
365
+
366
+ context 'without contemporary' do
367
+ before( :each ) do
368
+ if model_klass == RSpecModelEffectiveDateTestOverride
369
+ record = RSpecModelEffectiveDateTestOverride.find( @uuid_a )
370
+ record.destroy!
371
+
372
+ sleep( 0.1 ) # Be sure that there's nothing left at "now", within timer resolution
373
+ end
374
+ end
375
+
376
+ it 'finds no contemporary record' do
377
+ next unless model_klass == RSpecModelEffectiveDateTestOverride
378
+
379
+ @context.request.dated_at = nil
380
+ @context.request.uri_path_components = [ @uuid_a ]
381
+
382
+ result = RSpecModelEffectiveDateTestOverride.acquire_in!( @context )
383
+ expect( result ).to be_nil
384
+
385
+ expect( @context.response.halt_processing? ).to eq( true )
386
+ expect( @context.response.errors.errors.count ).to eq( 1 )
387
+
388
+ expect( @context.response.errors.errors[ 0 ][ 'code' ] ).to eq( 'generic.not_found' )
389
+ expect( @context.response.errors.errors[ 0 ][ 'reference' ] ).to eq( @uuid_a )
390
+ end
391
+
392
+ it 'finds a historic record' do
393
+ next unless model_klass == RSpecModelEffectiveDateTestOverride
394
+
395
+ @context.request.dated_at = @now - 4.hours
396
+ @context.request.uri_path_components = [ @uuid_a ]
397
+
398
+ result = RSpecModelEffectiveDateTestOverride.acquire_in!( @context )
399
+ expect( result ).to_not be_nil
400
+
401
+ expect( @context.response.halt_processing? ).to eq( false )
402
+ expect( result.data ).to eq( 'one' )
403
+ end
404
+
405
+ it 'indicates correctly that no contemporary exists during a far-backdated lookup' do
406
+ next unless model_klass == RSpecModelEffectiveDateTestOverride
407
+
408
+ @context.request.dated_at = @now - 1.year
409
+ @context.request.uri_path_components = [ @uuid_a ]
410
+
411
+ result = RSpecModelEffectiveDateTestOverride.acquire_in!( @context )
412
+ expect( result ).to be_nil
413
+
414
+ expect( @context.response.halt_processing? ).to eq( true )
415
+ expect( @context.response.errors.errors.count ).to eq( 1 )
416
+
417
+ expect( @context.response.errors.errors[ 0 ][ 'code' ] ).to eq( 'generic.not_found' )
418
+ expect( @context.response.errors.errors[ 0 ][ 'reference' ] ).to eq( @uuid_a )
419
+ end
420
+ end
421
+ end
422
+ end
423
+
424
+ end # 'shared_examples Hoodoo::ActiveRecord::Dated do'
272
425
 
273
426
  context "using default effective dating config" do
274
427