web_fetch 0.1.3 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +132 -52
- data/config/locales/en.yml +4 -0
- data/doc/examples/blocking_requests.rb +33 -0
- data/doc/examples/non_blocking_requests.rb +34 -0
- data/doc/examples/use_uid_for_request.rb +28 -0
- data/docker/Dockerfile +3 -0
- data/lib/web_fetch/client.rb +47 -17
- data/lib/web_fetch/concerns/client_http.rb +25 -0
- data/lib/web_fetch/errors.rb +15 -0
- data/lib/web_fetch/event_machine_helpers.rb +26 -13
- data/lib/web_fetch/http_helpers.rb +22 -10
- data/lib/web_fetch/promise.rb +75 -0
- data/lib/web_fetch/request.rb +64 -0
- data/lib/web_fetch/resources.rb +7 -3
- data/lib/web_fetch/result.rb +31 -0
- data/lib/web_fetch/retriever.rb +17 -4
- data/lib/web_fetch/router.rb +22 -10
- data/lib/web_fetch/server.rb +22 -15
- data/lib/web_fetch/storage.rb +9 -4
- data/lib/web_fetch/version.rb +1 -1
- data/lib/web_fetch.rb +5 -0
- data/spec/client_spec.rb +52 -3
- data/spec/promise_spec.rb +119 -0
- data/spec/request_spec.rb +73 -0
- data/spec/resources_spec.rb +17 -1
- data/spec/result_spec.rb +27 -0
- data/spec/retriever_spec.rb +11 -8
- data/spec/router_spec.rb +6 -1
- data/spec/spec_helper.rb +1 -0
- data/spec/storage_spec.rb +6 -3
- data/swagger.yaml +42 -6
- data/web_fetch.gemspec +1 -0
- metadata +27 -2
- data/doc/client_example.rb +0 -19
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WebFetch
|
4
|
+
class Promise
|
5
|
+
attr_reader :uid, :request, :result
|
6
|
+
|
7
|
+
def initialize(client, options = {})
|
8
|
+
@client = client
|
9
|
+
@uid = options[:uid]
|
10
|
+
@request = Request.from_hash(options[:request])
|
11
|
+
end
|
12
|
+
|
13
|
+
def fetch(options = {})
|
14
|
+
return @result if complete?
|
15
|
+
|
16
|
+
block = options.fetch(:wait, true)
|
17
|
+
@raw_result = find_or_retrieve(block)
|
18
|
+
(@result = build_result)
|
19
|
+
end
|
20
|
+
|
21
|
+
def custom
|
22
|
+
request&.custom
|
23
|
+
end
|
24
|
+
|
25
|
+
def complete?
|
26
|
+
return false if @result.nil?
|
27
|
+
return false if pending?
|
28
|
+
return true if @result
|
29
|
+
|
30
|
+
false
|
31
|
+
end
|
32
|
+
|
33
|
+
def pending?
|
34
|
+
return false if @result.nil?
|
35
|
+
|
36
|
+
@result == :pending
|
37
|
+
end
|
38
|
+
|
39
|
+
def success?
|
40
|
+
complete? && @raw_result[:response][:success]
|
41
|
+
end
|
42
|
+
|
43
|
+
def error
|
44
|
+
return nil unless complete?
|
45
|
+
|
46
|
+
@raw_result[:response][:error]
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def find_or_retrieve(block)
|
52
|
+
block ? @client.retrieve_by_uid(@uid) : @client.find_by_uid(@uid)
|
53
|
+
end
|
54
|
+
|
55
|
+
def build_result
|
56
|
+
return nil if @raw_result.nil?
|
57
|
+
return :pending if @raw_result[:pending]
|
58
|
+
return nil unless @raw_result[:response]
|
59
|
+
|
60
|
+
response = @raw_result[:response]
|
61
|
+
new_result(response)
|
62
|
+
end
|
63
|
+
|
64
|
+
def new_result(response)
|
65
|
+
Result.new(
|
66
|
+
body: response[:body],
|
67
|
+
headers: response[:headers],
|
68
|
+
status: response[:status],
|
69
|
+
success: @raw_result[:response][:success],
|
70
|
+
error: @raw_result[:response][:error],
|
71
|
+
uid: @uid
|
72
|
+
)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WebFetch
|
4
|
+
class Request
|
5
|
+
attr_writer :url, :query, :headers, :body, :custom
|
6
|
+
attr_reader :url, :query, :headers, :body, :custom
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
yield self
|
10
|
+
end
|
11
|
+
|
12
|
+
def method=(val)
|
13
|
+
@method = val.downcase.to_sym
|
14
|
+
end
|
15
|
+
|
16
|
+
def method
|
17
|
+
@method ||= :get
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_h
|
21
|
+
{
|
22
|
+
url: url,
|
23
|
+
query: query,
|
24
|
+
headers: headers,
|
25
|
+
body: body,
|
26
|
+
method: method,
|
27
|
+
custom: custom
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
def eql?(other)
|
32
|
+
# Makes testing WebFetch a bit easier (based on real world case I hit
|
33
|
+
# using WebFetch in a Rails app)
|
34
|
+
other.to_h == to_h
|
35
|
+
end
|
36
|
+
|
37
|
+
def ==(other)
|
38
|
+
eql?(other)
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.from_hash(hash)
|
42
|
+
hash_copy = hash.dup
|
43
|
+
request = build_request(hash_copy)
|
44
|
+
raise ArgumentError, "Unrecognized keys: #{hash}" unless hash_copy.empty?
|
45
|
+
|
46
|
+
request
|
47
|
+
end
|
48
|
+
|
49
|
+
class << self
|
50
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
51
|
+
def build_request(hash)
|
52
|
+
Request.new do |request|
|
53
|
+
request.url = hash.delete(:url) if hash.key?(:url)
|
54
|
+
request.query = hash.delete(:query) if hash.key?(:query)
|
55
|
+
request.headers = hash.delete(:headers) if hash.key?(:headers)
|
56
|
+
request.body = hash.delete(:body) if hash.key?(:body)
|
57
|
+
request.method = hash.delete(:method) if hash.key?(:method)
|
58
|
+
request.custom = hash.delete(:custom) if hash.key?(:custom)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
data/lib/web_fetch/resources.rb
CHANGED
@@ -19,8 +19,8 @@ module WebFetch
|
|
19
19
|
end
|
20
20
|
end
|
21
21
|
|
22
|
-
def retrieve(server, params)
|
23
|
-
retriever = Retriever.new(server, params)
|
22
|
+
def retrieve(server, params, options = {})
|
23
|
+
retriever = Retriever.new(server, params, options)
|
24
24
|
unless retriever.valid?
|
25
25
|
return { status: status(:unprocessable),
|
26
26
|
payload: { error: retriever.errors } }
|
@@ -28,6 +28,10 @@ module WebFetch
|
|
28
28
|
defer_if_found(retriever)
|
29
29
|
end
|
30
30
|
|
31
|
+
def find(server, params)
|
32
|
+
retrieve(server, params, block: false)
|
33
|
+
end
|
34
|
+
|
31
35
|
private
|
32
36
|
|
33
37
|
def status(name)
|
@@ -51,7 +55,7 @@ module WebFetch
|
|
51
55
|
{ status: status(:not_found),
|
52
56
|
payload: { error: retriever.not_found_error } }
|
53
57
|
else
|
54
|
-
{
|
58
|
+
{ request: found }
|
55
59
|
end
|
56
60
|
end
|
57
61
|
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WebFetch
|
4
|
+
class Result
|
5
|
+
attr_reader :body, :headers, :status, :error, :uid
|
6
|
+
|
7
|
+
def initialize(options = {})
|
8
|
+
@pending = options.fetch(:pending, false)
|
9
|
+
return if pending?
|
10
|
+
|
11
|
+
@body = options.fetch(:body)
|
12
|
+
@headers = options.fetch(:headers)
|
13
|
+
@status = options.fetch(:status)
|
14
|
+
@success = options.fetch(:success)
|
15
|
+
@error = options.fetch(:error)
|
16
|
+
@uid = options.fetch(:uid)
|
17
|
+
end
|
18
|
+
|
19
|
+
def pending?
|
20
|
+
@pending
|
21
|
+
end
|
22
|
+
|
23
|
+
def complete?
|
24
|
+
!pending?
|
25
|
+
end
|
26
|
+
|
27
|
+
def success?
|
28
|
+
@success
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/web_fetch/retriever.rb
CHANGED
@@ -7,17 +7,20 @@ module WebFetch
|
|
7
7
|
|
8
8
|
attr_reader :not_found_error
|
9
9
|
|
10
|
-
def initialize(server, params)
|
10
|
+
def initialize(server, params, options)
|
11
11
|
@uid = params[:uid]
|
12
12
|
@hash = params[:hash]
|
13
13
|
@server = server
|
14
|
+
@block = options.fetch(:block, true)
|
14
15
|
end
|
15
16
|
|
16
17
|
def find
|
17
|
-
|
18
|
-
return not_found if
|
18
|
+
request = @server.storage.fetch(@uid)
|
19
|
+
return not_found if request.nil?
|
20
|
+
return not_found if request.nil?
|
21
|
+
return request.merge(pending: true) if pending?(request)
|
19
22
|
|
20
|
-
|
23
|
+
request
|
21
24
|
end
|
22
25
|
|
23
26
|
private
|
@@ -35,5 +38,15 @@ module WebFetch
|
|
35
38
|
end
|
36
39
|
nil
|
37
40
|
end
|
41
|
+
|
42
|
+
def pending?(request)
|
43
|
+
return false if request.nil?
|
44
|
+
return false if request[:succeeded]
|
45
|
+
# User requested blocking operation so we will wait until item is ready
|
46
|
+
# rather than return a `pending` status
|
47
|
+
return false if @block
|
48
|
+
|
49
|
+
true
|
50
|
+
end
|
38
51
|
end
|
39
52
|
end
|
data/lib/web_fetch/router.rb
CHANGED
@@ -15,19 +15,21 @@ module WebFetch
|
|
15
15
|
def route(url, options = {})
|
16
16
|
@server = options.delete(:server)
|
17
17
|
options = { query_string: nil, method: 'GET' }.merge(options)
|
18
|
-
|
18
|
+
|
19
19
|
Logger.info("#{url}: #{options}")
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
20
|
+
|
21
|
+
json_params = build_params(options)
|
22
|
+
return { status: 400, payload: I18n.t(:bad_json) } if json_params.nil?
|
23
|
+
|
24
|
+
resource = @router.recognize(
|
25
|
+
url, method: normalize_http_method(options[:method])
|
26
|
+
)
|
27
|
+
resource.call(resource.params.merge(json_params))
|
26
28
|
end
|
27
29
|
|
28
30
|
private
|
29
31
|
|
30
|
-
# rubocop:disable Metrics/MethodLength
|
32
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
31
33
|
def setup
|
32
34
|
resource_finder = lambda do |name, env|
|
33
35
|
Resources.public_send(name, @server, env)
|
@@ -42,12 +44,16 @@ module WebFetch
|
|
42
44
|
resource_finder.call(:gather, params)
|
43
45
|
}
|
44
46
|
|
45
|
-
get '/retrieve', to: lambda { |params|
|
47
|
+
get '/retrieve/:uid', to: lambda { |params|
|
46
48
|
resource_finder.call(:retrieve, params)
|
47
49
|
}
|
50
|
+
|
51
|
+
get '/find/:uid', to: lambda { |params|
|
52
|
+
resource_finder.call(:find, params)
|
53
|
+
}
|
48
54
|
end
|
49
55
|
end
|
50
|
-
# rubocop:enable Metrics/MethodLength
|
56
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
51
57
|
|
52
58
|
def build_params(options)
|
53
59
|
params = Rack::Utils.parse_nested_query(options[:query_string])
|
@@ -55,6 +61,8 @@ module WebFetch
|
|
55
61
|
params = symbolize(params)
|
56
62
|
params.merge!(options[:post_data] || {})
|
57
63
|
params
|
64
|
+
rescue JSON::ParserError
|
65
|
+
nil
|
58
66
|
end
|
59
67
|
|
60
68
|
def merge_json(params)
|
@@ -67,5 +75,9 @@ module WebFetch
|
|
67
75
|
def merge_json!(params)
|
68
76
|
params.merge!(merge_json(params))
|
69
77
|
end
|
78
|
+
|
79
|
+
def normalize_http_method(method)
|
80
|
+
method.downcase.to_sym
|
81
|
+
end
|
70
82
|
end
|
71
83
|
end
|
data/lib/web_fetch/server.rb
CHANGED
@@ -20,14 +20,8 @@ module WebFetch
|
|
20
20
|
def process_http_request
|
21
21
|
result = @router.route(@http_request_uri, request_params)
|
22
22
|
response = EM::DelegatedHttpResponse.new(self)
|
23
|
-
|
24
23
|
default_headers(response)
|
25
|
-
|
26
|
-
if result[:deferred].nil?
|
27
|
-
respond_immediately(result, response)
|
28
|
-
else
|
29
|
-
wait_for_response(result[:deferred], response)
|
30
|
-
end
|
24
|
+
outcome(result, response)
|
31
25
|
end
|
32
26
|
|
33
27
|
# Note that #gather is called by WebFetch itself to asynchronously gather
|
@@ -35,15 +29,28 @@ module WebFetch
|
|
35
29
|
# #process_http_request and subsequently WebFetch::Router#route
|
36
30
|
def gather(targets)
|
37
31
|
targets.each do |target|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
head: request[:headers],
|
43
|
-
query: request.fetch(:query, {}),
|
44
|
-
body: request.fetch(:body, nil))
|
45
|
-
@storage.store(target[:uid], uid: target[:uid], http: http)
|
32
|
+
http = request_async(target[:request])
|
33
|
+
request = { uid: target[:uid], deferred: http }
|
34
|
+
apply_callbacks(request)
|
35
|
+
@storage.store(target[:uid], request)
|
46
36
|
end
|
47
37
|
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def outcome(result, response)
|
42
|
+
# User requested an unrecognised ID
|
43
|
+
return respond_immediately(result, response) if result[:request].nil?
|
44
|
+
|
45
|
+
# Fetch has already completed
|
46
|
+
return succeed(result, response) if result[:succeeded]
|
47
|
+
return fail_(result, response) if result[:failed]
|
48
|
+
|
49
|
+
# User requested non-blocking call
|
50
|
+
return pending(result, response) if result[:request][:pending]
|
51
|
+
|
52
|
+
# User requested blocking call
|
53
|
+
wait_for_response(result[:request], response)
|
54
|
+
end
|
48
55
|
end
|
49
56
|
end
|
data/lib/web_fetch/storage.rb
CHANGED
@@ -1,16 +1,21 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module WebFetch
|
4
|
-
# Rudimentary global storage for responses
|
4
|
+
# Rudimentary global storage for responses. The intention is that this will
|
5
|
+
# one day prescribe an interface to e.g. memcached
|
5
6
|
class Storage
|
6
|
-
@
|
7
|
+
@storage = {}
|
7
8
|
|
8
9
|
def self.store(key, obj)
|
9
|
-
@
|
10
|
+
@storage[key] = obj
|
10
11
|
end
|
11
12
|
|
12
13
|
def self.fetch(key)
|
13
|
-
@
|
14
|
+
@storage.fetch(key, nil)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.delete(key)
|
18
|
+
@storage.delete(key)
|
14
19
|
end
|
15
20
|
end
|
16
21
|
end
|
data/lib/web_fetch/version.rb
CHANGED
data/lib/web_fetch.rb
CHANGED
@@ -30,6 +30,7 @@ require 'web_fetch/event_machine_helpers'
|
|
30
30
|
require 'web_fetch/http_helpers'
|
31
31
|
require 'web_fetch/concerns/validatable'
|
32
32
|
require 'web_fetch/concerns/http_helpers'
|
33
|
+
require 'web_fetch/concerns/client_http'
|
33
34
|
require 'web_fetch/storage'
|
34
35
|
require 'web_fetch/server'
|
35
36
|
require 'web_fetch/router'
|
@@ -37,4 +38,8 @@ require 'web_fetch/resources'
|
|
37
38
|
require 'web_fetch/gatherer'
|
38
39
|
require 'web_fetch/retriever'
|
39
40
|
require 'web_fetch/client'
|
41
|
+
require 'web_fetch/request'
|
42
|
+
require 'web_fetch/promise'
|
43
|
+
require 'web_fetch/result'
|
44
|
+
require 'web_fetch/errors'
|
40
45
|
require 'web_fetch/version'
|
data/spec/client_spec.rb
CHANGED
@@ -6,6 +6,12 @@ describe WebFetch::Client do
|
|
6
6
|
before(:each) do
|
7
7
|
stub_request(:any, 'http://blah.blah/success')
|
8
8
|
.to_return(body: 'hello, everybody')
|
9
|
+
|
10
|
+
# XXX: This does not do what we would hope in EventMachine context as it
|
11
|
+
# locks the entire reactor. I really don't know how to make webmock hook
|
12
|
+
# into EM and delay the response so we can test #find_by_uid :(
|
13
|
+
stub_request(:any, 'http://blah.blah/slow_success')
|
14
|
+
.to_return(body: ->(_req) { sleep(0.1) && 'hi' })
|
9
15
|
end
|
10
16
|
|
11
17
|
it 'can be instantiated with host and port params' do
|
@@ -20,15 +26,40 @@ describe WebFetch::Client do
|
|
20
26
|
|
21
27
|
describe '#gather' do
|
22
28
|
it 'makes `gather` requests to a running server' do
|
23
|
-
|
24
|
-
|
29
|
+
web_request = WebFetch::Request.new do |request|
|
30
|
+
request.url = 'http://blah.blah/success'
|
31
|
+
request.custom = { my_key: 'my_value' }
|
32
|
+
end
|
33
|
+
result = client.gather([web_request])
|
34
|
+
expect(result.first).to be_a WebFetch::Promise
|
35
|
+
expect(result.first.uid).to_not be_nil
|
36
|
+
expect(result.first.custom).to eql(my_key: 'my_value')
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'passes any WebFetch server errors back to the user' do
|
40
|
+
expect { client.gather([]) }.to raise_error WebFetch::ClientError
|
25
41
|
end
|
26
42
|
end
|
27
43
|
|
44
|
+
describe '#fetch' do
|
45
|
+
let(:responses) { client.gather([{ url: 'http://blah.blah/success' }]) }
|
46
|
+
|
47
|
+
subject { client.fetch(responses.first.uid) }
|
48
|
+
|
49
|
+
it { is_expected.to be_a WebFetch::Result }
|
50
|
+
context 'no matching request found' do
|
51
|
+
subject { proc { client.fetch('not-found') } }
|
52
|
+
it { is_expected.to raise_error WebFetch::RequestNotFoundError }
|
53
|
+
end
|
54
|
+
|
55
|
+
# Tested more extensively in supporting methods #retrieve_by_uid and
|
56
|
+
# #find_by_uid below
|
57
|
+
end
|
58
|
+
|
28
59
|
describe '#retrieve_by_uid' do
|
29
60
|
it 'retrieves a gathered item' do
|
30
61
|
result = client.gather([{ url: 'http://blah.blah/success' }])
|
31
|
-
uid = result.first
|
62
|
+
uid = result.first.uid
|
32
63
|
|
33
64
|
retrieved = client.retrieve_by_uid(uid)
|
34
65
|
expect(retrieved[:response][:status]).to eql 200
|
@@ -44,6 +75,24 @@ describe WebFetch::Client do
|
|
44
75
|
end
|
45
76
|
end
|
46
77
|
|
78
|
+
describe '#find_by_uid' do
|
79
|
+
it 'returns a ready status when has been fetched' do
|
80
|
+
pending 'Find a good way to create a slow response without locking EM'
|
81
|
+
result = client.gather([{ url: 'http://blah.blah/slow_success' }])
|
82
|
+
uid = result.first[:uid]
|
83
|
+
|
84
|
+
found = client.find_by_uid(uid)
|
85
|
+
expect(found[:pending]).to be true
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'returns nil for non-requested items' do
|
89
|
+
client.gather([{ url: 'http://blah.blah/success' }])
|
90
|
+
|
91
|
+
retrieved = client.find_by_uid('lalalala')
|
92
|
+
expect(retrieved).to be_nil
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
47
96
|
describe '#create' do
|
48
97
|
it 'spawns a server and returns a client able to connect' do
|
49
98
|
client = described_class.create('localhost', 8077, log: File::NULL)
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe WebFetch::Promise do
|
4
|
+
let(:uid) { 'abc123' }
|
5
|
+
let(:request) { { url: 'http://blah', custom: { my_key: 'my_value' } } }
|
6
|
+
let(:options) { { uid: uid, request: request } }
|
7
|
+
let(:client) { WebFetch::Client.new('test-host', 8080) }
|
8
|
+
let(:response) { described_class.new(client, options) }
|
9
|
+
let(:retrieve_url) { "http://#{client.host}:#{client.port}/retrieve/#{uid}" }
|
10
|
+
let(:find_url) { "http://#{client.host}:#{client.port}/find/#{uid}" }
|
11
|
+
|
12
|
+
let(:client_success) do
|
13
|
+
double(retrieve_by_uid: { response: { success: true } })
|
14
|
+
end
|
15
|
+
|
16
|
+
let(:client_failure) do
|
17
|
+
double(retrieve_by_uid: { response: { success: false, error: 'foo' } })
|
18
|
+
end
|
19
|
+
|
20
|
+
let(:client_pending) do
|
21
|
+
double(retrieve_by_uid: { pending: true })
|
22
|
+
end
|
23
|
+
|
24
|
+
let(:client_not_started) do
|
25
|
+
double(retrieve_by_uid: nil)
|
26
|
+
end
|
27
|
+
|
28
|
+
subject { response }
|
29
|
+
|
30
|
+
it { is_expected.to be_a described_class }
|
31
|
+
|
32
|
+
describe '#fetch' do
|
33
|
+
before do
|
34
|
+
stub_request(:get, retrieve_url).to_return(body: {}.to_json)
|
35
|
+
stub_request(:get, find_url).to_return(body: {}.to_json)
|
36
|
+
end
|
37
|
+
|
38
|
+
subject { response.fetch(fetch_options) }
|
39
|
+
|
40
|
+
context 'blocking call' do
|
41
|
+
let(:fetch_options) { { wait: true } }
|
42
|
+
it { is_expected.to have_requested(:get, retrieve_url) }
|
43
|
+
end
|
44
|
+
|
45
|
+
context 'non-blocking call' do
|
46
|
+
let(:fetch_options) { { wait: false } }
|
47
|
+
it { is_expected.to have_requested(:get, find_url) }
|
48
|
+
end
|
49
|
+
|
50
|
+
context 'default (blocking)' do
|
51
|
+
subject { response.fetch }
|
52
|
+
it { is_expected.to have_requested(:get, retrieve_url) }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe '#result' do
|
57
|
+
before do
|
58
|
+
stub_request(:get, retrieve_url)
|
59
|
+
.to_return(
|
60
|
+
body: { response: { success: true, body: 'abc123' } }.to_json
|
61
|
+
)
|
62
|
+
response.fetch
|
63
|
+
end
|
64
|
+
|
65
|
+
subject { response.result }
|
66
|
+
it { is_expected.to be_a WebFetch::Result }
|
67
|
+
its(:body) { is_expected.to eql 'abc123' }
|
68
|
+
end
|
69
|
+
|
70
|
+
describe '#complete?, #success?, #pending?, #error' do
|
71
|
+
before { response.fetch }
|
72
|
+
|
73
|
+
subject { response }
|
74
|
+
|
75
|
+
context 'request succeeded' do
|
76
|
+
let(:client) { client_success }
|
77
|
+
its(:complete?) { is_expected.to be true }
|
78
|
+
its(:success?) { is_expected.to be true }
|
79
|
+
its(:pending?) { is_expected.to be false }
|
80
|
+
its(:error) { is_expected.to be_nil }
|
81
|
+
end
|
82
|
+
|
83
|
+
context 'request failed' do
|
84
|
+
let(:client) { client_failure }
|
85
|
+
its(:complete?) { is_expected.to be true }
|
86
|
+
its(:success?) { is_expected.to be false }
|
87
|
+
its(:pending?) { is_expected.to be false }
|
88
|
+
its(:error) { is_expected.to eql 'foo' }
|
89
|
+
end
|
90
|
+
|
91
|
+
context 'request pending' do
|
92
|
+
let(:client) { client_pending }
|
93
|
+
its(:complete?) { is_expected.to be false }
|
94
|
+
its(:success?) { is_expected.to be false }
|
95
|
+
its(:pending?) { is_expected.to be true }
|
96
|
+
its(:error) { is_expected.to be_nil }
|
97
|
+
end
|
98
|
+
|
99
|
+
context 'request not started' do
|
100
|
+
let(:client) { client_not_started }
|
101
|
+
its(:complete?) { is_expected.to be false }
|
102
|
+
its(:success?) { is_expected.to be false }
|
103
|
+
its(:pending?) { is_expected.to be false }
|
104
|
+
its(:error) { is_expected.to be_nil }
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
describe '#request' do
|
109
|
+
subject { response.request }
|
110
|
+
it { is_expected.to be_a WebFetch::Request }
|
111
|
+
its(:custom) { is_expected.to eql(my_key: 'my_value') }
|
112
|
+
end
|
113
|
+
|
114
|
+
describe '#custom' do
|
115
|
+
# I think it's good to expose this directly on the response even though its
|
116
|
+
# accessible on as `response.request.custom`
|
117
|
+
its(:custom) { is_expected.to eql(my_key: 'my_value') }
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe WebFetch::Request do
|
4
|
+
let(:request) do
|
5
|
+
described_class.new do |request|
|
6
|
+
request.url = 'http://blah'
|
7
|
+
request.method = 'GET'
|
8
|
+
request.query = { foo: 'bar' }
|
9
|
+
request.headers = { 'Content-Type' => 'application/baz' }
|
10
|
+
request.body = 'abc123'
|
11
|
+
request.custom = { my_custom_key: 'my_custom_value' }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
let(:hash) do
|
16
|
+
{
|
17
|
+
url: 'http://blah',
|
18
|
+
method: :get,
|
19
|
+
query: { foo: 'bar' },
|
20
|
+
headers: { 'Content-Type' => 'application/baz' },
|
21
|
+
body: 'abc123',
|
22
|
+
custom: { my_custom_key: 'my_custom_value' }
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
subject { request }
|
27
|
+
it { is_expected.to be_a described_class }
|
28
|
+
its(:url) { is_expected.to eql 'http://blah' }
|
29
|
+
its(:method) { is_expected.to eql :get } # Normalised to lower-case symbols
|
30
|
+
its(:query) { is_expected.to eql(foo: 'bar') }
|
31
|
+
its(:headers) { are_expected.to eql('Content-Type' => 'application/baz') }
|
32
|
+
its(:body) { is_expected.to eql 'abc123' }
|
33
|
+
its(:custom) { is_expected.to eql(my_custom_key: 'my_custom_value') }
|
34
|
+
its(:to_h) { is_expected.to eql hash }
|
35
|
+
|
36
|
+
describe '#get' do
|
37
|
+
let(:request) { described_class.new { |request| } }
|
38
|
+
subject { request.method }
|
39
|
+
it { is_expected.to eql :get }
|
40
|
+
end
|
41
|
+
|
42
|
+
describe '#from_hash' do
|
43
|
+
subject do
|
44
|
+
described_class.from_hash(
|
45
|
+
url: 'a', method: 'GET', query: {},
|
46
|
+
headers: {}, body: 'abc', custom: {}
|
47
|
+
)
|
48
|
+
end
|
49
|
+
|
50
|
+
it { is_expected.to be_a described_class }
|
51
|
+
its(:url) { is_expected.to eql 'a' }
|
52
|
+
its(:method) { is_expected.to eql :get }
|
53
|
+
its(:query) { is_expected.to eql({}) }
|
54
|
+
its(:headers) { are_expected.to eql({}) }
|
55
|
+
its(:body) { is_expected.to eql 'abc' }
|
56
|
+
its(:custom) { is_expected.to eql({}) }
|
57
|
+
|
58
|
+
context 'unrecognised keys' do
|
59
|
+
subject { proc { described_class.from_hash(unkown_key: 'foo') } }
|
60
|
+
it { is_expected.to raise_error(ArgumentError) }
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
describe '#==' do
|
65
|
+
let(:request_copy) { described_class.from_hash(request.to_h) }
|
66
|
+
it { is_expected.to eq(request_copy) }
|
67
|
+
end
|
68
|
+
|
69
|
+
describe '#eql' do
|
70
|
+
let(:request_copy) { described_class.from_hash(request.to_h) }
|
71
|
+
it { is_expected.to eql(request_copy) }
|
72
|
+
end
|
73
|
+
end
|