fizzy-api-client 0.1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3e7d416cc4e5989199b27614c9ea651a86510ffc823efd22837a995b643dd965
4
+ data.tar.gz: 5abf84d0a65f218974ace1361f5ed8d7ca79fb4512c90f260b86a55307881824
5
+ SHA512:
6
+ metadata.gz: 9382ce0f1278a8547d1fe2d9fd13f637cbe7d793ff9df02db8ed45eeec184d52c3752cb62cf4a227ae1d2aeb155159328aa83b2f6722485cf12c122f581acaa9
7
+ data.tar.gz: 974bf8afa1f54d417b906178b4640b8165fabd4ad1f978c97f98fed376263a56f2a04eddf2ceb94f2f0db15c5255d5babf160c8f962847f5f17a92403e1c4fc8
data/CHANGELOG.md ADDED
@@ -0,0 +1,45 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2025-12-21
9
+
10
+ ### Added
11
+
12
+ - Initial release of the Fizzy API Client gem
13
+ - Full CRUD support for all Fizzy resources:
14
+ - Identity (account info)
15
+ - Boards (create, read, update, delete)
16
+ - Cards (full lifecycle including close, reopen, postpone, triage, watch, gild)
17
+ - Columns (with named color support: blue, gray, tan, yellow, lime, aqua, violet, purple, pink)
18
+ - Comments (threaded discussions)
19
+ - Steps (checklist items with complete/incomplete)
20
+ - Reactions (emoji reactions)
21
+ - Tags (with toggle support)
22
+ - Users (including avatar upload and deactivation)
23
+ - Notifications (read/unread management)
24
+ - Direct Uploads (ActiveStorage integration)
25
+ - Named colors for columns (`:blue`, `:lime`, `:pink`, etc.) - no need to use CSS variables
26
+ - Golden ticket support (`gild_card`/`ungild_card`)
27
+ - Background image upload for cards
28
+ - Flexible configuration system:
29
+ - Global configuration via `FizzyApiClient.configure`
30
+ - Client-specific configuration
31
+ - Environment variable support (FIZZY_API_TOKEN, FIZZY_ACCOUNT, FIZZY_BASE_URL)
32
+ - Pagination support:
33
+ - Manual page navigation with `page:` parameter
34
+ - Automatic pagination with `auto_paginate: true`
35
+ - Error handling with specific error classes:
36
+ - AuthenticationError (401)
37
+ - ForbiddenError (403)
38
+ - NotFoundError (404)
39
+ - ValidationError (422)
40
+ - ServerError (500+)
41
+ - Comprehensive demo script that tests all features
42
+ - Self-hosted instance support via `base_url` configuration
43
+ - Account slug normalization (strips leading `/` from identity API slugs)
44
+ - Multipart/form-data support for file uploads
45
+ - Optional logger integration for debugging
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Fizzy Team
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,469 @@
1
+ # Fizzy API Client
2
+
3
+ A Ruby gem providing a clean, idiomatic interface to the [Fizzy](https://fizzy.do) API. Fizzy is a task management app from [37signals](https://37signals.com).
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'fizzy-api-client'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ ```bash
16
+ $ bundle install
17
+ ```
18
+
19
+ Or install it yourself as:
20
+
21
+ ```bash
22
+ $ gem install fizzy-api-client
23
+ ```
24
+
25
+ ## Quick Start
26
+
27
+ ```ruby
28
+ require 'fizzy_api_client'
29
+
30
+ # Configure globally
31
+ FizzyApiClient.configure do |config|
32
+ config.api_token = 'your-api-token'
33
+ config.account_slug = 'your-account'
34
+ end
35
+
36
+ # Or use environment variables:
37
+ # FIZZY_API_TOKEN, FIZZY_ACCOUNT, FIZZY_BASE_URL
38
+
39
+ client = FizzyApiClient::Client.new
40
+
41
+ # Get identity
42
+ identity = client.identity
43
+
44
+ # List boards
45
+ boards = client.boards
46
+
47
+ # Create a card (note: uses 'title' field, not 'name')
48
+ card = client.create_card(board_id: 'board_1', title: 'New Task')
49
+
50
+ # Add a step (note: uses 'content' field)
51
+ step = client.create_step(card['number'], content: 'First step')
52
+ ```
53
+
54
+ ## Rails Integration
55
+
56
+ ### Configuration with Credentials
57
+
58
+ Store your API credentials securely using Rails credentials:
59
+
60
+ ```bash
61
+ $ rails credentials:edit
62
+ ```
63
+
64
+ ```yaml
65
+ # config/credentials.yml.enc
66
+ fizzy:
67
+ api_token: your-api-token
68
+ account_slug: your-account
69
+ ```
70
+
71
+ ### Initializer
72
+
73
+ Create an initializer to configure the client:
74
+
75
+ ```ruby
76
+ # config/initializers/fizzy.rb
77
+ FizzyApiClient.configure do |config|
78
+ config.api_token = Rails.application.credentials.dig(:fizzy, :api_token)
79
+ config.account_slug = Rails.application.credentials.dig(:fizzy, :account_slug)
80
+ config.logger = Rails.logger if Rails.env.development?
81
+ end
82
+ ```
83
+
84
+ ### Controller Example
85
+
86
+ ```ruby
87
+ # app/controllers/tasks_controller.rb
88
+ class TasksController < ApplicationController
89
+ def index
90
+ @boards = fizzy_client.boards
91
+ end
92
+
93
+ def create
94
+ card = fizzy_client.create_card(
95
+ board_id: params[:board_id],
96
+ title: params[:title],
97
+ description: params[:description]
98
+ )
99
+
100
+ redirect_to tasks_path, notice: "Task ##{card['number']} created"
101
+ rescue FizzyApiClient::ApiError => e
102
+ redirect_to tasks_path, alert: "Failed to create task: #{e.message}"
103
+ end
104
+
105
+ def complete
106
+ fizzy_client.close_card(params[:card_number])
107
+ redirect_to tasks_path, notice: "Task completed"
108
+ end
109
+
110
+ private
111
+
112
+ def fizzy_client
113
+ @fizzy_client ||= FizzyApiClient::Client.new
114
+ end
115
+ end
116
+ ```
117
+
118
+ ### Background Job Example
119
+
120
+ ```ruby
121
+ # app/jobs/sync_fizzy_cards_job.rb
122
+ class SyncFizzyCardsJob < ApplicationJob
123
+ queue_as :default
124
+
125
+ def perform(board_id)
126
+ client = FizzyApiClient::Client.new
127
+ cards = client.cards(board_ids: [board_id], auto_paginate: true)
128
+
129
+ cards.each do |card_data|
130
+ Task.find_or_initialize_by(fizzy_number: card_data['number']).update!(
131
+ title: card_data['title'],
132
+ status: card_data['closed_at'].present? ? 'completed' : 'open',
133
+ synced_at: Time.current
134
+ )
135
+ end
136
+ end
137
+ end
138
+ ```
139
+
140
+ ### Per-Environment Configuration
141
+
142
+ ```ruby
143
+ # config/initializers/fizzy.rb
144
+ FizzyApiClient.configure do |config|
145
+ config.api_token = Rails.application.credentials.dig(:fizzy, :api_token)
146
+ config.account_slug = Rails.application.credentials.dig(:fizzy, :account_slug)
147
+
148
+ # Use different base URL for staging/development
149
+ if Rails.env.staging?
150
+ config.base_url = 'https://staging.fizzy.do'
151
+ end
152
+
153
+ # Enable logging in development
154
+ config.logger = Rails.logger if Rails.env.development?
155
+
156
+ # Shorter timeouts in test environment
157
+ if Rails.env.test?
158
+ config.timeout = 5
159
+ config.open_timeout = 2
160
+ end
161
+ end
162
+ ```
163
+
164
+ ## Configuration
165
+
166
+ ### Global Configuration
167
+
168
+ ```ruby
169
+ FizzyApiClient.configure do |config|
170
+ config.api_token = 'your-api-token'
171
+ config.account_slug = 'your-account'
172
+ config.base_url = 'https://app.fizzy.do' # default
173
+ config.timeout = 30 # seconds
174
+ config.open_timeout = 10 # seconds
175
+ config.logger = Logger.new($stdout) # optional
176
+ end
177
+ ```
178
+
179
+ ### Client Configuration
180
+
181
+ ```ruby
182
+ client = FizzyApiClient::Client.new(
183
+ api_token: 'your-api-token',
184
+ account_slug: 'your-account'
185
+ )
186
+ ```
187
+
188
+ ### Environment Variables
189
+
190
+ - `FIZZY_API_TOKEN` - Personal Access Token
191
+ - `FIZZY_ACCOUNT` - Default account slug
192
+ - `FIZZY_BASE_URL` - API base URL (for self-hosted instances)
193
+
194
+ ### Account Slug Normalization
195
+
196
+ The identity API returns account slugs with a leading slash (e.g., `/897362094`). This gem automatically normalizes slugs by stripping the leading slash.
197
+
198
+ ## Resources
199
+
200
+ ### Identity
201
+ ```ruby
202
+ client.identity
203
+ ```
204
+
205
+ ### Boards
206
+ ```ruby
207
+ # List and retrieve
208
+ client.boards
209
+ client.boards(page: 2)
210
+ client.boards(auto_paginate: true)
211
+ client.board('board_id')
212
+
213
+ # Create
214
+ client.create_board(name: 'New Board')
215
+ client.create_board(
216
+ name: 'Team Board',
217
+ all_access: false,
218
+ auto_postpone_period: 7,
219
+ public_description: '<p>Description</p>'
220
+ )
221
+
222
+ # Update
223
+ client.update_board('board_id', name: 'Updated Name')
224
+ client.update_board('board_id', user_ids: ['user_1', 'user_2']) # when all_access: false
225
+
226
+ # Delete
227
+ client.delete_board('board_id')
228
+ ```
229
+
230
+ ### Cards
231
+ Cards use `title` field (not `name`).
232
+
233
+ ```ruby
234
+ # List with filters
235
+ client.cards
236
+ client.cards(board_ids: ['board_1', 'board_2'])
237
+ client.cards(tag_ids: ['tag_1'], assignee_ids: ['user_1'])
238
+ client.cards(terms: ['bug', 'fix'])
239
+ client.cards(auto_paginate: true)
240
+
241
+ # Retrieve
242
+ client.card(42)
243
+
244
+ # Create
245
+ client.create_card(board_id: 'board_1', title: 'New Card')
246
+ client.create_card(
247
+ board_id: 'board_1',
248
+ title: 'Full Card',
249
+ description: 'Details here',
250
+ status: 'published',
251
+ tag_ids: ['tag_1', 'tag_2']
252
+ )
253
+
254
+ # Create with image
255
+ client.create_card(
256
+ board_id: 'board_1',
257
+ title: 'Card with Image',
258
+ image: '/path/to/image.png' # or File object
259
+ )
260
+
261
+ # Update
262
+ client.update_card(42, title: 'Updated Title')
263
+ client.update_card(42, image: '/path/to/new_image.png')
264
+
265
+ # Delete
266
+ client.delete_card(42)
267
+
268
+ # State changes
269
+ client.close_card(42)
270
+ client.reopen_card(42)
271
+ client.postpone_card(42) # or client.not_now_card(42)
272
+
273
+ # Triage (move to column)
274
+ client.triage_card(42, column_id: 'col_1')
275
+ client.untriage_card(42)
276
+
277
+ # Assignments and tags
278
+ client.toggle_assignment(42, assignee_id: 'user_1')
279
+ client.toggle_tag(42, tag_title: 'urgent')
280
+
281
+ # Watch/unwatch
282
+ client.watch_card(42)
283
+ client.unwatch_card(42)
284
+
285
+ # Golden ticket (highlight/pin a card)
286
+ client.gild_card(42)
287
+ client.ungild_card(42)
288
+ ```
289
+
290
+ ### Columns
291
+ ```ruby
292
+ # List and retrieve
293
+ client.columns('board_id')
294
+ client.column('board_id', 'column_id')
295
+
296
+ # Create with named color (recommended)
297
+ client.create_column(board_id: 'board_1', name: 'In Progress')
298
+ client.create_column(board_id: 'board_1', name: 'Review', color: :lime)
299
+ client.create_column(board_id: 'board_1', name: 'Urgent', color: :pink)
300
+
301
+ # Update with named color
302
+ client.update_column('board_id', 'column_id', name: 'Done')
303
+ client.update_column('board_id', 'column_id', color: :purple)
304
+
305
+ # Available colors: :blue (default), :gray, :tan, :yellow, :lime, :aqua, :violet, :purple, :pink
306
+ # CSS variable tokens are also supported: 'var(--color-card-3)'
307
+
308
+ # Delete
309
+ client.delete_column('board_id', 'column_id')
310
+ ```
311
+
312
+ ### Comments
313
+ ```ruby
314
+ # List and retrieve
315
+ client.comments(42)
316
+ client.comment(42, 'comment_id')
317
+
318
+ # Create
319
+ client.create_comment(42, body: 'This is a comment')
320
+ client.create_comment(42, body: 'Backdated comment', created_at: '2025-01-01T00:00:00Z')
321
+
322
+ # Update
323
+ client.update_comment(42, 'comment_id', body: 'Updated comment')
324
+
325
+ # Delete
326
+ client.delete_comment(42, 'comment_id')
327
+ ```
328
+
329
+ ### Steps
330
+ Steps use `content` field (not `name`).
331
+
332
+ ```ruby
333
+ # Retrieve
334
+ client.step(42, 'step_id')
335
+
336
+ # Create
337
+ client.create_step(42, content: 'Do this first')
338
+ client.create_step(42, content: 'Already done', completed: true)
339
+
340
+ # Update
341
+ client.update_step(42, 'step_id', content: 'Updated step')
342
+ client.update_step(42, 'step_id', completed: true)
343
+
344
+ # Convenience methods
345
+ client.complete_step(42, 'step_id')
346
+ client.incomplete_step(42, 'step_id')
347
+
348
+ # Delete
349
+ client.delete_step(42, 'step_id')
350
+ ```
351
+
352
+ ### Reactions
353
+ Reactions use `content` field (not `emoji`).
354
+
355
+ ```ruby
356
+ # List reactions on a comment
357
+ client.reactions(42, 'comment_id')
358
+
359
+ # Add reaction
360
+ client.add_reaction(42, 'comment_id', content: ':+1:')
361
+
362
+ # Remove reaction
363
+ client.remove_reaction(42, 'comment_id', 'reaction_id')
364
+ ```
365
+
366
+ ### Tags
367
+ Tags return `title` field (not `name`).
368
+
369
+ ```ruby
370
+ client.tags
371
+ client.tags(page: 2)
372
+ client.tags(auto_paginate: true)
373
+ ```
374
+
375
+ ### Users
376
+ ```ruby
377
+ # List and retrieve
378
+ client.users
379
+ client.users(auto_paginate: true)
380
+ client.user('user_id')
381
+
382
+ # Update
383
+ client.update_user('user_id', name: 'New Name')
384
+
385
+ # Update with avatar
386
+ client.update_user('user_id', avatar: '/path/to/avatar.png')
387
+ client.update_user('user_id', name: 'New Name', avatar: File.open('avatar.png'))
388
+
389
+ # Deactivate
390
+ client.deactivate_user('user_id')
391
+ ```
392
+
393
+ ### Notifications
394
+ ```ruby
395
+ # List
396
+ client.notifications
397
+ client.notifications(auto_paginate: true)
398
+
399
+ # Mark as read/unread
400
+ client.mark_notification_read('notification_id')
401
+ client.mark_notification_unread('notification_id')
402
+
403
+ # Mark all as read
404
+ client.mark_all_notifications_read
405
+ ```
406
+
407
+ ### Direct Uploads (File Attachments)
408
+ For uploading files via ActiveStorage:
409
+
410
+ ```ruby
411
+ # Simple file upload (handles all steps automatically)
412
+ signed_id = client.upload_file('/path/to/document.pdf')
413
+ signed_id = client.upload_file(file_io, filename: 'report.pdf', content_type: 'application/pdf')
414
+
415
+ # Manual direct upload (for advanced use cases)
416
+ upload = client.create_direct_upload(
417
+ filename: 'document.pdf',
418
+ byte_size: 1024,
419
+ checksum: Base64.strict_encode64(Digest::MD5.digest(content)),
420
+ content_type: 'application/pdf'
421
+ )
422
+ # Then PUT to upload['direct_upload']['url'] with the file content
423
+ # Use upload['signed_id'] to reference the uploaded file
424
+ ```
425
+
426
+ ## Pagination
427
+
428
+ ```ruby
429
+ # First page only (default)
430
+ boards = client.boards
431
+
432
+ # Specific page
433
+ boards = client.boards(page: 2)
434
+
435
+ # All pages (automatically follows pagination)
436
+ boards = client.boards(auto_paginate: true)
437
+ ```
438
+
439
+ ## Error Handling
440
+
441
+ ```ruby
442
+ begin
443
+ client.board('invalid')
444
+ rescue FizzyApiClient::NotFoundError => e
445
+ puts "Not found: #{e.message}"
446
+ rescue FizzyApiClient::AuthenticationError => e
447
+ puts "Auth failed: #{e.message}"
448
+ rescue FizzyApiClient::ForbiddenError => e
449
+ puts "Access denied: #{e.message}"
450
+ rescue FizzyApiClient::ValidationError => e
451
+ puts "Invalid data: #{e.message}"
452
+ rescue FizzyApiClient::ServerError => e
453
+ puts "Server error: #{e.message}"
454
+ rescue FizzyApiClient::ApiError => e
455
+ puts "API error #{e.status}: #{e.message}"
456
+ rescue FizzyApiClient::ConnectionError => e
457
+ puts "Connection failed: #{e.message}"
458
+ rescue FizzyApiClient::TimeoutError => e
459
+ puts "Request timed out: #{e.message}"
460
+ end
461
+ ```
462
+
463
+ ## Demo Script
464
+
465
+ A comprehensive demo script is included that tests every feature of the gem. See [examples/README.md](examples/README.md) for usage instructions.
466
+
467
+ ## License
468
+
469
+ The gem is available as open source under the terms of the [MIT License](LICENSE.txt).
@@ -0,0 +1,102 @@
1
+ # Demo Script
2
+
3
+ A comprehensive demo script that tests every feature of the fizzy-api-client gem against a live Fizzy instance.
4
+
5
+ ## Running the Demo
6
+
7
+ ```bash
8
+ # Basic usage
9
+ FIZZY_API_TOKEN=your-token FIZZY_ACCOUNT=your-account ruby examples/demo.rb
10
+
11
+ # For self-hosted Fizzy instances
12
+ FIZZY_BASE_URL=https://fizzy.mycompany.com FIZZY_API_TOKEN=your-token ruby examples/demo.rb
13
+
14
+ # With verbose output (shows full API responses)
15
+ VERBOSE=1 FIZZY_API_TOKEN=your-token FIZZY_ACCOUNT=your-account ruby examples/demo.rb
16
+
17
+ # Skip destructive operations (user updates, deactivation)
18
+ SKIP_DESTRUCTIVE=1 FIZZY_API_TOKEN=your-token FIZZY_ACCOUNT=your-account ruby examples/demo.rb
19
+ ```
20
+
21
+ ## Environment Variables
22
+
23
+ | Variable | Required | Description |
24
+ |----------|----------|-------------|
25
+ | `FIZZY_API_TOKEN` | Yes | Your Fizzy API token |
26
+ | `FIZZY_ACCOUNT` | No | Account slug (auto-detected if not set) |
27
+ | `FIZZY_BASE_URL` | No | API base URL for self-hosted instances |
28
+ | `VERBOSE` | No | Set to `1` to show full API responses |
29
+ | `SKIP_DESTRUCTIVE` | No | Set to `1` to skip operations that modify existing user data (update_user, deactivate_user) |
30
+
31
+ ## What the Demo Tests
32
+
33
+ The demo runs through all gem features:
34
+
35
+ 1. **Identity** - Authentication and account discovery
36
+ 2. **Boards** - List, get, create, update, delete
37
+ 3. **Columns** - List, get, create with named colors, update, delete
38
+ 4. **Cards** - Full CRUD, state changes (close/reopen/postpone), triage, watch, tags, assignments, background image upload, golden tickets
39
+ 5. **Steps** - Create, get, update, complete/incomplete, delete
40
+ 6. **Comments** - Full CRUD operations
41
+ 7. **Reactions** - Add, list, remove
42
+ 8. **Tags** - List with pagination
43
+ 9. **Users** - List, get single user
44
+ 10. **Notifications** - List, mark read/unread, mark all read
45
+ 11. **Direct Uploads** - File upload via ActiveStorage
46
+ 12. **Pagination** - Auto-pagination for all list endpoints
47
+ 13. **Error Handling** - Validates proper error responses
48
+ 14. **Cleanup** - Removes all test data created during the demo
49
+
50
+ ## Test Data
51
+
52
+ The demo creates temporary test data and cleans up after itself:
53
+
54
+ - Creates a test board
55
+ - Creates test columns with various colors
56
+ - Creates test cards (including one with `sydney.jpg` as background image)
57
+ - Creates test steps, comments, and reactions
58
+ - Tests golden ticket (gild/ungild) functionality
59
+ - Deletes all created resources at the end
60
+
61
+ ## Test Image
62
+
63
+ The `sydney.jpg` file is included for testing background image uploads on cards.
64
+
65
+ ## Expected Output
66
+
67
+ A successful run looks like:
68
+
69
+ ```
70
+ ============================================================
71
+ Fizzy API Client - Comprehensive Demo
72
+ ============================================================
73
+ Version: 0.1.0
74
+ Base URL: https://app.fizzy.do
75
+ Skip destructive: false
76
+ ============================================================
77
+
78
+ 1. IDENTITY
79
+ -----------
80
+ ✓ Get identity
81
+ Accounts: /123456
82
+
83
+ 2. BOARDS
84
+ ---------
85
+ ✓ List boards
86
+ ✓ Get single board
87
+ ✓ Create board
88
+ ...
89
+
90
+ ============================================================
91
+ DEMO SUMMARY
92
+ ============================================================
93
+ Passed: 70
94
+ Failed: 0
95
+ Skipped: 4
96
+ ============================================================
97
+ All tests passed!
98
+ ```
99
+
100
+ Skipped tests are expected for:
101
+ - Toggle assignment (when identity doesn't include current user)
102
+ - Update user, avatar, deactivate (skipped to protect real data)