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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 5251d36b00bfd269e7fce1bc324c2c9b491b9d0f0d30296807c0b1b724272adc
|
|
4
|
+
data.tar.gz: c590cec999d1b5c687456d8cdf1259712df2fcb26260048410ac5edd41dd772b
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 874a76ab2b404c5ad8bada234f9002ba4e50d3ad5e574352d0f6cc53991efa656f0adb0ddd1701df52e66966ed201740b1156c9aaa7a3193422c9ab82fa877ae
|
|
7
|
+
data.tar.gz: 05570761aa1e7a59823ed614505391415f883b41fcf7ba4b4197f51ad2ad07cf7ce9d7ecd7722ecfe7e60d759fb963638f7bfe621b20406233dcf1b0a1909400
|
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# 37signals house style
|
|
2
|
+
inherit_gem:
|
|
3
|
+
rubocop-37signals: rubocop.yml
|
|
4
|
+
|
|
5
|
+
AllCops:
|
|
6
|
+
TargetRubyVersion: 3.2
|
|
7
|
+
Exclude:
|
|
8
|
+
- "vendor/**/*"
|
|
9
|
+
- "lib/basecamp/generated/**/*"
|
|
10
|
+
|
|
11
|
+
# Documentation not required for tests
|
|
12
|
+
Style/Documentation:
|
|
13
|
+
Exclude:
|
|
14
|
+
- "test/**/*"
|
data/.yardopts
ADDED
data/README.md
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# Basecamp Ruby SDK
|
|
2
|
+
|
|
3
|
+
Official Ruby SDK for the [Basecamp API](https://github.com/basecamp/bc3-api).
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Ruby 3.2+
|
|
8
|
+
- Faraday HTTP client
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
Add to your Gemfile:
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
gem "basecamp-sdk"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Or install directly:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
gem install basecamp-sdk
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
require "basecamp"
|
|
28
|
+
|
|
29
|
+
# Create client with access token
|
|
30
|
+
client = Basecamp.client(access_token: ENV["BASECAMP_TOKEN"])
|
|
31
|
+
|
|
32
|
+
# Scope to an account
|
|
33
|
+
account = client.for_account(ENV["BASECAMP_ACCOUNT_ID"])
|
|
34
|
+
|
|
35
|
+
# List projects
|
|
36
|
+
account.projects.list.each do |project|
|
|
37
|
+
puts "#{project['id']}: #{project['name']}"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Get a specific project
|
|
41
|
+
project = account.projects.get(project_id: 12345)
|
|
42
|
+
|
|
43
|
+
# Create a todo
|
|
44
|
+
todo = account.todos.create(
|
|
45
|
+
project_id: 12345,
|
|
46
|
+
todolist_id: 67890,
|
|
47
|
+
content: "Review PR",
|
|
48
|
+
due_on: "2024-12-31"
|
|
49
|
+
)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Configuration
|
|
53
|
+
|
|
54
|
+
### Basic Configuration
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
config = Basecamp::Config.new(
|
|
58
|
+
base_url: "https://3.basecampapi.com", # Default
|
|
59
|
+
timeout: 30, # Request timeout in seconds
|
|
60
|
+
max_retries: 3, # Max retry attempts for GET requests
|
|
61
|
+
base_delay: 1.0, # Base delay for exponential backoff
|
|
62
|
+
max_pages: 10_000 # Max pages for pagination
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
token_provider = Basecamp::StaticTokenProvider.new(ENV["BASECAMP_TOKEN"])
|
|
66
|
+
client = Basecamp::Client.new(config: config, token_provider: token_provider)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Configuration Options
|
|
70
|
+
|
|
71
|
+
| Option | Default | Description |
|
|
72
|
+
|--------|---------|-------------|
|
|
73
|
+
| `base_url` | `https://3.basecampapi.com` | Basecamp API base URL |
|
|
74
|
+
| `timeout` | `30` | HTTP request timeout (seconds) |
|
|
75
|
+
| `max_retries` | `3` | Maximum retry attempts for GET requests |
|
|
76
|
+
| `base_delay` | `1.0` | Base delay for exponential backoff (seconds) |
|
|
77
|
+
| `max_jitter` | `0.1` | Maximum random jitter added to delays |
|
|
78
|
+
| `max_pages` | `10_000` | Maximum pages to fetch during pagination |
|
|
79
|
+
|
|
80
|
+
## OAuth Authentication
|
|
81
|
+
|
|
82
|
+
### Token Providers
|
|
83
|
+
|
|
84
|
+
The SDK supports multiple authentication patterns:
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
# Static token (simplest)
|
|
88
|
+
token_provider = Basecamp::StaticTokenProvider.new("your-access-token")
|
|
89
|
+
|
|
90
|
+
# OAuth with refresh
|
|
91
|
+
token_provider = Basecamp::OauthTokenProvider.new(
|
|
92
|
+
access_token: "access-token",
|
|
93
|
+
refresh_token: "refresh-token",
|
|
94
|
+
expires_at: Time.now + 3600,
|
|
95
|
+
client_id: ENV["BASECAMP_CLIENT_ID"],
|
|
96
|
+
client_secret: ENV["BASECAMP_CLIENT_SECRET"]
|
|
97
|
+
)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### OAuth Flow Helpers
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
# 1. Discover OAuth configuration
|
|
104
|
+
config = Basecamp::Oauth.discover_launchpad
|
|
105
|
+
|
|
106
|
+
# 2. Build authorization URL (redirect user here)
|
|
107
|
+
auth_url = "#{config.authorization_endpoint}?" + URI.encode_www_form(
|
|
108
|
+
type: "web_server",
|
|
109
|
+
client_id: ENV["BASECAMP_CLIENT_ID"],
|
|
110
|
+
redirect_uri: "https://myapp.com/callback"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# 3. Exchange code for tokens (in callback handler)
|
|
114
|
+
token = Basecamp::Oauth.exchange_code(
|
|
115
|
+
token_endpoint: config.token_endpoint,
|
|
116
|
+
code: params[:code],
|
|
117
|
+
redirect_uri: "https://myapp.com/callback",
|
|
118
|
+
client_id: ENV["BASECAMP_CLIENT_ID"],
|
|
119
|
+
client_secret: ENV["BASECAMP_CLIENT_SECRET"],
|
|
120
|
+
use_legacy_format: true # Required for Launchpad
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# 4. Use the token
|
|
124
|
+
client = Basecamp.client(access_token: token.access_token)
|
|
125
|
+
|
|
126
|
+
# 5. Refresh when needed
|
|
127
|
+
if token.expired?
|
|
128
|
+
token = Basecamp::Oauth.refresh_token(
|
|
129
|
+
token_endpoint: config.token_endpoint,
|
|
130
|
+
refresh_token: token.refresh_token,
|
|
131
|
+
use_legacy_format: true
|
|
132
|
+
)
|
|
133
|
+
end
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Services
|
|
137
|
+
|
|
138
|
+
The SDK provides 37 services covering the complete Basecamp API:
|
|
139
|
+
|
|
140
|
+
| Service | Description |
|
|
141
|
+
|---------|-------------|
|
|
142
|
+
| `projects` | Project management |
|
|
143
|
+
| `todos` | Todo items |
|
|
144
|
+
| `todolists` | Todo lists |
|
|
145
|
+
| `todosets` | Todo set containers |
|
|
146
|
+
| `todolist_groups` | Todolist grouping/folders |
|
|
147
|
+
| `people` | People/users |
|
|
148
|
+
| `comments` | Comments on recordings |
|
|
149
|
+
| `messages` | Message posts |
|
|
150
|
+
| `message_boards` | Message boards |
|
|
151
|
+
| `message_types` | Message categories |
|
|
152
|
+
| `campfires` | Chat rooms |
|
|
153
|
+
| `schedules` | Calendar schedules |
|
|
154
|
+
| `documents` | Documents |
|
|
155
|
+
| `vaults` | File folders |
|
|
156
|
+
| `uploads` | File uploads |
|
|
157
|
+
| `attachments` | Binary attachments |
|
|
158
|
+
| `recordings` | Generic recordings |
|
|
159
|
+
| `webhooks` | Webhook subscriptions |
|
|
160
|
+
| `subscriptions` | Notification subscriptions |
|
|
161
|
+
| `templates` | Project templates |
|
|
162
|
+
| `events` | Activity events |
|
|
163
|
+
| `checkins` | Automatic check-ins |
|
|
164
|
+
| `forwards` | Email forwards |
|
|
165
|
+
| `cards` | Card table cards |
|
|
166
|
+
| `card_tables` | Card tables (kanban) |
|
|
167
|
+
| `card_columns` | Card table columns |
|
|
168
|
+
| `card_steps` | Card workflow steps |
|
|
169
|
+
| `lineup` | Card lineup view |
|
|
170
|
+
| `tools` | Project dock tools |
|
|
171
|
+
| `search` | Full-text search |
|
|
172
|
+
| `reports` | Activity reports |
|
|
173
|
+
| `timeline` | Activity timeline |
|
|
174
|
+
| `timesheet` | Time tracking reports |
|
|
175
|
+
| `client_approvals` | Client approval workflows |
|
|
176
|
+
| `client_correspondences` | Client communications |
|
|
177
|
+
| `client_replies` | Client replies |
|
|
178
|
+
| `authorization` | Auth info |
|
|
179
|
+
|
|
180
|
+
## Pagination
|
|
181
|
+
|
|
182
|
+
All list methods return lazy Enumerators that automatically handle pagination:
|
|
183
|
+
|
|
184
|
+
```ruby
|
|
185
|
+
# Automatically fetches all pages
|
|
186
|
+
account.projects.list.each do |project|
|
|
187
|
+
puts project["name"]
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Take only what you need
|
|
191
|
+
first_10 = account.todos.list(todolist_id: 456).take(10)
|
|
192
|
+
|
|
193
|
+
# Convert to array (fetches all pages)
|
|
194
|
+
all_projects = account.projects.list.to_a
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Retry Behavior
|
|
198
|
+
|
|
199
|
+
GET requests automatically retry on transient failures with exponential backoff:
|
|
200
|
+
|
|
201
|
+
- **Retryable errors**: 429 (rate limit), 502, 503, 504 (gateway errors)
|
|
202
|
+
- **Backoff**: Exponential with jitter (1s, 2s, 4s...)
|
|
203
|
+
- **Rate limits**: Respects `Retry-After` header
|
|
204
|
+
|
|
205
|
+
Mutation operations (POST, PUT, DELETE) do **not** retry to prevent data duplication.
|
|
206
|
+
|
|
207
|
+
## Error Handling
|
|
208
|
+
|
|
209
|
+
```ruby
|
|
210
|
+
begin
|
|
211
|
+
account.projects.get(project_id: 99999)
|
|
212
|
+
rescue Basecamp::NotFoundError => e
|
|
213
|
+
puts "Project not found: #{e.message}"
|
|
214
|
+
rescue Basecamp::RateLimitError => e
|
|
215
|
+
puts "Rate limited, retry after: #{e.retry_after} seconds"
|
|
216
|
+
rescue Basecamp::AuthError => e
|
|
217
|
+
puts "Authentication failed: #{e.message}"
|
|
218
|
+
rescue Basecamp::APIError => e
|
|
219
|
+
puts "API error (#{e.http_status}): #{e.message}"
|
|
220
|
+
end
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Error Types
|
|
224
|
+
|
|
225
|
+
| Error | Description |
|
|
226
|
+
|-------|-------------|
|
|
227
|
+
| `APIError` | Base error class for all API errors |
|
|
228
|
+
| `AuthError` | Authentication failures (401) |
|
|
229
|
+
| `ForbiddenError` | Access denied (403) |
|
|
230
|
+
| `NotFoundError` | Resource not found (404) |
|
|
231
|
+
| `ValidationError` | Invalid request data (400, 422) |
|
|
232
|
+
| `RateLimitError` | Rate limit exceeded (429) |
|
|
233
|
+
| `NetworkError` | Connection failures |
|
|
234
|
+
|
|
235
|
+
## Observability Hooks
|
|
236
|
+
|
|
237
|
+
Monitor SDK behavior with hooks:
|
|
238
|
+
|
|
239
|
+
```ruby
|
|
240
|
+
class MyHooks
|
|
241
|
+
include Basecamp::Hooks
|
|
242
|
+
|
|
243
|
+
def on_request_start(info)
|
|
244
|
+
puts "Starting #{info.method} #{info.url}"
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def on_request_end(info, result)
|
|
248
|
+
puts "Completed in #{result.duration}s with status #{result.status_code}"
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def on_retry(info, attempt, error, delay)
|
|
252
|
+
puts "Retrying attempt #{attempt} after #{delay}s"
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def on_paginate(url, page)
|
|
256
|
+
puts "Fetching page #{page}"
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
client = Basecamp::Client.new(
|
|
261
|
+
config: config,
|
|
262
|
+
token_provider: token_provider,
|
|
263
|
+
hooks: MyHooks.new
|
|
264
|
+
)
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## Environment Variables
|
|
268
|
+
|
|
269
|
+
| Variable | Description |
|
|
270
|
+
|----------|-------------|
|
|
271
|
+
| `BASECAMP_TOKEN` | OAuth access token |
|
|
272
|
+
| `BASECAMP_ACCOUNT_ID` | Account ID |
|
|
273
|
+
| `BASECAMP_BASE_URL` | API base URL (default: `https://3.basecampapi.com`) |
|
|
274
|
+
|
|
275
|
+
## Development
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
# Install dependencies
|
|
279
|
+
bundle install
|
|
280
|
+
|
|
281
|
+
# Run tests
|
|
282
|
+
bundle exec rake test
|
|
283
|
+
|
|
284
|
+
# Run linter
|
|
285
|
+
bundle exec rubocop
|
|
286
|
+
|
|
287
|
+
# Generate types from OpenAPI
|
|
288
|
+
ruby scripts/generate-types.rb
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
## License
|
|
292
|
+
|
|
293
|
+
MIT
|
data/Rakefile
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bundler/gem_tasks'
|
|
4
|
+
require 'rake/testtask'
|
|
5
|
+
require 'rubocop/rake_task'
|
|
6
|
+
require 'yard'
|
|
7
|
+
|
|
8
|
+
Rake::TestTask.new(:test) do |t|
|
|
9
|
+
t.libs << 'test'
|
|
10
|
+
t.libs << 'lib'
|
|
11
|
+
t.test_files = FileList['test/**/*_test.rb']
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
RuboCop::RakeTask.new
|
|
15
|
+
|
|
16
|
+
YARD::Rake::YardocTask.new(:doc) do |t|
|
|
17
|
+
t.files = [ 'lib/**/*.rb' ]
|
|
18
|
+
t.options = [ '--output-dir', 'doc' ]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
desc 'Start an interactive console with the SDK loaded'
|
|
22
|
+
task :console do
|
|
23
|
+
exec 'bin/console'
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
task default: %i[test rubocop]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/basecamp/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'basecamp-sdk'
|
|
7
|
+
spec.version = Basecamp::VERSION
|
|
8
|
+
spec.authors = [ 'Basecamp' ]
|
|
9
|
+
spec.email = [ 'support@basecamp.com' ]
|
|
10
|
+
|
|
11
|
+
spec.summary = 'Official Ruby SDK for the Basecamp API'
|
|
12
|
+
spec.description = 'A Ruby SDK for the Basecamp API with automatic retry, ' \
|
|
13
|
+
'exponential backoff, Link header pagination, and observability hooks.'
|
|
14
|
+
spec.homepage = 'https://github.com/basecamp/basecamp-sdk'
|
|
15
|
+
spec.license = 'MIT'
|
|
16
|
+
spec.required_ruby_version = '>= 3.2.0'
|
|
17
|
+
|
|
18
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
|
19
|
+
spec.metadata['source_code_uri'] = spec.homepage
|
|
20
|
+
spec.metadata['changelog_uri'] = "#{spec.homepage}/releases"
|
|
21
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
|
22
|
+
|
|
23
|
+
# Specify which files should be added to the gem when it is released.
|
|
24
|
+
spec.files = Dir.chdir(__dir__) do
|
|
25
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
|
26
|
+
(File.expand_path(f) == __FILE__) ||
|
|
27
|
+
f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
spec.require_paths = [ 'lib' ]
|
|
31
|
+
|
|
32
|
+
# Runtime dependencies
|
|
33
|
+
spec.add_dependency 'faraday', '~> 2.0'
|
|
34
|
+
spec.add_dependency 'zeitwerk', '~> 2.6'
|
|
35
|
+
|
|
36
|
+
# Development dependencies
|
|
37
|
+
spec.add_development_dependency 'minitest', '~> 6.0'
|
|
38
|
+
spec.add_development_dependency 'rake', '~> 13.0'
|
|
39
|
+
spec.add_development_dependency 'rubocop-37signals'
|
|
40
|
+
spec.add_development_dependency 'simplecov', '~> 0.22'
|
|
41
|
+
spec.add_development_dependency 'webmock', '~> 3.24'
|
|
42
|
+
spec.add_development_dependency 'irb', '~> 1.15'
|
|
43
|
+
spec.add_development_dependency 'rdoc', '~> 7.1'
|
|
44
|
+
spec.add_development_dependency 'webrick', '~> 1.9'
|
|
45
|
+
spec.add_development_dependency 'yard', '~> 0.9'
|
|
46
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Basecamp
|
|
4
|
+
# AuthStrategy controls how authentication is applied to HTTP requests.
|
|
5
|
+
# The default strategy is BearerAuth, which uses a TokenProvider to set
|
|
6
|
+
# the Authorization header with a Bearer token.
|
|
7
|
+
#
|
|
8
|
+
# Custom strategies can implement alternative auth schemes such as
|
|
9
|
+
# cookie-based auth, API keys, or mutual TLS.
|
|
10
|
+
#
|
|
11
|
+
# To implement a custom strategy, create a class that responds to
|
|
12
|
+
# #authenticate(headers), where headers is a Hash that you can modify.
|
|
13
|
+
module AuthStrategy
|
|
14
|
+
# Apply authentication to the given headers hash.
|
|
15
|
+
# @param headers [Hash] the request headers to modify
|
|
16
|
+
def authenticate(headers)
|
|
17
|
+
raise NotImplementedError, "#{self.class} must implement #authenticate"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Bearer token authentication strategy (default).
|
|
22
|
+
# Sets the Authorization header with "Bearer {token}".
|
|
23
|
+
class BearerAuth
|
|
24
|
+
include AuthStrategy
|
|
25
|
+
|
|
26
|
+
# @param token_provider [TokenProvider] provides access tokens
|
|
27
|
+
def initialize(token_provider)
|
|
28
|
+
@token_provider = token_provider
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @return [TokenProvider] the underlying token provider
|
|
32
|
+
attr_reader :token_provider
|
|
33
|
+
|
|
34
|
+
def authenticate(headers)
|
|
35
|
+
headers["Authorization"] = "Bearer #{@token_provider.access_token}"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Basecamp
|
|
4
|
+
# Composes multiple Hooks implementations, calling them in sequence.
|
|
5
|
+
# Start events are called in order; end events are called in reverse order.
|
|
6
|
+
class ChainHooks
|
|
7
|
+
include Hooks
|
|
8
|
+
|
|
9
|
+
def initialize(*hooks)
|
|
10
|
+
@hooks = hooks
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def on_operation_start(info)
|
|
14
|
+
@hooks.each { |h| safe_call { h.on_operation_start(info) } }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def on_operation_end(info, result)
|
|
18
|
+
@hooks.reverse_each { |h| safe_call { h.on_operation_end(info, result) } }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def on_request_start(info)
|
|
22
|
+
@hooks.each { |h| safe_call { h.on_request_start(info) } }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def on_request_end(info, result)
|
|
26
|
+
@hooks.reverse_each { |h| safe_call { h.on_request_end(info, result) } }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def on_retry(info, attempt, error, delay)
|
|
30
|
+
@hooks.each { |h| safe_call { h.on_retry(info, attempt, error, delay) } }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def on_paginate(url, page)
|
|
34
|
+
@hooks.each { |h| safe_call { h.on_paginate(url, page) } }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def safe_call
|
|
40
|
+
yield
|
|
41
|
+
rescue => e
|
|
42
|
+
warn "Basecamp::ChainHooks: hook raised #{e.class}: #{e.message}"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|