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.
@@ -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