web_fetch 0.4.0 → 0.5.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/.gitignore +2 -0
- data/.strong_versions.yml +6 -0
- data/Makefile +11 -2
- data/README.md +25 -6
- data/Rakefile +5 -3
- data/bin/strong_versions +29 -0
- data/config/locales/en.yml +0 -3
- data/docker/Dockerfile +2 -2
- data/lib/web_fetch.rb +4 -1
- data/lib/web_fetch/client.rb +28 -29
- data/lib/web_fetch/concerns/http_helpers.rb +8 -35
- data/lib/web_fetch/gatherer.rb +65 -3
- data/lib/web_fetch/logger.rb +1 -2
- data/lib/web_fetch/promise.rb +3 -3
- data/lib/web_fetch/request.rb +8 -15
- data/lib/web_fetch/resources.rb +12 -22
- data/lib/web_fetch/response.rb +15 -10
- data/lib/web_fetch/retriever.rb +6 -25
- data/lib/web_fetch/server.rb +15 -28
- data/lib/web_fetch/storage.rb +17 -13
- data/lib/web_fetch/storage/memcached.rb +45 -0
- data/lib/web_fetch/storage/memory.rb +35 -0
- data/lib/web_fetch/storage/redis.rb +45 -0
- data/lib/web_fetch/version.rb +1 -1
- data/manifest +64 -0
- data/spec/client_spec.rb +3 -6
- data/spec/gatherer_spec.rb +38 -21
- data/spec/promise_spec.rb +49 -11
- data/spec/resources_spec.rb +14 -17
- data/spec/response_spec.rb +13 -9
- data/spec/retriever_spec.rb +16 -17
- data/spec/router_spec.rb +6 -4
- data/spec/spec_helper.rb +15 -7
- data/spec/storage/memcached_spec.rb +27 -0
- data/spec/storage/memory_spec.rb +5 -0
- data/spec/storage/redis_spec.rb +27 -0
- data/spec/storage/shared_examples.rb +27 -0
- data/web_fetch.gemspec +9 -5
- metadata +75 -11
- data/lib/web_fetch/concerns/event_machine_helpers.rb +0 -57
- data/spec/storage_spec.rb +0 -27
data/lib/web_fetch/promise.rb
CHANGED
@@ -6,15 +6,15 @@ module WebFetch
|
|
6
6
|
|
7
7
|
def initialize(client, options = {})
|
8
8
|
@client = client
|
9
|
-
@uid = options
|
10
|
-
@request = Request.from_hash(options
|
9
|
+
@uid = options.fetch(:uid)
|
10
|
+
@request = Request.from_hash(options.fetch(:request))
|
11
11
|
end
|
12
12
|
|
13
13
|
def fetch(options = {})
|
14
14
|
return @response if complete?
|
15
15
|
|
16
16
|
wait = options.fetch(:wait, true)
|
17
|
-
|
17
|
+
@response = @client.fetch(@uid, wait: wait)
|
18
18
|
end
|
19
19
|
|
20
20
|
def custom
|
data/lib/web_fetch/request.rb
CHANGED
@@ -2,19 +2,16 @@
|
|
2
2
|
|
3
3
|
module WebFetch
|
4
4
|
class Request
|
5
|
-
attr_writer :url, :query, :headers, :body, :custom
|
5
|
+
attr_writer :url, :query, :headers, :body, :custom, :method
|
6
6
|
attr_reader :url, :query, :headers, :body, :custom
|
7
7
|
|
8
8
|
def initialize
|
9
|
+
@method = 'GET'
|
9
10
|
yield self
|
10
11
|
end
|
11
12
|
|
12
|
-
def method=(val)
|
13
|
-
@method = val.downcase.to_sym
|
14
|
-
end
|
15
|
-
|
16
13
|
def method
|
17
|
-
@method
|
14
|
+
@method.downcase.to_sym
|
18
15
|
end
|
19
16
|
|
20
17
|
def to_h
|
@@ -38,27 +35,23 @@ module WebFetch
|
|
38
35
|
eql?(other)
|
39
36
|
end
|
40
37
|
|
41
|
-
def self.from_hash(hash)
|
38
|
+
def self.from_hash(hash, options = {})
|
42
39
|
hash_copy = hash.dup
|
43
40
|
request = build_request(hash_copy)
|
41
|
+
return request unless options.fetch(:validate, true)
|
44
42
|
raise ArgumentError, "Unrecognized keys: #{hash}" unless hash_copy.empty?
|
45
43
|
|
46
44
|
request
|
47
45
|
end
|
48
46
|
|
49
47
|
class << self
|
50
|
-
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
51
48
|
def build_request(hash)
|
52
49
|
Request.new do |request|
|
53
|
-
|
54
|
-
|
55
|
-
|
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)
|
50
|
+
%i[url query headers body method custom].each do |key|
|
51
|
+
request.send("#{key}=", hash.delete(key))
|
52
|
+
end
|
59
53
|
end
|
60
54
|
end
|
61
|
-
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
62
55
|
end
|
63
56
|
end
|
64
57
|
end
|
data/lib/web_fetch/resources.rb
CHANGED
@@ -6,23 +6,27 @@ module WebFetch
|
|
6
6
|
class Resources
|
7
7
|
class << self
|
8
8
|
def root(_server, _params)
|
9
|
-
{
|
9
|
+
{
|
10
|
+
status: status(:ok),
|
11
|
+
command: 'root',
|
12
|
+
payload: { application: 'WebFetch' }
|
13
|
+
}
|
10
14
|
end
|
11
15
|
|
12
16
|
def gather(server, params)
|
13
|
-
gatherer = Gatherer.new(server, params)
|
17
|
+
gatherer = Gatherer.new(server.storage, params)
|
14
18
|
if gatherer.valid?
|
15
|
-
{ status: status(:ok), payload: gatherer.start }
|
19
|
+
{ status: status(:ok), payload: gatherer.start, command: 'gather' }
|
16
20
|
else
|
17
21
|
{ status: status(:unprocessable),
|
18
|
-
payload: { error: gatherer.errors } }
|
22
|
+
payload: { error: gatherer.errors }, command: 'gather' }
|
19
23
|
end
|
20
24
|
end
|
21
25
|
|
22
26
|
def retrieve(server, params, options = {})
|
23
|
-
retriever = Retriever.new(server, params, options)
|
27
|
+
retriever = Retriever.new(server.storage, params, options)
|
24
28
|
unless retriever.valid?
|
25
|
-
return { status: status(:unprocessable),
|
29
|
+
return { status: status(:unprocessable), command: 'retrieve',
|
26
30
|
payload: { error: retriever.errors } }
|
27
31
|
end
|
28
32
|
defer_if_found(retriever)
|
@@ -37,26 +41,12 @@ module WebFetch
|
|
37
41
|
def status(name)
|
38
42
|
{
|
39
43
|
ok: 200,
|
40
|
-
unprocessable: 422
|
41
|
-
not_found: 404
|
44
|
+
unprocessable: 422
|
42
45
|
}.fetch(name)
|
43
46
|
end
|
44
47
|
|
45
|
-
def not_found(retriever)
|
46
|
-
{
|
47
|
-
status: status(:not_found),
|
48
|
-
payload: { error: retriever.not_found_error }
|
49
|
-
}
|
50
|
-
end
|
51
|
-
|
52
48
|
def defer_if_found(retriever)
|
53
|
-
|
54
|
-
if found.nil?
|
55
|
-
{ status: status(:not_found),
|
56
|
-
payload: { error: retriever.not_found_error } }
|
57
|
-
else
|
58
|
-
{ request: found }
|
59
|
-
end
|
49
|
+
{ command: 'retrieve', request: retriever.find }
|
60
50
|
end
|
61
51
|
end
|
62
52
|
end
|
data/lib/web_fetch/response.rb
CHANGED
@@ -4,18 +4,23 @@ module WebFetch
|
|
4
4
|
class Response
|
5
5
|
attr_reader :request, :body, :headers, :status, :error, :uid, :response_time
|
6
6
|
|
7
|
-
def initialize(
|
8
|
-
@pending =
|
7
|
+
def initialize(response)
|
8
|
+
@pending = response.fetch(:pending, false)
|
9
9
|
return if pending?
|
10
10
|
|
11
|
-
|
12
|
-
@
|
13
|
-
@
|
14
|
-
@request = Request.from_hash(
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
11
|
+
outcome = response.fetch(:request)
|
12
|
+
@uid = outcome.fetch(:uid)
|
13
|
+
@response_time = outcome.fetch(:response_time, nil)
|
14
|
+
@request = Request.from_hash(outcome.fetch(:request), validate: false)
|
15
|
+
initialize_response(outcome.fetch(:response))
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize_response(response)
|
19
|
+
@body = Base64.decode64(response.fetch(:body))
|
20
|
+
@headers = response.fetch(:headers)
|
21
|
+
@status = response.fetch(:status)
|
22
|
+
@success = response.fetch(:success)
|
23
|
+
@error = response.fetch(:error, nil)
|
19
24
|
end
|
20
25
|
|
21
26
|
def pending?
|
data/lib/web_fetch/retriever.rb
CHANGED
@@ -5,19 +5,16 @@ module WebFetch
|
|
5
5
|
class Retriever
|
6
6
|
include Validatable
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
def initialize(server, params, options)
|
8
|
+
def initialize(storage, params, options)
|
11
9
|
@uid = params[:uid]
|
12
10
|
@hash = params[:hash]
|
13
|
-
@
|
11
|
+
@storage = storage
|
14
12
|
@block = options.fetch(:block, true)
|
15
13
|
end
|
16
14
|
|
17
15
|
def find
|
18
|
-
request = @
|
19
|
-
return
|
20
|
-
return request.merge(pending: true) if pending?(request)
|
16
|
+
request = @storage.fetch(@uid) unless @uid.nil?
|
17
|
+
return pending if request.nil?
|
21
18
|
|
22
19
|
request
|
23
20
|
end
|
@@ -29,24 +26,8 @@ module WebFetch
|
|
29
26
|
error(:missing_hash_and_uid) if @uid.nil? && @hash.nil?
|
30
27
|
end
|
31
28
|
|
32
|
-
def
|
33
|
-
@
|
34
|
-
I18n.t(:uid_not_found)
|
35
|
-
elsif !@hash.nil?
|
36
|
-
I18n.t(:hash_not_found)
|
37
|
-
end
|
38
|
-
nil
|
39
|
-
end
|
40
|
-
|
41
|
-
def pending?(request)
|
42
|
-
return false if request.nil?
|
43
|
-
return false if request[:succeeded]
|
44
|
-
return false if request[:failed]
|
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
|
29
|
+
def pending
|
30
|
+
{ uid: @uid, pending: true }
|
50
31
|
end
|
51
32
|
end
|
52
33
|
end
|
data/lib/web_fetch/server.rb
CHANGED
@@ -8,52 +8,39 @@ module WebFetch
|
|
8
8
|
|
9
9
|
include EM::HttpServer
|
10
10
|
include HTTPHelpers
|
11
|
-
include EventMachineHelpers
|
12
11
|
|
13
12
|
def post_init
|
14
13
|
super
|
15
14
|
@router = Router.new
|
16
|
-
@storage = Storage
|
15
|
+
@storage = WebFetch::Storage.create
|
16
|
+
|
17
17
|
no_environment_strings
|
18
18
|
end
|
19
19
|
|
20
20
|
def process_http_request
|
21
|
-
|
21
|
+
resource = @router.route(@http_request_uri, request_params)
|
22
22
|
response = EM::DelegatedHttpResponse.new(self)
|
23
23
|
default_headers(response)
|
24
|
-
outcome(result, response)
|
25
|
-
end
|
26
24
|
|
27
|
-
|
28
|
-
# the required HTTP objects. All public API requests go via
|
29
|
-
# #process_http_request and subsequently WebFetch::Router#route
|
30
|
-
def gather(targets)
|
31
|
-
targets.each do |target|
|
32
|
-
http = request_async(target)
|
33
|
-
request = { uid: target[:uid],
|
34
|
-
start_time: target[:start_time],
|
35
|
-
request: target[:request],
|
36
|
-
deferred: http }
|
37
|
-
apply_callbacks(request)
|
38
|
-
@storage.store(target[:uid], request)
|
39
|
-
end
|
25
|
+
outcome(resource, response)
|
40
26
|
end
|
41
27
|
|
42
28
|
private
|
43
29
|
|
44
|
-
def
|
45
|
-
|
46
|
-
|
30
|
+
def immediate?(command)
|
31
|
+
%w[gather root].include?(command)
|
32
|
+
end
|
47
33
|
|
48
|
-
|
49
|
-
|
50
|
-
|
34
|
+
def outcome(resource, response)
|
35
|
+
command = resource[:command]
|
36
|
+
Logger.debug(command)
|
37
|
+
return respond_immediately(resource, response) if immediate?(command)
|
38
|
+
return pending(resource, response) if resource[:request][:pending]
|
51
39
|
|
52
|
-
|
53
|
-
return
|
40
|
+
succeeded = resource[:request][:response][:success]
|
41
|
+
return succeed(resource, response) if succeeded
|
54
42
|
|
55
|
-
|
56
|
-
wait_for_response(result[:request], response)
|
43
|
+
fail_(resource, response)
|
57
44
|
end
|
58
45
|
end
|
59
46
|
end
|
data/lib/web_fetch/storage.rb
CHANGED
@@ -1,21 +1,25 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module WebFetch
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
4
|
+
module Storage
|
5
|
+
class << self
|
6
|
+
def create
|
7
|
+
{
|
8
|
+
'memory' => Memory,
|
9
|
+
'memcached' => Memcached,
|
10
|
+
'redis' => Redis
|
11
|
+
}.fetch(backend).new
|
12
|
+
end
|
8
13
|
|
9
|
-
|
10
|
-
@storage[key] = obj
|
11
|
-
end
|
12
|
-
|
13
|
-
def self.fetch(key)
|
14
|
-
@storage.fetch(key, nil)
|
15
|
-
end
|
14
|
+
private
|
16
15
|
|
17
|
-
|
18
|
-
|
16
|
+
def backend
|
17
|
+
ENV.fetch('WEB_FETCH_BACK_END', 'memory')
|
18
|
+
end
|
19
19
|
end
|
20
20
|
end
|
21
21
|
end
|
22
|
+
|
23
|
+
require 'web_fetch/storage/memcached'
|
24
|
+
require 'web_fetch/storage/memory'
|
25
|
+
require 'web_fetch/storage/redis'
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WebFetch
|
4
|
+
module Storage
|
5
|
+
class Memcached
|
6
|
+
def initialize(client = nil)
|
7
|
+
require 'dalli' if client.nil?
|
8
|
+
@client = client || Dalli::Client
|
9
|
+
@config = {
|
10
|
+
host: ENV.fetch('WEB_FETCH_MEMCACHED_HOST', 'localhost'),
|
11
|
+
port: ENV.fetch('WEB_FETCH_MEMCACHED_PORT', '11211')
|
12
|
+
}
|
13
|
+
end
|
14
|
+
|
15
|
+
def store(key, obj)
|
16
|
+
storage.set(key, obj.to_json)
|
17
|
+
end
|
18
|
+
|
19
|
+
def fetch(key)
|
20
|
+
result = storage.get(key)
|
21
|
+
return JSON.parse(result, symbolize_names: true) unless result.nil?
|
22
|
+
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
|
26
|
+
def delete(key)
|
27
|
+
storage.delete(key)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def storage
|
33
|
+
@storage ||= begin
|
34
|
+
host = @config.fetch(:host)
|
35
|
+
port = @config.fetch(:port)
|
36
|
+
@client.new("#{host}:#{port}", expires_in: ttl)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def ttl
|
41
|
+
@ttl ||= ENV.fetch('WEB_FETCH_MEMCACHED_TTL', '60').to_i
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WebFetch
|
4
|
+
module Storage
|
5
|
+
class Memory
|
6
|
+
@storage = {}
|
7
|
+
|
8
|
+
class << self
|
9
|
+
attr_reader :storage
|
10
|
+
end
|
11
|
+
|
12
|
+
def clear
|
13
|
+
self.class.storage.clear
|
14
|
+
end
|
15
|
+
|
16
|
+
def store(key, obj)
|
17
|
+
storage[key] = obj
|
18
|
+
end
|
19
|
+
|
20
|
+
def fetch(key)
|
21
|
+
storage.fetch(key, nil)
|
22
|
+
end
|
23
|
+
|
24
|
+
def delete(key)
|
25
|
+
storage.delete(key)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def storage
|
31
|
+
self.class.storage
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WebFetch
|
4
|
+
module Storage
|
5
|
+
class Redis
|
6
|
+
def initialize(client = nil)
|
7
|
+
require 'redis' if client.nil?
|
8
|
+
@client = client || ::Redis
|
9
|
+
@config = {
|
10
|
+
host: ENV.fetch('WEB_FETCH_REDIS_HOST', 'localhost'),
|
11
|
+
port: ENV.fetch('WEB_FETCH_REDIS_PORT', '6379')
|
12
|
+
}
|
13
|
+
end
|
14
|
+
|
15
|
+
def store(key, obj)
|
16
|
+
storage.set(key, obj.to_json, ex: ttl)
|
17
|
+
end
|
18
|
+
|
19
|
+
def fetch(key)
|
20
|
+
result = storage.get(key)
|
21
|
+
return JSON.parse(result, symbolize_names: true) unless result.nil?
|
22
|
+
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
|
26
|
+
def delete(key)
|
27
|
+
storage.del(key)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def storage
|
33
|
+
@storage ||= begin
|
34
|
+
host = @config.fetch(:host)
|
35
|
+
port = @config.fetch(:port)
|
36
|
+
@client.new(url: "redis://#{host}:#{port}")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def ttl
|
41
|
+
@ttl ||= ENV.fetch('WEB_FETCH_REDIS_TTL', '60').to_i
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|