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