http 0.8.14 → 0.9.0.pre

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.
@@ -1,13 +0,0 @@
1
- module HTTP
2
- class Cache
3
- # NoOp cache. Always makes the request. Allows avoiding
4
- # conditionals in the request flow.
5
- class NullCache
6
- # @return [Response] the result of the provided block
7
- # @yield [request, options] so that the request can actually be made
8
- def perform(request, options)
9
- yield(request, options)
10
- end
11
- end
12
- end
13
- end
@@ -1,95 +0,0 @@
1
- require "http/headers"
2
- require "http/cache/headers"
3
-
4
- module HTTP
5
- class Request
6
- # Decorator for requests to provide convenience methods related to caching.
7
- class Caching < DelegateClass(HTTP::Request)
8
- INVALIDATING_METHODS = [:post, :put, :delete, :patch].freeze
9
- CACHEABLE_METHODS = [:get, :head].freeze
10
-
11
- # When was this request sent to the server
12
- #
13
- # @api public
14
- attr_accessor :sent_at
15
-
16
- # Inits a new instance
17
- # @api private
18
- def initialize(obj)
19
- super
20
- @requested_at = nil
21
- @received_at = nil
22
- end
23
-
24
- # @return [HTTP::Request::Caching]
25
- def caching
26
- self
27
- end
28
-
29
- # @return [Boolean] true iff request demands the resources cache entry be invalidated
30
- #
31
- # @api public
32
- def invalidates_cache?
33
- INVALIDATING_METHODS.include?(verb) ||
34
- cache_headers.no_store?
35
- end
36
-
37
- # @return [Boolean] true if request is cacheable
38
- #
39
- # @api public
40
- def cacheable?
41
- CACHEABLE_METHODS.include?(verb) &&
42
- !cache_headers.no_store?
43
- end
44
-
45
- # @return [Boolean] true iff the cache control info of this
46
- # request demands that the response be revalidated by the origin
47
- # server.
48
- #
49
- # @api public
50
- def skips_cache?
51
- 0 == cache_headers.max_age ||
52
- cache_headers.must_revalidate? ||
53
- cache_headers.no_cache?
54
- end
55
-
56
- # @return [HTTP::Request::Caching] new request based on this
57
- # one but conditional on the resource having changed since
58
- # `cached_response`
59
- #
60
- # @api public
61
- def conditional_on_changes_to(cached_response)
62
- self.class.new HTTP::Request.new(
63
- verb, uri, headers.merge(conditional_headers_for(cached_response)),
64
- proxy, body, version).caching
65
- end
66
-
67
- # @return [HTTP::Cache::Headers] cache control helper for this request
68
- # @api public
69
- def cache_headers
70
- @cache_headers ||= HTTP::Cache::Headers.new headers
71
- end
72
-
73
- def env
74
- {"rack-cache.cache_key" => lambda { |r| r.uri.to_s }}
75
- end
76
-
77
- private
78
-
79
- # @return [Headers] conditional request headers
80
- def conditional_headers_for(cached_response)
81
- headers = HTTP::Headers.new
82
-
83
- cached_response.headers.get(HTTP::Headers::ETAG).
84
- each { |etag| headers.add(HTTP::Headers::IF_NONE_MATCH, etag) }
85
-
86
- cached_response.headers.get(HTTP::Headers::LAST_MODIFIED).
87
- each { |last_mod| headers.add(HTTP::Headers::IF_MODIFIED_SINCE, last_mod) }
88
-
89
- headers.add(HTTP::Headers::CACHE_CONTROL, "max-age=0") if cache_headers.forces_revalidation?
90
-
91
- headers
92
- end
93
- end
94
- end
95
- end
@@ -1,143 +0,0 @@
1
- require "http/headers"
2
- require "http/cache/headers"
3
- require "http/response/string_body"
4
- require "http/response/io_body"
5
-
6
- module HTTP
7
- class Response
8
- # Decorator class for responses to provide convenience methods
9
- # related to caching.
10
- class Caching < DelegateClass(HTTP::Response)
11
- CACHEABLE_RESPONSE_CODES = [200, 203, 300, 301, 410].freeze
12
-
13
- def initialize(obj)
14
- super
15
- @requested_at = nil
16
- @received_at = nil
17
- end
18
-
19
- # @return [HTTP::Response::Caching]
20
- def caching
21
- self
22
- end
23
-
24
- # @return [Boolean] true iff this response is stale
25
- def stale?
26
- expired? || cache_headers.must_revalidate?
27
- end
28
-
29
- # @return [Boolean] true iff this response has expired
30
- def expired?
31
- current_age >= cache_headers.max_age
32
- end
33
-
34
- # @return [Boolean] true iff this response is cacheable
35
- #
36
- # ---
37
- # A Vary header field-value of "*" always fails to match and
38
- # subsequent requests on that resource can only be properly
39
- # interpreted by the
40
- def cacheable?
41
- @cacheable ||=
42
- begin
43
- CACHEABLE_RESPONSE_CODES.include?(code) \
44
- && !(cache_headers.vary_star? ||
45
- cache_headers.no_store? ||
46
- cache_headers.no_cache?)
47
- end
48
- end
49
-
50
- # @return [Numeric] the current age (in seconds) of this response
51
- #
52
- # ---
53
- # Algo from https://tools.ietf.org/html/rfc2616#section-13.2.3
54
- def current_age
55
- now = Time.now
56
- age_value = headers.get(HTTP::Headers::AGE).map(&:to_i).max || 0
57
-
58
- apparent_age = [0, received_at - server_response_time].max
59
- corrected_received_age = [apparent_age, age_value].max
60
- response_delay = [0, received_at - requested_at].max
61
- corrected_initial_age = corrected_received_age + response_delay
62
- resident_time = [0, now - received_at].max
63
-
64
- corrected_initial_age + resident_time
65
- end
66
-
67
- # @return [Time] the time at which this response was requested
68
- def requested_at
69
- @requested_at ||= received_at
70
- end
71
- attr_writer :requested_at
72
-
73
- # @return [Time] the time at which this response was received
74
- def received_at
75
- @received_at ||= Time.now
76
- end
77
- attr_writer :received_at
78
-
79
- # Update self based on this response being revalidated by the
80
- # server.
81
- def validated!(validating_response)
82
- headers.merge!(validating_response.headers)
83
- self.requested_at = validating_response.requested_at
84
- self.received_at = validating_response.received_at
85
- end
86
-
87
- # @return [HTTP::Cache::Headers] cache control headers helper object.
88
- def cache_headers
89
- @cache_headers ||= HTTP::Cache::Headers.new headers
90
- end
91
-
92
- def body
93
- @body ||= if __getobj__.body.respond_to? :each
94
- __getobj__.body
95
- else
96
- StringBody.new(__getobj__.body.to_s)
97
- end
98
- end
99
-
100
- def body=(new_body)
101
- @body = if new_body.respond_to?(:readpartial) && new_body.respond_to?(:read)
102
- # IO-ish, probably a rack cache response body
103
- IoBody.new(new_body)
104
-
105
- elsif new_body.respond_to? :join
106
- # probably an array of body parts (rack cache does this sometimes)
107
- StringBody.new(new_body.join(""))
108
-
109
- elsif new_body.respond_to? :readpartial
110
- # normal body, just use it.
111
- new_body
112
-
113
- else
114
- # backstop, just to_s it
115
- StringBody.new(new_body.to_s)
116
- end
117
- end
118
-
119
- def vary
120
- headers.get(HTTP::Headers::VARY).first
121
- end
122
-
123
- protected
124
-
125
- # @return [Time] the time at which the server generated this response.
126
- def server_response_time
127
- headers.get(HTTP::Headers::DATE).
128
- map(&method(:to_time_or_epoch)).
129
- max || begin
130
- # set it if it is not already set
131
- headers[HTTP::Headers::DATE] = received_at.httpdate
132
- received_at
133
- end
134
- end
135
-
136
- def to_time_or_epoch(t_str)
137
- Time.httpdate(t_str)
138
- rescue ArgumentError
139
- Time.at(0)
140
- end
141
- end
142
- end
143
- end
@@ -1,63 +0,0 @@
1
- module HTTP
2
- class Response
3
- # A Body class that wraps an IO, rather than a the client
4
- # object.
5
- class IoBody
6
- include Enumerable
7
- extend Forwardable
8
-
9
- # @return [String,nil] the next `size` octets part of the
10
- # body, or nil if whole body has already been read.
11
- def readpartial(size = HTTP::Connection::BUFFER_SIZE)
12
- stream!
13
- return nil if stream.eof?
14
-
15
- stream.readpartial(size)
16
- end
17
-
18
- # Iterate over the body, allowing it to be enumerable
19
- def each
20
- while (part = readpartial)
21
- yield part
22
- end
23
- end
24
-
25
- # @return [String] eagerly consume the entire body as a string
26
- def to_s
27
- @contents ||= readall
28
- end
29
- alias_method :to_str, :to_s
30
-
31
- def_delegator :to_s, :empty?
32
-
33
- # Assert that the body is actively being streamed
34
- def stream!
35
- fail StateError, "body has already been consumed" if @streaming == false
36
- @streaming = true
37
- end
38
-
39
- # Easier to interpret string inspect
40
- def inspect
41
- "#<#{self.class}:#{object_id.to_s(16)} @streaming=#{!!@streaming}>"
42
- end
43
-
44
- protected
45
-
46
- def initialize(an_io)
47
- @streaming = nil
48
- @stream = an_io
49
- end
50
-
51
- attr_reader :contents, :stream
52
-
53
- def readall
54
- fail StateError, "body is being streamed" unless @streaming.nil?
55
-
56
- @streaming = false
57
- "".tap do |buf|
58
- buf << stream.read until stream.eof?
59
- end
60
- end
61
- end
62
- end
63
- end
@@ -1,53 +0,0 @@
1
- module HTTP
2
- class Response
3
- # A Body class that wraps a String, rather than a the client
4
- # object.
5
- class StringBody
6
- include Enumerable
7
- extend Forwardable
8
-
9
- # @return [String,nil] the next `size` octets part of the
10
- # body, or nil if whole body has already been read.
11
- def readpartial(size = @contents.length)
12
- stream!
13
- return nil if @streaming_offset >= @contents.length
14
-
15
- @contents[@streaming_offset, size].tap do |part|
16
- @streaming_offset += (part.length + 1)
17
- end
18
- end
19
-
20
- # Iterate over the body, allowing it to be enumerable
21
- def each
22
- yield @contents
23
- end
24
-
25
- # @return [String] eagerly consume the entire body as a string
26
- def to_s
27
- @contents
28
- end
29
- alias_method :to_str, :to_s
30
-
31
- def_delegator :@contents, :empty?
32
-
33
- # Assert that the body is actively being streamed
34
- def stream!
35
- fail StateError, "body has already been consumed" if @streaming == false
36
- @streaming = true
37
- end
38
-
39
- # Easier to interpret string inspect
40
- def inspect
41
- "#<#{self.class}:#{object_id.to_s(16)}>"
42
- end
43
-
44
- protected
45
-
46
- def initialize(contents)
47
- @contents = contents
48
- @streaming = nil
49
- @streaming_offset = 0
50
- end
51
- end
52
- end
53
- end
@@ -1,77 +0,0 @@
1
- RSpec.describe HTTP::Cache::Headers do
2
- subject(:cache_headers) { described_class.new headers }
3
-
4
- describe ".new" do
5
- it "accepts instance of HTTP::Headers" do
6
- expect { described_class.new HTTP::Headers.new }.not_to raise_error
7
- end
8
-
9
- it "it rejects any object that does not respond to #headers" do
10
- expect { described_class.new double }.to raise_error HTTP::Error
11
- end
12
- end
13
-
14
- context "with <Cache-Control: private>" do
15
- let(:headers) { {"Cache-Control" => "private"} }
16
- it { is_expected.to be_private }
17
- end
18
-
19
- context "with <Cache-Control: public>" do
20
- let(:headers) { {"Cache-Control" => "public"} }
21
- it { is_expected.to be_public }
22
- end
23
-
24
- context "with <Cache-Control: no-cache>" do
25
- let(:headers) { {"Cache-Control" => "no-cache"} }
26
- it { is_expected.to be_no_cache }
27
- end
28
-
29
- context "with <Cache-Control: no-store>" do
30
- let(:headers) { {"Cache-Control" => "no-store"} }
31
- it { is_expected.to be_no_store }
32
- end
33
-
34
- describe "#max_age" do
35
- subject { cache_headers.max_age }
36
-
37
- context "with <Cache-Control: max-age=100>" do
38
- let(:headers) { {"Cache-Control" => "max-age=100"} }
39
- it { is_expected.to eq 100 }
40
- end
41
-
42
- context "with <Expires: {100 seconds from now}>" do
43
- let(:headers) { {"Expires" => (Time.now + 100).httpdate} }
44
- it { is_expected.to be_within(1).of(100) }
45
- end
46
-
47
- context "with <Expires: {100 seconds before now}>" do
48
- let(:headers) { {"Expires" => (Time.now - 100).httpdate} }
49
- it { is_expected.to eq 0 }
50
- end
51
-
52
- context "with <Expires: -1>" do
53
- let(:headers) { {"Expires" => "-1"} }
54
- it { is_expected.to eq 0 }
55
- end
56
- end
57
-
58
- context "with <Vary: *>" do
59
- let(:headers) { {"Vary" => "*"} }
60
- it { is_expected.to be_vary_star }
61
- end
62
-
63
- context "with no cache related headers" do
64
- let(:headers) { {} }
65
-
66
- it { is_expected.not_to be_private }
67
- it { is_expected.not_to be_public }
68
- it { is_expected.not_to be_no_cache }
69
- it { is_expected.not_to be_no_store }
70
- it { is_expected.not_to be_vary_star }
71
-
72
- describe "#max_age" do
73
- subject { cache_headers.max_age }
74
- it { is_expected.to eq Float::INFINITY }
75
- end
76
- end
77
- end