smartsheet 1.0.0.beta.0

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