koine-rest_client 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Koine
4
+ module RestClient
5
+ # bad request error
6
+ class BadRequestError < Error
7
+ end
8
+ end
9
+ 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,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Koine
4
+ module RestClient
5
+ # base class for http errors
6
+ class Error < StandardError
7
+ attr_reader :response
8
+
9
+ def initialize(response)
10
+ @response = response
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Koine
4
+ module RestClient
5
+ # internal server error
6
+ class InternalServerError < Error
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Koine
4
+ module RestClient
5
+ # not found error
6
+ class NotFoundError < Error
7
+ end
8
+ end
9
+ 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