rest-client-wrapper 3.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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