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.
- checksums.yaml +4 -4
- data/.gitignore +11 -11
- data/.rubocop.yml +4 -4
- data/.travis.yml +13 -6
- data/.yardopts +3 -3
- data/CHANGELOG.md +103 -0
- data/Gemfile +6 -6
- data/LICENSE +202 -202
- data/README.md +214 -141
- data/Rakefile +29 -23
- data/bin/console +14 -14
- data/bin/setup +8 -8
- data/lib/smartsheet.rb +2 -2
- data/lib/smartsheet/api/body_builder.rb +25 -25
- data/lib/smartsheet/api/endpoint_spec.rb +54 -36
- data/lib/smartsheet/api/faraday_adapter/faraday_net_client.rb +45 -42
- data/lib/smartsheet/api/faraday_adapter/faraday_response.rb +70 -70
- data/lib/smartsheet/api/faraday_adapter/middleware/faraday_error_translator.rb +20 -20
- data/lib/smartsheet/api/faraday_adapter/middleware/response_parser.rb +25 -25
- data/lib/smartsheet/api/file_spec.rb +31 -31
- data/lib/smartsheet/api/header_builder.rb +90 -84
- data/lib/smartsheet/api/request.rb +42 -29
- data/lib/smartsheet/api/request_client.rb +43 -27
- data/lib/smartsheet/api/request_logger.rb +182 -182
- data/lib/smartsheet/api/request_spec.rb +57 -44
- data/lib/smartsheet/api/response_net_client_decorator.rb +54 -54
- data/lib/smartsheet/api/retry_logic.rb +40 -40
- data/lib/smartsheet/api/retry_net_client_decorator.rb +37 -37
- data/lib/smartsheet/api/url_builder.rb +25 -25
- data/lib/smartsheet/client.rb +191 -185
- data/lib/smartsheet/constants.rb +15 -15
- data/lib/smartsheet/endpoints/contacts/contacts.rb +30 -30
- data/lib/smartsheet/endpoints/favorites/favorites.rb +159 -159
- data/lib/smartsheet/endpoints/folders/folders.rb +125 -125
- data/lib/smartsheet/endpoints/groups/groups.rb +83 -83
- data/lib/smartsheet/endpoints/home/home.rb +20 -20
- data/lib/smartsheet/endpoints/reports/reports.rb +100 -100
- data/lib/smartsheet/endpoints/reports/reports_share.rb +69 -69
- data/lib/smartsheet/endpoints/search/search.rb +30 -30
- data/lib/smartsheet/endpoints/server_info/server_info.rb +21 -21
- data/lib/smartsheet/endpoints/share/share.rb +58 -58
- data/lib/smartsheet/endpoints/sheets/automation_rules.rb +56 -0
- data/lib/smartsheet/endpoints/sheets/cells.rb +82 -82
- data/lib/smartsheet/endpoints/sheets/columns.rb +66 -66
- data/lib/smartsheet/endpoints/sheets/comments.rb +64 -64
- data/lib/smartsheet/endpoints/sheets/comments_attachments.rb +78 -78
- data/lib/smartsheet/endpoints/sheets/cross_sheet_references.rb +45 -0
- data/lib/smartsheet/endpoints/sheets/discussions.rb +84 -84
- data/lib/smartsheet/endpoints/sheets/discussions_attachments.rb +22 -22
- data/lib/smartsheet/endpoints/sheets/rows.rb +106 -95
- data/lib/smartsheet/endpoints/sheets/rows_attachments.rb +92 -92
- data/lib/smartsheet/endpoints/sheets/sheets.rb +326 -317
- data/lib/smartsheet/endpoints/sheets/sheets_attachments.rb +174 -174
- data/lib/smartsheet/endpoints/sheets/sheets_share.rb +69 -69
- data/lib/smartsheet/endpoints/sights/sights.rb +101 -101
- data/lib/smartsheet/endpoints/sights/sights_share.rb +69 -69
- data/lib/smartsheet/endpoints/templates/templates.rb +29 -29
- data/lib/smartsheet/endpoints/token/token.rb +65 -60
- data/lib/smartsheet/endpoints/update_requests/sent_update_requests.rb +44 -44
- data/lib/smartsheet/endpoints/update_requests/update_requests.rb +74 -74
- data/lib/smartsheet/endpoints/users/alternate_emails.rb +79 -79
- data/lib/smartsheet/endpoints/users/users.rb +77 -77
- data/lib/smartsheet/endpoints/webhooks/webhooks.rb +71 -71
- data/lib/smartsheet/endpoints/workspaces/workspaces.rb +87 -87
- data/lib/smartsheet/endpoints/workspaces/workspaces_share.rb +70 -70
- data/lib/smartsheet/error.rb +69 -69
- data/lib/smartsheet/general_request.rb +74 -74
- data/lib/smartsheet/version.rb +5 -5
- data/smartsheet.gemspec +54 -52
- 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
|
-
@
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
.merge(
|
21
|
-
.merge(
|
22
|
-
.merge(
|
23
|
-
.merge(
|
24
|
-
.merge(
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
{
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
{
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
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(
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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/
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
#
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|