web_fetch 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +10 -0
  5. data/.ruby-version +1 -0
  6. data/Gemfile +5 -0
  7. data/Gemfile.lock +120 -0
  8. data/LICENSE +7 -0
  9. data/README.md +149 -0
  10. data/TODO +0 -0
  11. data/bin/rspec +29 -0
  12. data/bin/rubocop +29 -0
  13. data/bin/web_fetch_control +6 -0
  14. data/bin/web_fetch_server +30 -0
  15. data/config/locales/en.yml +12 -0
  16. data/doc/client_example.rb +19 -0
  17. data/doc/web_fetch_architecture.png +0 -0
  18. data/lib/web_fetch/client.rb +101 -0
  19. data/lib/web_fetch/concerns/http_helpers.rb +64 -0
  20. data/lib/web_fetch/concerns/validatable.rb +31 -0
  21. data/lib/web_fetch/event_machine_helpers.rb +36 -0
  22. data/lib/web_fetch/gatherer.rb +62 -0
  23. data/lib/web_fetch/helpers.rb +11 -0
  24. data/lib/web_fetch/http_helpers.rb +71 -0
  25. data/lib/web_fetch/logger.rb +29 -0
  26. data/lib/web_fetch/resources.rb +59 -0
  27. data/lib/web_fetch/retriever.rb +39 -0
  28. data/lib/web_fetch/router.rb +71 -0
  29. data/lib/web_fetch/server.rb +49 -0
  30. data/lib/web_fetch/storage.rb +16 -0
  31. data/lib/web_fetch/version.rb +5 -0
  32. data/lib/web_fetch.rb +40 -0
  33. data/spec/client_spec.rb +63 -0
  34. data/spec/concerns/validatable_spec.rb +53 -0
  35. data/spec/features/http_fetching_spec.rb +0 -0
  36. data/spec/gatherer_spec.rb +109 -0
  37. data/spec/helpers_spec.rb +18 -0
  38. data/spec/i18n_spec.rb +8 -0
  39. data/spec/resources_spec.rb +42 -0
  40. data/spec/retriever_spec.rb +68 -0
  41. data/spec/router_spec.rb +43 -0
  42. data/spec/server_spec.rb +96 -0
  43. data/spec/spec_helper.rb +55 -0
  44. data/spec/storage_spec.rb +24 -0
  45. data/swagger.yaml +115 -0
  46. data/web_fetch.gemspec +41 -0
  47. metadata +314 -0
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe WebFetch::Gatherer do
4
+ let(:server) { WebFetch::MockServer.new }
5
+
6
+ let(:valid_params) do
7
+ { requests: [
8
+ { url: 'http://localhost:8089' },
9
+ { url: 'http://remotehost:8089' }
10
+ ] }
11
+ end
12
+
13
+ it 'is initialisable with params' do
14
+ expect(described_class.new(server, valid_params)).to be_a described_class
15
+ end
16
+
17
+ describe 'validation' do
18
+ context 'invalid' do
19
+ it 'is invalid if `requests` parameter is not passed' do
20
+ gatherer = described_class.new(server, {})
21
+ expect(gatherer.valid?).to be false
22
+ expect(gatherer.errors).to include I18n.t(:requests_missing)
23
+ end
24
+
25
+ it 'is invalid if `requests` is not an array parameter' do
26
+ gatherer = described_class.new(server, requests: 'hello')
27
+ expect(gatherer.valid?).to be false
28
+ expect(gatherer.errors).to include I18n.t(:requests_not_array)
29
+ end
30
+
31
+ it 'is invalid if `requests` is an empty array' do
32
+ gatherer = described_class.new(server, requests: [])
33
+ expect(gatherer.valid?).to be false
34
+ expect(gatherer.errors).to include I18n.t(:requests_empty)
35
+ end
36
+
37
+ it 'is invalid if `url` missing from any requests' do
38
+ gatherer = described_class.new(server, requests: [{ url: 'hello' }, {}])
39
+ expect(gatherer.valid?).to be false
40
+ expect(gatherer.errors).to include I18n.t(:missing_url)
41
+ end
42
+ end
43
+
44
+ context 'valid' do
45
+ it 'is valid when passed valid params' do
46
+ expect(described_class.new(server, valid_params).valid?).to be true
47
+ end
48
+ end
49
+ end
50
+
51
+ describe '#start' do
52
+ it 'returns a hash containing sha1 hashes of requests' do
53
+ result = described_class.new(server, valid_params).start
54
+ hash = Digest::SHA1.new.digest(JSON.dump(valid_params[:requests].first))
55
+ expect(result[:requests].first[:hash]).to eql Digest.hexencode(hash)
56
+ end
57
+
58
+ it 'respects url, headers, http method and query when calculating sha1' do
59
+ req1 = { url: 'http://blah', query_string: 'a=1',
60
+ headers: { 'Content-Type' => 'whatever' } }
61
+ req2 = { url: 'http://blah', query_string: 'b=2',
62
+ headers: { 'Content-Type' => 'whatever' } }
63
+ req3 = { url: 'http://hello', query_string: 'a=1',
64
+ headers: { 'Content-Type' => 'whatever' } }
65
+ req4 = { url: 'http://blah', query_string: 'a=1',
66
+ headers: { 'Content-Type' => 'hello' } }
67
+ req5 = { url: 'http://blah', query_string: 'a=1',
68
+ headers: { 'Content-Type' => 'hello' },
69
+ method: 'PUT' }
70
+ results = [req1, req2, req3, req4, req5].map do |req|
71
+ described_class.new(server, requests: [req], _server: server).start
72
+ end
73
+ hashes = results.map { |res| res[:requests].first[:hash] }
74
+ expect(hashes.uniq.length).to eql 5
75
+ end
76
+
77
+ it 'returns a hash containing unique IDs for requests' do
78
+ result = described_class.new(server, valid_params).start
79
+ uid1 = result[:requests][0][:uid]
80
+ uid2 = result[:requests][1][:uid]
81
+ expect(uid1).to_not eql uid2
82
+ end
83
+
84
+ describe 'auxiliary request data' do
85
+ it 'is included in response' do
86
+ # Ensure that the requester can embed their own identifiers to link to
87
+ # the uid of the delegated request
88
+ params = { requests: [url: '-', bob: 'hello'],
89
+ _server: server }
90
+ result = described_class.new(server, params).start
91
+ expect(result[:requests].first[:request][:bob]).to eql 'hello'
92
+ end
93
+
94
+ it 'does not affect request hash' do
95
+ # Ensure that only pertinent values are used to compute hash (i.e.
96
+ # adding auxiliary data will still allow retrieval by hash for
97
+ # otherwise duplicate requests
98
+ params1 = { requests: [url: 'http://blah', bob: 'hello'],
99
+ _server: server }
100
+ result1 = described_class.new(server, params1).start
101
+
102
+ params2 = { requests: [url: 'http://blah', not_bob: 'good bye'],
103
+ _server: server }
104
+ result2 = described_class.new(server, params2).start
105
+ expect(result1[:requests][0][:hash]).to eql result2[:requests][0][:hash]
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe WebFetch::Helpers do
4
+ class HelperIncluder
5
+ include WebFetch::Helpers
6
+ end
7
+
8
+ subject { HelperIncluder.new }
9
+
10
+ describe '#symbolize' do
11
+ it 'handles variously-nested hashes and symbolizes all keys' do
12
+ nested_hash = { 'a': 1, 'b': { 'c': 2, 'd': [{ 'e': 3 }] } }
13
+ expect(subject.symbolize(nested_hash)).to eql(
14
+ a: 1, b: { c: 2, d: [{ e: 3 }] }
15
+ )
16
+ end
17
+ end
18
+ end
data/spec/i18n_spec.rb ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe 'Internationalisation' do
4
+ it 'accesses translation files and generates translations' do
5
+ # Just a quick sanity check to make sure the translation file is loaded
6
+ expect(I18n.t(:requests_missing)).to eql '`requests` parameter missing'
7
+ end
8
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe WebFetch::Resources do
4
+ let(:server) { WebFetch::MockServer.new }
5
+
6
+ describe '.root' do
7
+ it 'responds with application name' do
8
+ expect(described_class.root(nil, nil))
9
+ .to eql(status: 200, payload: { application: 'WebFetch' })
10
+ end
11
+ end
12
+
13
+ describe '.gather' do
14
+ let(:result) do
15
+ described_class.gather(server, requests: [{ url: 'http://google.com' }])
16
+ end
17
+
18
+ it 'provides a `gather` resource' do
19
+ expect(result[:status]).to eql 200
20
+ end
21
+
22
+ it 'responds with hash' do
23
+ expect(result[:payload]).to be_a Hash
24
+ end
25
+ end
26
+
27
+ describe 'retrieve' do
28
+ it 'gives 404 not found when unrecognised uid requested' do
29
+ result = described_class.retrieve(server, uid: '123')
30
+ expect(result[:status]).to eql 404
31
+ error = result[:payload][:error]
32
+ expect(error).to eql I18n.t(:uid_not_found)
33
+ end
34
+
35
+ it 'gives 404 not found when unrecognised hash requested' do
36
+ result = described_class.retrieve(server, hash: 'abc')
37
+ expect(result[:status]).to eql 404
38
+ error = result[:payload][:error]
39
+ expect(error).to eql I18n.t(:hash_not_found)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe WebFetch::Retriever do
4
+ let(:server) { WebFetch::MockServer.new }
5
+ let(:valid_params) { { uid: 'abc123' } }
6
+
7
+ it 'is initialisable with params' do
8
+ expect(described_class.new(server, valid_params)).to be_a described_class
9
+ end
10
+
11
+ describe 'validation' do
12
+ context 'valid' do
13
+ it 'is valid when `uid` given' do
14
+ retriever = described_class.new(server, uid: 'abc123')
15
+ expect(retriever.valid?).to be true
16
+ end
17
+
18
+ it 'is valid when `hash` given' do
19
+ retriever = described_class.new(server, hash: 'def456')
20
+ expect(retriever.valid?).to be true
21
+ end
22
+ end
23
+
24
+ context 'invalid' do
25
+ it 'is invalid if both `hash` and `uid` given' do
26
+ retriever = described_class.new(server, hash: 'def456', uid: 'abc123')
27
+ expect(retriever.valid?).to be false
28
+ expect(retriever.errors).to include I18n.t(:hash_or_uid_but_not_both)
29
+ end
30
+
31
+ it 'is invalid if neither `hash` nor `uid` given' do
32
+ retriever = described_class.new(server, {})
33
+ expect(retriever.valid?).to be false
34
+ expect(retriever.errors).to include I18n.t(:missing_hash_and_uid)
35
+ end
36
+ end
37
+ end
38
+
39
+ describe '#find' do
40
+ it 'returns `nil` when given uid has not been requested' do
41
+ retriever = described_class.new(server, uid: 'nope')
42
+ expect(retriever.find).to be_nil
43
+ end
44
+
45
+ it 'returns `nil` when given hash has not been requested' do
46
+ retriever = described_class.new(server, hash: 'also nope')
47
+ expect(retriever.find).to be_nil
48
+ end
49
+
50
+ it 'returns payload when request has been retrieved' do
51
+ # This test is somewhat useless as we have to mock the behaviour of our
52
+ # fake Server instance a little too much (since the actual Server object
53
+ # we're interested in exists in a separate EventMachine thread while we
54
+ # run our tests). The full stack is tested by the Client specs, however.
55
+ url = 'http://blah.blah/success'
56
+ stub_request(:any, url)
57
+
58
+ gatherer = WebFetch::Gatherer.new(server, requests: [{ url: url }])
59
+ response = gatherer.start
60
+ uid = response[:requests].first[:uid]
61
+ expect(server).to receive(:storage)
62
+ .and_return(uid => { body: 'fake body' })
63
+
64
+ retriever = described_class.new(server, uid: uid)
65
+ expect(retriever.find[:body]).to eql 'fake body'
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe WebFetch::Router do
4
+ let(:router) { described_class.new }
5
+
6
+ it 'can be initialised' do
7
+ expect(router).to be_a described_class
8
+ end
9
+
10
+ describe '#route' do
11
+ it 'provides a route to GET /' do
12
+ expect(router.route('/'))
13
+ .to eql(status: 200, payload: { application: 'WebFetch' })
14
+ end
15
+
16
+ it 'provides a route to POST /gather' do
17
+ expect(WebFetch::Resources).to receive(:gather).and_return('hello')
18
+ expect(router.route('/gather', method: 'POST')).to eql 'hello'
19
+ end
20
+
21
+ it 'provides a route to GET /retrieve' do
22
+ expect(WebFetch::Resources).to receive(:retrieve).and_return('hello')
23
+ expect(router.route('/retrieve', method: 'GET')).to eql 'hello'
24
+ end
25
+
26
+ it 'decodes `json` parameter and merges into request params' do
27
+ json = { a: 10, b: [1, 2, 3] }
28
+ expect(WebFetch::Resources).to receive(:gather).with(nil, json)
29
+ router.route('/gather',
30
+ method: 'POST',
31
+ query_string: "json=#{JSON.dump(json)}",
32
+ server: nil)
33
+ end
34
+
35
+ it 'returns appropriate response when invaid json provided' do
36
+ result = router.route('/gather',
37
+ method: 'POST',
38
+ query_string: 'json=uh oh :(',
39
+ server: nil)
40
+ expect(result).to eql(status: 400, payload: I18n.t(:bad_json))
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe WebFetch::Server do
4
+ let(:port) { 8089 }
5
+ let(:host) { 'localhost' }
6
+ let(:host_uri) { "http://#{host}:#{port}" }
7
+
8
+ it 'accepts HTTP connections' do
9
+ response = get(host_uri)
10
+ expect(response.success?).to be true
11
+ expect(JSON.parse(response.body)['application']).to eql 'WebFetch'
12
+ end
13
+
14
+ describe '/gather' do
15
+ before(:each) do
16
+ stub_request(:any, 'http://blah.blah/success')
17
+ stub_request(:any, 'http://blah.blah/success?a=1')
18
+ end
19
+
20
+ it 'responds "Unprocessable Entity" when incomplete params passed' do
21
+ response = post("#{host_uri}/gather", bob: ':(')
22
+ expect(response.status).to eql 422
23
+ end
24
+
25
+ it 'responds with uid for each request' do
26
+ params = { requests: [{ url: 'http://blah.blah/success' }] }
27
+ response = post("#{host_uri}/gather", params)
28
+ expect(JSON.parse(response.body)['requests'].first['uid'])
29
+ .to_not be_empty
30
+ end
31
+
32
+ it 'responds with a hash that respects url, query string and headers' do
33
+ params1 = { url: 'http://blah.blah/success' }
34
+ params2 = { url: 'http://blah.blah/success?a=1' }
35
+ params3 = { url: 'http://blah.blah/success',
36
+ headers: { 'Content-Type' => 'whatever' } }
37
+
38
+ responses = [params1, params2, params3].map do |params|
39
+ post("#{host_uri}/gather", requests: [params])
40
+ end
41
+ hashes = responses.map do |res|
42
+ JSON.parse(res.body)['requests'].first['hash']
43
+ end
44
+ expect(hashes.uniq.length).to eql 3
45
+ end
46
+ end
47
+
48
+ describe '#gather' do
49
+ it 'respects a given url' do
50
+ stub = stub_request(:any, 'http://blah.blah/success')
51
+ params = { requests: [{ url: 'http://blah.blah/success' }] }
52
+ post("#{host_uri}/gather", params)
53
+ expect(stub).to have_been_requested
54
+ end
55
+
56
+ it 'respects given query parameters' do
57
+ stub = stub_request(:any, 'http://blah.blah/success?a=1')
58
+ params = { requests: [{ url: 'http://blah.blah/success?a=1' }] }
59
+ post("#{host_uri}/gather", params)
60
+ expect(stub).to have_been_requested
61
+ end
62
+
63
+ it 'respects given headers' do
64
+ stub = stub_request(:any, 'http://blah.blah/success')
65
+ .with(headers: { 'Content-Type' => 'whatever' })
66
+ request = { url: 'http://blah.blah/success',
67
+ headers: { 'Content-Type' => 'whatever' } }
68
+ post("#{host_uri}/gather", requests: [request])
69
+ expect(stub).to have_been_requested
70
+ end
71
+
72
+ it 'respects given http method' do
73
+ stub = stub_request(:post, 'http://blah.blah/success?a=1')
74
+ params = { requests: [{ url: 'http://blah.blah/success?a=1',
75
+ method: 'POST' }] }
76
+ post("#{host_uri}/gather", params)
77
+ expect(stub).to have_been_requested
78
+ end
79
+ end
80
+
81
+ private
82
+
83
+ def get(uri)
84
+ Faraday.get(uri)
85
+ end
86
+
87
+ def post(uri, params)
88
+ parsed = URI.parse(uri)
89
+ base_uri = "#{parsed.scheme}://#{parsed.host}:#{parsed.port}"
90
+ conn = Faraday.new(url: base_uri)
91
+ conn.post do |request|
92
+ request.url parsed.path
93
+ request.body = params.to_json
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'web_fetch'
4
+ require 'pp'
5
+ require 'byebug'
6
+ require 'webmock/rspec'
7
+
8
+ WebMock.disable_net_connect!(allow_localhost: true)
9
+
10
+ # This is pretty ugly but seems to do the job
11
+ puts 'Starting test server'
12
+ WebFetch::Logger.logger(File::NULL)
13
+
14
+ Thread.new do
15
+ EM.run do
16
+ EM.start_server 'localhost', 8089, WebFetch::Server
17
+ end
18
+ end
19
+ waiting = true
20
+ while waiting
21
+ begin
22
+ res = Faraday.get('http://localhost:8089/')
23
+ rescue Faraday::ConnectionFailed
24
+ res = nil
25
+ end
26
+ waiting = !res.nil? && res.status != 200
27
+ sleep 0.1
28
+ end
29
+ puts 'Test server started'
30
+
31
+ module WebFetch
32
+ class MockServer
33
+ def gather(requests); end
34
+
35
+ def storage
36
+ Storage
37
+ end
38
+ end
39
+ end
40
+
41
+ RSpec.configure do |config|
42
+ config.expect_with :rspec do |expectations|
43
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
44
+ end
45
+
46
+ config.mock_with :rspec do |mocks|
47
+ mocks.verify_partial_doubles = true
48
+ end
49
+
50
+ config.shared_context_metadata_behavior = :apply_to_host_groups
51
+
52
+ config.order = :random
53
+
54
+ Kernel.srand config.seed
55
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe WebFetch::Storage do
4
+ describe '.store' do
5
+ it 'accepts a key and value to store' do
6
+ expect do
7
+ described_class.store(:key, :value)
8
+ end.to_not raise_error
9
+ end
10
+ end
11
+
12
+ describe '.fetch' do
13
+ it 'fetches stored values' do
14
+ described_class.store(:key, :value)
15
+ expect(described_class.fetch(:key)).to eql :value
16
+ end
17
+
18
+ it 'removes item from storage after retrieval' do
19
+ described_class.store(:key, :value)
20
+ described_class.fetch(:key)
21
+ expect(described_class.fetch(:key)).to be_nil
22
+ end
23
+ end
24
+ end
data/swagger.yaml ADDED
@@ -0,0 +1,115 @@
1
+ swagger: '2.0'
2
+ info:
3
+ title: WebFetch API
4
+ description: Asynchronously fetch HTTP entities
5
+ version: "0.0.1"
6
+ schemes:
7
+ - http
8
+ produces:
9
+ - application/json
10
+ paths:
11
+ /:
12
+ get:
13
+ summary: Status
14
+ description: |
15
+ Identifies WebFetch server.
16
+ responses:
17
+ 200:
18
+ description: An object with an application identifier
19
+ schema:
20
+ type: object
21
+ properties:
22
+ application:
23
+ type: string
24
+ description: WebFetch
25
+
26
+ /gather:
27
+ post:
28
+ summary: Initiate fetching one or more HTTP entities.
29
+ description: |
30
+ Receives an array of HTTP entities as objects and returns an array of objects providing a unique identifier, a hash, and the original request parameters. The unique identifier can be used to retrieve the entity when it has completed downloading.
31
+ parameters:
32
+ - name: requests
33
+ in: body
34
+ description: HTTP entities to be gathered.
35
+ required: true
36
+ schema:
37
+ type: array
38
+ items:
39
+ $ref: '#/definitions/Request'
40
+ responses:
41
+ 200:
42
+ description: An array of objects providing job IDs and original parameters.
43
+ schema:
44
+ $ref: '#/definitions/GatherResponse'
45
+
46
+
47
+ /retrieve/{id}:
48
+ get:
49
+ summary: Retrieve a gathered HTTP entity.
50
+ description: |
51
+ Receives a unique identifier and returns the previously requested HTTP entity. This action will block until the entity has been successfully downloaded.
52
+ parameters:
53
+ - name: id
54
+ in: path
55
+ type: string
56
+ description: Unique identifier for HTTP entity.
57
+ required: true
58
+ responses:
59
+ 200:
60
+ description: An object containing HTTP entity elements.
61
+ schema:
62
+ type: object
63
+ items:
64
+ $ref: '#/definitions/Retrieved'
65
+
66
+ definitions:
67
+ Request:
68
+ type: object
69
+ properties:
70
+ url:
71
+ type: string
72
+ description: URL of desired HTTP entity.
73
+ method:
74
+ type: string
75
+ default: GET
76
+ description: HTTP method.
77
+ headers:
78
+ type: object
79
+ description: HTTP headers.
80
+ query:
81
+ type: object
82
+ description: Query parameters.
83
+ body:
84
+ type: string
85
+ description: HTTP body.
86
+ Retrieved:
87
+ type: object
88
+ properties:
89
+ response:
90
+ type: object
91
+ description: Requested HTTP entity elements.
92
+ properties:
93
+ success:
94
+ type: boolean
95
+ body:
96
+ type: string
97
+ headers:
98
+ type: object
99
+ status:
100
+ type: integer
101
+
102
+ GatherResponse:
103
+ type: array
104
+ items:
105
+ type: object
106
+ properties:
107
+ uid:
108
+ type: string
109
+ description: Unique identifier for requested HTTP entity.
110
+ hash:
111
+ type: string
112
+ description: SHA1 hash of request based on url, query parameters, headers, method [currently this serves no purpose].
113
+ request:
114
+ type: object
115
+ description: Original requested HTTP parameters.
data/web_fetch.gemspec ADDED
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'web_fetch/version'
6
+
7
+ # rubocop:disable Metrics/BlockLength
8
+ Gem::Specification.new do |s|
9
+ s.name = 'web_fetch'
10
+ s.version = WebFetch::VERSION
11
+ s.date = '2016-10-16'
12
+ s.summary = 'Async HTTP fetcher'
13
+ s.description = 'Fetches HTTP responses as batch requests concurrently'
14
+ s.authors = ['Bob Farrell']
15
+ s.email = 'robertanthonyfarrell@gmail.com'
16
+ s.files = `git ls-files`.split($RS)
17
+ s.homepage = 'https://github.com/bobf/web_fetch'
18
+ s.licenses = ['MIT']
19
+ s.require_paths = ['lib']
20
+ s.executables << 'web_fetch_server'
21
+ s.executables << 'web_fetch_control'
22
+
23
+ s.add_dependency 'activesupport', '~> 4.0'
24
+ s.add_dependency 'childprocess', '~> 0.5'
25
+ s.add_dependency 'daemons', '~> 1.2'
26
+ s.add_dependency 'em-http-request', '~> 1.1'
27
+ s.add_dependency 'em-logger', '~> 0.1'
28
+ s.add_dependency 'eventmachine', '~> 1.0'
29
+ s.add_dependency 'eventmachine_httpserver', '~> 0.2'
30
+ s.add_dependency 'faraday', '~> 0.9'
31
+ s.add_dependency 'hanami-router', '~> 0.7'
32
+ s.add_dependency 'hanami-utils', '0.8.0'
33
+ s.add_dependency 'i18n', '~> 0.7'
34
+ s.add_dependency 'rack', '~> 1.6'
35
+
36
+ s.add_development_dependency 'byebug', '~> 9.0'
37
+ s.add_development_dependency 'rspec', '~> 3.5'
38
+ s.add_development_dependency 'rubocop', '~> 0.59.2'
39
+ s.add_development_dependency 'webmock', '~> 3.4'
40
+ end
41
+ # rubocop:enable Metrics/BlockLength