web_fetch 0.1.3 → 0.2.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 +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
|