hoodoo 1.6.1 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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