smartsheet 1.0.0 → 1.1.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.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +11 -11
  3. data/.rubocop.yml +4 -4
  4. data/.travis.yml +13 -6
  5. data/.yardopts +3 -3
  6. data/CHANGELOG.md +103 -0
  7. data/Gemfile +6 -6
  8. data/LICENSE +202 -202
  9. data/README.md +214 -141
  10. data/Rakefile +29 -23
  11. data/bin/console +14 -14
  12. data/bin/setup +8 -8
  13. data/lib/smartsheet.rb +2 -2
  14. data/lib/smartsheet/api/body_builder.rb +25 -25
  15. data/lib/smartsheet/api/endpoint_spec.rb +54 -36
  16. data/lib/smartsheet/api/faraday_adapter/faraday_net_client.rb +45 -42
  17. data/lib/smartsheet/api/faraday_adapter/faraday_response.rb +70 -70
  18. data/lib/smartsheet/api/faraday_adapter/middleware/faraday_error_translator.rb +20 -20
  19. data/lib/smartsheet/api/faraday_adapter/middleware/response_parser.rb +25 -25
  20. data/lib/smartsheet/api/file_spec.rb +31 -31
  21. data/lib/smartsheet/api/header_builder.rb +90 -84
  22. data/lib/smartsheet/api/request.rb +42 -29
  23. data/lib/smartsheet/api/request_client.rb +43 -27
  24. data/lib/smartsheet/api/request_logger.rb +182 -182
  25. data/lib/smartsheet/api/request_spec.rb +57 -44
  26. data/lib/smartsheet/api/response_net_client_decorator.rb +54 -54
  27. data/lib/smartsheet/api/retry_logic.rb +40 -40
  28. data/lib/smartsheet/api/retry_net_client_decorator.rb +37 -37
  29. data/lib/smartsheet/api/url_builder.rb +25 -25
  30. data/lib/smartsheet/client.rb +191 -185
  31. data/lib/smartsheet/constants.rb +15 -15
  32. data/lib/smartsheet/endpoints/contacts/contacts.rb +30 -30
  33. data/lib/smartsheet/endpoints/favorites/favorites.rb +159 -159
  34. data/lib/smartsheet/endpoints/folders/folders.rb +125 -125
  35. data/lib/smartsheet/endpoints/groups/groups.rb +83 -83
  36. data/lib/smartsheet/endpoints/home/home.rb +20 -20
  37. data/lib/smartsheet/endpoints/reports/reports.rb +100 -100
  38. data/lib/smartsheet/endpoints/reports/reports_share.rb +69 -69
  39. data/lib/smartsheet/endpoints/search/search.rb +30 -30
  40. data/lib/smartsheet/endpoints/server_info/server_info.rb +21 -21
  41. data/lib/smartsheet/endpoints/share/share.rb +58 -58
  42. data/lib/smartsheet/endpoints/sheets/automation_rules.rb +56 -0
  43. data/lib/smartsheet/endpoints/sheets/cells.rb +82 -82
  44. data/lib/smartsheet/endpoints/sheets/columns.rb +66 -66
  45. data/lib/smartsheet/endpoints/sheets/comments.rb +64 -64
  46. data/lib/smartsheet/endpoints/sheets/comments_attachments.rb +78 -78
  47. data/lib/smartsheet/endpoints/sheets/cross_sheet_references.rb +45 -0
  48. data/lib/smartsheet/endpoints/sheets/discussions.rb +84 -84
  49. data/lib/smartsheet/endpoints/sheets/discussions_attachments.rb +22 -22
  50. data/lib/smartsheet/endpoints/sheets/rows.rb +106 -95
  51. data/lib/smartsheet/endpoints/sheets/rows_attachments.rb +92 -92
  52. data/lib/smartsheet/endpoints/sheets/sheets.rb +326 -317
  53. data/lib/smartsheet/endpoints/sheets/sheets_attachments.rb +174 -174
  54. data/lib/smartsheet/endpoints/sheets/sheets_share.rb +69 -69
  55. data/lib/smartsheet/endpoints/sights/sights.rb +101 -101
  56. data/lib/smartsheet/endpoints/sights/sights_share.rb +69 -69
  57. data/lib/smartsheet/endpoints/templates/templates.rb +29 -29
  58. data/lib/smartsheet/endpoints/token/token.rb +65 -60
  59. data/lib/smartsheet/endpoints/update_requests/sent_update_requests.rb +44 -44
  60. data/lib/smartsheet/endpoints/update_requests/update_requests.rb +74 -74
  61. data/lib/smartsheet/endpoints/users/alternate_emails.rb +79 -79
  62. data/lib/smartsheet/endpoints/users/users.rb +77 -77
  63. data/lib/smartsheet/endpoints/webhooks/webhooks.rb +71 -71
  64. data/lib/smartsheet/endpoints/workspaces/workspaces.rb +87 -87
  65. data/lib/smartsheet/endpoints/workspaces/workspaces_share.rb +70 -70
  66. data/lib/smartsheet/error.rb +69 -69
  67. data/lib/smartsheet/general_request.rb +74 -74
  68. data/lib/smartsheet/version.rb +5 -5
  69. data/smartsheet.gemspec +54 -52
  70. metadata +34 -3
@@ -1,31 +1,31 @@
1
- require 'cgi'
2
- require 'faraday'
3
-
4
- module Smartsheet
5
- module API
6
- # Specification for a file attachment by path, target filename, and MIME content type
7
- class PathFileSpec
8
- attr_reader :upload_io, :filename, :content_type, :file_length
9
-
10
- def initialize(path, filename, content_type)
11
- @file_length = File.size(path)
12
- @filename = filename.nil? ? File.basename(path) : filename
13
- @upload_io = Faraday::UploadIO.new(path, content_type, CGI::escape(@filename))
14
- @content_type = content_type
15
- end
16
- end
17
-
18
- # Specification for a file attachment by {::File}, target filename, file length, and MIME
19
- # content type
20
- class ObjectFileSpec
21
- attr_reader :upload_io, :filename, :content_type, :file_length
22
-
23
- def initialize(file, filename, file_length, content_type)
24
- @file_length = file_length
25
- @filename = filename
26
- @upload_io = Faraday::UploadIO.new(file, content_type, CGI::escape(filename))
27
- @content_type = content_type
28
- end
29
- end
30
- end
31
- end
1
+ require 'cgi'
2
+ require 'faraday'
3
+
4
+ module Smartsheet
5
+ module API
6
+ # Specification for a file attachment by path, target filename, and MIME content type
7
+ class PathFileSpec
8
+ attr_reader :upload_io, :filename, :content_type, :file_length
9
+
10
+ def initialize(path, filename, content_type)
11
+ @file_length = File.size(path)
12
+ @filename = (filename.nil? || filename.empty?) ? File.basename(path) : filename
13
+ @upload_io = Faraday::UploadIO.new(path, content_type, CGI::escape(@filename))
14
+ @content_type = content_type
15
+ end
16
+ end
17
+
18
+ # Specification for a file attachment by {::File}, target filename, file length, and MIME
19
+ # content type
20
+ class ObjectFileSpec
21
+ attr_reader :upload_io, :filename, :content_type, :file_length
22
+
23
+ def initialize(file, filename, file_length, content_type)
24
+ @file_length = file_length
25
+ @filename = filename
26
+ @upload_io = Faraday::UploadIO.new(file, content_type, CGI::escape(filename))
27
+ @content_type = content_type
28
+ end
29
+ end
30
+ end
31
+ end
@@ -1,85 +1,91 @@
1
- require 'cgi'
2
- require 'smartsheet/version'
3
- require 'smartsheet/constants'
4
-
5
- module Smartsheet
6
- module API
7
- # Constructs headers for accessing the Smartsheet API
8
- class HeaderBuilder
9
- include Smartsheet::Constants
10
- def initialize(token, endpoint_spec, request_spec, assume_user: nil)
11
- @token = token
12
- @endpoint_spec = endpoint_spec
13
- @request_spec = request_spec
14
- @assume_user = assume_user
15
- end
16
-
17
- def build
18
- base_headers
19
- .merge(endpoint_headers)
20
- .merge(content_type)
21
- .merge(content_disposition)
22
- .merge(content_length)
23
- .merge(request_headers)
24
- .merge(assume_user)
25
- end
26
-
27
- private
28
-
29
- attr_accessor :endpoint_spec, :request_spec
30
- attr_reader :token
31
-
32
- def base_headers
33
- base = {
34
- Accept: JSON_TYPE,
35
- 'User-Agent': "#{USER_AGENT}/#{Smartsheet::VERSION}"
36
- }
37
- base[:Authorization] = "Bearer #{token}" if endpoint_spec.requires_auth?
38
-
39
- base
40
- end
41
-
42
- def assume_user
43
- if @assume_user.nil?
44
- {}
45
- else
46
- {'Assume-User': CGI::escape(@assume_user)}
47
- end
48
- end
49
-
50
- def endpoint_headers
51
- endpoint_spec.headers
52
- end
53
-
54
- def content_type
55
- if endpoint_spec.sending_json? && request_spec.body
56
- {'Content-Type': JSON_TYPE}
57
- elsif endpoint_spec.sending_file?
58
- {'Content-Type': request_spec.content_type}
59
- else
60
- {}
61
- end
62
- end
63
-
64
- def content_disposition
65
- if endpoint_spec.sending_file?
66
- {'Content-Disposition': "attachment; filename=\"#{CGI::escape(request_spec.filename)}\""}
67
- else
68
- {}
69
- end
70
- end
71
-
72
- def content_length
73
- if endpoint_spec.sending_file?
74
- {'Content-Length': request_spec.file_length.to_s}
75
- else
76
- {}
77
- end
78
- end
79
-
80
- def request_headers
81
- request_spec.header_overrides
82
- end
83
- end
84
- end
1
+ require 'cgi'
2
+ require 'smartsheet/version'
3
+ require 'smartsheet/constants'
4
+
5
+ module Smartsheet
6
+ module API
7
+ # Constructs headers for accessing the Smartsheet API
8
+ class HeaderBuilder
9
+ include Smartsheet::Constants
10
+ def initialize(token, endpoint_spec, request_spec, app_user_agent: nil, assume_user: nil)
11
+ @token = token
12
+ @endpoint_spec = endpoint_spec
13
+ @request_spec = request_spec
14
+ @app_user_agent = app_user_agent
15
+ @assume_user = assume_user
16
+ end
17
+
18
+ def build
19
+ base_headers
20
+ .merge(assume_user)
21
+ .merge(endpoint_headers)
22
+ .merge(content_type)
23
+ .merge(content_disposition)
24
+ .merge(content_length)
25
+ .merge(request_headers)
26
+ end
27
+
28
+ private
29
+
30
+ attr_accessor :endpoint_spec, :request_spec
31
+ attr_reader :token
32
+
33
+ def base_headers
34
+ base = {
35
+ Accept: JSON_TYPE,
36
+ 'User-Agent': user_agent
37
+ }
38
+ base[:Authorization] = "Bearer #{token}" if endpoint_spec.requires_auth?
39
+
40
+ base
41
+ end
42
+
43
+ def user_agent
44
+ "#{USER_AGENT}/#{Smartsheet::VERSION}" +
45
+ (@app_user_agent.nil? ? '' : "/#{@app_user_agent}")
46
+ end
47
+
48
+ def assume_user
49
+ if @assume_user.nil?
50
+ {}
51
+ else
52
+ {'Assume-User': CGI::escape(@assume_user)}
53
+ end
54
+ end
55
+
56
+ def endpoint_headers
57
+ endpoint_spec.headers
58
+ end
59
+
60
+ def content_type
61
+ if endpoint_spec.sending_json? && request_spec.body
62
+ {'Content-Type': JSON_TYPE}
63
+ elsif endpoint_spec.sending_file?
64
+ {'Content-Type': request_spec.content_type}
65
+ else
66
+ {}
67
+ end
68
+ end
69
+
70
+ def content_disposition
71
+ if endpoint_spec.sending_file?
72
+ {'Content-Disposition': "attachment; filename=\"#{CGI::escape(request_spec.filename)}\""}
73
+ else
74
+ {}
75
+ end
76
+ end
77
+
78
+ def content_length
79
+ if endpoint_spec.sending_file?
80
+ {'Content-Length': request_spec.file_length.to_s}
81
+ else
82
+ {}
83
+ end
84
+ end
85
+
86
+ def request_headers
87
+ request_spec.header_overrides
88
+ end
89
+ end
90
+ end
85
91
  end
@@ -1,30 +1,43 @@
1
- require 'smartsheet/api/url_builder'
2
- require 'smartsheet/api/header_builder'
3
- require 'smartsheet/api/body_builder'
4
-
5
- module Smartsheet
6
- module API
7
- # Full specification for a single request to an endpoint
8
- class Request
9
- attr_reader :method, :url, :headers, :params, :body
10
-
11
- def initialize(token, endpoint_spec, request_spec, base_url, assume_user: nil)
12
- @method = endpoint_spec.method
13
- @url = Smartsheet::API::UrlBuilder.new(endpoint_spec, request_spec, base_url).build
14
- @headers = Smartsheet::API::HeaderBuilder.new(token, endpoint_spec, request_spec, assume_user: assume_user).build
15
- @params = request_spec.params
16
- @body = Smartsheet::API::BodyBuilder.new(endpoint_spec, request_spec).build
17
- end
18
-
19
- def ==(other)
20
- other.class == self.class && other.equality_state == equality_state
21
- end
22
-
23
- protected
24
-
25
- def equality_state
26
- [method, url, headers, params, body]
27
- end
28
- end
29
- end
1
+ require 'smartsheet/api/url_builder'
2
+ require 'smartsheet/api/header_builder'
3
+ require 'smartsheet/api/body_builder'
4
+
5
+ module Smartsheet
6
+ module API
7
+ # Full specification for a single request to an endpoint
8
+ class Request
9
+ attr_reader :method, :url, :headers, :params, :body
10
+
11
+ def initialize(
12
+ token,
13
+ endpoint_spec,
14
+ request_spec,
15
+ base_url,
16
+ app_user_agent: nil,
17
+ assume_user: nil
18
+ )
19
+ @method = endpoint_spec.method
20
+ @url = Smartsheet::API::UrlBuilder.new(endpoint_spec, request_spec, base_url).build
21
+ @headers = Smartsheet::API::HeaderBuilder.new(
22
+ token,
23
+ endpoint_spec,
24
+ request_spec,
25
+ app_user_agent: app_user_agent,
26
+ assume_user: assume_user
27
+ ).build
28
+ @params = request_spec.params
29
+ @body = Smartsheet::API::BodyBuilder.new(endpoint_spec, request_spec).build
30
+ end
31
+
32
+ def ==(other)
33
+ other.class == self.class && other.equality_state == equality_state
34
+ end
35
+
36
+ protected
37
+
38
+ def equality_state
39
+ [method, url, headers, params, body]
40
+ end
41
+ end
42
+ end
30
43
  end
@@ -1,28 +1,44 @@
1
- require 'smartsheet/error'
2
-
3
- module Smartsheet
4
- module API
5
- # Composes {EndpointSpec endpoint specifications} and {RequestSpec request specifications} to
6
- # form a single {Request} that it submits to the provided client
7
- class RequestClient
8
- def initialize(token, client, base_url, assume_user: nil, logger: MuteRequestLogger.new)
9
- @token = token
10
- @client = client
11
- @assume_user = assume_user
12
- @logger = logger
13
- @base_url = base_url
14
- end
15
-
16
- def make_request(endpoint_spec, request_spec)
17
- request = Request.new(token, endpoint_spec, request_spec, base_url, assume_user: assume_user)
18
-
19
- logger.log_request(request)
20
- client.make_request(request)
21
- end
22
-
23
- private
24
-
25
- attr_reader :token, :client, :assume_user, :logger, :base_url
26
- end
27
- end
1
+ require 'smartsheet/version'
2
+ require 'smartsheet/error'
3
+
4
+ module Smartsheet
5
+ module API
6
+ # Composes {EndpointSpec endpoint specifications} and {RequestSpec request specifications} to
7
+ # form a single {Request} that it submits to the provided client
8
+ class RequestClient
9
+ def initialize(
10
+ token,
11
+ client,
12
+ base_url,
13
+ app_user_agent: nil,
14
+ assume_user: nil,
15
+ logger: MuteRequestLogger.new
16
+ )
17
+ @token = token
18
+ @client = client
19
+ @app_user_agent = app_user_agent
20
+ @assume_user = assume_user
21
+ @logger = logger
22
+ @base_url = base_url
23
+ end
24
+
25
+ def make_request(endpoint_spec, request_spec)
26
+ request = Request.new(
27
+ token,
28
+ endpoint_spec,
29
+ request_spec,
30
+ base_url,
31
+ app_user_agent: app_user_agent,
32
+ assume_user: assume_user
33
+ )
34
+
35
+ logger.log_request(request)
36
+ client.make_request(request)
37
+ end
38
+
39
+ private
40
+
41
+ attr_reader :token, :client, :app_user_agent, :assume_user, :logger, :base_url
42
+ end
43
+ end
28
44
  end
@@ -1,183 +1,183 @@
1
- require 'logger'
2
-
3
- module Smartsheet
4
- module API
5
- # Censors strings and hash values for select blacklisted keys
6
- class Censor
7
- EXPOSED_CHARS = 4
8
- KEY_TO_STRING = ->(k){ k.to_s }
9
- KEY_TO_DOWNCASE_STRING = ->(k){ k.to_s.downcase }
10
-
11
- def initialize(*blacklist)
12
- @blacklist = Set.new(blacklist)
13
- end
14
-
15
- def censor_hash(h, case_insensitive: false)
16
- if case_insensitive
17
- _censor_hash(h, KEY_TO_DOWNCASE_STRING, downcased_blacklist)
18
- else
19
- _censor_hash(h, KEY_TO_STRING, blacklist)
20
- end
21
- end
22
-
23
- def censor(str)
24
- total_length = str.length
25
- censored_length = [total_length - EXPOSED_CHARS, 0].max
26
- ('*' * censored_length) + str[censored_length...total_length]
27
- end
28
-
29
- private
30
-
31
- def _censor_hash(h, key_transform, cased_blacklist)
32
- h.collect do |(k, v)|
33
- new_v =
34
- cased_blacklist.include?(key_transform.call(k)) ?
35
- censor(v) :
36
- v
37
-
38
- [k, new_v]
39
- end.to_h
40
- end
41
-
42
- def downcased_blacklist
43
- blacklist.collect { |x| x.downcase }
44
- end
45
-
46
- attr_reader :blacklist
47
- end
48
-
49
- # Logs request and response information, while censoring OAuth-relevant keys
50
- class RequestLogger
51
- QUERY_PARAM_CENSOR = Censor.new 'code', 'client_id', 'hash', 'refresh_token'
52
- HEADER_CENSOR = Censor.new 'authorization'
53
- PAYLOAD_CENSOR = Censor.new 'access_token', 'refresh_token'
54
-
55
- TRUNCATED_BODY_LENGTH = 1024
56
-
57
- def initialize(logger, log_full_body:)
58
- @logger = logger
59
- @log_full_body = log_full_body
60
- end
61
-
62
- def log_request(request)
63
- log_request_basics(Logger::INFO, request)
64
- log_headers('Request', request)
65
- log_body('Request', request.body)
66
- end
67
-
68
- def log_retry_attempt(request, response, attempt_num)
69
- logger.warn { "Request attempt #{attempt_num} failed" }
70
- log_request_basics(Logger::WARN, request)
71
- log_api_error(Logger::WARN, response)
72
- end
73
-
74
- def log_retry_failure(num_tries)
75
- try_word = num_tries == 1 ? 'try' : 'tries'
76
- logger.error { "Request failed after #{num_tries} #{try_word}" }
77
- end
78
-
79
- def log_successful_response(response)
80
- log_status(Logger::INFO, response)
81
- log_headers('Response', response)
82
- log_body('Response', response.result)
83
- end
84
-
85
- def log_api_error_response(request, error)
86
- log_request_basics(Logger::ERROR, request)
87
- log_api_error(Logger::ERROR, error)
88
- end
89
-
90
- def log_http_error_response(request, error)
91
- log_request_basics(Logger::ERROR, request)
92
- log_http_error(Logger::ERROR, error)
93
- end
94
-
95
- private
96
-
97
- attr_reader :logger, :log_full_body
98
-
99
- def log_request_basics(level, request)
100
- logger.log(level) { "Request: #{request.method.upcase} #{build_logging_url(request)}" }
101
- end
102
-
103
- def build_logging_url(request)
104
- query_params = QUERY_PARAM_CENSOR.censor_hash(request.params)
105
- query_param_str =
106
- if query_params.empty?
107
- ''
108
- else
109
- '?' + query_params.collect { |(k, v)| "#{k}=#{v}" }.join('&') # TODO: URI Encoding
110
- end
111
- request.url + query_param_str
112
- end
113
-
114
- def log_api_error(level, response)
115
- log_status(level, response)
116
- logger.log(level) do
117
- "#{response.error_code}: #{response.message} - Ref ID: #{response.ref_id}"
118
- end
119
- log_headers('Response', response)
120
- end
121
-
122
- def log_http_error(level, response)
123
- log_status(level, response)
124
- log_headers('Response', response)
125
- end
126
-
127
- def log_status(level, response)
128
- logger.log(level) { "Response: #{response.status_code} #{response.reason_phrase}" }
129
- end
130
-
131
- def log_headers(context, req_or_resp)
132
- censored_hash = HEADER_CENSOR.censor_hash(req_or_resp.headers, case_insensitive: true)
133
- logger.debug { "#{context} Headers: #{censored_hash}" }
134
- end
135
-
136
- def log_body(context, body)
137
- return unless body
138
-
139
- body_str =
140
- if body.is_a? String
141
- body
142
- elsif body.is_a? Hash
143
- PAYLOAD_CENSOR.censor_hash(body).to_s
144
- else
145
- '<Binary body>'
146
- end
147
-
148
- body_str = truncate_body(body_str) unless log_full_body
149
-
150
- logger.debug "#{context} Body: #{body_str}"
151
- end
152
-
153
- def truncate_body(body_str)
154
- if body_str.length > TRUNCATED_BODY_LENGTH
155
- body_str[0...TRUNCATED_BODY_LENGTH] + '...'
156
- else
157
- body_str
158
- end
159
- end
160
- end
161
-
162
- # Stubs all request logging methods by doing nothing (see {RequestLogger})
163
- class MuteRequestLogger
164
- def log_request(request)
165
- end
166
-
167
- def log_retry_attempt(request, response, attempt_num)
168
- end
169
-
170
- def log_retry_failure(num_retries)
171
- end
172
-
173
- def log_successful_response(response)
174
- end
175
-
176
- def log_api_error_response(request, error)
177
- end
178
-
179
- def log_http_error_response(request, error)
180
- end
181
- end
182
- end
1
+ require 'logger'
2
+
3
+ module Smartsheet
4
+ module API
5
+ # Censors strings and hash values for select blacklisted keys
6
+ class Censor
7
+ EXPOSED_CHARS = 4
8
+ KEY_TO_STRING = ->(k){ k.to_s }
9
+ KEY_TO_DOWNCASE_STRING = ->(k){ k.to_s.downcase }
10
+
11
+ def initialize(*blacklist)
12
+ @blacklist = Set.new(blacklist)
13
+ end
14
+
15
+ def censor_hash(h, case_insensitive: false)
16
+ if case_insensitive
17
+ _censor_hash(h, KEY_TO_DOWNCASE_STRING, downcased_blacklist)
18
+ else
19
+ _censor_hash(h, KEY_TO_STRING, blacklist)
20
+ end
21
+ end
22
+
23
+ def censor(str)
24
+ total_length = str.length
25
+ censored_length = [total_length - EXPOSED_CHARS, 0].max
26
+ ('*' * censored_length) + str[censored_length...total_length]
27
+ end
28
+
29
+ private
30
+
31
+ def _censor_hash(h, key_transform, cased_blacklist)
32
+ h.collect do |(k, v)|
33
+ new_v =
34
+ cased_blacklist.include?(key_transform.call(k)) ?
35
+ censor(v) :
36
+ v
37
+
38
+ [k, new_v]
39
+ end.to_h
40
+ end
41
+
42
+ def downcased_blacklist
43
+ blacklist.collect { |x| x.downcase }
44
+ end
45
+
46
+ attr_reader :blacklist
47
+ end
48
+
49
+ # Logs request and response information, while censoring OAuth-relevant keys
50
+ class RequestLogger
51
+ QUERY_PARAM_CENSOR = Censor.new 'code', 'client_id', 'hash', 'refresh_token'
52
+ HEADER_CENSOR = Censor.new 'authorization'
53
+ PAYLOAD_CENSOR = Censor.new 'access_token', 'refresh_token'
54
+
55
+ TRUNCATED_BODY_LENGTH = 1024
56
+
57
+ def initialize(logger, log_full_body:)
58
+ @logger = logger
59
+ @log_full_body = log_full_body
60
+ end
61
+
62
+ def log_request(request)
63
+ log_request_basics(Logger::INFO, request)
64
+ log_headers('Request', request)
65
+ log_body('Request', request.body)
66
+ end
67
+
68
+ def log_retry_attempt(request, response, attempt_num)
69
+ logger.warn { "Request attempt #{attempt_num} failed" }
70
+ log_request_basics(Logger::WARN, request)
71
+ log_api_error(Logger::WARN, response)
72
+ end
73
+
74
+ def log_retry_failure(num_tries)
75
+ try_word = num_tries == 1 ? 'try' : 'tries'
76
+ logger.error { "Request failed after #{num_tries} #{try_word}" }
77
+ end
78
+
79
+ def log_successful_response(response)
80
+ log_status(Logger::INFO, response)
81
+ log_headers('Response', response)
82
+ log_body('Response', response.result)
83
+ end
84
+
85
+ def log_api_error_response(request, error)
86
+ log_request_basics(Logger::ERROR, request)
87
+ log_api_error(Logger::ERROR, error)
88
+ end
89
+
90
+ def log_http_error_response(request, error)
91
+ log_request_basics(Logger::ERROR, request)
92
+ log_http_error(Logger::ERROR, error)
93
+ end
94
+
95
+ private
96
+
97
+ attr_reader :logger, :log_full_body
98
+
99
+ def log_request_basics(level, request)
100
+ logger.log(level) { "Request: #{request.method.upcase} #{build_logging_url(request)}" }
101
+ end
102
+
103
+ def build_logging_url(request)
104
+ query_params = QUERY_PARAM_CENSOR.censor_hash(request.params)
105
+ query_param_str =
106
+ if query_params.empty?
107
+ ''
108
+ else
109
+ '?' + query_params.collect { |(k, v)| "#{k}=#{v}" }.join('&') # TODO: URI Encoding
110
+ end
111
+ request.url + query_param_str
112
+ end
113
+
114
+ def log_api_error(level, response)
115
+ log_status(level, response)
116
+ logger.log(level) do
117
+ "#{response.error_code}: #{response.message} - Ref ID: #{response.ref_id}"
118
+ end
119
+ log_headers('Response', response)
120
+ end
121
+
122
+ def log_http_error(level, response)
123
+ log_status(level, response)
124
+ log_headers('Response', response)
125
+ end
126
+
127
+ def log_status(level, response)
128
+ logger.log(level) { "Response: #{response.status_code} #{response.reason_phrase}" }
129
+ end
130
+
131
+ def log_headers(context, req_or_resp)
132
+ censored_hash = HEADER_CENSOR.censor_hash(req_or_resp.headers, case_insensitive: true)
133
+ logger.debug { "#{context} Headers: #{censored_hash}" }
134
+ end
135
+
136
+ def log_body(context, body)
137
+ return unless body
138
+
139
+ body_str =
140
+ if body.is_a? String
141
+ body
142
+ elsif body.is_a? Hash
143
+ PAYLOAD_CENSOR.censor_hash(body).to_s
144
+ else
145
+ '<Binary body>'
146
+ end
147
+
148
+ body_str = truncate_body(body_str) unless log_full_body
149
+
150
+ logger.debug "#{context} Body: #{body_str}"
151
+ end
152
+
153
+ def truncate_body(body_str)
154
+ if body_str.length > TRUNCATED_BODY_LENGTH
155
+ body_str[0...TRUNCATED_BODY_LENGTH] + '...'
156
+ else
157
+ body_str
158
+ end
159
+ end
160
+ end
161
+
162
+ # Stubs all request logging methods by doing nothing (see {RequestLogger})
163
+ class MuteRequestLogger
164
+ def log_request(request)
165
+ end
166
+
167
+ def log_retry_attempt(request, response, attempt_num)
168
+ end
169
+
170
+ def log_retry_failure(num_retries)
171
+ end
172
+
173
+ def log_successful_response(response)
174
+ end
175
+
176
+ def log_api_error_response(request, error)
177
+ end
178
+
179
+ def log_http_error_response(request, error)
180
+ end
181
+ end
182
+ end
183
183
  end