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.
@@ -6,15 +6,15 @@ module WebFetch
6
6
 
7
7
  def initialize(client, options = {})
8
8
  @client = client
9
- @uid = options[:uid]
10
- @request = Request.from_hash(options[:request])
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
- (@response = @client.fetch(@uid, wait: wait))
17
+ @response = @client.fetch(@uid, wait: wait)
18
18
  end
19
19
 
20
20
  def custom
@@ -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 ||= :get
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
- 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)
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
@@ -6,23 +6,27 @@ module WebFetch
6
6
  class Resources
7
7
  class << self
8
8
  def root(_server, _params)
9
- { status: status(:ok), payload: { application: 'WebFetch' } }
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
- found = retriever.find
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
@@ -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(options = {})
8
- @pending = options.fetch(:pending, false)
7
+ def initialize(response)
8
+ @pending = response.fetch(:pending, false)
9
9
  return if pending?
10
10
 
11
- @body = options.fetch(:body)
12
- @headers = options.fetch(:headers)
13
- @status = options.fetch(:status)
14
- @request = Request.from_hash(options.fetch(:request))
15
- @success = options.fetch(:success)
16
- @error = options.fetch(:error)
17
- @uid = options.fetch(:uid)
18
- @response_time = options.fetch(:response_time)
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?
@@ -5,19 +5,16 @@ module WebFetch
5
5
  class Retriever
6
6
  include Validatable
7
7
 
8
- attr_reader :not_found_error
9
-
10
- def initialize(server, params, options)
8
+ def initialize(storage, params, options)
11
9
  @uid = params[:uid]
12
10
  @hash = params[:hash]
13
- @server = server
11
+ @storage = storage
14
12
  @block = options.fetch(:block, true)
15
13
  end
16
14
 
17
15
  def find
18
- request = @server.storage.fetch(@uid)
19
- return not_found if request.nil?
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 not_found
33
- @not_found_error = if !@uid.nil?
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
@@ -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
- result = @router.route(@http_request_uri, request_params)
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
- # Note that #gather is called by WebFetch itself to asynchronously gather
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 outcome(result, response)
45
- # User requested an unrecognised ID
46
- return respond_immediately(result, response) if result[:request].nil?
30
+ def immediate?(command)
31
+ %w[gather root].include?(command)
32
+ end
47
33
 
48
- # Fetch has already completed
49
- return succeed(result, response) if result[:succeeded]
50
- return fail_(result, response) if result[:failed]
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
- # User requested non-blocking call
53
- return pending(result, response) if result[:request][:pending]
40
+ succeeded = resource[:request][:response][:success]
41
+ return succeed(resource, response) if succeeded
54
42
 
55
- # User requested blocking call
56
- wait_for_response(result[:request], response)
43
+ fail_(resource, response)
57
44
  end
58
45
  end
59
46
  end
@@ -1,21 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module WebFetch
4
- # Rudimentary global storage for responses. The intention is that this will
5
- # one day prescribe an interface to e.g. memcached
6
- class Storage
7
- @storage = {}
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
- def self.store(key, obj)
10
- @storage[key] = obj
11
- end
12
-
13
- def self.fetch(key)
14
- @storage.fetch(key, nil)
15
- end
14
+ private
16
15
 
17
- def self.delete(key)
18
- @storage.delete(key)
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