basecamp-sdk 0.2.1
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 +7 -0
- data/.rubocop.yml +14 -0
- data/.yardopts +6 -0
- data/README.md +293 -0
- data/Rakefile +26 -0
- data/basecamp-sdk.gemspec +46 -0
- data/lib/basecamp/auth_strategy.rb +38 -0
- data/lib/basecamp/chain_hooks.rb +45 -0
- data/lib/basecamp/client.rb +428 -0
- data/lib/basecamp/config.rb +143 -0
- data/lib/basecamp/errors.rb +289 -0
- data/lib/basecamp/generated/metadata.json +2281 -0
- data/lib/basecamp/generated/services/attachments_service.rb +24 -0
- data/lib/basecamp/generated/services/boosts_service.rb +70 -0
- data/lib/basecamp/generated/services/campfires_service.rb +122 -0
- data/lib/basecamp/generated/services/card_columns_service.rb +103 -0
- data/lib/basecamp/generated/services/card_steps_service.rb +57 -0
- data/lib/basecamp/generated/services/card_tables_service.rb +20 -0
- data/lib/basecamp/generated/services/cards_service.rb +66 -0
- data/lib/basecamp/generated/services/checkins_service.rb +157 -0
- data/lib/basecamp/generated/services/client_approvals_service.rb +28 -0
- data/lib/basecamp/generated/services/client_correspondences_service.rb +28 -0
- data/lib/basecamp/generated/services/client_replies_service.rb +30 -0
- data/lib/basecamp/generated/services/client_visibility_service.rb +21 -0
- data/lib/basecamp/generated/services/comments_service.rb +49 -0
- data/lib/basecamp/generated/services/documents_service.rb +52 -0
- data/lib/basecamp/generated/services/events_service.rb +20 -0
- data/lib/basecamp/generated/services/forwards_service.rb +67 -0
- data/lib/basecamp/generated/services/lineup_service.rb +44 -0
- data/lib/basecamp/generated/services/message_boards_service.rb +20 -0
- data/lib/basecamp/generated/services/message_types_service.rb +59 -0
- data/lib/basecamp/generated/services/messages_service.rb +75 -0
- data/lib/basecamp/generated/services/people_service.rb +73 -0
- data/lib/basecamp/generated/services/projects_service.rb +63 -0
- data/lib/basecamp/generated/services/recordings_service.rb +64 -0
- data/lib/basecamp/generated/services/reports_service.rb +56 -0
- data/lib/basecamp/generated/services/schedules_service.rb +92 -0
- data/lib/basecamp/generated/services/search_service.rb +31 -0
- data/lib/basecamp/generated/services/subscriptions_service.rb +50 -0
- data/lib/basecamp/generated/services/templates_service.rb +82 -0
- data/lib/basecamp/generated/services/timeline_service.rb +20 -0
- data/lib/basecamp/generated/services/timesheets_service.rb +81 -0
- data/lib/basecamp/generated/services/todolist_groups_service.rb +41 -0
- data/lib/basecamp/generated/services/todolists_service.rb +53 -0
- data/lib/basecamp/generated/services/todos_service.rb +106 -0
- data/lib/basecamp/generated/services/todosets_service.rb +20 -0
- data/lib/basecamp/generated/services/tools_service.rb +80 -0
- data/lib/basecamp/generated/services/uploads_service.rb +61 -0
- data/lib/basecamp/generated/services/vaults_service.rb +49 -0
- data/lib/basecamp/generated/services/webhooks_service.rb +63 -0
- data/lib/basecamp/generated/types.rb +3196 -0
- data/lib/basecamp/hooks.rb +70 -0
- data/lib/basecamp/http.rb +440 -0
- data/lib/basecamp/logger_hooks.rb +46 -0
- data/lib/basecamp/noop_hooks.rb +9 -0
- data/lib/basecamp/oauth/discovery.rb +123 -0
- data/lib/basecamp/oauth/errors.rb +35 -0
- data/lib/basecamp/oauth/exchange.rb +291 -0
- data/lib/basecamp/oauth/pkce.rb +68 -0
- data/lib/basecamp/oauth/types.rb +133 -0
- data/lib/basecamp/oauth.rb +56 -0
- data/lib/basecamp/oauth_token_provider.rb +108 -0
- data/lib/basecamp/operation_info.rb +17 -0
- data/lib/basecamp/request_info.rb +10 -0
- data/lib/basecamp/request_result.rb +14 -0
- data/lib/basecamp/security.rb +112 -0
- data/lib/basecamp/services/attachments_service.rb +33 -0
- data/lib/basecamp/services/authorization_service.rb +47 -0
- data/lib/basecamp/services/base_service.rb +146 -0
- data/lib/basecamp/services/campfires_service.rb +141 -0
- data/lib/basecamp/services/card_columns_service.rb +106 -0
- data/lib/basecamp/services/card_steps_service.rb +86 -0
- data/lib/basecamp/services/card_tables_service.rb +23 -0
- data/lib/basecamp/services/cards_service.rb +93 -0
- data/lib/basecamp/services/checkins_service.rb +127 -0
- data/lib/basecamp/services/client_approvals_service.rb +33 -0
- data/lib/basecamp/services/client_correspondences_service.rb +33 -0
- data/lib/basecamp/services/client_replies_service.rb +35 -0
- data/lib/basecamp/services/comments_service.rb +63 -0
- data/lib/basecamp/services/documents_service.rb +74 -0
- data/lib/basecamp/services/events_service.rb +27 -0
- data/lib/basecamp/services/forwards_service.rb +80 -0
- data/lib/basecamp/services/lineup_service.rb +67 -0
- data/lib/basecamp/services/message_boards_service.rb +24 -0
- data/lib/basecamp/services/message_types_service.rb +79 -0
- data/lib/basecamp/services/messages_service.rb +133 -0
- data/lib/basecamp/services/people_service.rb +73 -0
- data/lib/basecamp/services/projects_service.rb +67 -0
- data/lib/basecamp/services/recordings_service.rb +127 -0
- data/lib/basecamp/services/reports_service.rb +80 -0
- data/lib/basecamp/services/schedules_service.rb +156 -0
- data/lib/basecamp/services/search_service.rb +36 -0
- data/lib/basecamp/services/subscriptions_service.rb +67 -0
- data/lib/basecamp/services/templates_service.rb +96 -0
- data/lib/basecamp/services/timeline_service.rb +62 -0
- data/lib/basecamp/services/timesheet_service.rb +68 -0
- data/lib/basecamp/services/todolist_groups_service.rb +100 -0
- data/lib/basecamp/services/todolists_service.rb +104 -0
- data/lib/basecamp/services/todos_service.rb +156 -0
- data/lib/basecamp/services/todosets_service.rb +23 -0
- data/lib/basecamp/services/tools_service.rb +89 -0
- data/lib/basecamp/services/uploads_service.rb +84 -0
- data/lib/basecamp/services/vaults_service.rb +84 -0
- data/lib/basecamp/services/webhooks_service.rb +88 -0
- data/lib/basecamp/static_token_provider.rb +24 -0
- data/lib/basecamp/token_provider.rb +42 -0
- data/lib/basecamp/version.rb +6 -0
- data/lib/basecamp/webhooks/event.rb +52 -0
- data/lib/basecamp/webhooks/rack_middleware.rb +49 -0
- data/lib/basecamp/webhooks/receiver.rb +161 -0
- data/lib/basecamp/webhooks/verify.rb +36 -0
- data/lib/basecamp.rb +107 -0
- data/scripts/generate-metadata.rb +106 -0
- data/scripts/generate-services.rb +778 -0
- data/scripts/generate-types.rb +191 -0
- metadata +316 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Error types and codes for the Basecamp SDK.
|
|
4
|
+
module Basecamp
|
|
5
|
+
# Error codes for API responses
|
|
6
|
+
module ErrorCode
|
|
7
|
+
USAGE = "usage"
|
|
8
|
+
NOT_FOUND = "not_found"
|
|
9
|
+
AUTH = "auth_required"
|
|
10
|
+
FORBIDDEN = "forbidden"
|
|
11
|
+
RATE_LIMIT = "rate_limit"
|
|
12
|
+
NETWORK = "network"
|
|
13
|
+
API = "api_error"
|
|
14
|
+
AMBIGUOUS = "ambiguous"
|
|
15
|
+
VALIDATION = "validation"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Exit codes for CLI tools
|
|
19
|
+
module ExitCode
|
|
20
|
+
OK = 0
|
|
21
|
+
USAGE = 1
|
|
22
|
+
NOT_FOUND = 2
|
|
23
|
+
AUTH = 3
|
|
24
|
+
FORBIDDEN = 4
|
|
25
|
+
RATE_LIMIT = 5
|
|
26
|
+
NETWORK = 6
|
|
27
|
+
API = 7
|
|
28
|
+
AMBIGUOUS = 8
|
|
29
|
+
VALIDATION = 9
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Base error class for all Basecamp SDK errors.
|
|
33
|
+
# Provides structured error handling with codes, hints, and CLI exit codes.
|
|
34
|
+
#
|
|
35
|
+
# @example Catching errors
|
|
36
|
+
# begin
|
|
37
|
+
# client.projects.list
|
|
38
|
+
# rescue Basecamp::Error => e
|
|
39
|
+
# puts "#{e.code}: #{e.message}"
|
|
40
|
+
# puts "Hint: #{e.hint}" if e.hint
|
|
41
|
+
# exit e.exit_code
|
|
42
|
+
# end
|
|
43
|
+
class Error < StandardError
|
|
44
|
+
# @return [String] error category code
|
|
45
|
+
attr_reader :code
|
|
46
|
+
|
|
47
|
+
# @return [String, nil] user-friendly hint for resolving the error
|
|
48
|
+
attr_reader :hint
|
|
49
|
+
|
|
50
|
+
# @return [Integer, nil] HTTP status code that caused the error
|
|
51
|
+
attr_reader :http_status
|
|
52
|
+
|
|
53
|
+
# @return [Boolean] whether the operation can be retried
|
|
54
|
+
attr_reader :retryable
|
|
55
|
+
|
|
56
|
+
# @return [Integer, nil] seconds to wait before retrying (for rate limits)
|
|
57
|
+
attr_reader :retry_after
|
|
58
|
+
|
|
59
|
+
# @return [Exception, nil] original error that caused this error
|
|
60
|
+
attr_reader :cause
|
|
61
|
+
|
|
62
|
+
# @param code [String] error category code
|
|
63
|
+
# @param message [String] error message
|
|
64
|
+
# @param hint [String, nil] user-friendly hint
|
|
65
|
+
# @param http_status [Integer, nil] HTTP status code
|
|
66
|
+
# @param retryable [Boolean] whether operation can be retried
|
|
67
|
+
# @param retry_after [Integer, nil] seconds to wait before retry
|
|
68
|
+
# @param cause [Exception, nil] underlying cause
|
|
69
|
+
def initialize(code:, message:, hint: nil, http_status: nil, retryable: false, retry_after: nil, cause: nil)
|
|
70
|
+
super(message)
|
|
71
|
+
@code = code
|
|
72
|
+
@hint = hint
|
|
73
|
+
@http_status = http_status
|
|
74
|
+
@retryable = retryable
|
|
75
|
+
@retry_after = retry_after
|
|
76
|
+
@cause = cause
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Returns the exit code for CLI applications.
|
|
80
|
+
# @return [Integer]
|
|
81
|
+
def exit_code
|
|
82
|
+
self.class.exit_code_for(@code)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Returns whether this error can be retried.
|
|
86
|
+
# @return [Boolean]
|
|
87
|
+
def retryable?
|
|
88
|
+
@retryable
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Maps error codes to exit codes.
|
|
92
|
+
# @param code [String]
|
|
93
|
+
# @return [Integer]
|
|
94
|
+
def self.exit_code_for(code)
|
|
95
|
+
case code
|
|
96
|
+
when ErrorCode::USAGE then ExitCode::USAGE
|
|
97
|
+
when ErrorCode::NOT_FOUND then ExitCode::NOT_FOUND
|
|
98
|
+
when ErrorCode::AUTH then ExitCode::AUTH
|
|
99
|
+
when ErrorCode::FORBIDDEN then ExitCode::FORBIDDEN
|
|
100
|
+
when ErrorCode::RATE_LIMIT then ExitCode::RATE_LIMIT
|
|
101
|
+
when ErrorCode::NETWORK then ExitCode::NETWORK
|
|
102
|
+
when ErrorCode::API then ExitCode::API
|
|
103
|
+
when ErrorCode::AMBIGUOUS then ExitCode::AMBIGUOUS
|
|
104
|
+
when ErrorCode::VALIDATION then ExitCode::VALIDATION
|
|
105
|
+
else ExitCode::API
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Raised when there's a usage error (invalid arguments, missing config).
|
|
111
|
+
class UsageError < Error
|
|
112
|
+
def initialize(message, hint: nil)
|
|
113
|
+
super(code: ErrorCode::USAGE, message: message, hint: hint)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Raised when a resource is not found (404).
|
|
118
|
+
class NotFoundError < Error
|
|
119
|
+
def initialize(resource, identifier, hint: nil)
|
|
120
|
+
super(
|
|
121
|
+
code: ErrorCode::NOT_FOUND,
|
|
122
|
+
message: "#{resource} not found: #{identifier}",
|
|
123
|
+
hint: hint,
|
|
124
|
+
http_status: 404
|
|
125
|
+
)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Raised when authentication fails (401).
|
|
130
|
+
class AuthError < Error
|
|
131
|
+
def initialize(message = "Authentication required", hint: nil, cause: nil)
|
|
132
|
+
super(
|
|
133
|
+
code: ErrorCode::AUTH,
|
|
134
|
+
message: message,
|
|
135
|
+
hint: hint || "Check your access token or refresh it if expired",
|
|
136
|
+
http_status: 401,
|
|
137
|
+
cause: cause
|
|
138
|
+
)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Raised when access is denied (403).
|
|
143
|
+
class ForbiddenError < Error
|
|
144
|
+
def initialize(message = "Access denied", hint: nil)
|
|
145
|
+
super(
|
|
146
|
+
code: ErrorCode::FORBIDDEN,
|
|
147
|
+
message: message,
|
|
148
|
+
hint: hint || "You do not have permission to access this resource",
|
|
149
|
+
http_status: 403
|
|
150
|
+
)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Creates a forbidden error due to insufficient OAuth scope.
|
|
154
|
+
def self.insufficient_scope
|
|
155
|
+
new("Access denied: insufficient scope", hint: "Re-authenticate with full scope")
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Raised when rate limited (429).
|
|
160
|
+
class RateLimitError < Error
|
|
161
|
+
def initialize(retry_after: nil, cause: nil)
|
|
162
|
+
hint = retry_after ? "Try again in #{retry_after} seconds" : "Please slow down requests"
|
|
163
|
+
super(
|
|
164
|
+
code: ErrorCode::RATE_LIMIT,
|
|
165
|
+
message: "Rate limit exceeded",
|
|
166
|
+
hint: hint,
|
|
167
|
+
http_status: 429,
|
|
168
|
+
retryable: true,
|
|
169
|
+
retry_after: retry_after,
|
|
170
|
+
cause: cause
|
|
171
|
+
)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Raised when there's a network error (connection, timeout, DNS).
|
|
176
|
+
class NetworkError < Error
|
|
177
|
+
def initialize(message = "Network error", cause: nil)
|
|
178
|
+
super(
|
|
179
|
+
code: ErrorCode::NETWORK,
|
|
180
|
+
message: message,
|
|
181
|
+
hint: cause&.message || "Check your network connection",
|
|
182
|
+
retryable: true,
|
|
183
|
+
cause: cause
|
|
184
|
+
)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Raised for generic API errors.
|
|
189
|
+
class APIError < Error
|
|
190
|
+
def initialize(message, http_status: nil, hint: nil, retryable: false, cause: nil)
|
|
191
|
+
super(
|
|
192
|
+
code: ErrorCode::API,
|
|
193
|
+
message: message,
|
|
194
|
+
hint: hint,
|
|
195
|
+
http_status: http_status,
|
|
196
|
+
retryable: retryable,
|
|
197
|
+
cause: cause
|
|
198
|
+
)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Creates an APIError from an HTTP status code.
|
|
202
|
+
# @param status [Integer] HTTP status code
|
|
203
|
+
# @param message [String, nil] optional error message
|
|
204
|
+
# @return [APIError]
|
|
205
|
+
def self.from_status(status, message = nil)
|
|
206
|
+
message ||= "Request failed (HTTP #{status})"
|
|
207
|
+
retryable = status >= 500 && status < 600
|
|
208
|
+
new(message, http_status: status, retryable: retryable)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Raised when a name/identifier matches multiple resources.
|
|
213
|
+
class AmbiguousError < Error
|
|
214
|
+
# @return [Array<String>] list of matching resources
|
|
215
|
+
attr_reader :matches
|
|
216
|
+
|
|
217
|
+
def initialize(resource, matches: [])
|
|
218
|
+
@matches = matches
|
|
219
|
+
hint = if matches.any? && matches.length <= 5
|
|
220
|
+
"Did you mean: #{matches.join(", ")}"
|
|
221
|
+
else
|
|
222
|
+
"Be more specific"
|
|
223
|
+
end
|
|
224
|
+
super(
|
|
225
|
+
code: ErrorCode::AMBIGUOUS,
|
|
226
|
+
message: "Ambiguous #{resource}",
|
|
227
|
+
hint: hint
|
|
228
|
+
)
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Raised for validation errors (400, 422).
|
|
233
|
+
class ValidationError < Error
|
|
234
|
+
def initialize(message, hint: nil, http_status: 400)
|
|
235
|
+
super(
|
|
236
|
+
code: ErrorCode::VALIDATION,
|
|
237
|
+
message: message,
|
|
238
|
+
hint: hint,
|
|
239
|
+
http_status: http_status
|
|
240
|
+
)
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Maps an HTTP response to the appropriate error class.
|
|
245
|
+
#
|
|
246
|
+
# @param status [Integer] HTTP status code
|
|
247
|
+
# @param body [String, nil] response body (will attempt JSON parse)
|
|
248
|
+
# @param retry_after [Integer, nil] Retry-After header value
|
|
249
|
+
# @return [Error]
|
|
250
|
+
def self.error_from_response(status, body = nil, retry_after: nil)
|
|
251
|
+
message = parse_error_message(body) || "Request failed"
|
|
252
|
+
|
|
253
|
+
case status
|
|
254
|
+
when 400, 422
|
|
255
|
+
ValidationError.new(message, http_status: status)
|
|
256
|
+
when 401
|
|
257
|
+
AuthError.new(message)
|
|
258
|
+
when 403
|
|
259
|
+
ForbiddenError.new(message)
|
|
260
|
+
when 404
|
|
261
|
+
NotFoundError.new("Resource", "unknown")
|
|
262
|
+
when 429
|
|
263
|
+
RateLimitError.new(retry_after: retry_after)
|
|
264
|
+
when 500
|
|
265
|
+
APIError.new("Server error (500)", http_status: 500, retryable: true)
|
|
266
|
+
when 502, 503, 504
|
|
267
|
+
APIError.new("Gateway error (#{status})", http_status: status, retryable: true)
|
|
268
|
+
else
|
|
269
|
+
APIError.from_status(status, message)
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Parses error message from response body.
|
|
274
|
+
# @param body [String, nil]
|
|
275
|
+
# @return [String, nil]
|
|
276
|
+
def self.parse_error_message(body)
|
|
277
|
+
return nil if body.nil? || body.empty?
|
|
278
|
+
|
|
279
|
+
# Guard against oversized error bodies before parsing
|
|
280
|
+
Basecamp::Security.check_body_size!(body, Basecamp::Security::MAX_ERROR_BODY_BYTES, "Error")
|
|
281
|
+
|
|
282
|
+
data = JSON.parse(body)
|
|
283
|
+
msg = data["error"] || data["message"]
|
|
284
|
+
msg ? Basecamp::Security.truncate(msg) : nil
|
|
285
|
+
rescue JSON::ParserError, Basecamp::APIError
|
|
286
|
+
# Return nil on parse errors or oversized bodies to preserve normal error type mapping
|
|
287
|
+
nil
|
|
288
|
+
end
|
|
289
|
+
end
|