web_fetch 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|