web_fetch 0.1.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.
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