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.
data/examples/demo.rb ADDED
@@ -0,0 +1,636 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Comprehensive demo script to test every feature of fizzy-api-client
5
+ #
6
+ # Usage:
7
+ # FIZZY_API_TOKEN=xxx FIZZY_ACCOUNT=yyy ruby examples/demo.rb
8
+ # Or for self-hosted:
9
+ # FIZZY_BASE_URL=https://fizzy.mycompany.com FIZZY_API_TOKEN=xxx ruby examples/demo.rb
10
+ #
11
+ # Options:
12
+ # SKIP_DESTRUCTIVE=1 - Skip operations that modify existing data (deactivate_user, etc.)
13
+ # VERBOSE=1 - Show full response data
14
+
15
+ require_relative "../lib/fizzy_api_client"
16
+ require "tempfile"
17
+
18
+ class Demo
19
+ def initialize
20
+ @passed = 0
21
+ @failed = 0
22
+ @skipped = 0
23
+ @verbose = ENV["VERBOSE"] == "1"
24
+ @skip_destructive = ENV["SKIP_DESTRUCTIVE"] == "1"
25
+ end
26
+
27
+ def run
28
+ print_banner
29
+ check_configuration
30
+
31
+ @client = FizzyApiClient::Client.new
32
+
33
+ # Run all tests
34
+ test_identity
35
+ test_boards
36
+ test_columns
37
+ test_cards
38
+ test_steps
39
+ test_comments
40
+ test_reactions
41
+ test_tags
42
+ test_users
43
+ test_notifications
44
+ test_direct_uploads
45
+ test_pagination
46
+ test_error_handling
47
+
48
+ cleanup
49
+ print_summary
50
+ end
51
+
52
+ private
53
+
54
+ def print_banner
55
+ puts "=" * 60
56
+ puts "Fizzy API Client - Comprehensive Demo"
57
+ puts "=" * 60
58
+ puts "Version: #{FizzyApiClient::VERSION}"
59
+ puts "Base URL: #{ENV['FIZZY_BASE_URL'] || 'https://app.fizzy.do'}"
60
+ puts "Skip destructive: #{@skip_destructive}"
61
+ puts "=" * 60
62
+ end
63
+
64
+ def check_configuration
65
+ unless ENV["FIZZY_API_TOKEN"]
66
+ puts "\nError: FIZZY_API_TOKEN environment variable required"
67
+ puts "Usage: FIZZY_API_TOKEN=xxx FIZZY_ACCOUNT=yyy ruby examples/demo.rb"
68
+ exit 1
69
+ end
70
+ end
71
+
72
+ def print_header(text)
73
+ puts "\n#{text}"
74
+ puts "-" * text.length
75
+ end
76
+
77
+ def pass(text, data = nil)
78
+ @passed += 1
79
+ puts " ✓ #{text}"
80
+ puts " #{data.inspect}" if @verbose && data
81
+ end
82
+
83
+ def fail(text, error)
84
+ @failed += 1
85
+ puts " ✗ #{text}"
86
+ puts " Error: #{error.message}"
87
+ end
88
+
89
+ def skip(text, reason = nil)
90
+ @skipped += 1
91
+ puts " ○ #{text} (skipped#{reason ? ": #{reason}" : ''})"
92
+ end
93
+
94
+ def test(description)
95
+ result = yield
96
+ pass(description, result)
97
+ result
98
+ rescue => e
99
+ fail(description, e)
100
+ nil
101
+ end
102
+
103
+ # ============================================================
104
+ # IDENTITY
105
+ # ============================================================
106
+ def test_identity
107
+ print_header "1. IDENTITY"
108
+
109
+ @identity = test("Get identity") { @client.identity }
110
+
111
+ if @identity && @identity["accounts"]&.any?
112
+ accounts = @identity["accounts"].map { |a| a["slug"] }
113
+ puts " Accounts: #{accounts.join(', ')}"
114
+
115
+ unless @client.account_slug
116
+ first_account = @identity["accounts"].first["slug"]
117
+ @client.account_slug = first_account
118
+ puts " Using account: #{@client.account_slug}"
119
+ end
120
+ else
121
+ puts " No accounts found - cannot continue"
122
+ exit 1
123
+ end
124
+ end
125
+
126
+ # ============================================================
127
+ # BOARDS
128
+ # ============================================================
129
+ def test_boards
130
+ print_header "2. BOARDS"
131
+
132
+ # List boards
133
+ @boards = test("List boards") { @client.boards }
134
+ return unless @boards&.any?
135
+
136
+ @board = @boards.first
137
+ puts " Using board: #{@board['name']} (#{@board['id']})"
138
+
139
+ # Get single board
140
+ test("Get single board") { @client.board(@board["id"]) }
141
+
142
+ # Create board
143
+ @test_board = test("Create board") do
144
+ @client.create_board(name: "API Demo Test Board")
145
+ end
146
+
147
+ if @test_board
148
+ # Update board
149
+ test("Update board name") do
150
+ @client.update_board(@test_board["id"], name: "API Demo Test Board (Updated)")
151
+ end
152
+
153
+ test("Update board settings") do
154
+ @client.update_board(@test_board["id"], auto_postpone_period: 14)
155
+ end
156
+ end
157
+ end
158
+
159
+ # ============================================================
160
+ # COLUMNS
161
+ # ============================================================
162
+ def test_columns
163
+ print_header "3. COLUMNS"
164
+
165
+ return skip("Columns tests", "no test board") unless @test_board
166
+
167
+ # List columns
168
+ columns = test("List columns") { @client.columns(@test_board["id"]) }
169
+
170
+ # Create column with named color (NEW FEATURE!)
171
+ @test_column = test("Create column with named color :lime") do
172
+ @client.create_column(board_id: @test_board["id"], name: "In Progress", color: :lime)
173
+ end
174
+
175
+ if @test_column
176
+ # Get single column
177
+ test("Get single column") do
178
+ @client.column(@test_board["id"], @test_column["id"])
179
+ end
180
+
181
+ # Update column with named color
182
+ test("Update column with named color :purple") do
183
+ @client.update_column(@test_board["id"], @test_column["id"], color: :purple)
184
+ end
185
+
186
+ # Update column name
187
+ test("Update column name") do
188
+ @client.update_column(@test_board["id"], @test_column["id"], name: "Review")
189
+ end
190
+
191
+ # Test all named colors
192
+ test("Create column with :blue (default)") do
193
+ @client.create_column(board_id: @test_board["id"], name: "Blue Column", color: :blue)
194
+ end
195
+
196
+ test("Create column with :pink") do
197
+ @client.create_column(board_id: @test_board["id"], name: "Pink Column", color: :pink)
198
+ end
199
+
200
+ test("Create column with CSS variable (backwards compat)") do
201
+ @client.create_column(board_id: @test_board["id"], name: "Yellow Column", color: "var(--color-card-3)")
202
+ end
203
+ end
204
+ end
205
+
206
+ # ============================================================
207
+ # CARDS
208
+ # ============================================================
209
+ def test_cards
210
+ print_header "4. CARDS"
211
+
212
+ return skip("Cards tests", "no board") unless @board
213
+
214
+ # List cards
215
+ test("List cards") { @client.cards }
216
+
217
+ test("List cards with board filter") do
218
+ @client.cards(board_ids: [@board["id"]])
219
+ end
220
+
221
+ # Create card
222
+ @test_card = test("Create card") do
223
+ @client.create_card(board_id: @board["id"], title: "API Demo Test Card")
224
+ end
225
+
226
+ return unless @test_card
227
+
228
+ @card_number = @test_card["number"]
229
+ puts " Created card ##{@card_number}"
230
+
231
+ # Get single card
232
+ test("Get single card") { @client.card(@card_number) }
233
+
234
+ # Update card
235
+ test("Update card title") do
236
+ @client.update_card(@card_number, title: "API Demo Test Card (Updated)")
237
+ end
238
+
239
+ test("Update card description") do
240
+ @client.update_card(@card_number, description: "This card was created by the API demo script.")
241
+ end
242
+
243
+ # Card state changes
244
+ test("Close card") { @client.close_card(@card_number) }
245
+ test("Reopen card") { @client.reopen_card(@card_number) }
246
+ test("Postpone card (not_now)") { @client.postpone_card(@card_number) }
247
+
248
+ # Triage - need a column on the same board as the card
249
+ # Create a column on @board for triage testing
250
+ triage_column = test("Create column for triage test") do
251
+ @client.create_column(board_id: @board["id"], name: "Triage Test Column", color: :aqua)
252
+ end
253
+
254
+ if triage_column
255
+ test("Triage card to column") do
256
+ @client.triage_card(@card_number, column_id: triage_column["id"])
257
+ end
258
+ test("Untriage card") { @client.untriage_card(@card_number) }
259
+ # Clean up triage column
260
+ @client.delete_column(@board["id"], triage_column["id"]) rescue nil
261
+ end
262
+
263
+ # Watch/unwatch
264
+ test("Watch card") { @client.watch_card(@card_number) }
265
+ test("Unwatch card") { @client.unwatch_card(@card_number) }
266
+
267
+ # Toggle tag - create a tag by toggling with a new title
268
+ test("Toggle tag on card (creates tag if needed)") do
269
+ @client.toggle_tag(@card_number, tag_title: "demo-test-tag")
270
+ end
271
+
272
+ # Toggle it off
273
+ test("Toggle tag off card") do
274
+ @client.toggle_tag(@card_number, tag_title: "demo-test-tag")
275
+ end
276
+
277
+ # Toggle assignment - use current user from identity
278
+ if @identity && @identity["user"]
279
+ current_user_id = @identity["user"]["id"]
280
+ test("Toggle assignment on card") do
281
+ @client.toggle_assignment(@card_number, assignee_id: current_user_id)
282
+ end
283
+ test("Toggle assignment off card") do
284
+ @client.toggle_assignment(@card_number, assignee_id: current_user_id)
285
+ end
286
+ else
287
+ skip("Toggle assignment on card", "no current user in identity")
288
+ end
289
+
290
+ # Card with image upload using sydney.jpg
291
+ image_path = File.expand_path("sydney.jpg", __dir__)
292
+ if File.exist?(image_path)
293
+ @image_card = test("Create card with background image (sydney.jpg)") do
294
+ card = @client.create_card(board_id: @board["id"], title: "Card with Sydney Image", image: image_path)
295
+ @cards_to_cleanup ||= []
296
+ @cards_to_cleanup << card["number"]
297
+ card
298
+ end
299
+
300
+ # Verify card has image_url
301
+ if @image_card && @image_card["image_url"]
302
+ puts " Image URL: #{@image_card['image_url'][0..60]}..."
303
+ end
304
+ else
305
+ skip("Create card with background image", "sydney.jpg not found")
306
+ end
307
+
308
+ # Golden ticket (gild/ungild)
309
+ test("Make card a golden ticket") { @client.gild_card(@card_number) }
310
+
311
+ # Verify card is now golden
312
+ gilded_card = test("Verify card is golden") do
313
+ card = @client.card(@card_number)
314
+ raise "Card is not golden!" unless card["golden"]
315
+ card
316
+ end
317
+
318
+ test("Remove golden ticket status") { @client.ungild_card(@card_number) }
319
+
320
+ # Verify card is no longer golden
321
+ test("Verify card is not golden") do
322
+ card = @client.card(@card_number)
323
+ raise "Card is still golden!" if card["golden"]
324
+ card
325
+ end
326
+ end
327
+
328
+ # ============================================================
329
+ # STEPS
330
+ # ============================================================
331
+ def test_steps
332
+ print_header "5. STEPS"
333
+
334
+ return skip("Steps tests", "no test card") unless @card_number
335
+
336
+ # Create step
337
+ @test_step = test("Create step") do
338
+ @client.create_step(@card_number, content: "Demo step from API client")
339
+ end
340
+
341
+ return unless @test_step
342
+
343
+ step_id = @test_step["id"]
344
+
345
+ # Get single step
346
+ test("Get single step") { @client.step(@card_number, step_id) }
347
+
348
+ # Update step
349
+ test("Update step content") do
350
+ @client.update_step(@card_number, step_id, content: "Updated step content")
351
+ end
352
+
353
+ # Complete/incomplete step
354
+ test("Complete step") { @client.complete_step(@card_number, step_id) }
355
+ test("Incomplete step") { @client.incomplete_step(@card_number, step_id) }
356
+
357
+ # Create another step for deletion test
358
+ step2 = test("Create step for deletion") do
359
+ @client.create_step(@card_number, content: "Step to delete")
360
+ end
361
+
362
+ if step2
363
+ test("Delete step") { @client.delete_step(@card_number, step2["id"]) }
364
+ end
365
+ end
366
+
367
+ # ============================================================
368
+ # COMMENTS
369
+ # ============================================================
370
+ def test_comments
371
+ print_header "6. COMMENTS"
372
+
373
+ return skip("Comments tests", "no test card") unless @card_number
374
+
375
+ # Create comment
376
+ @test_comment = test("Create comment") do
377
+ @client.create_comment(@card_number, body: "This is a test comment from the API demo.")
378
+ end
379
+
380
+ return unless @test_comment
381
+
382
+ comment_id = @test_comment["id"]
383
+
384
+ # List comments
385
+ test("List comments") { @client.comments(@card_number) }
386
+
387
+ # Get single comment
388
+ test("Get single comment") { @client.comment(@card_number, comment_id) }
389
+
390
+ # Update comment
391
+ test("Update comment") do
392
+ @client.update_comment(@card_number, comment_id, body: "Updated comment body")
393
+ end
394
+
395
+ # Create another comment for deletion test
396
+ comment2 = test("Create comment for deletion") do
397
+ @client.create_comment(@card_number, body: "Comment to delete")
398
+ end
399
+
400
+ if comment2
401
+ test("Delete comment") { @client.delete_comment(@card_number, comment2["id"]) }
402
+ end
403
+ end
404
+
405
+ # ============================================================
406
+ # REACTIONS
407
+ # ============================================================
408
+ def test_reactions
409
+ print_header "7. REACTIONS"
410
+
411
+ return skip("Reactions tests", "no test comment") unless @test_comment
412
+
413
+ comment_id = @test_comment["id"]
414
+
415
+ # Add reaction
416
+ @test_reaction = test("Add reaction") do
417
+ @client.add_reaction(@card_number, comment_id, content: ":+1:")
418
+ end
419
+
420
+ return unless @test_reaction
421
+
422
+ # List reactions
423
+ test("List reactions") { @client.reactions(@card_number, comment_id) }
424
+
425
+ # Remove reaction
426
+ test("Remove reaction") do
427
+ @client.remove_reaction(@card_number, comment_id, @test_reaction["id"])
428
+ end
429
+ end
430
+
431
+ # ============================================================
432
+ # TAGS
433
+ # ============================================================
434
+ def test_tags
435
+ print_header "8. TAGS"
436
+
437
+ # List tags
438
+ test("List tags") { @client.tags }
439
+
440
+ # List tags with pagination
441
+ test("List tags page 1") { @client.tags(page: 1) }
442
+ end
443
+
444
+ # ============================================================
445
+ # USERS
446
+ # ============================================================
447
+ def test_users
448
+ print_header "9. USERS"
449
+
450
+ # List users
451
+ @users = test("List users") { @client.users }
452
+
453
+ return unless @users&.any?
454
+
455
+ user = @users.first
456
+ puts " First user: #{user['name']} (#{user['id']})"
457
+
458
+ # Get single user
459
+ test("Get single user") { @client.user(user["id"]) }
460
+
461
+ # Update user (skip in destructive mode to avoid changing real data)
462
+ if @skip_destructive
463
+ skip("Update user", "destructive operation")
464
+ skip("Update user with avatar", "destructive operation")
465
+ skip("Deactivate user", "destructive operation")
466
+ else
467
+ # These would modify real user data, so we skip them by default
468
+ skip("Update user", "would modify real data - use SKIP_DESTRUCTIVE=0 to enable")
469
+ skip("Update user with avatar", "would modify real data")
470
+ skip("Deactivate user", "would modify real data")
471
+ end
472
+ end
473
+
474
+ # ============================================================
475
+ # NOTIFICATIONS
476
+ # ============================================================
477
+ def test_notifications
478
+ print_header "10. NOTIFICATIONS"
479
+
480
+ # List notifications
481
+ notifications = test("List notifications") { @client.notifications }
482
+
483
+ if notifications&.any?
484
+ notification = notifications.first
485
+ notification_id = notification["id"]
486
+
487
+ test("Mark notification read") { @client.mark_notification_read(notification_id) }
488
+ test("Mark notification unread") { @client.mark_notification_unread(notification_id) }
489
+ else
490
+ skip("Mark notification read", "no notifications")
491
+ skip("Mark notification unread", "no notifications")
492
+ end
493
+
494
+ # Mark all read (always works, even with no notifications)
495
+ test("Mark all notifications read") { @client.mark_all_notifications_read }
496
+ end
497
+
498
+ # ============================================================
499
+ # DIRECT UPLOADS
500
+ # ============================================================
501
+ def test_direct_uploads
502
+ print_header "11. DIRECT UPLOADS"
503
+
504
+ test("Create direct upload") do
505
+ content = "test file content"
506
+ checksum = Base64.strict_encode64(Digest::MD5.digest(content))
507
+
508
+ @client.create_direct_upload(
509
+ filename: "test.txt",
510
+ byte_size: content.bytesize,
511
+ checksum: checksum,
512
+ content_type: "text/plain"
513
+ )
514
+ end
515
+
516
+ test("Upload file helper") do
517
+ Tempfile.create(["test", ".txt"]) do |f|
518
+ f.write("Hello from Fizzy API Client demo!")
519
+ f.rewind
520
+ @client.upload_file(f.path)
521
+ end
522
+ end
523
+ end
524
+
525
+ # ============================================================
526
+ # PAGINATION
527
+ # ============================================================
528
+ def test_pagination
529
+ print_header "12. PAGINATION"
530
+
531
+ test("List boards with auto_paginate") do
532
+ @client.boards(auto_paginate: true)
533
+ end
534
+
535
+ test("List cards with auto_paginate") do
536
+ @client.cards(auto_paginate: true)
537
+ end
538
+
539
+ test("List tags with auto_paginate") do
540
+ @client.tags(auto_paginate: true)
541
+ end
542
+
543
+ test("List users with auto_paginate") do
544
+ @client.users(auto_paginate: true)
545
+ end
546
+ end
547
+
548
+ # ============================================================
549
+ # ERROR HANDLING
550
+ # ============================================================
551
+ def test_error_handling
552
+ print_header "13. ERROR HANDLING"
553
+
554
+ test("NotFoundError for invalid card") do
555
+ begin
556
+ @client.card(999_999_999)
557
+ raise "Should have raised NotFoundError"
558
+ rescue FizzyApiClient::NotFoundError => e
559
+ "Correctly raised NotFoundError: #{e.message}"
560
+ end
561
+ end
562
+
563
+ test("NotFoundError for invalid board") do
564
+ begin
565
+ @client.board("invalid_board_id_xyz")
566
+ raise "Should have raised NotFoundError"
567
+ rescue FizzyApiClient::NotFoundError => e
568
+ "Correctly raised NotFoundError: #{e.message}"
569
+ end
570
+ end
571
+
572
+ test("ValidationError for invalid color") do
573
+ begin
574
+ FizzyApiClient::Colors.resolve(:invalid_color)
575
+ raise "Should have raised ArgumentError"
576
+ rescue ArgumentError => e
577
+ "Correctly raised ArgumentError: #{e.message}"
578
+ end
579
+ end
580
+ end
581
+
582
+ # ============================================================
583
+ # CLEANUP
584
+ # ============================================================
585
+ def cleanup
586
+ print_header "14. CLEANUP"
587
+
588
+ # Delete test card
589
+ if @card_number
590
+ test("Delete test card ##{@card_number}") do
591
+ @client.delete_card(@card_number)
592
+ end
593
+ end
594
+
595
+ # Delete any additional cards created during testing
596
+ (@cards_to_cleanup || []).each do |card_number|
597
+ test("Delete card ##{card_number}") do
598
+ @client.delete_card(card_number)
599
+ end
600
+ end
601
+
602
+ # Delete test column
603
+ if @test_column && @test_board
604
+ test("Delete test column") do
605
+ @client.delete_column(@test_board["id"], @test_column["id"])
606
+ end
607
+ end
608
+
609
+ # Delete test board
610
+ if @test_board
611
+ test("Delete test board") do
612
+ @client.delete_board(@test_board["id"])
613
+ end
614
+ end
615
+ end
616
+
617
+ def print_summary
618
+ puts "\n" + "=" * 60
619
+ puts "DEMO SUMMARY"
620
+ puts "=" * 60
621
+ puts " Passed: #{@passed}"
622
+ puts " Failed: #{@failed}"
623
+ puts " Skipped: #{@skipped}"
624
+ puts "=" * 60
625
+
626
+ if @failed > 0
627
+ puts "Some tests failed!"
628
+ exit 1
629
+ else
630
+ puts "All tests passed!"
631
+ end
632
+ end
633
+ end
634
+
635
+ # Run the demo
636
+ Demo.new.run
Binary file
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/fizzy_api_client/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "fizzy-api-client"
7
+ spec.version = FizzyApiClient::VERSION
8
+ spec.authors = ["Fizzy Team"]
9
+ spec.email = ["support@fizzy.do"]
10
+
11
+ spec.summary = "Ruby client for the Fizzy API"
12
+ spec.description = "A clean, idiomatic Ruby interface to the Fizzy API with minimal dependencies"
13
+ spec.homepage = "https://github.com/robzolkos/fizzy-api-client"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 3.0.0"
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = spec.homepage
19
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
20
+
21
+ spec.files = Dir.glob("{lib,examples}/**/*", File::FNM_DOTMATCH).reject { |f| File.directory?(f) } +
22
+ %w[LICENSE.txt README.md CHANGELOG.md fizzy-api-client.gemspec]
23
+ spec.require_paths = ["lib"]
24
+
25
+ # Required for Ruby 3.4+ (moved from stdlib to bundled gems)
26
+ spec.add_dependency "base64"
27
+
28
+ # Required for Ruby 3.5+ (will be moved from stdlib to bundled gems)
29
+ spec.add_dependency "ostruct"
30
+ spec.add_dependency "logger"
31
+ end