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.
Files changed (116) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +14 -0
  3. data/.yardopts +6 -0
  4. data/README.md +293 -0
  5. data/Rakefile +26 -0
  6. data/basecamp-sdk.gemspec +46 -0
  7. data/lib/basecamp/auth_strategy.rb +38 -0
  8. data/lib/basecamp/chain_hooks.rb +45 -0
  9. data/lib/basecamp/client.rb +428 -0
  10. data/lib/basecamp/config.rb +143 -0
  11. data/lib/basecamp/errors.rb +289 -0
  12. data/lib/basecamp/generated/metadata.json +2281 -0
  13. data/lib/basecamp/generated/services/attachments_service.rb +24 -0
  14. data/lib/basecamp/generated/services/boosts_service.rb +70 -0
  15. data/lib/basecamp/generated/services/campfires_service.rb +122 -0
  16. data/lib/basecamp/generated/services/card_columns_service.rb +103 -0
  17. data/lib/basecamp/generated/services/card_steps_service.rb +57 -0
  18. data/lib/basecamp/generated/services/card_tables_service.rb +20 -0
  19. data/lib/basecamp/generated/services/cards_service.rb +66 -0
  20. data/lib/basecamp/generated/services/checkins_service.rb +157 -0
  21. data/lib/basecamp/generated/services/client_approvals_service.rb +28 -0
  22. data/lib/basecamp/generated/services/client_correspondences_service.rb +28 -0
  23. data/lib/basecamp/generated/services/client_replies_service.rb +30 -0
  24. data/lib/basecamp/generated/services/client_visibility_service.rb +21 -0
  25. data/lib/basecamp/generated/services/comments_service.rb +49 -0
  26. data/lib/basecamp/generated/services/documents_service.rb +52 -0
  27. data/lib/basecamp/generated/services/events_service.rb +20 -0
  28. data/lib/basecamp/generated/services/forwards_service.rb +67 -0
  29. data/lib/basecamp/generated/services/lineup_service.rb +44 -0
  30. data/lib/basecamp/generated/services/message_boards_service.rb +20 -0
  31. data/lib/basecamp/generated/services/message_types_service.rb +59 -0
  32. data/lib/basecamp/generated/services/messages_service.rb +75 -0
  33. data/lib/basecamp/generated/services/people_service.rb +73 -0
  34. data/lib/basecamp/generated/services/projects_service.rb +63 -0
  35. data/lib/basecamp/generated/services/recordings_service.rb +64 -0
  36. data/lib/basecamp/generated/services/reports_service.rb +56 -0
  37. data/lib/basecamp/generated/services/schedules_service.rb +92 -0
  38. data/lib/basecamp/generated/services/search_service.rb +31 -0
  39. data/lib/basecamp/generated/services/subscriptions_service.rb +50 -0
  40. data/lib/basecamp/generated/services/templates_service.rb +82 -0
  41. data/lib/basecamp/generated/services/timeline_service.rb +20 -0
  42. data/lib/basecamp/generated/services/timesheets_service.rb +81 -0
  43. data/lib/basecamp/generated/services/todolist_groups_service.rb +41 -0
  44. data/lib/basecamp/generated/services/todolists_service.rb +53 -0
  45. data/lib/basecamp/generated/services/todos_service.rb +106 -0
  46. data/lib/basecamp/generated/services/todosets_service.rb +20 -0
  47. data/lib/basecamp/generated/services/tools_service.rb +80 -0
  48. data/lib/basecamp/generated/services/uploads_service.rb +61 -0
  49. data/lib/basecamp/generated/services/vaults_service.rb +49 -0
  50. data/lib/basecamp/generated/services/webhooks_service.rb +63 -0
  51. data/lib/basecamp/generated/types.rb +3196 -0
  52. data/lib/basecamp/hooks.rb +70 -0
  53. data/lib/basecamp/http.rb +440 -0
  54. data/lib/basecamp/logger_hooks.rb +46 -0
  55. data/lib/basecamp/noop_hooks.rb +9 -0
  56. data/lib/basecamp/oauth/discovery.rb +123 -0
  57. data/lib/basecamp/oauth/errors.rb +35 -0
  58. data/lib/basecamp/oauth/exchange.rb +291 -0
  59. data/lib/basecamp/oauth/pkce.rb +68 -0
  60. data/lib/basecamp/oauth/types.rb +133 -0
  61. data/lib/basecamp/oauth.rb +56 -0
  62. data/lib/basecamp/oauth_token_provider.rb +108 -0
  63. data/lib/basecamp/operation_info.rb +17 -0
  64. data/lib/basecamp/request_info.rb +10 -0
  65. data/lib/basecamp/request_result.rb +14 -0
  66. data/lib/basecamp/security.rb +112 -0
  67. data/lib/basecamp/services/attachments_service.rb +33 -0
  68. data/lib/basecamp/services/authorization_service.rb +47 -0
  69. data/lib/basecamp/services/base_service.rb +146 -0
  70. data/lib/basecamp/services/campfires_service.rb +141 -0
  71. data/lib/basecamp/services/card_columns_service.rb +106 -0
  72. data/lib/basecamp/services/card_steps_service.rb +86 -0
  73. data/lib/basecamp/services/card_tables_service.rb +23 -0
  74. data/lib/basecamp/services/cards_service.rb +93 -0
  75. data/lib/basecamp/services/checkins_service.rb +127 -0
  76. data/lib/basecamp/services/client_approvals_service.rb +33 -0
  77. data/lib/basecamp/services/client_correspondences_service.rb +33 -0
  78. data/lib/basecamp/services/client_replies_service.rb +35 -0
  79. data/lib/basecamp/services/comments_service.rb +63 -0
  80. data/lib/basecamp/services/documents_service.rb +74 -0
  81. data/lib/basecamp/services/events_service.rb +27 -0
  82. data/lib/basecamp/services/forwards_service.rb +80 -0
  83. data/lib/basecamp/services/lineup_service.rb +67 -0
  84. data/lib/basecamp/services/message_boards_service.rb +24 -0
  85. data/lib/basecamp/services/message_types_service.rb +79 -0
  86. data/lib/basecamp/services/messages_service.rb +133 -0
  87. data/lib/basecamp/services/people_service.rb +73 -0
  88. data/lib/basecamp/services/projects_service.rb +67 -0
  89. data/lib/basecamp/services/recordings_service.rb +127 -0
  90. data/lib/basecamp/services/reports_service.rb +80 -0
  91. data/lib/basecamp/services/schedules_service.rb +156 -0
  92. data/lib/basecamp/services/search_service.rb +36 -0
  93. data/lib/basecamp/services/subscriptions_service.rb +67 -0
  94. data/lib/basecamp/services/templates_service.rb +96 -0
  95. data/lib/basecamp/services/timeline_service.rb +62 -0
  96. data/lib/basecamp/services/timesheet_service.rb +68 -0
  97. data/lib/basecamp/services/todolist_groups_service.rb +100 -0
  98. data/lib/basecamp/services/todolists_service.rb +104 -0
  99. data/lib/basecamp/services/todos_service.rb +156 -0
  100. data/lib/basecamp/services/todosets_service.rb +23 -0
  101. data/lib/basecamp/services/tools_service.rb +89 -0
  102. data/lib/basecamp/services/uploads_service.rb +84 -0
  103. data/lib/basecamp/services/vaults_service.rb +84 -0
  104. data/lib/basecamp/services/webhooks_service.rb +88 -0
  105. data/lib/basecamp/static_token_provider.rb +24 -0
  106. data/lib/basecamp/token_provider.rb +42 -0
  107. data/lib/basecamp/version.rb +6 -0
  108. data/lib/basecamp/webhooks/event.rb +52 -0
  109. data/lib/basecamp/webhooks/rack_middleware.rb +49 -0
  110. data/lib/basecamp/webhooks/receiver.rb +161 -0
  111. data/lib/basecamp/webhooks/verify.rb +36 -0
  112. data/lib/basecamp.rb +107 -0
  113. data/scripts/generate-metadata.rb +106 -0
  114. data/scripts/generate-services.rb +778 -0
  115. data/scripts/generate-types.rb +191 -0
  116. 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