smartsheet 1.0.0.beta.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 (68) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.rubocop.yml +5 -0
  4. data/.travis.yml +6 -0
  5. data/Gemfile +6 -0
  6. data/LICENSE +202 -0
  7. data/README.md +139 -0
  8. data/Rakefile +13 -0
  9. data/bin/console +14 -0
  10. data/bin/setup +8 -0
  11. data/lib/smartsheet.rb +3 -0
  12. data/lib/smartsheet/api/body_builder.rb +26 -0
  13. data/lib/smartsheet/api/endpoint_spec.rb +35 -0
  14. data/lib/smartsheet/api/faraday_adapter/faraday_net_client.rb +42 -0
  15. data/lib/smartsheet/api/faraday_adapter/faraday_response.rb +60 -0
  16. data/lib/smartsheet/api/faraday_adapter/middleware/faraday_error_translator.rb +20 -0
  17. data/lib/smartsheet/api/faraday_adapter/middleware/response_parser.rb +25 -0
  18. data/lib/smartsheet/api/file_spec.rb +28 -0
  19. data/lib/smartsheet/api/header_builder.rb +85 -0
  20. data/lib/smartsheet/api/request.rb +29 -0
  21. data/lib/smartsheet/api/request_client.rb +26 -0
  22. data/lib/smartsheet/api/request_logger.rb +148 -0
  23. data/lib/smartsheet/api/request_spec.rb +43 -0
  24. data/lib/smartsheet/api/response_net_client_decorator.rb +47 -0
  25. data/lib/smartsheet/api/retry_logic.rb +37 -0
  26. data/lib/smartsheet/api/retry_net_client_decorator.rb +36 -0
  27. data/lib/smartsheet/api/url_builder.rb +25 -0
  28. data/lib/smartsheet/client.rb +115 -0
  29. data/lib/smartsheet/constants.rb +16 -0
  30. data/lib/smartsheet/endpoints/contacts/contacts.rb +29 -0
  31. data/lib/smartsheet/endpoints/favorites/favorites.rb +158 -0
  32. data/lib/smartsheet/endpoints/folders/folders.rb +124 -0
  33. data/lib/smartsheet/endpoints/groups/groups.rb +82 -0
  34. data/lib/smartsheet/endpoints/home/home.rb +19 -0
  35. data/lib/smartsheet/endpoints/reports/reports.rb +96 -0
  36. data/lib/smartsheet/endpoints/reports/reports_share.rb +68 -0
  37. data/lib/smartsheet/endpoints/search/search.rb +29 -0
  38. data/lib/smartsheet/endpoints/server_info/server_info.rb +19 -0
  39. data/lib/smartsheet/endpoints/share/share.rb +58 -0
  40. data/lib/smartsheet/endpoints/sheets/cells.rb +80 -0
  41. data/lib/smartsheet/endpoints/sheets/columns.rb +65 -0
  42. data/lib/smartsheet/endpoints/sheets/comments.rb +60 -0
  43. data/lib/smartsheet/endpoints/sheets/comments_attachments.rb +77 -0
  44. data/lib/smartsheet/endpoints/sheets/discussions.rb +80 -0
  45. data/lib/smartsheet/endpoints/sheets/discussions_attachments.rb +21 -0
  46. data/lib/smartsheet/endpoints/sheets/rows.rb +91 -0
  47. data/lib/smartsheet/endpoints/sheets/rows_attachments.rb +91 -0
  48. data/lib/smartsheet/endpoints/sheets/sheets.rb +301 -0
  49. data/lib/smartsheet/endpoints/sheets/sheets_attachments.rb +173 -0
  50. data/lib/smartsheet/endpoints/sheets/sheets_share.rb +68 -0
  51. data/lib/smartsheet/endpoints/sights/sights.rb +97 -0
  52. data/lib/smartsheet/endpoints/sights/sights_share.rb +68 -0
  53. data/lib/smartsheet/endpoints/templates/templates.rb +28 -0
  54. data/lib/smartsheet/endpoints/token/token.rb +57 -0
  55. data/lib/smartsheet/endpoints/update_requests/sent_update_requests.rb +42 -0
  56. data/lib/smartsheet/endpoints/update_requests/update_requests.rb +69 -0
  57. data/lib/smartsheet/endpoints/users/alternate_emails.rb +77 -0
  58. data/lib/smartsheet/endpoints/users/users.rb +73 -0
  59. data/lib/smartsheet/endpoints/webhooks/webhooks.rb +70 -0
  60. data/lib/smartsheet/endpoints/workspaces/workspaces.rb +83 -0
  61. data/lib/smartsheet/endpoints/workspaces/workspaces_share.rb +68 -0
  62. data/lib/smartsheet/error.rb +30 -0
  63. data/lib/smartsheet/general_request.rb +53 -0
  64. data/lib/smartsheet/version.rb +5 -0
  65. data/read-write-sheet/config.json +4 -0
  66. data/read-write-sheet/read_write_sheet.rb +89 -0
  67. data/smartsheet.gemspec +47 -0
  68. metadata +279 -0
@@ -0,0 +1,35 @@
1
+ require_relative 'url_builder'
2
+
3
+ module Smartsheet
4
+ module API
5
+ class EndpointSpec
6
+ attr_reader :method, :url_segments, :spec
7
+
8
+ def initialize(method, url, **spec)
9
+ @method = method
10
+ @url_segments = url
11
+ @spec = spec
12
+ end
13
+
14
+ def requires_auth?
15
+ !spec.key?(:no_auth)
16
+ end
17
+
18
+ def requires_body?
19
+ spec.key? :body_type
20
+ end
21
+
22
+ def sending_file?
23
+ requires_body? && spec[:body_type] == :file
24
+ end
25
+
26
+ def sending_json?
27
+ requires_body? && spec[:body_type] == :json
28
+ end
29
+
30
+ def headers
31
+ spec.key?(:headers) ? spec[:headers] : {}
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,42 @@
1
+ require 'faraday'
2
+ require 'smartsheet/api/request'
3
+ require 'smartsheet/api/faraday_adapter/middleware/faraday_error_translator'
4
+ require 'smartsheet/api/faraday_adapter/middleware/response_parser'
5
+
6
+ module Smartsheet
7
+ module API
8
+ class FaradayNetClient
9
+ def initialize
10
+ create_connection
11
+ end
12
+
13
+ # Expected output:
14
+ # - returned Success Response
15
+ # - returned Error Response
16
+ # - thrown Request Error
17
+ def make_request(request)
18
+ response = conn.send(request.method) do |req|
19
+ req.url(request.url)
20
+ req.headers = request.headers
21
+ req.params = request.params
22
+ req.body = request.body
23
+ end
24
+
25
+ response.body
26
+ end
27
+
28
+ private
29
+
30
+ def create_connection
31
+ @conn = Faraday.new do |conn|
32
+ conn.use Middleware::FaradayErrorTranslator
33
+ conn.use Middleware::ResponseParser
34
+
35
+ conn.adapter Faraday.default_adapter
36
+ end
37
+ end
38
+
39
+ attr_reader :conn
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,60 @@
1
+ module Smartsheet
2
+ module API
3
+ class FaradayResponse
4
+ def self.from_response_env(faraday_response)
5
+ if faraday_response[:body].kind_of?(Hash) && faraday_response[:body].key?(:errorCode)
6
+ FaradayErrorResponse.new(faraday_response[:body], faraday_response)
7
+ else
8
+ FaradaySuccessResponse.new(faraday_response[:body], faraday_response)
9
+ end
10
+ end
11
+
12
+ attr_reader :status_code, :reason_phrase, :headers
13
+
14
+ def initialize(faraday_response)
15
+ @status_code = faraday_response[:status]
16
+ @reason_phrase = faraday_response[:reason_phrase]
17
+ @headers = faraday_response[:response_headers]
18
+ end
19
+ end
20
+
21
+ class FaradayErrorResponse < FaradayResponse
22
+ RETRYABLE_ERRORS = 4001..4004
23
+
24
+ attr_reader :error_code, :message, :ref_id, :detail
25
+
26
+ def initialize(result, faraday_response)
27
+ super(faraday_response)
28
+ @error_code = result[:errorCode]
29
+ @message = result[:message]
30
+ @ref_id = result[:refId]
31
+ @detail = result[:detail]
32
+ end
33
+
34
+ def should_retry?
35
+ RETRYABLE_ERRORS.include? error_code
36
+ end
37
+
38
+ def success?
39
+ false
40
+ end
41
+ end
42
+
43
+ class FaradaySuccessResponse < FaradayResponse
44
+ attr_reader :result
45
+
46
+ def initialize(result, faraday_response)
47
+ super(faraday_response)
48
+ @result = result
49
+ end
50
+
51
+ def should_retry?
52
+ false
53
+ end
54
+
55
+ def success?
56
+ true
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,20 @@
1
+ require 'faraday'
2
+ require 'smartsheet/error'
3
+
4
+ module Smartsheet
5
+ module API
6
+ module Middleware
7
+ class FaradayErrorTranslator < Faraday::Middleware
8
+ def initialize(app)
9
+ super(app)
10
+ end
11
+
12
+ def call(env)
13
+ @app.call(env)
14
+ rescue Faraday::Error => e
15
+ raise Smartsheet::RequestError, e
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,25 @@
1
+ require 'faraday'
2
+ require 'json'
3
+ require 'smartsheet/api/faraday_adapter/faraday_response'
4
+
5
+ module Smartsheet
6
+ module API
7
+ module Middleware
8
+ class ResponseParser < Faraday::Middleware
9
+ def initialize(app)
10
+ super(app)
11
+ end
12
+
13
+ def call(env)
14
+ @app.call(env).on_complete do |response_env|
15
+ if response_env[:response_headers]['content-type'] =~ /\bjson\b/
16
+ response_env[:body] = JSON.parse(response_env[:body], symbolize_names: true)
17
+ end
18
+
19
+ response_env[:body] = FaradayResponse.from_response_env response_env
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,28 @@
1
+ require 'cgi'
2
+ require 'faraday'
3
+
4
+ module Smartsheet
5
+ module API
6
+ class PathFileSpec
7
+ attr_reader :upload_io, :filename, :content_type, :file_length
8
+
9
+ def initialize(path, filename, content_type)
10
+ @file_length = File.size(path)
11
+ @filename = filename.nil? ? File.basename(path) : filename
12
+ @upload_io = Faraday::UploadIO.new(path, content_type, CGI::escape(@filename))
13
+ @content_type = content_type
14
+ end
15
+ end
16
+
17
+ class ObjectFileSpec
18
+ attr_reader :upload_io, :filename, :content_type, :file_length
19
+
20
+ def initialize(file, filename, file_length, content_type)
21
+ @file_length = file_length
22
+ @filename = filename
23
+ @upload_io = Faraday::UploadIO.new(file, content_type, CGI::escape(filename))
24
+ @content_type = content_type
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,85 @@
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
85
+ end
@@ -0,0 +1,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
+ class Request
8
+ attr_reader :method, :url, :headers, :params, :body
9
+
10
+ def initialize(token, endpoint_spec, request_spec, base_url, assume_user: nil)
11
+ @method = endpoint_spec.method
12
+ @url = Smartsheet::API::UrlBuilder.new(endpoint_spec, request_spec, base_url).build
13
+ @headers = Smartsheet::API::HeaderBuilder.new(token, endpoint_spec, request_spec, assume_user: assume_user).build
14
+ @params = request_spec.params
15
+ @body = Smartsheet::API::BodyBuilder.new(endpoint_spec, request_spec).build
16
+ end
17
+
18
+ def ==(other)
19
+ other.class == self.class && other.equality_state == equality_state
20
+ end
21
+
22
+ protected
23
+
24
+ def equality_state
25
+ [method, url, headers, params, body]
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,26 @@
1
+ require 'smartsheet/error'
2
+
3
+ module Smartsheet
4
+ module API
5
+ class RequestClient
6
+ def initialize(token, client, base_url, assume_user: nil, logger: MuteRequestLogger.new)
7
+ @token = token
8
+ @client = client
9
+ @assume_user = assume_user
10
+ @logger = logger
11
+ @base_url = base_url
12
+ end
13
+
14
+ def make_request(endpoint_spec, request_spec)
15
+ request = Request.new(token, endpoint_spec, request_spec, base_url, assume_user: assume_user)
16
+
17
+ logger.log_request(request)
18
+ client.make_request(request)
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :token, :client, :assume_user, :logger, :base_url
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,148 @@
1
+ require 'logger'
2
+
3
+ module Smartsheet
4
+ module API
5
+ class Censor
6
+ EXPOSED_CHARS = 4
7
+
8
+ def initialize(*blacklist)
9
+ @blacklist = Set.new(blacklist)
10
+ end
11
+
12
+ def censor_hash(h)
13
+ h.collect do |(k, v)|
14
+ new_v = blacklist.include?(k.to_s) ? censor(v) : v
15
+ [k, new_v]
16
+ end.to_h
17
+ end
18
+
19
+ def censor(str)
20
+ total_length = str.length
21
+ censored_length = [total_length - EXPOSED_CHARS, 0].max
22
+ ('*' * censored_length) + str[censored_length...total_length]
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :blacklist
28
+ end
29
+
30
+ class RequestLogger
31
+ QUERY_PARAM_CENSOR = Censor.new 'code', 'client_id', 'hash', 'refresh_token'
32
+ HEADER_CENSOR = Censor.new 'authorization'
33
+ PAYLOAD_CENSOR = Censor.new 'access_token', 'refresh_token'
34
+
35
+ TRUNCATED_BODY_LENGTH = 1024
36
+
37
+ def initialize(logger, log_full_body:)
38
+ @logger = logger
39
+ @log_full_body = log_full_body
40
+ end
41
+
42
+ def log_request(request)
43
+ log_request_basics(Logger::INFO, request)
44
+ log_headers('Request', request)
45
+ log_body('Request', request.body)
46
+ end
47
+
48
+ def log_retry_attempt(request, response, attempt_num)
49
+ logger.warn { "Request attempt #{attempt_num} failed" }
50
+ log_request_basics(Logger::WARN, request)
51
+ log_api_error(Logger::WARN, response)
52
+ end
53
+
54
+ def log_retry_failure(num_tries)
55
+ try_word = num_tries == 1 ? 'try' : 'tries'
56
+ logger.error { "Request failed after #{num_tries} #{try_word}" }
57
+ end
58
+
59
+ def log_successful_response(response)
60
+ log_status(Logger::INFO, response)
61
+ log_headers('Response', response)
62
+ log_body('Response', response.result)
63
+ end
64
+
65
+ def log_error_response(request, error)
66
+ log_request_basics(Logger::ERROR, request)
67
+ log_api_error(Logger::ERROR, error)
68
+ end
69
+
70
+ private
71
+
72
+ attr_reader :logger, :log_full_body
73
+
74
+ def log_request_basics(level, request)
75
+ logger.log(level) { "Request: #{request.method} #{build_logging_url(request)}" }
76
+ end
77
+
78
+ def build_logging_url(request)
79
+ query_params = QUERY_PARAM_CENSOR.censor_hash(request.params)
80
+ query_param_str =
81
+ if query_params.empty?
82
+ ''
83
+ else
84
+ '?' + query_params.collect { |(k, v)| "#{k}=#{v}" }.join('&') # TODO: URI Encoding
85
+ end
86
+ request.url + query_param_str
87
+ end
88
+
89
+ def log_api_error(level, response)
90
+ log_status(level, response)
91
+ logger.log(level) do
92
+ "#{response.error_code}: #{response.message} - Ref ID: #{response.ref_id}"
93
+ end
94
+ log_headers('Response', response)
95
+ end
96
+
97
+ def log_status(level, response)
98
+ logger.log(level) { "Response: #{response.status_code} #{response.reason_phrase}" }
99
+ end
100
+
101
+ def log_headers(context, req_or_resp)
102
+ logger.debug { "#{context} Headers: #{HEADER_CENSOR.censor_hash(req_or_resp.headers)}" }
103
+ end
104
+
105
+ def log_body(context, body)
106
+ return unless body
107
+
108
+ body_str =
109
+ if body.is_a? String
110
+ body
111
+ elsif body.is_a? Hash
112
+ PAYLOAD_CENSOR.censor_hash(body).to_s
113
+ else
114
+ '<Binary body>'
115
+ end
116
+
117
+ body_str = truncate_body(body_str) unless log_full_body
118
+
119
+ logger.debug "#{context} Body: #{body_str}"
120
+ end
121
+
122
+ def truncate_body(body_str)
123
+ if body_str.length > TRUNCATED_BODY_LENGTH
124
+ body_str[0...TRUNCATED_BODY_LENGTH] + '...'
125
+ else
126
+ body_str
127
+ end
128
+ end
129
+ end
130
+
131
+ class MuteRequestLogger
132
+ def log_request(request)
133
+ end
134
+
135
+ def log_retry_attempt(request, response, attempt_num)
136
+ end
137
+
138
+ def log_retry_failure(num_retries)
139
+ end
140
+
141
+ def log_successful_response(response)
142
+ end
143
+
144
+ def log_error_response(request, error)
145
+ end
146
+ end
147
+ end
148
+ end