interval_response 0.1.6 → 0.1.7

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  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: []