koine-rest_client 1.0.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.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.reek.yml +6 -0
- data/.rspec +2 -0
- data/.rubocop.yml +44 -0
- data/.travis.yml +7 -0
- data/CHANGELOG.md +11 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +139 -0
- data/Rakefile +8 -0
- data/bin/console +15 -0
- data/bin/mock_server +25 -0
- data/bin/setup +8 -0
- data/koine-rest_client.gemspec +46 -0
- data/lib/koine/rest_client.rb +21 -0
- data/lib/koine/rest_client/adapters/http_party_adapter.rb +42 -0
- data/lib/koine/rest_client/async_builder.rb +69 -0
- data/lib/koine/rest_client/async_queue.rb +36 -0
- data/lib/koine/rest_client/bad_request_error.rb +9 -0
- data/lib/koine/rest_client/client.rb +90 -0
- data/lib/koine/rest_client/error.rb +14 -0
- data/lib/koine/rest_client/internal_server_error.rb +9 -0
- data/lib/koine/rest_client/not_found_error.rb +9 -0
- data/lib/koine/rest_client/request.rb +91 -0
- data/lib/koine/rest_client/response_parser.rb +32 -0
- data/lib/koine/rest_client/rspec_mock_client.rb +148 -0
- data/lib/koine/rest_client/version.rb +7 -0
- data/lib/koine/url.rb +84 -0
- metadata +244 -0
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'httparty'
|
4
|
+
require 'koine/url'
|
5
|
+
require 'koine/rest_client/error'
|
6
|
+
require 'koine/rest_client/version'
|
7
|
+
require 'koine/rest_client/client'
|
8
|
+
require 'koine/rest_client/async_builder'
|
9
|
+
require 'koine/rest_client/async_queue'
|
10
|
+
require 'koine/rest_client/response_parser'
|
11
|
+
require 'koine/rest_client/bad_request_error'
|
12
|
+
require 'koine/rest_client/not_found_error'
|
13
|
+
require 'koine/rest_client/internal_server_error'
|
14
|
+
require 'koine/rest_client/request'
|
15
|
+
require 'koine/rest_client/adapters/http_party_adapter'
|
16
|
+
|
17
|
+
module Koine
|
18
|
+
# The gem namespace
|
19
|
+
module RestClient
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'httparty'
|
4
|
+
|
5
|
+
module Koine
|
6
|
+
module RestClient
|
7
|
+
module Adapters
|
8
|
+
# adapter for HTTParty client
|
9
|
+
class HttpPartyAdapter
|
10
|
+
def initialize(http_party_client = HTTParty)
|
11
|
+
@client = http_party_client
|
12
|
+
end
|
13
|
+
|
14
|
+
def send_request(request)
|
15
|
+
send("send_#{request.method}", request)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def send_post(request)
|
21
|
+
@client.post(request.url, request.options)
|
22
|
+
end
|
23
|
+
|
24
|
+
def send_get(request)
|
25
|
+
@client.get(request.url, request.options)
|
26
|
+
end
|
27
|
+
|
28
|
+
def send_put(request)
|
29
|
+
@client.put(request.url, request.options)
|
30
|
+
end
|
31
|
+
|
32
|
+
def send_patch(request)
|
33
|
+
@client.patch(request.url, request.options)
|
34
|
+
end
|
35
|
+
|
36
|
+
def send_delete(request)
|
37
|
+
@client.delete(request.url, request.options)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Koine
|
4
|
+
module RestClient
|
5
|
+
# takes care of async requests
|
6
|
+
class AsyncBuilder
|
7
|
+
def initialize(client, response_parser, queue = AsyncQueue.new)
|
8
|
+
@client = client
|
9
|
+
@response_parser = response_parser
|
10
|
+
@queue = queue
|
11
|
+
@error_handler = proc do |error|
|
12
|
+
raise error
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def get(*args, &block)
|
17
|
+
queue(:get, *args, &block)
|
18
|
+
end
|
19
|
+
|
20
|
+
def post(*args, &block)
|
21
|
+
queue(:post, *args, &block)
|
22
|
+
end
|
23
|
+
|
24
|
+
def put(*args, &block)
|
25
|
+
queue(:put, *args, &block)
|
26
|
+
end
|
27
|
+
|
28
|
+
def patch(*args, &block)
|
29
|
+
queue(:patch, *args, &block)
|
30
|
+
end
|
31
|
+
|
32
|
+
def delete(*args, &block)
|
33
|
+
queue(:delete, *args, &block)
|
34
|
+
end
|
35
|
+
|
36
|
+
def parsed_responses
|
37
|
+
blocks = @queue.map { |_request, block| block }
|
38
|
+
threads = @queue.map do |request|
|
39
|
+
Thread.new { @client.perform_request(request) }
|
40
|
+
end
|
41
|
+
@queue.clear
|
42
|
+
responses = threads.map(&:value)
|
43
|
+
parse_responses(responses, blocks)
|
44
|
+
end
|
45
|
+
|
46
|
+
def on_error(&block)
|
47
|
+
@error_handler = block
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def parse_responses(responses, blocks)
|
53
|
+
responses.map.with_index do |response, index|
|
54
|
+
block = blocks[index]
|
55
|
+
begin
|
56
|
+
@response_parser.parse(response, &block)
|
57
|
+
rescue StandardError => exception
|
58
|
+
@error_handler.call(exception)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def queue(type, *args, &block)
|
64
|
+
request = @client.__send__("create_#{type}_request", *args)
|
65
|
+
@queue.push(request, &block)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Koine
|
4
|
+
module RestClient
|
5
|
+
# queue for async requests
|
6
|
+
class AsyncQueue
|
7
|
+
def initialize
|
8
|
+
@items = []
|
9
|
+
end
|
10
|
+
|
11
|
+
def push(item, &block)
|
12
|
+
@items.push([item, block])
|
13
|
+
end
|
14
|
+
|
15
|
+
def each
|
16
|
+
@items.each do |item|
|
17
|
+
yield(item[0], item[1])
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def map
|
22
|
+
@items.map do |item|
|
23
|
+
yield(item[0], item[1])
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def clear
|
28
|
+
@items.clear
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_a
|
32
|
+
@items.to_a
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :reek:DataClump
|
4
|
+
# :reek:FeatureEnvy
|
5
|
+
module Koine
|
6
|
+
module RestClient
|
7
|
+
class Client
|
8
|
+
def initialize(
|
9
|
+
adapter: Adapters::HttpPartyAdapter.new,
|
10
|
+
response_parser: ResponseParser.new,
|
11
|
+
base_request: Request.new
|
12
|
+
)
|
13
|
+
@adapter = adapter
|
14
|
+
@response_parser = response_parser
|
15
|
+
@request = base_request
|
16
|
+
end
|
17
|
+
|
18
|
+
def get(path, query = {}, options = {}, &block)
|
19
|
+
request = create_get_request(path, query, options)
|
20
|
+
response = perform_request(request)
|
21
|
+
parse_response(response, &block)
|
22
|
+
end
|
23
|
+
|
24
|
+
def create_get_request(path, query = {}, options = {})
|
25
|
+
create_request(:get, path, options.merge(query_params: query))
|
26
|
+
end
|
27
|
+
|
28
|
+
def post(path, body = {}, options = {}, &block)
|
29
|
+
request = create_post_request(path, body, options)
|
30
|
+
response = perform_request(request)
|
31
|
+
parse_response(response, &block)
|
32
|
+
end
|
33
|
+
|
34
|
+
def create_post_request(path, body = {}, options = {})
|
35
|
+
create_request(:post, path, options.merge(body: body))
|
36
|
+
end
|
37
|
+
|
38
|
+
def put(path, body = {}, options = {}, &block)
|
39
|
+
request = create_put_request(path, body, options)
|
40
|
+
response = perform_request(request)
|
41
|
+
parse_response(response, &block)
|
42
|
+
end
|
43
|
+
|
44
|
+
def create_put_request(path, body = {}, options = {})
|
45
|
+
create_request(:put, path, options.merge(body: body))
|
46
|
+
end
|
47
|
+
|
48
|
+
def patch(path, body = {}, options = {}, &block)
|
49
|
+
request = create_patch_request(path, body, options)
|
50
|
+
response = perform_request(request)
|
51
|
+
parse_response(response, &block)
|
52
|
+
end
|
53
|
+
|
54
|
+
def create_patch_request(path, body = {}, options = {})
|
55
|
+
create_request(:patch, path, options.merge(body: body))
|
56
|
+
end
|
57
|
+
|
58
|
+
def delete(path, body = {}, options = {}, &block)
|
59
|
+
request = create_delete_request(path, body, options)
|
60
|
+
response = perform_request(request)
|
61
|
+
parse_response(response, &block)
|
62
|
+
end
|
63
|
+
|
64
|
+
def create_delete_request(path, body = {}, options = {})
|
65
|
+
create_request(:delete, path, options.merge(body: body))
|
66
|
+
end
|
67
|
+
|
68
|
+
def async
|
69
|
+
builder = AsyncBuilder.new(self, @response_parser)
|
70
|
+
yield(builder)
|
71
|
+
builder.parsed_responses
|
72
|
+
end
|
73
|
+
|
74
|
+
def perform_request(request)
|
75
|
+
@adapter.send_request(request)
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def create_request(method, path, options = {})
|
81
|
+
options = options.merge(method: method, path: path)
|
82
|
+
@request.with_added_options(options)
|
83
|
+
end
|
84
|
+
|
85
|
+
def parse_response(response, &block)
|
86
|
+
@response_parser.parse(response, &block)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Koine
|
4
|
+
module RestClient
|
5
|
+
# request object
|
6
|
+
# :reek:TooManyInstanceVariables
|
7
|
+
class Request
|
8
|
+
attr_reader :method
|
9
|
+
attr_reader :base_url
|
10
|
+
attr_reader :path
|
11
|
+
attr_reader :headers
|
12
|
+
attr_reader :query_params
|
13
|
+
attr_reader :body
|
14
|
+
|
15
|
+
def initialize(base_url: '', query_params: {}, path: '', headers: {}, method: 'get')
|
16
|
+
@method = method
|
17
|
+
@base_url = base_url
|
18
|
+
@path = path
|
19
|
+
@query_params = query_params
|
20
|
+
@headers = headers
|
21
|
+
end
|
22
|
+
|
23
|
+
def with_method(method)
|
24
|
+
new(:method, method)
|
25
|
+
end
|
26
|
+
|
27
|
+
def with_path(path)
|
28
|
+
new(:path, path)
|
29
|
+
end
|
30
|
+
|
31
|
+
def with_base_url(base_url)
|
32
|
+
new(:base_url, base_url)
|
33
|
+
end
|
34
|
+
|
35
|
+
def with_added_query_params(query_params)
|
36
|
+
new(:query_params, @query_params.merge(query_params).compact)
|
37
|
+
end
|
38
|
+
|
39
|
+
def with_added_headers(headers)
|
40
|
+
new(:headers, @headers.merge(headers).compact)
|
41
|
+
end
|
42
|
+
|
43
|
+
def with_body(body)
|
44
|
+
new(:body, body)
|
45
|
+
end
|
46
|
+
|
47
|
+
def url
|
48
|
+
url = "#{@base_url.delete_suffix('/')}/#{path.delete_prefix('/')}"
|
49
|
+
Url.new(url).with_query_params(query_params).to_s(unescape: ',')
|
50
|
+
end
|
51
|
+
|
52
|
+
def options
|
53
|
+
{ body: body, headers: headers }.compact.reject do |_key, value|
|
54
|
+
value.empty?
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# :reek:ManualDispatch
|
59
|
+
def with_added_options(options)
|
60
|
+
object = self
|
61
|
+
options.each do |key, value|
|
62
|
+
if respond_to?("with_#{key}")
|
63
|
+
object = object.send("with_#{key}", value)
|
64
|
+
end
|
65
|
+
|
66
|
+
if respond_to?("with_added_#{key}")
|
67
|
+
object = object.send("with_added_#{key}", value)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
object
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
attr_writer :method
|
76
|
+
attr_writer :base_url
|
77
|
+
attr_writer :path
|
78
|
+
attr_writer :query_params
|
79
|
+
attr_writer :headers
|
80
|
+
attr_writer :body
|
81
|
+
|
82
|
+
# :reek:FeatureEnvy
|
83
|
+
def new(attribute, value)
|
84
|
+
dup.tap do |object|
|
85
|
+
object.send("#{attribute}=", value)
|
86
|
+
object.freeze
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Koine
|
4
|
+
module RestClient
|
5
|
+
# either returns response or raises errors
|
6
|
+
class ResponseParser
|
7
|
+
def parse(response)
|
8
|
+
if block_given?
|
9
|
+
yield(response)
|
10
|
+
end
|
11
|
+
|
12
|
+
code = Integer(response.code)
|
13
|
+
|
14
|
+
if code.between?(200, 299)
|
15
|
+
return response.parsed_response
|
16
|
+
end
|
17
|
+
|
18
|
+
raise error_for_code(code), response
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def error_for_code(code)
|
24
|
+
{
|
25
|
+
400 => BadRequestError,
|
26
|
+
404 => NotFoundError,
|
27
|
+
500 => InternalServerError
|
28
|
+
}.fetch(code) { Error }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Koine
|
4
|
+
module RestClient
|
5
|
+
# mock client
|
6
|
+
class RspecMockClient
|
7
|
+
attr_reader :client_mock
|
8
|
+
|
9
|
+
def initialize(rspec, response_parser: ResponseParser.new)
|
10
|
+
@client_mock = rspec.instance_double(Koine::RestClient::Client)
|
11
|
+
@builder = MockFactory.new(rspec, self)
|
12
|
+
@collected = []
|
13
|
+
@response_parser = response_parser
|
14
|
+
@error_handler = proc do |error|
|
15
|
+
raise error
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def on_error(&block)
|
20
|
+
@error_handler = block
|
21
|
+
end
|
22
|
+
|
23
|
+
def mock
|
24
|
+
yield(@builder)
|
25
|
+
end
|
26
|
+
|
27
|
+
def get(*args, &block)
|
28
|
+
parse(@client_mock.get(*args), &block)
|
29
|
+
end
|
30
|
+
|
31
|
+
def post(*args, &block)
|
32
|
+
parse(@client_mock.post(*args), &block)
|
33
|
+
end
|
34
|
+
|
35
|
+
def put(*args, &block)
|
36
|
+
parse(@client_mock.put(*args), &block)
|
37
|
+
end
|
38
|
+
|
39
|
+
def patch(*args, &block)
|
40
|
+
parse(@client_mock.patch(*args), &block)
|
41
|
+
end
|
42
|
+
|
43
|
+
def delete(*args, &block)
|
44
|
+
parse(@client_mock.delete(*args), &block)
|
45
|
+
end
|
46
|
+
|
47
|
+
def async
|
48
|
+
@async = true
|
49
|
+
yield(self)
|
50
|
+
@async = false
|
51
|
+
|
52
|
+
responses = @collected.dup
|
53
|
+
@collected.clear
|
54
|
+
responses.map(&:parsed_response)
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def parse(response, &block)
|
60
|
+
@response_parser.parse(response, &block).tap do |_parsed|
|
61
|
+
if @async
|
62
|
+
@collected << response
|
63
|
+
end
|
64
|
+
end
|
65
|
+
rescue StandardError => exception
|
66
|
+
unless @async
|
67
|
+
raise exception
|
68
|
+
end
|
69
|
+
|
70
|
+
@collected << MockResponse.new.tap do |new_response|
|
71
|
+
new_response.parsed_response = @error_handler.call(exception)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# mock response
|
77
|
+
class MockResponse
|
78
|
+
attr_accessor :code
|
79
|
+
attr_accessor :parsed_response
|
80
|
+
end
|
81
|
+
|
82
|
+
# mock factory
|
83
|
+
class MockFactory < SimpleDelegator
|
84
|
+
def initialize(rspec, client_proxy)
|
85
|
+
super(rspec)
|
86
|
+
@client_proxy = client_proxy
|
87
|
+
end
|
88
|
+
|
89
|
+
def get(*args)
|
90
|
+
create_mock(:get, *args)
|
91
|
+
end
|
92
|
+
|
93
|
+
def post(*args)
|
94
|
+
create_mock(:post, *args)
|
95
|
+
end
|
96
|
+
|
97
|
+
def put(*args)
|
98
|
+
create_mock(:put, *args)
|
99
|
+
end
|
100
|
+
|
101
|
+
def patch(*args)
|
102
|
+
create_mock(:patch, *args)
|
103
|
+
end
|
104
|
+
|
105
|
+
def delete(*args)
|
106
|
+
create_mock(:delete, *args)
|
107
|
+
end
|
108
|
+
|
109
|
+
def on_error(&block)
|
110
|
+
@client_proxy.on_error(&block)
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
def create_mock(method, *args)
|
116
|
+
allowed = allow(@client_proxy.client_mock).to receive(method)
|
117
|
+
MockBuilder.new(allowed).with(*args)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# mock builder
|
122
|
+
class MockBuilder
|
123
|
+
def initialize(mock)
|
124
|
+
@mock = mock
|
125
|
+
end
|
126
|
+
|
127
|
+
def with(*args)
|
128
|
+
@mock.with(*args)
|
129
|
+
self
|
130
|
+
end
|
131
|
+
|
132
|
+
def will_return(body: {}, code: 200)
|
133
|
+
response = MockResponse.new
|
134
|
+
response.parsed_response = body
|
135
|
+
response.code = code
|
136
|
+
if block_given?
|
137
|
+
response = yield(response)
|
138
|
+
end
|
139
|
+
@mock.and_return(response)
|
140
|
+
self
|
141
|
+
end
|
142
|
+
|
143
|
+
def on_error(&block)
|
144
|
+
@error_handler = block
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|