hoodoo 1.6.1 → 1.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,15 +1,7 @@
1
1
  ---
2
- !binary "U0hBMQ==":
3
- metadata.gz: !binary |-
4
- ODNjYzEzYzhiMTY1OTlkMjBkNDBlMmQ0MzNhYzAyZDJhNTFlMzBhNA==
5
- data.tar.gz: !binary |-
6
- M2I5ZmQyMmU0YTljY2JkNmE1ZGU4YzhhMzdmMzcxMDVmMGZiNzY2Nw==
2
+ SHA1:
3
+ metadata.gz: 21732b5cd34041936e018495404cb3950ff70b85
4
+ data.tar.gz: b37d1a9ef5e225653be451d8e775ea07cf5a9690
7
5
  SHA512:
8
- metadata.gz: !binary |-
9
- NGY3NjZhYzM5MDVkODhkMGQ5MzQ5NzY2MmQ1Y2I0YTRlYWI1NDI4MGU0YTc5
10
- NjJkNDZmODE4NTRkODA3ZGY0N2M0OWY5NmZjNTFiNzk0YzQxODVmM2RlZDc4
11
- YmViMDFkMWY0MzZlNThkMzk0MWY5ZDRjM2Q1MTRlMTI5YmY4NmE=
12
- data.tar.gz: !binary |-
13
- NTQ4OWZjYjA5Y2ZhOWIzN2NkODM4NWUwZTc5MDdiOGE0OTVhNGMxNjY5NzIy
14
- MTgwYWExNDk4M2NjNWQ4MDU3ZDJjNzk2Y2ZjYmQyNmU1Y2M2NzhhMDAxNDc3
15
- YjkyNmIxYTZjZDNlZGNhMDRmNTc2MDQxNWI3NjFlMDg3ZTIzNzQ=
6
+ metadata.gz: 39c52f8db587f906e2c377ab9fae3939697b872366c07aace735439538f6412fe12efae8996652b746ab31e8704334329e5d71125ca14c60be978f9352aa0308
7
+ data.tar.gz: bc4e9624bff7ad092b1ca7e0f86e64c2b8c634ee12649fd22bbb27de26d360dd870dfe4d7644639b5cc30fabbd30294e13c004e9efde77cff07f35fe3879a2eb
@@ -53,6 +53,7 @@ module Hoodoo
53
53
  model.class_attribute(
54
54
  :nz_co_loyalty_hoodoo_show_id_fields,
55
55
  :nz_co_loyalty_hoodoo_show_id_substitute,
56
+ :nz_co_loyalty_hoodoo_estimate_counts_with,
56
57
  :nz_co_loyalty_hoodoo_search_with,
57
58
  :nz_co_loyalty_hoodoo_filter_with,
58
59
  {
@@ -373,7 +374,12 @@ module Hoodoo
373
374
  # end
374
375
  #
375
376
  # Note the use of helper method #dataset_size to count the total
376
- # amount of results in the dataset without pagination.
377
+ # amount of results in the dataset without pagination. A resource may
378
+ # alternatively choose to use #estimated_dataset_size for a fast count
379
+ # estimation, or neither (though this is generally not recommended) or
380
+ # - permissible but unusual - include both.
381
+ #
382
+ # context.response.set_resources( results, nil, finder.estimated_dataset_size )
377
383
  #
378
384
  # The service middleware enforces sane values for things like list
379
385
  # offsets, sort keys and so-on according to service interface
@@ -495,7 +501,137 @@ module Hoodoo
495
501
  # +dataset_size+ parameter.
496
502
  #
497
503
  def dataset_size
498
- return all.limit( nil ).offset( nil ).count
504
+ return all.limit( nil ).offset( nil ).count()
505
+ end
506
+
507
+ # As #dataset_size, but allows a configurable counting back-end via
508
+ # #estimated_count and #estimate_counts_with. This method is intended
509
+ # to be used for fast count estimations, usually for performance
510
+ # reasons if an accurate #dataset_size count is too slow to compute.
511
+ #
512
+ def estimated_dataset_size
513
+ return all.limit( nil ).offset( nil ).estimated_count()
514
+ end
515
+
516
+ # In absence of other configuration, this method just calls through
517
+ # to Active Record's #count, but you can override the counting
518
+ # mechanism with a Proc which gets called to do the counting instead.
519
+ #
520
+ # The use case is for databases where counting may be slow for some
521
+ # reason. For example, in PostgreSQL 9, the MVCC model means that big
522
+ # tables under heavy write load may take extremely long times to be
523
+ # counted as a full sequential row scan gets activated. In the case
524
+ # of PostgreSQL, there's an estimation available as an alternative;
525
+ # its accuracy depends on how often the +ANALYZE+ command is run, but
526
+ # at least its execution speed is always very small.
527
+ #
528
+ # The #estimated_dataset_size method runs through here for counting so
529
+ # you need to ensure that your count estimation method can cope with
530
+ # whatever queries that might arise from the scope chains involved in
531
+ # instances of the model at hand, within the service code that uses
532
+ # that model.
533
+ #
534
+ # Specify a count estimation Proc with #estimate_counts_with. Such
535
+ # blocks are permitted to return +nil+ if the estimation is considered
536
+ # to be wildly wrong or unobtainable; in that case, the returned value
537
+ # for the estimated count will be +nil+ too.
538
+ #
539
+ def estimated_count
540
+ counter = self.nz_co_loyalty_hoodoo_estimate_counts_with
541
+
542
+ if ( counter.nil? )
543
+ return all.count
544
+ else
545
+ return counter.call( all.to_sql )
546
+ end
547
+ end
548
+
549
+ # This method is related to #estimated_count, so read the documentation
550
+ # for that as an introduction first.
551
+ #
552
+ # In #estimated_count, a PostgreSQL example is given. Continuing with
553
+ # this, we could implement an estimation mechanism via Hoodoo's fast
554
+ # counter with something like the approach described here:
555
+ #
556
+ # * https://wiki.postgresql.org/wiki/Count_estimate
557
+ # * http://www.verygoodindicators.com/blog/2015/04/07/faster-count-queries/
558
+ #
559
+ # First, you would need a migration in your service to implement the
560
+ # estimation method as a PLPGSQL function:
561
+ #
562
+ # class CreateFastCountFunction < ActiveRecord::Migration
563
+ # def up
564
+ # execute <<-SQL
565
+ # CREATE FUNCTION estimated_count(query text) RETURNS integer AS
566
+ # $func$
567
+ # DECLARE
568
+ # rec record;
569
+ # rows integer;
570
+ # BEGIN
571
+ # FOR rec IN EXECUTE 'EXPLAIN ' || query LOOP
572
+ # rows := substring(rec."QUERY PLAN" FROM ' rows=([[:digit:]]+)');
573
+ # EXIT WHEN rows IS NOT NULL;
574
+ # END LOOP;
575
+ #
576
+ # RETURN rows;
577
+ # END
578
+ # $func$ LANGUAGE plpgsql;
579
+ # SQL
580
+ # end
581
+ #
582
+ # def down
583
+ # execute "DROP FUNCTION estimated_count(query text);"
584
+ # end
585
+ # end
586
+ #
587
+ # This takes arbitrary query text so should cope with pretty much any
588
+ # kind of ActiveRecord query chain and resulting SQL. Run the database
589
+ # migration, then define a Proc which calls the new function:
590
+ #
591
+ # counter = Proc.new do | sql |
592
+ # begin
593
+ # sql = sql.gsub( "'", "''" ) # Escape SQL for insertion below
594
+ # ActiveRecord::Base.connection.execute(
595
+ # "SELECT estimated_count('#{ sql }')"
596
+ # ).first[ 'estimated_count' ].to_i
597
+ # rescue
598
+ # nil
599
+ # end
600
+ #
601
+ # Suppose we have a model called +Purchase+; next tell this model to
602
+ # use the above Proc for fast counting and use it:
603
+ #
604
+ # Purchase.estimate_counts_with( counter )
605
+ #
606
+ # Purchase.estimated_count()
607
+ # # => An integer; and you can use scope chains, just like #count:
608
+ # Purchase.where(...conditions...).estimated_count()
609
+ # # => An integer
610
+ #
611
+ # A real-life example showing how running PostgreSQL's +ANALYZE+
612
+ # command can make a difference:
613
+ #
614
+ # [1] pry(main)> Purchase.estimated_count
615
+ # => 68
616
+ # [2] pry(main)> Purchase.count
617
+ # => 76
618
+ # [3] pry(main)> ActiveRecord::Base.connection.execute( 'ANALYZE' )
619
+ # => #<PG::Result:0x007f89b62cdcc8 status=PGRES_COMMAND_OK ntuples=0 nfields=0 cmd_tuples=0>
620
+ # [4] pry(main)> Purchase.estimated_count
621
+ # => 76
622
+ #
623
+ # Parameters:
624
+ #
625
+ # +proc+:: The Proc to call. It must accept one parameter, which is the
626
+ # SQL query for which the count is to be run, as a String. It
627
+ # must evaluate to an Integer estimation, or +nil+ if it is
628
+ # not able to provide any/useful estimations, in its opinion.
629
+ #
630
+ # Pass +nil+ to remove the custom counter method and restore
631
+ # default behaviour.
632
+ #
633
+ def estimate_counts_with( proc )
634
+ self.nz_co_loyalty_hoodoo_estimate_counts_with = proc
499
635
  end
500
636
 
501
637
  # Specify a search mapping for use by #list to automatically restrict
@@ -20,9 +20,18 @@ module Hoodoo
20
20
 
21
21
  # For lists, the (optional) total size of the data set, of which
22
22
  # the contents of this Array will often only represent a single
23
- # page. If unknown, the value is +nil+.
23
+ # page. If unknown, the value is +nil+, but as an alternative, an
24
+ # estimated size may be available in #estimated_dataset_size.
24
25
  #
25
26
  attr_accessor :dataset_size
27
+
28
+ # For lists, the (optional) estimated size of the data set, of
29
+ # which the contents of this Array will often only represent a
30
+ # single page. If unknown, the value is +nil+. The accuracy of
31
+ # the estimation is unknown.
32
+ #
33
+ attr_accessor :estimated_dataset_size
34
+
26
35
  end
27
36
 
28
37
  end
@@ -76,7 +76,8 @@ module Hoodoo
76
76
  # call and supports Hoodoo::Client::AugmentedArray#dataset_size which
77
77
  # (if the called Resource endpoint implementation provides the
78
78
  # information) gives the total size of the data set at the time of
79
- # calling.
79
+ # calling. Hoodoo::Client::AugmentedArray#estimated_dataset_size
80
+ # likewise gives access to the estimated count, if available.
80
81
  #
81
82
  # The other 4 methods return a Hoodoo::Client::AugmentedHash. This is a
82
83
  # Hash subclass. Both the Array and Hash subclasses provide a common
@@ -325,9 +325,12 @@ module Hoodoo
325
325
  # part, else the hash part.
326
326
 
327
327
  if ( parsed[ '_data' ].is_a?( ::Array ) )
328
- size = parsed[ '_dataset_size' ]
329
- parsed = parsed[ '_data' ]
330
- parsed.dataset_size = size
328
+ size = parsed[ '_dataset_size' ]
329
+ estimated_size = parsed[ '_estimated_dataset_size' ]
330
+
331
+ parsed = parsed[ '_data' ]
332
+ parsed.dataset_size = size
333
+ parsed.estimated_dataset_size = estimated_size
331
334
 
332
335
  elsif ( parsed[ 'kind' ] == 'Errors' )
333
336
 
@@ -923,8 +923,9 @@ module Hoodoo; module Services
923
923
  body = local_response.body
924
924
 
925
925
  if action == :list && body.is_a?( ::Array )
926
- result = Hoodoo::Client::AugmentedArray.new( body )
927
- result.dataset_size = local_response.dataset_size
926
+ result = Hoodoo::Client::AugmentedArray.new( body )
927
+ result.dataset_size = local_response.dataset_size
928
+ result.estimated_dataset_size = local_response.estimated_dataset_size
928
929
 
929
930
  elsif action != :list && body.is_a?( ::Hash )
930
931
  result = Hoodoo::Client::AugmentedHash[ body ]
@@ -1947,8 +1948,24 @@ module Hoodoo; module Services
1947
1948
  # describing the current interaction. Updated on exit.
1948
1949
  #
1949
1950
  def deal_with_content_type_header( interaction )
1950
- content_type = interaction.rack_request.media_type
1951
- content_encoding = interaction.rack_request.content_charset
1951
+
1952
+ # An in-the-wild Content-Type header value of
1953
+ # "application/json; charset=utf-8, application/x-www-form-urlencoded"
1954
+ # from Postman caused Rack 1.6.4 to break and raise an exception. Trap
1955
+ # any exceptions from the Rack request calls below and assume that they
1956
+ # indicate a malformed header.
1957
+ #
1958
+ begin
1959
+ content_type = interaction.rack_request.media_type
1960
+ content_encoding = interaction.rack_request.content_charset
1961
+ rescue
1962
+ interaction.context.response.errors.add_error(
1963
+ 'platform.malformed',
1964
+ 'message' => "Content-Type '#{ interaction.rack_request.content_type || "<unknown>" }' is malformed"
1965
+ )
1966
+
1967
+ return
1968
+ end
1952
1969
 
1953
1970
  content_type.downcase! unless content_type.nil?
1954
1971
  content_encoding.downcase! unless content_encoding.nil?
@@ -71,13 +71,21 @@ module Hoodoo; module Services
71
71
  attr_accessor :body
72
72
  alias_method :set_resource, :body=
73
73
 
74
- # Read back a the dataset size given by a prior call to #set_resources,
74
+ # Read back a dataset size given by a prior call to #set_resources,
75
75
  # or +nil+ if none has been provided (either the response contains no
76
76
  # list yet/at all, or an Array was given but the dataset size was not
77
- # supplied).
77
+ # supplied). If the dataset size is absent, an estimation may be
78
+ # present; see #estimated_dataset_size.
78
79
  #
79
80
  attr_reader :dataset_size
80
81
 
82
+ # Read back an estimated dataset size given by a prior call to
83
+ # #set_resources, or +nil+ if none has been provided (either the
84
+ # response contains no list yet/at all, or an Array was given but
85
+ # a dataset size estimation was not supplied).
86
+ #
87
+ attr_reader :estimated_dataset_size
88
+
81
89
  # Create a new instance, ready to take on a response. The service
82
90
  # middleware is responsible for doing this.
83
91
  #
@@ -90,12 +98,13 @@ module Hoodoo; module Services
90
98
  raise "Hoodoo::Services::Response.new must be given a valid Interaction ID (got '#{ interaction_id.inspect }')"
91
99
  end
92
100
 
93
- @interaction_id = interaction_id
94
- @errors = Hoodoo::Errors.new()
95
- @headers = {}
96
- @http_status_code = 200
97
- @body = {}
98
- @dataset_size = nil
101
+ @interaction_id = interaction_id
102
+ @errors = Hoodoo::Errors.new()
103
+ @headers = {}
104
+ @http_status_code = 200
105
+ @body = {}
106
+ @dataset_size = nil
107
+ @estimated_dataset_size = nil
99
108
 
100
109
  end
101
110
 
@@ -112,26 +121,67 @@ module Hoodoo; module Services
112
121
  # array of items. Although you can just assign an array to either of
113
122
  # #body or #set_resource, calling #set_resources is more semantically
114
123
  # correct and provides an additional feature; you can specify the total
115
- # number of items in the dataset.
124
+ # number of items in the dataset either precisely, or as an estimation.
116
125
  #
117
126
  # For example, if you were listing a page of 50 resource instances but
118
127
  # the total matching dataset of that list included 344 instances, you
119
128
  # would pass 344 in the +dataset_size+ input parameter. This is optional
120
129
  # but highly recommended as it is often very useful for calling clients.
121
130
  #
131
+ # If for any reason you aren't able to quickly produce an accurate count
132
+ # but _can_ produce an estimation, call #set_estimated_resources instead.
133
+ #
122
134
  # +array+:: Array of resource representations (Ruby Array with
123
135
  # Ruby Hash entries representing rendered resources,
124
136
  # ideally through the Hoodoo::Presenters framework).
125
137
  #
126
138
  # +dataset_size+:: Optional _total_ number of items in the entire dataset
127
139
  # of which +array+ is, most likely, just a subset due to
128
- # paginated lists via offset and limit parameters.
140
+ # paginated lists via offset and limit parameters. This
141
+ # value was accurate at the instant of counting.
129
142
  #
130
143
  def set_resources( array, dataset_size = nil )
131
144
  self.body = array
132
145
  @dataset_size = dataset_size
133
146
  end
134
147
 
148
+ # A companion to #set_resources. See the documentation of that method for
149
+ # background information.
150
+ #
151
+ # If the persistence layer in use and data volumes expected for a given
152
+ # resource make accurate counting too slow to compute, your persistence
153
+ # layer might support a mechanism for producing an _estimated_ count
154
+ # quickly instead. For example, PostgreSQL 9's row counting can be slow
155
+ # due to MVCC but there are PostgreSQL-specific ways of obtaining a row
156
+ # count estimation quickly. If this applies to you, call here to
157
+ # correctly specify the estimation in a way that makes it clear to the
158
+ # calling client that it's not an accurate result.
159
+ #
160
+ # Technically you could call *both* this *and* #set_resources to set both
161
+ # an accurate and an estimated count, though it's hard to imagine a use
162
+ # case for this outside of testing scenarios; but note that each call
163
+ # will override any previous setting of the #body property.
164
+ #
165
+ # If using the Hoodoo::ActiveRecord extensions for your persistence layer,
166
+ # then please also see
167
+ # Hoodoo::ActiveRecord::Finder::ClassMethods::estimated_dataset_size.
168
+ #
169
+ # +array+:: Array of resource representations (Ruby Array
170
+ # with Ruby Hash entries representing rendered
171
+ # resources, ideally through the
172
+ # Hoodoo::Presenters framework).
173
+ #
174
+ # +estimated_dataset_size+:: Optional _total_ number of items in the
175
+ # entire dataset of which +array+ is, most
176
+ # likely, just a subset due to paginated lists
177
+ # via offset and limit parameters; this value
178
+ # is an estimation with undefined accuracy.
179
+ #
180
+ def set_estimated_resources( array, estimated_dataset_size = nil )
181
+ self.body = array
182
+ @estimated_dataset_size = estimated_dataset_size
183
+ end
184
+
135
185
  # Add an HTTP header to the internal collection that will be used for the
136
186
  # response. Trying to set data for the same HTTP header name more than once
137
187
  # will result in an exception being raised unless the +overwrite+ parameter
@@ -289,14 +339,19 @@ module Hoodoo; module Services
289
339
 
290
340
  if body_data.is_a?( ::Array )
291
341
  response_hash = { '_data' => body_data }
292
- response_hash[ '_dataset_size' ] = @dataset_size unless @dataset_size.nil?
342
+ response_hash[ '_dataset_size' ] = @dataset_size unless @dataset_size.nil?
343
+ response_hash[ '_estimated_dataset_size' ] = @estimated_dataset_size unless @estimated_dataset_size.nil?
293
344
  response_string = ::JSON.generate( response_hash )
345
+
294
346
  elsif body_data.is_a?( ::Hash )
295
347
  response_string = ::JSON.generate( body_data )
348
+
296
349
  elsif body_data.is_a?( ::String )
297
350
  response_string = body_data
351
+
298
352
  else
299
353
  raise "Hoodoo::Services::Response\#for_rack given unrecognised body data class '#{ body_data.class.name }'"
354
+
300
355
  end
301
356
 
302
357
  rack_response.write( response_string )
@@ -12,6 +12,6 @@ module Hoodoo
12
12
  # The Hoodoo gem version. If this changes, ensure that the date in
13
13
  # "hoodoo.gemspec" is correct and run "bundle install" (or "update").
14
14
  #
15
- VERSION = '1.6.1'
15
+ VERSION = '1.7.0'
16
16
 
17
17
  end
@@ -435,6 +435,8 @@ describe Hoodoo::ActiveRecord::Finder do
435
435
 
436
436
  context '#list' do
437
437
  it 'lists with pages, offsets and counts' do
438
+ expect_any_instance_of( RSpecModelFinderTest ).to_not receive( :estimated_dataset_size )
439
+
438
440
  @list_params.offset = 1 # 0 is first record
439
441
  @list_params.limit = 1
440
442
 
@@ -453,6 +455,121 @@ describe Hoodoo::ActiveRecord::Finder do
453
455
 
454
456
  # ==========================================================================
455
457
 
458
+ context 'counting' do
459
+ it 'lists with a normal count' do
460
+ finder = RSpecModelFinderTest.list( @list_params )
461
+
462
+ expect( finder ).to receive( :dataset_size ).at_least( :once ).and_call_original
463
+ expect( finder ).to receive( :count ).at_least( :once ).and_call_original
464
+
465
+ expect( finder ).to_not receive( :estimated_dataset_size )
466
+ expect( finder ).to_not receive( :estimated_count )
467
+
468
+ result = finder.dataset_size()
469
+
470
+ expect( result ).to_not be_nil
471
+ end
472
+
473
+ it 'lists with an estimated count' do
474
+ finder = RSpecModelFinderTest.list( @list_params )
475
+
476
+ expect( finder ).to_not receive( :dataset_size )
477
+
478
+ expect( finder ).to receive( :estimated_dataset_size ).at_least( :once ).and_call_original
479
+ expect( finder ).to receive( :estimated_count ).at_least( :once ).and_call_original
480
+ expect( finder ).to receive( :count ).at_least( :once ).and_call_original
481
+
482
+ result = finder.estimated_dataset_size
483
+
484
+ expect( result ).to_not be_nil
485
+ end
486
+
487
+ context 'RDoc-recommended PostgreSQL migration example' do
488
+ before :each do
489
+ ActiveRecord::Base.connection.execute <<-SQL
490
+ CREATE FUNCTION estimated_count(query text) RETURNS integer AS
491
+ $func$
492
+ DECLARE
493
+ rec record;
494
+ rows integer;
495
+ BEGIN
496
+ FOR rec IN EXECUTE 'EXPLAIN ' || query LOOP
497
+ rows := substring(rec."QUERY PLAN" FROM ' rows=([[:digit:]]+)');
498
+ EXIT WHEN rows IS NOT NULL;
499
+ END LOOP;
500
+
501
+ RETURN rows;
502
+ END
503
+ $func$ LANGUAGE plpgsql;
504
+ SQL
505
+
506
+ counter = Proc.new do | sql |
507
+ begin
508
+ ActiveRecord::Base.connection.execute(
509
+ "SELECT estimated_count('#{ sql}')"
510
+ ).first[ 'estimated_count' ].to_i
511
+ rescue
512
+ nil
513
+ end
514
+ end
515
+
516
+ RSpecModelFinderTest.estimate_counts_with( counter )
517
+
518
+ # Tests start by ensuring the database knows about the current object count.
519
+ #
520
+ ActiveRecord::Base.connection.execute( 'ANALYZE' )
521
+ end
522
+
523
+ after :each do
524
+ ActiveRecord::Base.connection.execute "DROP FUNCTION estimated_count(query text);"
525
+ RSpecModelFinderTest.estimate_counts_with( nil )
526
+ end
527
+
528
+ context 'estimate' do
529
+ before :each do
530
+ @initial_accurate_count = RSpecModelFinderTest.count
531
+
532
+ # The outer 'before' code ensures an accurate initial count of 3,
533
+ # but we're going add in a few more unestimated items.
534
+ #
535
+ @uncounted1 = RSpecModelFinderTest.new.save!
536
+ @uncounted2 = RSpecModelFinderTest.new.save!
537
+ @uncounted3 = RSpecModelFinderTest.new.save!
538
+
539
+ @subsequent_accurate_count = RSpecModelFinderTest.count
540
+ end
541
+
542
+ it 'is initially inaccurate' do
543
+ finder = RSpecModelFinderTest.list( @list_params )
544
+ result = finder.estimated_dataset_size
545
+ expect( result ).to eq( @initial_accurate_count )
546
+ end
547
+
548
+ # The outer 'before' code kind of already tests this anyway since if
549
+ # the analyze call therein didn't work, prerequisites in the tests
550
+ # would be wrong and other tests would fail. It's useful to
551
+ # double-check something this important though.
552
+ #
553
+ it 'is accurate after ANALYZE' do
554
+ ActiveRecord::Base.connection.execute( 'ANALYZE' )
555
+
556
+ finder = RSpecModelFinderTest.list( @list_params )
557
+ result = finder.estimated_dataset_size
558
+ expect( result ).to eq( @subsequent_accurate_count )
559
+ end
560
+
561
+ it 'is "nil" if the Proc evaluates thus' do
562
+ RSpecModelFinderTest.estimate_counts_with( Proc.new() { | sql | nil } )
563
+ finder = RSpecModelFinderTest.list( @list_params )
564
+ result = finder.estimated_dataset_size
565
+ expect( result ).to be_nil
566
+ end
567
+ end
568
+ end
569
+ end
570
+
571
+ # ==========================================================================
572
+
456
573
  context 'search' do
457
574
  it 'searches without chain' do
458
575
  @list_params.search_data = {
@@ -80,10 +80,13 @@ class RSpecClientTestTargetImplementation < Hoodoo::Services::Implementation
80
80
  return
81
81
  end
82
82
 
83
- context.response.set_resources(
84
- [ mock( context ), mock( context ), mock( context ) ],
85
- 3
86
- )
83
+ resources = [ mock( context ), mock( context ), mock( context ) ]
84
+
85
+ if context.request.embeds.include?( 'estimated_counts_please' )
86
+ context.response.set_estimated_resources( resources, resources.count )
87
+ else
88
+ context.response.set_resources( resources, resources.count )
89
+ end
87
90
  end
88
91
 
89
92
  def create( context )
@@ -171,7 +174,7 @@ class RSpecClientTestTargetInterface < Hoodoo::Services::Interface
171
174
  endpoint :r_spec_client_test_targets, RSpecClientTestTargetImplementation
172
175
  public_actions :show
173
176
  actions :list, :create, :update, :delete
174
- embeds :foo, :bar, :baz
177
+ embeds :foo, :bar, :baz, :estimated_counts_please
175
178
  end
176
179
  end
177
180
 
@@ -588,6 +591,7 @@ describe Hoodoo::Client do
588
591
  result = @endpoint.list( query_hash )
589
592
  expect( result.platform_errors.has_errors? ).to eq( false )
590
593
  expect( result.dataset_size ).to eq( result.size )
594
+ expect( result.estimated_dataset_size ).to be_nil
591
595
 
592
596
  expect( result[ 0 ][ 'embeds' ] ).to eq( embeds )
593
597
  expect( result[ 0 ][ 'language' ] ).to eq( @expected_locale )
@@ -636,6 +640,19 @@ describe Hoodoo::Client do
636
640
 
637
641
  option_based_expectations( result )
638
642
  end
643
+
644
+ it "provides estimations" do
645
+ query_hash = { '_embed' => 'estimated_counts_please' }
646
+
647
+ result = @endpoint.list( query_hash )
648
+ expect( result.platform_errors.has_errors? ).to eq( false )
649
+ expect( result.dataset_size ).to be_nil
650
+ expect( result.estimated_dataset_size ).to eq( result.size )
651
+
652
+ expect( result[ 0 ][ 'language' ] ).to eq( @expected_locale )
653
+
654
+ option_based_expectations( result )
655
+ end
639
656
  end
640
657
 
641
658
  before :each do
@@ -854,6 +871,7 @@ describe Hoodoo::Client do
854
871
  result = @endpoint.list( query_hash )
855
872
  expect( result.platform_errors.has_errors? ).to eq( false )
856
873
  expect( result.dataset_size ).to eq( result.size )
874
+ expect( result.estimated_dataset_size ).to be_nil
857
875
 
858
876
  expect( result[ 0 ][ 'embeds' ] ).to eq( embeds )
859
877
  expect( result[ 0 ][ 'language' ] ).to eq( @expected_locale )
@@ -903,6 +921,19 @@ describe Hoodoo::Client do
903
921
  option_based_expectations( result )
904
922
  end
905
923
 
924
+ it "provides estimations" do
925
+ query_hash = { '_embed' => 'estimated_counts_please' }
926
+
927
+ result = @endpoint.list( query_hash )
928
+ expect( result.platform_errors.has_errors? ).to eq( false )
929
+ expect( result.dataset_size ).to be_nil
930
+ expect( result.estimated_dataset_size ).to eq( result.size )
931
+
932
+ expect( result[ 0 ][ 'language' ] ).to eq( @expected_locale )
933
+
934
+ option_based_expectations( result )
935
+ end
936
+
906
937
  it 'automatically retries' do
907
938
  result = @endpoint.list()
908
939
  expect( result.platform_errors.has_errors? ).to eq( false )
@@ -8,6 +8,7 @@ describe Hoodoo::Services::Middleware do
8
8
 
9
9
  class RSpecTestServiceExoticStubImplementation < Hoodoo::Services::Implementation
10
10
  def list( context )
11
+ context.response.set_estimated_resources( [], 88 )
11
12
  context.response.set_resources( [], 99 )
12
13
  end
13
14
  end
@@ -151,11 +152,13 @@ describe Hoodoo::Services::Middleware do
151
152
  endpoint = @mw.inter_resource_endpoint_for( 'Version', 2, @interaction )
152
153
 
153
154
  # The endpoint should've been called locally; the implementation at
154
- # the top of this file sets an empty array with dataset size 99.
155
+ # the top of this file sets an empty array with dataset size 99 *and*
156
+ # an estimated dataset size of 88.
155
157
 
156
158
  mock_result = endpoint.list()
157
159
  expect( mock_result ).to be_empty
158
160
  expect( mock_result.dataset_size ).to eq( 99 )
161
+ expect( mock_result.estimated_dataset_size ).to eq( 88 )
159
162
  end
160
163
 
161
164
  it 'complains about a missing Alchemy instance' do
@@ -558,6 +561,7 @@ describe Hoodoo::Services::Middleware do
558
561
 
559
562
  expect( mock_result ).to eq( Hoodoo::Client::AugmentedArray.new )
560
563
  expect( mock_result.dataset_size ).to eq(99)
564
+ expect( mock_result.estimated_dataset_size ).to eq( 88 )
561
565
  expect( mock_result.platform_errors.has_errors? ).to eq( false )
562
566
  end
563
567
  end
@@ -23,7 +23,9 @@ class RSpecTestInterResourceCallsAImplementation < Hoodoo::Services::Implementat
23
23
  if search_offset > 0
24
24
  context.response.add_error( 'service_calls_a.triggered', 'reference' => { :offset => search_offset } )
25
25
  else
26
- context.response.set_resources( [1,2,3,4], 4321 )
26
+ array = [ 1, 2, 3, 4 ]
27
+ context.response.set_estimated_resources( array, 1234 )
28
+ context.response.set_resources( array, 4321 )
27
29
  expectable_hook( context )
28
30
  end
29
31
  end
@@ -415,6 +417,7 @@ describe Hoodoo::Services::Middleware::InterResourceLocal do
415
417
  expect_any_instance_of(RSpecTestInterResourceCallsBImplementation).to receive(:expectable_result_hook).once do | ignored_rspec_mock_instance, result |
416
418
  expect(result).to eq([1,2,3,4])
417
419
  expect(result.dataset_size).to eq(4321)
420
+ expect(result.estimated_dataset_size).to eq(1234)
418
421
  end
419
422
 
420
423
  get '/v1/rspec_test_inter_resource_calls_b',
@@ -28,14 +28,15 @@ class TestEchoImplementation < Hoodoo::Services::Implementation
28
28
  return
29
29
  end
30
30
 
31
- context.response.set_resources(
32
- [
33
- { 'list0' => TestEchoImplementation.to_h( context ) },
34
- { 'list1' => TestEchoImplementation.to_h( context ) },
35
- { 'list2' => TestEchoImplementation.to_h( context ) }
36
- ],
37
- 49
38
- )
31
+ array =
32
+ [
33
+ { 'list0' => TestEchoImplementation.to_h( context ) },
34
+ { 'list1' => TestEchoImplementation.to_h( context ) },
35
+ { 'list2' => TestEchoImplementation.to_h( context ) }
36
+ ]
37
+
38
+ context.response.set_resources( array, 49 )
39
+ context.response.set_estimated_resources( array, 50 )
39
40
  end
40
41
 
41
42
  def show( context )
@@ -189,15 +190,16 @@ class TestCallImplementation < Hoodoo::Services::Implementation
189
190
 
190
191
  return if result.adds_errors_to?( context.response.errors )
191
192
 
192
- context.response.set_resources(
193
- [
194
- { 'listA' => result },
195
- { 'listB' => result },
196
- { 'listC' => result },
197
- { 'options' => result.response_options }
198
- ],
199
- ( result.dataset_size || 0 ) + 2
200
- )
193
+ array =
194
+ [
195
+ { 'listA' => result },
196
+ { 'listB' => result },
197
+ { 'listC' => result },
198
+ { 'options' => result.response_options }
199
+ ]
200
+
201
+ context.response.set_resources( array, ( result.dataset_size || 0 ) + 2 )
202
+ context.response.set_estimated_resources( array, ( result.estimated_dataset_size || 0 ) + 2 )
201
203
  end
202
204
 
203
205
  def show( context )
@@ -434,6 +436,7 @@ describe Hoodoo::Services::Middleware do
434
436
  }
435
437
  )
436
438
  expect( parsed[ '_dataset_size' ] ).to eq( 49 )
439
+ expect( parsed[ '_estimated_dataset_size' ] ).to eq( 50 )
437
440
  end
438
441
 
439
442
  it 'lists things with callbacks', :check_callbacks => true do
@@ -957,6 +960,7 @@ describe Hoodoo::Services::Middleware do
957
960
  )
958
961
 
959
962
  expect( parsed[ '_dataset_size' ]).to eq( 51 )
963
+ expect( parsed[ '_estimated_dataset_size' ] ).to eq( 52 )
960
964
 
961
965
  expect( parsed[ '_data' ][ 3 ] ).to_not be_nil
962
966
  expect_response_options_for( parsed[ '_data' ][ 3 ][ 'options' ] )
@@ -225,7 +225,7 @@ describe Hoodoo::Services::Middleware do
225
225
  expect(result['errors'][0]['message']).to eq("Content-Type 'application/json' does not match supported types '[\"application/json\"]' and/or encodings '[\"utf-8\"]'")
226
226
  end
227
227
 
228
- it 'should complain about incorrect content type' do
228
+ it 'complains about incorrect content types' do
229
229
  get '/v2/rspec_test_service_stub', nil, { 'CONTENT_TYPE' => 'some/thing; charset=utf-8' }
230
230
 
231
231
  expect(last_response.status).to eq(422)
@@ -235,7 +235,7 @@ describe Hoodoo::Services::Middleware do
235
235
  expect(result['errors'][0]['message']).to eq("Content-Type 'some/thing; charset=utf-8' does not match supported types '[\"application/json\"]' and/or encodings '[\"utf-8\"]'")
236
236
  end
237
237
 
238
- it 'should complain about incorrect content type' do
238
+ it 'complains about incorrect content type charsets' do
239
239
  get '/v2/rspec_test_service_stub', nil, { 'CONTENT_TYPE' => 'application/json; charset=madeup' }
240
240
 
241
241
  expect(last_response.status).to eq(422)
@@ -245,6 +245,25 @@ describe Hoodoo::Services::Middleware do
245
245
  expect(result['errors'][0]['message']).to eq("Content-Type 'application/json; charset=madeup' does not match supported types '[\"application/json\"]' and/or encodings '[\"utf-8\"]'")
246
246
  end
247
247
 
248
+ it 'rejects malformed attempts to specify a list of options' do
249
+ types =
250
+ [
251
+ 'application/json; charset=utf-8, application/x-www-form-urlencoded',
252
+ 'application/x-www-form-urlencoded, application/json; charset=utf-8',
253
+ 'application/x-www-form-urlencoded, application/json; charset=utf-8, application/json; charset=madeup'
254
+ ]
255
+
256
+ types.each do | type |
257
+ get '/v2/rspec_test_service_stub', nil, { 'CONTENT_TYPE' => type }
258
+
259
+ expect(last_response.status).to eq(422)
260
+
261
+ result = JSON.parse(last_response.body)
262
+ expect(result['errors'][0]['code']).to eq('platform.malformed')
263
+ expect(result['errors'][0]['message']).to eq("Content-Type '#{ type }' is malformed")
264
+ end
265
+ end
266
+
248
267
  it 'should generate interaction IDs and other standard headers even for error states' do
249
268
  get '/v2/rspec_test_service_stub'
250
269
 
@@ -202,7 +202,7 @@ describe Hoodoo::Services::Response do
202
202
  expect(body.body).to eq([expected])
203
203
  end
204
204
 
205
- it 'should return non-error condition Rack data correctly with an Array body' do
205
+ it 'returns non-error condition Rack data correctly with an Array body' do
206
206
  response_array = [ { this: 'should not be ignored' }, { neither: 'should this' } ]
207
207
  @r.body = response_array
208
208
 
@@ -214,6 +214,47 @@ describe Hoodoo::Services::Response do
214
214
  expect(body.body).to eq([expected])
215
215
  end
216
216
 
217
+ it 'returns non-error condition Rack data correctly with a dataset size' do
218
+ response_array = [ { this: 'should not be ignored' }, { neither: 'should this' } ]
219
+ @r.set_resources( response_array, response_array.count )
220
+
221
+ status, headers, body = @r.for_rack
222
+
223
+ expected = JSON.generate( { '_data' => response_array, '_dataset_size' => response_array.count } )
224
+ expect( status ).to eq( 200 )
225
+ expect( headers ).to eq( { 'Content-Length' => expected.length.to_s } )
226
+ expect( body.body ).to eq( [ expected ] )
227
+ end
228
+
229
+ it 'returns non-error condition Rack data correctly with an estimated dataset size' do
230
+ response_array = [ { this: 'should not be ignored' }, { neither: 'should this' } ]
231
+ @r.set_estimated_resources( response_array, response_array.count )
232
+
233
+ status, headers, body = @r.for_rack
234
+
235
+ expected = JSON.generate( { '_data' => response_array, '_estimated_dataset_size' => response_array.count } )
236
+ expect( status ).to eq( 200 )
237
+ expect( headers ).to eq( { 'Content-Length' => expected.length.to_s } )
238
+ expect( body.body ).to eq( [ expected ] )
239
+ end
240
+
241
+ it 'returns non-error condition Rack data correctly with both an accurate and an estimated dataset size' do
242
+ response_array = [ { this: 'should not be ignored' }, { neither: 'should this' } ]
243
+
244
+ @r.set_resources( response_array, response_array.count )
245
+ @r.set_estimated_resources( response_array, response_array.count )
246
+
247
+ status, headers, body = @r.for_rack
248
+
249
+ expected = JSON.generate( { '_data' => response_array,
250
+ '_dataset_size' => response_array.count,
251
+ '_estimated_dataset_size' => response_array.count } )
252
+
253
+ expect( status ).to eq( 200 )
254
+ expect( headers ).to eq( { 'Content-Length' => expected.length.to_s } )
255
+ expect( body.body ).to eq( [ expected ] )
256
+ end
257
+
217
258
  it 'should allow pre-encoded strings in the body' do
218
259
  @r.body = 'Hello World!'
219
260
 
@@ -232,7 +273,7 @@ describe Hoodoo::Services::Response do
232
273
  end
233
274
  end
234
275
 
235
- context "#not_found" do
276
+ context '#not_found' do
236
277
 
237
278
  let(:ident) { 'an_ident' }
238
279
  before { @r.not_found(ident) }
@@ -247,4 +288,34 @@ describe Hoodoo::Services::Response do
247
288
  expect(@r.halt_processing?).to eq(true)
248
289
  end
249
290
  end
291
+
292
+ context '#set_resources and #set_estimated_resources' do
293
+ it '#set_resources sets #body and #dataset_size' do
294
+ array = [ 1, 2, 3, 4 ]
295
+ @r.set_resources( array, 4321 )
296
+ expect( @r.body ).to match_array( array )
297
+ expect( @r.dataset_size ).to eq( 4321 )
298
+ expect( @r.estimated_dataset_size ).to be_nil
299
+ end
300
+
301
+ it '#set_estimated_resources sets #body and #estimated_dataset_size' do
302
+ array = [ 4, 3, 2, 1 ]
303
+ @r.set_estimated_resources( array, 1234 )
304
+ expect( @r.body ).to match_array( array )
305
+ expect( @r.dataset_size ).to be_nil
306
+ expect( @r.estimated_dataset_size ).to eq( 1234 )
307
+ end
308
+
309
+ it 'both together set all properties with the most recent call setting #body' do
310
+ array_1 = [ 1, 2, 3, 4 ]
311
+ @r.set_resources( array_1, 4321 )
312
+
313
+ array_2 = [ 4, 3, 2, 1 ]
314
+ @r.set_estimated_resources( array_2, 1234 )
315
+
316
+ expect( @r.body ).to match_array( array_2 )
317
+ expect( @r.dataset_size ).to eq( 4321 )
318
+ expect( @r.estimated_dataset_size ).to eq( 1234 )
319
+ end
320
+ end
250
321
  end
metadata CHANGED
@@ -1,265 +1,265 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hoodoo
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.1
4
+ version: 1.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Loyalty New Zealand
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-03-29 00:00:00.000000000 Z
11
+ date: 2016-04-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: kgio
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ~>
17
+ - - "~>"
18
18
  - !ruby/object:Gem::Version
19
19
  version: '2.9'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ~>
24
+ - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '2.9'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: dalli
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - ~>
31
+ - - "~>"
32
32
  - !ruby/object:Gem::Version
33
33
  version: '2.7'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - ~>
38
+ - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '2.7'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rake
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - ~>
45
+ - - "~>"
46
46
  - !ruby/object:Gem::Version
47
47
  version: '10.4'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - ~>
52
+ - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '10.4'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: simplecov-rcov
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - ~>
59
+ - - "~>"
60
60
  - !ruby/object:Gem::Version
61
61
  version: '0.2'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
- - - ~>
66
+ - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0.2'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: rdoc
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
- - - ~>
73
+ - - "~>"
74
74
  - !ruby/object:Gem::Version
75
75
  version: '4.2'
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
- - - ~>
80
+ - - "~>"
81
81
  - !ruby/object:Gem::Version
82
82
  version: '4.2'
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: rack-test
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
- - - ~>
87
+ - - "~>"
88
88
  - !ruby/object:Gem::Version
89
89
  version: '0.6'
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
- - - ~>
94
+ - - "~>"
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0.6'
97
97
  - !ruby/object:Gem::Dependency
98
98
  name: alchemy-flux
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
- - - ~>
101
+ - - "~>"
102
102
  - !ruby/object:Gem::Version
103
103
  version: '1.0'
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
- - - ~>
108
+ - - "~>"
109
109
  - !ruby/object:Gem::Version
110
110
  version: '1.0'
111
111
  - !ruby/object:Gem::Dependency
112
112
  name: rspec
113
113
  requirement: !ruby/object:Gem::Requirement
114
114
  requirements:
115
- - - ~>
115
+ - - "~>"
116
116
  - !ruby/object:Gem::Version
117
117
  version: '3.3'
118
118
  type: :development
119
119
  prerelease: false
120
120
  version_requirements: !ruby/object:Gem::Requirement
121
121
  requirements:
122
- - - ~>
122
+ - - "~>"
123
123
  - !ruby/object:Gem::Version
124
124
  version: '3.3'
125
125
  - !ruby/object:Gem::Dependency
126
126
  name: rspec-mocks
127
127
  requirement: !ruby/object:Gem::Requirement
128
128
  requirements:
129
- - - ~>
129
+ - - "~>"
130
130
  - !ruby/object:Gem::Version
131
131
  version: '3.3'
132
132
  type: :development
133
133
  prerelease: false
134
134
  version_requirements: !ruby/object:Gem::Requirement
135
135
  requirements:
136
- - - ~>
136
+ - - "~>"
137
137
  - !ruby/object:Gem::Version
138
138
  version: '3.3'
139
139
  - !ruby/object:Gem::Dependency
140
140
  name: activerecord
141
141
  requirement: !ruby/object:Gem::Requirement
142
142
  requirements:
143
- - - ~>
143
+ - - "~>"
144
144
  - !ruby/object:Gem::Version
145
145
  version: '4.2'
146
146
  type: :development
147
147
  prerelease: false
148
148
  version_requirements: !ruby/object:Gem::Requirement
149
149
  requirements:
150
- - - ~>
150
+ - - "~>"
151
151
  - !ruby/object:Gem::Version
152
152
  version: '4.2'
153
153
  - !ruby/object:Gem::Dependency
154
154
  name: activesupport
155
155
  requirement: !ruby/object:Gem::Requirement
156
156
  requirements:
157
- - - ~>
157
+ - - "~>"
158
158
  - !ruby/object:Gem::Version
159
159
  version: '4.2'
160
160
  type: :development
161
161
  prerelease: false
162
162
  version_requirements: !ruby/object:Gem::Requirement
163
163
  requirements:
164
- - - ~>
164
+ - - "~>"
165
165
  - !ruby/object:Gem::Version
166
166
  version: '4.2'
167
167
  - !ruby/object:Gem::Dependency
168
168
  name: database_cleaner
169
169
  requirement: !ruby/object:Gem::Requirement
170
170
  requirements:
171
- - - ~>
171
+ - - "~>"
172
172
  - !ruby/object:Gem::Version
173
173
  version: 1.4.0
174
174
  type: :development
175
175
  prerelease: false
176
176
  version_requirements: !ruby/object:Gem::Requirement
177
177
  requirements:
178
- - - ~>
178
+ - - "~>"
179
179
  - !ruby/object:Gem::Version
180
180
  version: 1.4.0
181
181
  - !ruby/object:Gem::Dependency
182
182
  name: pg
183
183
  requirement: !ruby/object:Gem::Requirement
184
184
  requirements:
185
- - - ~>
185
+ - - "~>"
186
186
  - !ruby/object:Gem::Version
187
187
  version: '0.18'
188
188
  type: :development
189
189
  prerelease: false
190
190
  version_requirements: !ruby/object:Gem::Requirement
191
191
  requirements:
192
- - - ~>
192
+ - - "~>"
193
193
  - !ruby/object:Gem::Version
194
194
  version: '0.18'
195
195
  - !ruby/object:Gem::Dependency
196
196
  name: byebug
197
197
  requirement: !ruby/object:Gem::Requirement
198
198
  requirements:
199
- - - ~>
199
+ - - "~>"
200
200
  - !ruby/object:Gem::Version
201
201
  version: '3.5'
202
202
  type: :development
203
203
  prerelease: false
204
204
  version_requirements: !ruby/object:Gem::Requirement
205
205
  requirements:
206
- - - ~>
206
+ - - "~>"
207
207
  - !ruby/object:Gem::Version
208
208
  version: '3.5'
209
209
  - !ruby/object:Gem::Dependency
210
210
  name: timecop
211
211
  requirement: !ruby/object:Gem::Requirement
212
212
  requirements:
213
- - - ~>
213
+ - - "~>"
214
214
  - !ruby/object:Gem::Version
215
215
  version: '0.8'
216
216
  type: :development
217
217
  prerelease: false
218
218
  version_requirements: !ruby/object:Gem::Requirement
219
219
  requirements:
220
- - - ~>
220
+ - - "~>"
221
221
  - !ruby/object:Gem::Version
222
222
  version: '0.8'
223
223
  - !ruby/object:Gem::Dependency
224
224
  name: raygun4ruby
225
225
  requirement: !ruby/object:Gem::Requirement
226
226
  requirements:
227
- - - ~>
227
+ - - "~>"
228
228
  - !ruby/object:Gem::Version
229
229
  version: '1.1'
230
230
  type: :development
231
231
  prerelease: false
232
232
  version_requirements: !ruby/object:Gem::Requirement
233
233
  requirements:
234
- - - ~>
234
+ - - "~>"
235
235
  - !ruby/object:Gem::Version
236
236
  version: '1.1'
237
237
  - !ruby/object:Gem::Dependency
238
238
  name: airbrake
239
239
  requirement: !ruby/object:Gem::Requirement
240
240
  requirements:
241
- - - ~>
241
+ - - "~>"
242
242
  - !ruby/object:Gem::Version
243
243
  version: '4.3'
244
244
  type: :development
245
245
  prerelease: false
246
246
  version_requirements: !ruby/object:Gem::Requirement
247
247
  requirements:
248
- - - ~>
248
+ - - "~>"
249
249
  - !ruby/object:Gem::Version
250
250
  version: '4.3'
251
251
  - !ruby/object:Gem::Dependency
252
252
  name: le
253
253
  requirement: !ruby/object:Gem::Requirement
254
254
  requirements:
255
- - - ~>
255
+ - - "~>"
256
256
  - !ruby/object:Gem::Version
257
257
  version: '2.6'
258
258
  type: :development
259
259
  prerelease: false
260
260
  version_requirements: !ruby/object:Gem::Requirement
261
261
  requirements:
262
- - - ~>
262
+ - - "~>"
263
263
  - !ruby/object:Gem::Version
264
264
  version: '2.6'
265
265
  description: Simplify the implementation of consistent services within an API-based
@@ -495,17 +495,17 @@ require_paths:
495
495
  - lib
496
496
  required_ruby_version: !ruby/object:Gem::Requirement
497
497
  requirements:
498
- - - ! '>='
498
+ - - ">="
499
499
  - !ruby/object:Gem::Version
500
500
  version: '2.1'
501
501
  required_rubygems_version: !ruby/object:Gem::Requirement
502
502
  requirements:
503
- - - ! '>='
503
+ - - ">="
504
504
  - !ruby/object:Gem::Version
505
505
  version: '0'
506
506
  requirements: []
507
507
  rubyforge_project:
508
- rubygems_version: 2.4.5
508
+ rubygems_version: 2.2.5
509
509
  signing_key:
510
510
  specification_version: 4
511
511
  summary: Opinionated APIs