rest-client-wrapper 3.0.1

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,67 @@
1
+ # Copyright (C) 2019 The University of Adelaide
2
+ #
3
+ # This file is part of Rest-Client-Wrapper.
4
+ #
5
+ # Rest-Client-Wrapper is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # Rest-Client-Wrapper is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with Rest-Client-Wrapper. If not, see <http://www.gnu.org/licenses/>.
17
+ #
18
+
19
+ require_relative "paginate"
20
+ require_relative "../exceptions"
21
+
22
+ module RestClientWrapper
23
+
24
+ # Paginator
25
+ module Paginator
26
+
27
+ include Paginate
28
+
29
+ # Echo
30
+ class Echo
31
+
32
+ attr_accessor :rest_client
33
+
34
+ def initialize(limit: Paginate::DEFAULT_PAGINATION_PAGE_SIZE)
35
+ @rest_client = nil
36
+ @config = { limit: limit }
37
+ end
38
+
39
+ def paginate(http_method:, uri:, query_params: {}, headers: {}, data: false)
40
+ raise RestClientError.new("Client not set, unable to make API call", nil, nil) unless @rest_client
41
+
42
+ query_params.reverse_merge!(@config)
43
+ responses = []
44
+ loop do
45
+ response = @rest_client.make_request({ http_method: http_method, uri: uri, query_params: query_params, headers: headers })
46
+ block_given? ? yield(response) : (responses << response)
47
+ links = _pagination_links(response)
48
+ break unless links.key?(:offset)
49
+
50
+ query_params[:offset] = links[:offset]
51
+ end
52
+ return data ? responses.map(&:body).pluck(:data).flatten : responses
53
+ end
54
+
55
+ private
56
+
57
+ def _pagination_links(response)
58
+ next_l = response&.body&.[](:next) || ""
59
+ next_h = Rack::Utils.parse_query(URI.parse(next_l)&.query)
60
+ return next_h.symbolize_keys!
61
+ end
62
+
63
+ end
64
+
65
+ end
66
+
67
+ end
@@ -0,0 +1,78 @@
1
+ # Copyright (C) 2019 The University of Adelaide
2
+ #
3
+ # This file is part of Rest-Client-Wrapper.
4
+ #
5
+ # Rest-Client-Wrapper is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # Rest-Client-Wrapper is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with Rest-Client-Wrapper. If not, see <http://www.gnu.org/licenses/>.
17
+ #
18
+
19
+ require_relative "paginate"
20
+ require_relative "../exceptions"
21
+
22
+ module RestClientWrapper
23
+
24
+ # Paginator
25
+ module Paginator
26
+
27
+ include Paginate
28
+
29
+ # HeaderLink
30
+ class HeaderLink
31
+
32
+ attr_accessor :rest_client
33
+
34
+ def initialize(per_page: Paginate::DEFAULT_PAGINATION_PAGE_SIZE)
35
+ @rest_client = nil
36
+ @config = { page: nil, per_page: per_page }
37
+ end
38
+
39
+ def paginate(http_method:, uri:, query_params: {}, headers: {}, data: false)
40
+ raise RestClientError.new("Client not set, unable to make API call", nil, nil) unless @rest_client
41
+
42
+ query_params.reverse_merge!(@config)
43
+ responses = []
44
+ loop.with_index(1) do |_, page|
45
+ query_params[:page] = page
46
+ response = @rest_client.make_request({ http_method: http_method, uri: uri, query_params: query_params, headers: headers })
47
+ block_given? ? yield(response) : (responses << response)
48
+ links = _pagination_links(response)
49
+ break unless links.key?(:next)
50
+ end
51
+ return data ? responses.map(&:body).flatten : responses
52
+ end
53
+
54
+ private
55
+
56
+ def _pagination_links(response)
57
+ re_uri = "\<(.*?)\>".freeze
58
+ re_rel = "current|next|first|last".freeze
59
+ links_a = response&.headers&.[](:link)&.split(",") || []
60
+ links_h = {}
61
+ links_a.each do |rel_link|
62
+ link_parts = rel_link.split(";")
63
+ next unless link_parts.length == 2
64
+
65
+ uri_match = link_parts[0].match(re_uri)
66
+ rel_match = link_parts[1].match(re_rel)
67
+ next if (uri_match.nil? || rel_match.nil?) || (uri_match.captures.length != 1 || rel_match.length != 1)
68
+
69
+ links_h[rel_match[0]] = uri_match.captures[0]
70
+ end
71
+ return links_h.symbolize_keys!
72
+ end
73
+
74
+ end
75
+
76
+ end
77
+
78
+ end
@@ -0,0 +1,78 @@
1
+ # Copyright (C) 2019 The University of Adelaide
2
+ #
3
+ # This file is part of Rest-Client-Wrapper.
4
+ #
5
+ # Rest-Client-Wrapper is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # Rest-Client-Wrapper is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with Rest-Client-Wrapper. If not, see <http://www.gnu.org/licenses/>.
17
+ #
18
+
19
+ require_relative "paginate"
20
+ require_relative "../exceptions"
21
+
22
+ module RestClientWrapper
23
+
24
+ # Paginator
25
+ module Paginator
26
+
27
+ include Paginate
28
+
29
+ # HeaderLink
30
+ class HeaderLink
31
+
32
+ attr_accessor :rest_client
33
+
34
+ def initialize(per_page: Paginate::DEFAULT_PAGINATION_PAGE_SIZE)
35
+ @rest_client = nil
36
+ @config = { page: nil, per_page: per_page }
37
+ end
38
+
39
+ def paginate(http_method:, uri:, query_params: {}, headers: {}, data: false)
40
+ raise RestClientError.new("Client not set, unable to make API call", nil, nil) unless @rest_client
41
+
42
+ query_params.reverse_merge!(@config)
43
+ responses = []
44
+ loop.with_index(1) do |_, page|
45
+ query_params[:page] = page
46
+ response = @rest_client.make_request({ http_method: http_method, uri: uri, query_params: query_params, headers: headers })
47
+ block_given? ? yield(response) : (responses << response)
48
+ links = _pagination_links(response)
49
+ break unless links.key?(:next)
50
+ end
51
+ return data ? responses.map(&:body).flatten : responses
52
+ end
53
+
54
+ private
55
+
56
+ def _pagination_links(response)
57
+ re_uri = "\<(.*?)\>".freeze
58
+ re_rel = "current|next|first|last".freeze
59
+ links_a = response&.headers&.[](:link)&.split(",") || []
60
+ links_h = {}
61
+ links_a.each do |rel_link|
62
+ link_parts = rel_link.split(";")
63
+ next unless link_parts.length == 2
64
+
65
+ uri_match = link_parts[0].match(re_uri)
66
+ rel_match = link_parts[1].match(re_rel)
67
+ next if (uri_match.nil? || rel_match.nil?) || (uri_match.captures.length != 1 || rel_match.length != 1)
68
+
69
+ links_h[rel_match[0]] = uri_match.captures[0]
70
+ end
71
+ return links_h.symbolize_keys!
72
+ end
73
+
74
+ end
75
+
76
+ end
77
+
78
+ end
@@ -0,0 +1,32 @@
1
+ # Copyright (C) 2019 The University of Adelaide
2
+ #
3
+ # This file is part of Rest-Client-Wrapper.
4
+ #
5
+ # Rest-Client-Wrapper is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # Rest-Client-Wrapper is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with Rest-Client-Wrapper. If not, see <http://www.gnu.org/licenses/>.
17
+ #
18
+
19
+ module RestClientWrapper
20
+
21
+ # Interface for paginators to implement
22
+ module Paginate
23
+
24
+ DEFAULT_PAGINATION_PAGE_SIZE = 1000
25
+
26
+ def paginate(_http_method:, _uri:, _payload:, _headers:, _data:)
27
+ raise NotImplementedError
28
+ end
29
+
30
+ end
31
+
32
+ end
@@ -0,0 +1,75 @@
1
+ # Copyright (C) 2019 The University of Adelaide
2
+ #
3
+ # This file is part of Rest-Client-Wrapper.
4
+ #
5
+ # Rest-Client-Wrapper is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # Rest-Client-Wrapper is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with Rest-Client-Wrapper. If not, see <http://www.gnu.org/licenses/>.
17
+ #
18
+
19
+ module RestClientWrapper
20
+
21
+ # Request
22
+ class Request
23
+
24
+ attr_reader :uri, :headers, :http_method, :payload, :segment_params, :query_params
25
+ attr_writer :uri
26
+
27
+ DEFAULT_CONTENT_TYPE = { content_type: :json, accept: :json }.freeze # default content type for post and put requests
28
+ VALID_HTTP_METHODS = %i[get post put patch delete connect options trace].freeze
29
+ HTTP_METHOD_FOR_JSON = %i[post put patch].freeze
30
+
31
+ def initialize(http_method:, uri:, segment_params: {}, payload: {}, query_params: {}, headers: {}) # rubocop:disable Metrics/ParameterLists
32
+ @uri = uri
33
+ self.headers = headers
34
+ self.segment_params = segment_params
35
+ self.payload = payload
36
+ self.query_params = query_params
37
+ self.http_method = http_method
38
+ end
39
+
40
+ def http_method=(http_method)
41
+ raise TypeError, "Request http_method parameters is not a symbol" unless http_method.is_a?(Symbol)
42
+ raise ArgumentError, "Not a valid http method" unless VALID_HTTP_METHODS.include?(http_method)
43
+
44
+ headers[:content_type] = DEFAULT_CONTENT_TYPE[:content_type] unless headers.key?(:content_type) || !HTTP_METHOD_FOR_JSON.include?(http_method)
45
+ headers[:accept] = DEFAULT_CONTENT_TYPE[:accept] unless headers.key?(:accept) || !HTTP_METHOD_FOR_JSON.include?(http_method)
46
+ @http_method = http_method
47
+ end
48
+
49
+ def payload=(payload)
50
+ raise TypeError, "Request payload parameters is not a hash" unless payload.is_a?(Hash)
51
+
52
+ @payload = payload
53
+ end
54
+
55
+ def segment_params=(segment_params)
56
+ raise TypeError, "Request segment parameters is not a hash" unless segment_params.is_a?(Hash)
57
+
58
+ @segment_params = segment_params
59
+ end
60
+
61
+ def query_params=(query_params)
62
+ raise TypeError, "Request query parameters is not a hash" unless query_params.is_a?(Hash)
63
+
64
+ @query_params = query_params
65
+ end
66
+
67
+ def headers=(headers)
68
+ raise TypeError, "Request headers parameters is not a hash" unless headers.is_a?(Hash)
69
+
70
+ @headers.nil? ? @headers = headers : @headers.merge!(headers)
71
+ end
72
+
73
+ end
74
+
75
+ end
@@ -0,0 +1,34 @@
1
+ # Copyright (C) 2019 The University of Adelaide
2
+ #
3
+ # This file is part of Rest-Client-Wrapper.
4
+ #
5
+ # Rest-Client-Wrapper is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # Rest-Client-Wrapper is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with Rest-Client-Wrapper. If not, see <http://www.gnu.org/licenses/>.
17
+ #
18
+
19
+ module RestClientWrapper
20
+
21
+ # Response
22
+ class Response
23
+
24
+ attr_reader :headers, :body, :code
25
+
26
+ def initialize(headers, body, code)
27
+ @headers = headers
28
+ @body = body
29
+ @code = code
30
+ end
31
+
32
+ end
33
+
34
+ end
@@ -0,0 +1,178 @@
1
+ # Copyright (C) 2019 The University of Adelaide
2
+ #
3
+ # This file is part of Rest-Client-Wrapper.
4
+ #
5
+ # Rest-Client-Wrapper is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # Rest-Client-Wrapper is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with Rest-Client-Wrapper. If not, see <http://www.gnu.org/licenses/>.
17
+ #
18
+
19
+ require "active_support"
20
+ require "active_support/core_ext"
21
+ require "json"
22
+ require "rack"
23
+ require "rest_client"
24
+ require_relative "exceptions"
25
+ require_relative "http"
26
+
27
+ module RestClientWrapper
28
+
29
+ # RestClient
30
+ class RestClient
31
+
32
+ include Http
33
+
34
+ attr_accessor :authenticator, :paginator
35
+
36
+ DEFAULT_RETRY = { max_retry: 0, wait: 0 }.freeze
37
+ DEFAULT_CONFIG = {
38
+ retries: {
39
+ 401 => { max_retry: 1, wait: 0 }, # unauthorized
40
+ 429 => { max_retry: 3, wait: 3 } # too many requests
41
+ }
42
+ }.freeze
43
+
44
+ def initialize(host:, config: {})
45
+ @host = host
46
+ @config = config
47
+ @retry_configs = {}.reverse_merge(DEFAULT_CONFIG[:retries])
48
+
49
+ @config[:retries]&.each do |k, v|
50
+ next unless Rack::Utils::HTTP_STATUS_CODES[k.to_i] # skip invalid codes
51
+
52
+ @retry_configs[k.to_i] = v.reverse_merge(DEFAULT_RETRY)
53
+ end
54
+
55
+ _reset_retries
56
+ end
57
+
58
+ def execute(request:)
59
+ _reset_retries
60
+ _validate_request(request)
61
+ _set_auth(request)
62
+ url = _build_uri(request)
63
+
64
+ loop do
65
+ access_token = @authenticator.is_a?(Authenticator::Oauth) ? @authenticator.access_token : nil
66
+ response_code = nil
67
+
68
+ begin
69
+ payload = Request::HTTP_METHOD_FOR_JSON.include?(request.http_method) ? request.payload.to_json : request.payload
70
+ request.headers[:params] = request.query_params
71
+ response = ::RestClient::Request.execute({ method: request.http_method, url: url, payload: payload, headers: request.headers })
72
+ response_code = response&.code
73
+ rescue StandardError => e
74
+ response_code = e.response&.code
75
+ # Any response that doesn't have a status of 200...207 will be raised as an exception from Rest-Client
76
+ raise RestClientError.new("API request encountered an unhandled exception", e.response, e) unless _retry?(response_code)
77
+ end
78
+
79
+ return Response.new(response.headers, _parse_response(response), response.code) if Http.success?(response_code)
80
+ raise RestClientNotSuccessful.new("API request was not successful", response) unless _retry?(response_code)
81
+
82
+ _wait_and_retry(response_code, access_token)
83
+ next
84
+ end
85
+ end
86
+
87
+ def make_request(http_method:, uri:, payload: {}, query_params: {}, headers: {})
88
+ parsed_uri = URI.parse(uri)
89
+
90
+ raise ArgumentError, "URL host does not match config host of instance, unable to make API call" if parsed_uri.absolute? && @host.casecmp("#{ parsed_uri.scheme }://#{ parsed_uri.host }").nonzero?
91
+
92
+ uri = parsed_uri.absolute? ? parsed_uri.path : uri
93
+ request = Request.new({ http_method: http_method, uri: uri, payload: payload, query_params: query_params })
94
+ request.headers = headers
95
+ return self.execute({ request: request })
96
+ end
97
+
98
+ def make_request_for_pages(http_method:, uri:, query_params: {}, headers: {}, data: false)
99
+ raise RestClientError.new("Paginator not set, unable to make API call", nil, nil) unless @paginator
100
+
101
+ @paginator.rest_client ||= self
102
+ return @paginator.paginate({ http_method: http_method, uri: uri, query_params: query_params, headers: headers, data: data })
103
+ end
104
+
105
+ private
106
+
107
+ def _set_auth(request)
108
+ return if @authenticator.nil?
109
+
110
+ auth = @authenticator.respond_to?(:generate_auth) ? @authenticator.generate_auth : {}
111
+ if @authenticator.is_a?(Authenticator::Custom)
112
+ case @authenticator.type
113
+ when :query_param
114
+ request.query_params.merge!(auth)
115
+ when :header
116
+ request.headers.merge!(auth)
117
+ end
118
+ else
119
+ request.headers.merge!(auth)
120
+ end
121
+ end
122
+
123
+ def _build_uri(request)
124
+ return format("#{ @host }#{ request.uri }", request.segment_params)
125
+ end
126
+
127
+ def _validate_request(request)
128
+ # Regex to find segments in uri with the pattern <segment_param>
129
+ url_segments = request.uri.scan(/\<(.*?)\>/).flatten
130
+ url_segments.each do |url_segment|
131
+ raise ArgumentError, "Segment parameter not provided for #{ url_segment }. URI #{ request.uri }" unless request.segment_params.include? url_segment.to_sym
132
+ end
133
+ return true
134
+ end
135
+
136
+ def _parse_response(response)
137
+ return nil unless response.respond_to?(:body)
138
+
139
+ parsed_body =
140
+ case MIME::Types[response&.headers&.[](:content_type)].first
141
+ when "application/json"
142
+ _parse_json(response)
143
+ else
144
+ response.body
145
+ end
146
+
147
+ return parsed_body
148
+ rescue StandardError => e
149
+ raise RestClientError.new("Response could not be parsed", response, e)
150
+ end
151
+
152
+ def _parse_json(response)
153
+ return { ok: true } if response.body == "ok".to_json # Handle special case for Echo delete responses
154
+
155
+ return JSON.parse(response.body, { object_class: Hash, symbolize_names: true })
156
+ rescue StandardError => e
157
+ raise RestClientError.new("Response could not be parsed as JSON", response, e)
158
+ end
159
+
160
+ def _reset_retries
161
+ @retry_configs.each_value { |v| v[:retry] = 0 }
162
+ end
163
+
164
+ def _retry?(response_code)
165
+ return true if @retry_configs.key?(response_code) && @retry_configs[response_code][:retry] < @retry_configs[response_code][:max_retry]
166
+
167
+ return false
168
+ end
169
+
170
+ def _wait_and_retry(response_code, access_token)
171
+ sleep(@retry_configs[response_code][:wait].to_f)
172
+ Authenticator::Oauth.authenticate({ client_id: @authenticator&.client_id, access_token: access_token }) if Http.unauthorized?(response_code) && @authenticator.is_a?(Authenticator::Oauth)
173
+ @retry_configs[response_code][:retry] += 1
174
+ end
175
+
176
+ end
177
+
178
+ end