interval_response 0.1.6 → 0.1.7

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,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 627cc1b428d5adbde188c9718a9fabeacf4d852126500439ec672f1caea8b88f
4
- data.tar.gz: e42cae558b987ef86bc9bb351a3e5196bf68af9bfa2f0918a6f16d8fbbb52036
3
+ metadata.gz: 05cde032dcb1552e0814acf6904f300a840a9bca04399d932c4860f985adea1f
4
+ data.tar.gz: 8f04da33316e49efd6d15145ab19adec358f7035f7d5e07721b216e1f7ec9796
5
5
  SHA512:
6
- metadata.gz: 991b923a00ccb2b510e45044cf578c19e38a6ce65669188c91c16b736dfee0aba1352126d5552e114acc6a30d8ecc322d478c1af3091e3b7761d395cd521d5ec
7
- data.tar.gz: e229ea7a13015012c7ecf8125b0f401e8b24b236de19a5a65b9c67afac6121966c269d31062a1a84a713e5caa25485e30eaf9e7c73a6bf3e452c2dd25ea4f67c
6
+ metadata.gz: c74910d5332103d7240e28f848d092d70299178735a9ba38dcd5e307e5d11859890201bc0ba6072b0c94fe96885994fb9195d4b83a90316b10986dfd29d69ebf
7
+ data.tar.gz: 6835c6fc24fe5874de7b7b5e653b75b41cf2ef3c78d7909fb4194bee6dd764baf4bb7e4634ef7fc5ccdf7c720bc292152ac6b9d046d36bdb65140ccbede9e391
data/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ # 0.1.7
2
+
3
+ * Move `RackResponseWrapper` into the main namespace
4
+ * Add `#satisfied_with_first_interval?` so that certain Range: requests can be served using a redirect
5
+ * Add `#multiple_ranges?` so that one can choose not to honor multipart Range requests
6
+
7
+ # 0.1.6
8
+
9
+ * Create a base response type (`Abstract`) which has the same interface as the rest of the responses
10
+
1
11
  # 0.1.5
2
12
 
3
13
  * Change the API of `IntervalResponse.new` to accept the Rack `env` hash directly, without having the caller extract the header values manually.
@@ -5,6 +5,7 @@ module IntervalResponse
5
5
  class Error < StandardError; end
6
6
 
7
7
  require_relative "interval_response/version"
8
+ require_relative "interval_response/rack_body_wrapper"
8
9
  require_relative "interval_response/sequence"
9
10
  require_relative "interval_response/abstract"
10
11
  require_relative "interval_response/empty"
@@ -1,70 +1,32 @@
1
1
  # Base class for all response types, primarily for ease of documentation
2
2
  class IntervalResponse::Abstract
3
- # The Rack body wrapper is intended to return as the third element
4
- # of the Rack response triplet. It supports the #each method
5
- # and will call to the IntervalResponse object given to it
6
- # at instantiation, filling up a pre-allocated String object
7
- # with the bytes to be served out.
8
- class RackBodyWrapper
9
- # Default size of the chunk (String buffer) which is going to be
10
- # yielded to the caller of the `each` method.
11
- # Set toroughly one TCP kernel buffer
12
- CHUNK_SIZE = 65 * 1024
13
-
14
- def initialize(with_interval_response, chunk_size:)
15
- @chunk_size = chunk_size
16
- @interval_response = with_interval_response
17
- end
18
-
19
- def each
20
- buf = String.new(capacity: @chunk_size)
21
- @interval_response.each do |segment, range_in_segment|
22
- case segment
23
- when IntervalResponse::LazyFile
24
- segment.with do |file_handle|
25
- with_each_chunk(range_in_segment) do |offset, read_n|
26
- file_handle.seek(offset, IO::SEEK_SET)
27
- yield file_handle.read(read_n, buf)
28
- end
29
- end
30
- when String
31
- with_each_chunk(range_in_segment) do |offset, read_n|
32
- yield segment.slice(offset, read_n)
33
- end
34
- when IO, Tempfile
35
- with_each_chunk(range_in_segment) do |offset, read_n|
36
- segment.seek(offset, IO::SEEK_SET)
37
- yield segment.read(read_n, buf)
38
- end
39
- else
40
- raise TypeError, "RackBodyWrapper only supports IOs or Strings"
41
- end
42
- end
43
- ensure
44
- buf.clear
45
- end
46
-
47
- private
48
-
49
- def with_each_chunk(range_in_segment)
50
- range_size = range_in_segment.end - range_in_segment.begin + 1
51
- start_at_offset = range_in_segment.begin
52
- n_whole_segments, remainder = range_size.divmod(@chunk_size)
53
-
54
- n_whole_segments.times do |n|
55
- unit_offset = start_at_offset + (n * @chunk_size)
56
- yield unit_offset, @chunk_size
57
- end
3
+ def to_rack_response_triplet(headers: nil, chunk_size: IntervalResponse::RackBodyWrapper::CHUNK_SIZE)
4
+ [status_code, headers.to_h.merge(self.headers), IntervalResponse::RackBodyWrapper.new(self, chunk_size: chunk_size)]
5
+ end
58
6
 
59
- if remainder > 0
60
- unit_offset = start_at_offset + (n_whole_segments * @chunk_size)
61
- yield unit_offset, remainder
62
- end
63
- end
7
+ # Tells whether this response is responding with multiple ranges. If you want to simulate S3 for example,
8
+ # it might be relevant to deny a response from being served if it does respond with multiple ranges -
9
+ # IntervalResponse supports these responses just fine, but S3 doesn't.
10
+ def multiple_ranges?
11
+ false
64
12
  end
65
13
 
66
- def to_rack_response_triplet(headers: nil, chunk_size: RackBodyWrapper::CHUNK_SIZE)
67
- [status_code, headers.to_h.merge(self.headers), RackBodyWrapper.new(self, chunk_size: chunk_size)]
14
+ # Tells whether this entire requested range can be satisfied with the first available segment within the given Sequence.
15
+ # If it is, then you can redirect to the URL of the first segment instead of streaming the response
16
+ # through - which can be cheaper for your application server. Note that you can redirect to the resource of the first
17
+ # interval only, because otherwise your `Range` header will no longer match. Suppose you have a stitched resource
18
+ # consisting of two segments:
19
+ #
20
+ # [bytes 0..456]
21
+ # [bytes 457..890]
22
+ #
23
+ # and your client requests `Range: bytes=0-33`. You can redirect the client to the location of the first interval,
24
+ # and the `Range:` header will be retransmitted to that location and will be satisfied. However, imagine you are requesting
25
+ # the `Range: bytes=510-512` - you _could_ redirect just to the second interval, but the `Range` header is not going to be
26
+ # adjusted by the client, and you are not going to receive the correct slice of the resource. That's why you can only
27
+ # redirect to the first interval only.
28
+ def satisfied_with_first_interval?
29
+ false
68
30
  end
69
31
 
70
32
  # @param interval_sequence[IntervalResponse::Sequence] the sequence the response is built for
@@ -1,9 +1,12 @@
1
1
  # Serves out a response that contains the entire resource
2
2
  class IntervalResponse::Full < IntervalResponse::Abstract
3
+ def initialize(*)
4
+ super
5
+ @http_range_for_entire_resource = 0..(@interval_sequence.size - 1)
6
+ end
7
+
3
8
  def each
4
- # serve the part of the interval map
5
- full_range = 0..(@interval_sequence.size - 1)
6
- @interval_sequence.each_in_range(full_range) do |segment, range_in_segment|
9
+ @interval_sequence.each_in_range(@http_range_for_entire_resource) do |segment, range_in_segment|
7
10
  yield(segment, range_in_segment)
8
11
  end
9
12
  end
@@ -16,6 +19,14 @@ class IntervalResponse::Full < IntervalResponse::Abstract
16
19
  @interval_sequence.size
17
20
  end
18
21
 
22
+ def satisfied_with_first_interval?
23
+ @interval_sequence.first_interval_only?(@http_range_for_entire_resource)
24
+ end
25
+
26
+ def multiple_ranges?
27
+ false
28
+ end
29
+
19
30
  def headers
20
31
  {
21
32
  'Accept-Ranges' => 'bytes',
@@ -47,6 +47,14 @@ class IntervalResponse::Multi < IntervalResponse::Abstract
47
47
  }
48
48
  end
49
49
 
50
+ def satisfied_with_first_interval?
51
+ @interval_sequence.first_interval_only?(*@http_ranges)
52
+ end
53
+
54
+ def multiple_ranges?
55
+ true
56
+ end
57
+
50
58
  private
51
59
 
52
60
  def compute_envelope_size
@@ -0,0 +1,65 @@
1
+ # The Rack body wrapper is intended to be returned as the third element
2
+ # of the Rack response triplet. It supports the #each method
3
+ # and will call to the IntervalResponse object given to it
4
+ # at instantiation, filling up a pre-allocated String object
5
+ # with the bytes to be served out. The String object will then be repeatedly
6
+ # yielded to the Rack webserver with the response data. Since Ruby strings
7
+ # are mutable, the String object will be sized to a certain capacity and reused
8
+ # across calls to save allocations.
9
+ class IntervalResponse::RackBodyWrapper
10
+ # Default size of the chunk (String buffer) which is going to be
11
+ # yielded to the caller of the `each` method.
12
+ # Set toroughly one TCP kernel buffer
13
+ CHUNK_SIZE = 65 * 1024
14
+
15
+ def initialize(with_interval_response, chunk_size:)
16
+ @chunk_size = chunk_size
17
+ @interval_response = with_interval_response
18
+ end
19
+
20
+ def each
21
+ buf = String.new(capacity: @chunk_size)
22
+ @interval_response.each do |segment, range_in_segment|
23
+ case segment
24
+ when IntervalResponse::LazyFile
25
+ segment.with do |file_handle|
26
+ with_each_chunk(range_in_segment) do |offset, read_n|
27
+ file_handle.seek(offset, IO::SEEK_SET)
28
+ yield file_handle.read(read_n, buf)
29
+ end
30
+ end
31
+ when String
32
+ with_each_chunk(range_in_segment) do |offset, read_n|
33
+ yield segment.slice(offset, read_n)
34
+ end
35
+ when IO, Tempfile
36
+ with_each_chunk(range_in_segment) do |offset, read_n|
37
+ segment.seek(offset, IO::SEEK_SET)
38
+ yield segment.read(read_n, buf)
39
+ end
40
+ else
41
+ raise TypeError, "RackBodyWrapper only supports IOs or Strings"
42
+ end
43
+ end
44
+ ensure
45
+ buf.clear
46
+ end
47
+
48
+ private
49
+
50
+ def with_each_chunk(range_in_segment)
51
+ range_size = range_in_segment.end - range_in_segment.begin + 1
52
+ start_at_offset = range_in_segment.begin
53
+ n_whole_segments, remainder = range_size.divmod(@chunk_size)
54
+
55
+ n_whole_segments.times do |n|
56
+ unit_offset = start_at_offset + (n * @chunk_size)
57
+ yield unit_offset, @chunk_size
58
+ end
59
+
60
+ if remainder > 0
61
+ unit_offset = start_at_offset + (n_whole_segments * @chunk_size)
62
+ yield unit_offset, remainder
63
+ end
64
+ end
65
+ end
@@ -42,6 +42,8 @@ class IntervalResponse::Sequence
42
42
  def add_segment(segment, size:, etag: size)
43
43
  if size > 0
44
44
  etag_quoted = '"%s"' % etag
45
+ # We save the index of the interval inside the Struct so that we can
46
+ # use `bsearch` later instead of requiring `bsearch_index` to be available
45
47
  @intervals << Interval.new(segment, size, @size, @intervals.length, etag_quoted)
46
48
  @size += size
47
49
  end
@@ -58,31 +60,38 @@ class IntervalResponse::Sequence
58
60
  # need to retrieve data from the inner Sequence which is one of the segments, the call will
59
61
  # yield the segments from the inner Sequence, "drilling down" as deep as is appropriate.
60
62
  #
63
+ # Three arguments will be yielded to the block - the segment (the "meat" of an interval, which
64
+ # is the object given when the interval was added to the Sequence), the range within the interval
65
+ # (which is always going to be an inclusive `Range` of integers) and a boolean flag indicating whether
66
+ # this interval is the very first interval in the requested subset of the sequence. This flag honors nesting
67
+ # (if you have arbitrarily nested interval Sequences and you request something from the first interval of
68
+ # several Sequences deep it will still indicate `true`).
69
+ #
61
70
  # @param from_range_in_resource[Range] an inclusive Range that specifies the range within the segment map
62
- # @yield segment[Object], range_in_segment[Range]
71
+ # @yield segment[Object], range_in_segment[Range], is_first_interval[Boolean]
63
72
  def each_in_range(from_range_in_resource)
64
73
  # Skip empty ranges
65
74
  requested_range_size = (from_range_in_resource.end - from_range_in_resource.begin) + 1
66
75
  return if requested_range_size < 1
67
76
 
68
- # ...and if the range misses our intervals completely
77
+ # Then walk through included intervals. If the range misses
78
+ # our intervals completely included_intervals will be empty.
69
79
  included_intervals = intervals_within_range(from_range_in_resource)
70
-
71
- # And normal case - walk through included intervals
72
80
  included_intervals.each do |interval|
73
81
  int_start = interval.offset
74
82
  int_end = interval.offset + interval.size - 1
75
83
  req_start = from_range_in_resource.begin
76
84
  req_end = from_range_in_resource.end
77
85
  range_within_interval = (max(int_start, req_start) - int_start)..(min(int_end, req_end) - int_start)
86
+ is_first_interval = interval.position == 0
78
87
 
79
88
  # Allow Sequences to be composed together
80
89
  if interval.segment.respond_to?(:each_in_range)
81
- interval.segment.each_in_range(range_within_interval) do |sub_segment, sub_range|
82
- yield(sub_segment, sub_range)
90
+ interval.segment.each_in_range(range_within_interval) do |sub_segment, sub_range, is_first_nested_interval|
91
+ yield(sub_segment, sub_range, is_first_interval && is_first_nested_interval)
83
92
  end
84
93
  else
85
- yield(interval.segment, range_within_interval)
94
+ yield(interval.segment, range_within_interval, is_first_interval)
86
95
  end
87
96
  end
88
97
  end
@@ -132,6 +141,19 @@ class IntervalResponse::Sequence
132
141
  '"%s"' % d.hexdigest
133
142
  end
134
143
 
144
+ # Tells whether all of the given `ranges` will be satisfied from the first interval only. This can be used to
145
+ # redirect to the resource at that interval instead of proxying it through, since the `Range` header won't need to
146
+ # be adjusted
147
+ def first_interval_only?(*ranges)
148
+ ranges.map do |range|
149
+ each_in_range(range) do |_, _, is_first_interval|
150
+ return false unless is_first_interval
151
+ end
152
+ end
153
+
154
+ true
155
+ end
156
+
135
157
  private
136
158
 
137
159
  def max(a, b)
@@ -143,6 +165,10 @@ class IntervalResponse::Sequence
143
165
  end
144
166
 
145
167
  def interval_under(offset)
168
+ # For our purposes we would be better served by `bsearch_index`, but it is not available
169
+ # on older Ruby versions which we otherwise can splendidly support. Since when we retrieve
170
+ # the interval under offset we are going to need the index anyway, and since calling `Array#index`
171
+ # will incur another linear scan of the array, we save the index of the interval with the interval itself.
146
172
  @intervals.bsearch do |interval|
147
173
  # bsearch expects a 0 return value for "exact match".
148
174
  # -1 tells it "look to my left" and 1 "look to my right",
@@ -32,4 +32,8 @@ class IntervalResponse::Single < IntervalResponse::Abstract
32
32
  'ETag' => etag,
33
33
  }
34
34
  end
35
+
36
+ def satisfied_with_first_interval?
37
+ @interval_sequence.first_interval_only?(@http_range)
38
+ end
35
39
  end
@@ -1,3 +1,3 @@
1
1
  module IntervalResponse
2
- VERSION = "0.1.6"
2
+ VERSION = "0.1.7"
3
3
  end
@@ -17,19 +17,35 @@ RSpec.describe IntervalResponse::Sequence do
17
17
  expect(seq.size).to eq(6 + 12 + 17)
18
18
  expect { |b|
19
19
  seq.each_in_range(0..0, &b)
20
- }.to yield_with_args(a, 0..0)
20
+ }.to yield_with_args(a, 0..0, true)
21
21
 
22
22
  expect { |b|
23
23
  seq.each_in_range(0..7, &b)
24
- }.to yield_successive_args([a, 0..5], [b, 0..1])
24
+ }.to yield_successive_args([a, 0..5, true], [b, 0..1, false])
25
25
 
26
26
  expect { |b|
27
27
  seq.each_in_range(7..27, &b)
28
- }.to yield_successive_args([b, 1..11], [c, 0..9])
28
+ }.to yield_successive_args([b, 1..11, false], [c, 0..9, false])
29
29
 
30
30
  expect { |b|
31
31
  seq.each_in_range(0..(6 + 12 - 1), &b)
32
- }.to yield_successive_args([a, 0..5], [b, 0..11])
32
+ }.to yield_successive_args([a, 0..5, true], [b, 0..11, false])
33
+ end
34
+
35
+ it 'indicates whether the first interval will satisfy a set of Ranges' do
36
+ seq = described_class.new
37
+
38
+ a = double(:a, size: 6)
39
+ b = double(:b, size: 12)
40
+ c = double(:c, size: 17)
41
+ seq << a << b << c
42
+
43
+ expect(seq).to be_first_interval_only(0..0)
44
+ expect(seq).to be_first_interval_only(0..0, 0..5)
45
+ expect(seq).not_to be_first_interval_only(0..6)
46
+ expect(seq).not_to be_first_interval_only(3..8)
47
+ expect(seq).not_to be_first_interval_only(15..16)
48
+ expect(seq).not_to be_first_interval_only(0..0, 15..16)
33
49
  end
34
50
 
35
51
  it 'generates the ETag for an empty sequence, and the etag contains data' do
@@ -91,7 +107,7 @@ RSpec.describe IntervalResponse::Sequence do
91
107
  seq = described_class.new(a, b, c)
92
108
  expect { |b|
93
109
  seq.each_in_range(0..27, &b)
94
- }.to yield_successive_args([a, 0..2], [b, 0..3], [c, 0..0])
110
+ }.to yield_successive_args([a, 0..2, true], [b, 0..3, false], [c, 0..0, false])
95
111
  end
96
112
 
97
113
  it 'is composable' do
@@ -103,7 +119,7 @@ RSpec.describe IntervalResponse::Sequence do
103
119
 
104
120
  expect { |b|
105
121
  seq.each_in_range(0..27, &b)
106
- }.to yield_successive_args([a, 0..2], [b, 0..3], [c, 0..0])
122
+ }.to yield_successive_args([a, 0..2, true], [b, 0..3, false], [c, 0..0, false])
107
123
  end
108
124
 
109
125
  it 'has close to linear performance with large number of ranges and intervals' do
@@ -56,6 +56,9 @@ RSpec.describe IntervalResponse do
56
56
  'ETag' => seq.etag,
57
57
  )
58
58
  expect(response.etag).to eq(seq.etag)
59
+ expect(response).not_to be_multiple_ranges
60
+ expect(response).not_to be_satisfied_with_first_interval
61
+
59
62
  expect { |b|
60
63
  response.each(&b)
61
64
  }.to yield_successive_args([segment_a, 0..2], [segment_b, 0..3], [segment_c, 0..0])
@@ -72,6 +75,8 @@ RSpec.describe IntervalResponse do
72
75
  'ETag' => seq.etag
73
76
  )
74
77
  expect(response.etag).to eq(seq.etag)
78
+ expect(response).not_to be_multiple_ranges
79
+ expect(response).not_to be_satisfied_with_first_interval
75
80
  end
76
81
 
77
82
  it 'returns a single HTTP range if the client asked for it and it can be satisfied' do
@@ -86,11 +91,34 @@ RSpec.describe IntervalResponse do
86
91
  'ETag' => seq.etag,
87
92
  )
88
93
  expect(response.etag).to eq(seq.etag)
94
+ expect(response).not_to be_multiple_ranges
95
+ expect(response).not_to be_satisfied_with_first_interval
96
+
89
97
  expect { |b|
90
98
  response.each(&b)
91
99
  }.to yield_successive_args([segment_a, 2..2], [segment_b, 0..1])
92
100
  end
93
101
 
102
+ it 'returns a single HTTP range if the client asked for it and hints it can be satisfied from the first interval' do
103
+ response = IntervalResponse.new(seq, "HTTP_RANGE" => "bytes=0-0")
104
+ expect(response.status_code).to eq(206)
105
+ expect(response.content_length).to eq(1)
106
+ expect(response.headers).to eq(
107
+ "Accept-Ranges" => "bytes",
108
+ "Content-Length" => "1",
109
+ "Content-Range" => "bytes 0-0/8",
110
+ "Content-Type" => "binary/octet-stream",
111
+ 'ETag' => seq.etag,
112
+ )
113
+ expect(response.etag).to eq(seq.etag)
114
+ expect(response).not_to be_multiple_ranges
115
+ expect(response).to be_satisfied_with_first_interval
116
+
117
+ expect { |b|
118
+ response.each(&b)
119
+ }.to yield_successive_args([segment_a, 0..0])
120
+ end
121
+
94
122
  it 'returns a single HTTP range if the client asked for it and it can be satisfied, ETag matches' do
95
123
  response = IntervalResponse.new(seq, "HTTP_RANGE" => "bytes=2-4", "HTTP_IF_RANGE" => seq.etag)
96
124
  expect(response.status_code).to eq(206)
@@ -108,7 +136,7 @@ RSpec.describe IntervalResponse do
108
136
  }.to yield_successive_args([segment_a, 2..2], [segment_b, 0..1])
109
137
  end
110
138
 
111
- it 'responss with the entier resource if the Range is satisfiable but the If-Range specifies a different ETag than the sequence' do
139
+ it 'responds with the entire resource if the Range is satisfiable but the If-Range specifies a different ETag than the sequence' do
112
140
  response = IntervalResponse.new(seq, "HTTP_RANGE" => "bytes=12901-", "HTTP_IF_RANGE" => '"different"')
113
141
  expect(response.status_code).to eq(200)
114
142
  expect(response.content_length).to eq(8)
@@ -119,6 +147,8 @@ RSpec.describe IntervalResponse do
119
147
  'ETag' => seq.etag,
120
148
  )
121
149
  expect(response.etag).to eq(seq.etag)
150
+ expect(response).not_to be_multiple_ranges
151
+ expect(response).not_to be_satisfied_with_first_interval
122
152
  end
123
153
 
124
154
  it 'responds with the range that can be satisfied if asked for 2, of which one is unsatisfiable' do
@@ -133,6 +163,8 @@ RSpec.describe IntervalResponse do
133
163
  'ETag' => seq.etag,
134
164
  )
135
165
  expect(response.etag).to eq(seq.etag)
166
+ expect(response).not_to be_multiple_ranges
167
+ expect(response).not_to be_satisfied_with_first_interval
136
168
 
137
169
  expect { |b|
138
170
  response.each(&b)
@@ -152,6 +184,8 @@ RSpec.describe IntervalResponse do
152
184
  'ETag' => seq.etag,
153
185
  )
154
186
  expect(response.etag).to eq(seq.etag)
187
+ expect(response).to be_multiple_ranges
188
+ expect(response).to be_satisfied_with_first_interval
155
189
 
156
190
  output = StringIO.new
157
191
  response.each do |segment, range|
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: interval_response
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
4
+ version: 0.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Julik Tarkhanov
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-11-27 00:00:00.000000000 Z
11
+ date: 2021-05-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -148,6 +148,7 @@ files:
148
148
  - lib/interval_response/invalid.rb
149
149
  - lib/interval_response/lazy_file.rb
150
150
  - lib/interval_response/multi.rb
151
+ - lib/interval_response/rack_body_wrapper.rb
151
152
  - lib/interval_response/sequence.rb
152
153
  - lib/interval_response/single.rb
153
154
  - lib/interval_response/version.rb
@@ -162,7 +163,7 @@ metadata:
162
163
  homepage_uri: https://github.com/WeTransfer/interval_response
163
164
  source_code_uri: https://github.com/WeTransfer/interval_response
164
165
  changelog_uri: https://github.com/WeTransfer/interval_response
165
- post_install_message:
166
+ post_install_message:
166
167
  rdoc_options: []
167
168
  require_paths:
168
169
  - lib
@@ -178,7 +179,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
178
179
  version: '0'
179
180
  requirements: []
180
181
  rubygems_version: 3.0.3
181
- signing_key:
182
+ signing_key:
182
183
  specification_version: 4
183
184
  summary: Assemble HTTP responses from spliced sequences of payloads
184
185
  test_files: []