monday_ruby 1.1.0 → 1.2.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.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/.env +1 -1
  3. data/.rubocop.yml +2 -1
  4. data/CHANGELOG.md +14 -0
  5. data/CONTRIBUTING.md +104 -0
  6. data/README.md +146 -142
  7. data/docs/.vitepress/config.mjs +255 -0
  8. data/docs/.vitepress/theme/index.js +4 -0
  9. data/docs/.vitepress/theme/style.css +43 -0
  10. data/docs/README.md +80 -0
  11. data/docs/explanation/architecture.md +507 -0
  12. data/docs/explanation/best-practices/errors.md +478 -0
  13. data/docs/explanation/best-practices/performance.md +1084 -0
  14. data/docs/explanation/best-practices/rate-limiting.md +630 -0
  15. data/docs/explanation/best-practices/testing.md +820 -0
  16. data/docs/explanation/column-values.md +857 -0
  17. data/docs/explanation/design.md +795 -0
  18. data/docs/explanation/graphql.md +356 -0
  19. data/docs/explanation/migration/v1.md +808 -0
  20. data/docs/explanation/pagination.md +447 -0
  21. data/docs/guides/advanced/batch.md +1274 -0
  22. data/docs/guides/advanced/complex-queries.md +1114 -0
  23. data/docs/guides/advanced/errors.md +818 -0
  24. data/docs/guides/advanced/pagination.md +934 -0
  25. data/docs/guides/advanced/rate-limiting.md +981 -0
  26. data/docs/guides/authentication.md +286 -0
  27. data/docs/guides/boards/create.md +386 -0
  28. data/docs/guides/boards/delete.md +405 -0
  29. data/docs/guides/boards/duplicate.md +511 -0
  30. data/docs/guides/boards/query.md +530 -0
  31. data/docs/guides/boards/update.md +453 -0
  32. data/docs/guides/columns/create.md +452 -0
  33. data/docs/guides/columns/metadata.md +492 -0
  34. data/docs/guides/columns/query.md +455 -0
  35. data/docs/guides/columns/update-multiple.md +459 -0
  36. data/docs/guides/columns/update-values.md +509 -0
  37. data/docs/guides/files/add-to-column.md +40 -0
  38. data/docs/guides/files/add-to-update.md +37 -0
  39. data/docs/guides/files/clear-column.md +33 -0
  40. data/docs/guides/first-request.md +285 -0
  41. data/docs/guides/folders/manage.md +750 -0
  42. data/docs/guides/groups/items.md +626 -0
  43. data/docs/guides/groups/manage.md +501 -0
  44. data/docs/guides/installation.md +169 -0
  45. data/docs/guides/items/create.md +493 -0
  46. data/docs/guides/items/delete.md +514 -0
  47. data/docs/guides/items/query.md +605 -0
  48. data/docs/guides/items/subitems.md +483 -0
  49. data/docs/guides/items/update.md +699 -0
  50. data/docs/guides/updates/manage.md +619 -0
  51. data/docs/guides/use-cases/dashboard.md +1421 -0
  52. data/docs/guides/use-cases/import.md +1962 -0
  53. data/docs/guides/use-cases/task-management.md +1381 -0
  54. data/docs/guides/workspaces/manage.md +502 -0
  55. data/docs/index.md +69 -0
  56. data/docs/package-lock.json +2468 -0
  57. data/docs/package.json +13 -0
  58. data/docs/reference/client.md +540 -0
  59. data/docs/reference/configuration.md +586 -0
  60. data/docs/reference/errors.md +693 -0
  61. data/docs/reference/resources/account.md +208 -0
  62. data/docs/reference/resources/activity-log.md +369 -0
  63. data/docs/reference/resources/board-view.md +359 -0
  64. data/docs/reference/resources/board.md +393 -0
  65. data/docs/reference/resources/column.md +543 -0
  66. data/docs/reference/resources/file.md +236 -0
  67. data/docs/reference/resources/folder.md +386 -0
  68. data/docs/reference/resources/group.md +507 -0
  69. data/docs/reference/resources/item.md +348 -0
  70. data/docs/reference/resources/subitem.md +267 -0
  71. data/docs/reference/resources/update.md +259 -0
  72. data/docs/reference/resources/workspace.md +213 -0
  73. data/docs/reference/response.md +560 -0
  74. data/docs/tutorial/first-integration.md +713 -0
  75. data/lib/monday/client.rb +24 -0
  76. data/lib/monday/configuration.rb +5 -0
  77. data/lib/monday/request.rb +15 -0
  78. data/lib/monday/resources/base.rb +4 -0
  79. data/lib/monday/resources/file.rb +56 -0
  80. data/lib/monday/util.rb +1 -0
  81. data/lib/monday/version.rb +1 -1
  82. metadata +87 -4
@@ -0,0 +1,795 @@
1
+ # Design Decisions
2
+
3
+ This document explores the key design decisions behind the monday_ruby gem, explaining the rationale, trade-offs, and principles that guide its implementation.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Design Philosophy](#design-philosophy)
8
+ - [Client-Resource Pattern Choice](#client-resource-pattern-choice)
9
+ - [Configuration Design](#configuration-design)
10
+ - [Error Exception Hierarchy](#error-exception-hierarchy)
11
+ - [GraphQL Abstraction Level](#graphql-abstraction-level)
12
+ - [Response Object Design](#response-object-design)
13
+ - [The base64 Dependency](#the-base64-dependency)
14
+ - [Future Design Considerations](#future-design-considerations)
15
+
16
+ ## Design Philosophy
17
+
18
+ The monday_ruby gem is built on several core principles that inform all design decisions:
19
+
20
+ ### 1. Ruby-Idiomatic Interface
21
+
22
+ The gem should feel natural to Ruby developers, even if they've never used GraphQL. This means:
23
+ - Using snake_case method names (`board.query` not `board.Query`)
24
+ - Accepting Ruby data structures (hashes, arrays, symbols)
25
+ - Following Ruby conventions for error handling (exceptions, not error codes)
26
+ - Providing sensible defaults that work for common cases
27
+
28
+ ### 2. Explicit Over Implicit
29
+
30
+ While the gem hides GraphQL complexity, it remains explicit about what it's doing:
31
+ - Method names clearly indicate the operation (`create`, `query`, `update`, `delete`)
32
+ - Users explicitly specify what data they want via the `select` parameter
33
+ - No hidden network calls or lazy loading
34
+ - Query building is transparent (the query string could be logged/inspected)
35
+
36
+ ### 3. Flexibility Without Complexity
37
+
38
+ The gem provides simple defaults for common cases while allowing customization:
39
+ - Default field selections for quick usage
40
+ - Override options for specific needs
41
+ - Extensible architecture for adding resources
42
+ - No forced opinions about how to structure application code
43
+
44
+ ### 4. Fail Fast and Clearly
45
+
46
+ When things go wrong, the gem should make it obvious:
47
+ - Specific exception types for different error categories
48
+ - Response objects attached to exceptions for debugging
49
+ - No silent failures or generic error messages
50
+ - API errors are surfaced, not swallowed
51
+
52
+ These principles create a gem that's approachable for beginners but powerful for advanced users.
53
+
54
+ ## Client-Resource Pattern Choice
55
+
56
+ The decision to use a client-resource pattern rather than alternative approaches deserves deeper exploration.
57
+
58
+ ### The Decision
59
+
60
+ Resources are instantiated through the client and hold a reference back to it:
61
+
62
+ ```ruby
63
+ client = Monday::Client.new(token: "...")
64
+ client.board.query(...) # client.board is a Board instance
65
+ ```
66
+
67
+ Rather than:
68
+ - Static methods: `Monday::Board.query(client, ...)`
69
+ - Standalone instances: `board = Monday::Board.new(token: "...")`
70
+ - Global configuration: `Monday.configure(...); Monday::Board.query(...)`
71
+
72
+ ### Why This Design?
73
+
74
+ **1. Configuration Scoping**
75
+
76
+ By tying resources to a client instance, configuration becomes scoped:
77
+
78
+ ```ruby
79
+ client_a = Monday::Client.new(token: "token_a")
80
+ client_b = Monday::Client.new(token: "token_b")
81
+
82
+ client_a.board.query(...) # Uses token_a
83
+ client_b.board.query(...) # Uses token_b
84
+ ```
85
+
86
+ This is critical for applications that interact with multiple monday.com accounts or need different configurations (like different timeouts) for different request types.
87
+
88
+ **2. Dependency Injection**
89
+
90
+ Resources receive their dependencies (the client) through their constructor. This makes testing easier:
91
+
92
+ ```ruby
93
+ # In tests
94
+ mock_client = double("Client")
95
+ board = Monday::Resources::Board.new(mock_client)
96
+ ```
97
+
98
+ Resources don't need global state or singleton instances to function.
99
+
100
+ **3. Discoverability**
101
+
102
+ The client acts as a namespace for all available resources. You can discover what's available by exploring the client:
103
+
104
+ ```ruby
105
+ client.methods.grep(/^[a-z]/) # Shows all resource accessors
106
+ ```
107
+
108
+ This is harder with static methods spread across multiple classes.
109
+
110
+ **4. Shared State**
111
+
112
+ All resources on a client share the same configuration, connection, and error handling. This ensures consistency:
113
+
114
+ ```ruby
115
+ client = Monday::Client.new(
116
+ token: "...",
117
+ open_timeout: 5,
118
+ read_timeout: 30
119
+ )
120
+
121
+ # All resources use the same timeouts
122
+ client.board.query(...)
123
+ client.item.create(...)
124
+ client.group.delete(...)
125
+ ```
126
+
127
+ ### Trade-offs
128
+
129
+ **Advantages:**
130
+ - Configuration scoping (multiple clients possible)
131
+ - Clear dependency relationships
132
+ - Consistent behavior across resources
133
+ - Easy to test with mocks
134
+
135
+ **Disadvantages:**
136
+ - More verbose than static methods: `client.board.query(...)` vs `Board.query(...)`
137
+ - Resources can't be used independently (always need a client)
138
+ - Extra initialization step (creating the client)
139
+
140
+ For a library wrapping an authenticated API, these trade-offs favor the client-resource pattern. The scoping and dependency injection benefits outweigh the verbosity.
141
+
142
+ ## Configuration Design
143
+
144
+ The gem supports both global and instance-level configuration, which is unusual. Most libraries choose one approach.
145
+
146
+ ### The Dual System
147
+
148
+ **Global Configuration:**
149
+ ```ruby
150
+ Monday.configure do |config|
151
+ config.token = "..."
152
+ config.version = "2023-07"
153
+ end
154
+
155
+ client = Monday::Client.new # Uses global config
156
+ ```
157
+
158
+ **Instance Configuration:**
159
+ ```ruby
160
+ client = Monday::Client.new(
161
+ token: "...",
162
+ version: "2023-07"
163
+ ) # Creates its own config
164
+ ```
165
+
166
+ ### Why Both?
167
+
168
+ This design serves different use cases:
169
+
170
+ **Global configuration** is ideal for:
171
+ - Simple applications with one monday.com account
172
+ - Setting defaults for all clients
173
+ - Quick prototyping and scripts
174
+ - Rails applications (config in an initializer)
175
+
176
+ **Instance configuration** is ideal for:
177
+ - Multi-tenant applications
178
+ - Testing (different configs for different test scenarios)
179
+ - Applications integrating with multiple monday.com accounts
180
+ - Overriding specific settings for specific requests
181
+
182
+ ### Implementation Details
183
+
184
+ The implementation is elegantly simple:
185
+
186
+ ```ruby
187
+ def configure(config_args)
188
+ return Monday.config if config_args.empty?
189
+ Configuration.new(**config_args)
190
+ end
191
+ ```
192
+
193
+ If no arguments are provided, use the global singleton. Otherwise, create a new `Configuration` instance. This means:
194
+
195
+ - No global client instance (would prevent multiple configurations)
196
+ - Global config is lazy-loaded (created on first access)
197
+ - Instance configs are independent (don't affect global or each other)
198
+
199
+ ### Alternative Approaches
200
+
201
+ **Only Global Configuration:**
202
+ Many Ruby libraries (like Octokit, Faraday) use only global configuration. This is simpler but makes multi-tenant scenarios difficult:
203
+
204
+ ```ruby
205
+ # Hard to manage multiple accounts
206
+ Monday.configure(token: "account_a_token")
207
+ result_a = Monday::Board.query(...)
208
+
209
+ Monday.configure(token: "account_b_token") # Overwrites!
210
+ result_b = Monday::Board.query(...)
211
+ ```
212
+
213
+ **Only Instance Configuration:**
214
+ Some libraries require explicit configuration for every client. This is flexible but verbose for single-account applications:
215
+
216
+ ```ruby
217
+ # Repetitive in simple cases
218
+ client1 = Monday::Client.new(token: "...", version: "2023-07", host: "...")
219
+ client2 = Monday::Client.new(token: "...", version: "2023-07", host: "...")
220
+ ```
221
+
222
+ **Inheritance Pattern:**
223
+ Some libraries allow instance configs to inherit from global and override selectively:
224
+
225
+ ```ruby
226
+ Monday.configure do |config|
227
+ config.version = "2023-07"
228
+ config.host = "https://api.monday.com/v2"
229
+ end
230
+
231
+ client = Monday::Client.new(token: "...") # Inherits version and host, sets token
232
+ ```
233
+
234
+ The current design doesn't support inheritance. Instance configs are completely independent from global config. This is simpler to understand (no merge logic) but less flexible.
235
+
236
+ ### Trade-offs
237
+
238
+ The dual system is more complex than a single approach, but it serves real use cases. The simplicity of the implementation (just return global or create instance) keeps maintenance burden low.
239
+
240
+ Future versions could add inheritance if user demand exists, but the current design satisfies the common cases.
241
+
242
+ ## Error Exception Hierarchy
243
+
244
+ The gem defines a hierarchy of exception classes that mirror monday.com's error types.
245
+
246
+ ### The Hierarchy
247
+
248
+ ```
249
+ Monday::Error (base)
250
+ ├── Monday::InternalServerError (500)
251
+ ├── Monday::AuthorizationError (401, 403)
252
+ ├── Monday::RateLimitError (429)
253
+ ├── Monday::ResourceNotFoundError (404)
254
+ ├── Monday::InvalidRequestError (400)
255
+ └── Monday::ComplexityError (GraphQL complexity)
256
+ ```
257
+
258
+ ### Design Rationale
259
+
260
+ **1. Catchall with Specificity**
261
+
262
+ Users can rescue all API errors:
263
+ ```ruby
264
+ rescue Monday::Error => e
265
+ # Handle any monday.com error
266
+ end
267
+ ```
268
+
269
+ Or specific types:
270
+ ```ruby
271
+ rescue Monday::RateLimitError => e
272
+ sleep 60
273
+ retry
274
+ rescue Monday::AuthorizationError => e
275
+ refresh_token
276
+ retry
277
+ rescue Monday::Error => e
278
+ log_error(e)
279
+ end
280
+ ```
281
+
282
+ **2. HTTP Semantics**
283
+
284
+ The exception hierarchy follows HTTP status code semantics. This makes the gem's behavior predictable to developers familiar with REST APIs, even though monday.com uses GraphQL.
285
+
286
+ **3. Rich Error Objects**
287
+
288
+ All exceptions include:
289
+ - `message`: Human-readable error description
290
+ - `response`: The full Response object for debugging
291
+ - `code`: HTTP status code or custom error code
292
+
293
+ This allows detailed error handling:
294
+ ```ruby
295
+ rescue Monday::Error => e
296
+ puts "Error: #{e.message}"
297
+ puts "Status: #{e.code}"
298
+ puts "Body: #{e.response.body.inspect}"
299
+ puts "Error data: #{e.error_data.inspect}"
300
+ end
301
+ ```
302
+
303
+ ### The Mapping Problem
304
+
305
+ monday.com returns errors in inconsistent formats:
306
+ - HTTP status codes (401, 404, 429, 500)
307
+ - GraphQL error codes (`ComplexityException`, `USER_UNAUTHORIZED`)
308
+ - Different key names (`code` vs `error_code`)
309
+ - Errors in arrays vs. top-level objects
310
+
311
+ The gem handles this with two mapping methods:
312
+
313
+ **`Util.status_code_exceptions_mapping`** - Maps HTTP codes to exceptions:
314
+ ```ruby
315
+ {
316
+ "500" => InternalServerError,
317
+ "429" => RateLimitError,
318
+ "404" => ResourceNotFoundError,
319
+ # ...
320
+ }
321
+ ```
322
+
323
+ **`Util.response_error_exceptions_mapping`** - Maps API error codes to exceptions:
324
+ ```ruby
325
+ {
326
+ "ComplexityException" => [ComplexityError, 429],
327
+ "USER_UNAUTHORIZED" => [AuthorizationError, 403],
328
+ "InvalidBoardIdException" => [InvalidRequestError, 400],
329
+ # ...
330
+ }
331
+ ```
332
+
333
+ The client tries both approaches:
334
+ 1. Check HTTP status code → raise default exception if not 2xx
335
+ 2. Check response body error codes → raise specific exception
336
+
337
+ This handles both HTTP-level errors (network issues, auth failures) and GraphQL-level errors (invalid queries, business logic failures).
338
+
339
+ ### Why Not One Generic Exception?
340
+
341
+ A single `Monday::Error` would be simpler:
342
+
343
+ ```ruby
344
+ # Hypothetical simpler design
345
+ raise Monday::Error.new(message: error_message, code: error_code)
346
+ ```
347
+
348
+ But this loses semantic information. Users would have to check error codes or messages to determine the error type:
349
+
350
+ ```ruby
351
+ rescue Monday::Error => e
352
+ if e.code == 429
353
+ # Rate limit
354
+ elsif e.code == 401
355
+ # Auth error
356
+ end
357
+ end
358
+ ```
359
+
360
+ Specific exception types make error handling clearer and more robust (error messages can change, but exception types are part of the API contract).
361
+
362
+ ### Trade-offs
363
+
364
+ **Advantages:**
365
+ - Semantic error handling (rescue specific types)
366
+ - Follows HTTP conventions
367
+ - Rich error information
368
+ - Extensible (new exception types can be added)
369
+
370
+ **Disadvantages:**
371
+ - More classes to maintain
372
+ - Mapping tables need updates when monday.com adds error codes
373
+ - Users must learn the exception hierarchy
374
+
375
+ The benefits of semantic error handling outweigh the maintenance cost, especially as the gem matures and error types stabilize.
376
+
377
+ ## GraphQL Abstraction Level
378
+
379
+ A key design question is: How much GraphQL should the gem expose?
380
+
381
+ ### The Chosen Abstraction
382
+
383
+ The gem provides a **high-level abstraction** that hides GraphQL entirely:
384
+
385
+ ```ruby
386
+ client.board.query(
387
+ args: {ids: [123]},
388
+ select: ["id", "name", {"items" => ["id"]}]
389
+ )
390
+ ```
391
+
392
+ Users don't write GraphQL queries. They call Ruby methods with Ruby data structures.
393
+
394
+ ### Alternative Abstraction Levels
395
+
396
+ **Low-Level (Expose GraphQL):**
397
+ ```ruby
398
+ client.execute(<<~GRAPHQL)
399
+ query {
400
+ boards(ids: [123]) {
401
+ id
402
+ name
403
+ items { id }
404
+ }
405
+ }
406
+ GRAPHQL
407
+ ```
408
+
409
+ **Medium-Level (Query builders):**
410
+ ```ruby
411
+ client.query do |q|
412
+ q.boards(ids: [123]) do |b|
413
+ b.field :id
414
+ b.field :name
415
+ b.field :items do |i|
416
+ i.field :id
417
+ end
418
+ end
419
+ end
420
+ ```
421
+
422
+ **High-Level (Hide GraphQL):**
423
+ ```ruby
424
+ client.board.query(args: {ids: [123]}, select: ["id", "name", {"items" => ["id"]}])
425
+ ```
426
+
427
+ ### Why High-Level?
428
+
429
+ **1. Accessibility**
430
+
431
+ Most Ruby developers haven't used GraphQL. By hiding it, the gem is accessible to a broader audience. Users can be productive without learning GraphQL syntax, schema introspection, or query optimization.
432
+
433
+ **2. Consistency**
434
+
435
+ All methods follow the same pattern: `args` for parameters, `select` for fields. This consistency makes the API predictable. Once you understand `board.query`, you understand `item.query`.
436
+
437
+ **3. Simplicity**
438
+
439
+ No query builder DSL to learn. No GraphQL client library to understand. Just method calls with hashes and arrays.
440
+
441
+ **4. Monday.com Specifics**
442
+
443
+ The abstraction can encode monday.com-specific knowledge:
444
+
445
+ ```ruby
446
+ # Default field selections that make sense for monday.com
447
+ def query(args: {}, select: DEFAULT_SELECT)
448
+ # DEFAULT_SELECT = ["id", "name", "description"]
449
+ end
450
+ ```
451
+
452
+ Users get sensible defaults without knowing what fields exist.
453
+
454
+ ### Trade-offs
455
+
456
+ **Advantages:**
457
+ - No GraphQL knowledge required
458
+ - Consistent API across resources
459
+ - Defaults encode monday.com best practices
460
+ - Simple to use for common cases
461
+
462
+ **Disadvantages:**
463
+ - Can't use all GraphQL features (aliases, fragments, directives)
464
+ - Abstraction can leak (some monday.com concepts don't map cleanly)
465
+ - Less flexible than raw GraphQL
466
+ - Users must learn the gem's API instead of standard GraphQL
467
+
468
+ ### When the Abstraction Leaks
469
+
470
+ The high-level abstraction sometimes reveals its GraphQL underpinnings:
471
+
472
+ **Field selection syntax** mirrors GraphQL structure:
473
+ ```ruby
474
+ select: ["id", {"items" => ["id", "name"]}]
475
+ # Generates: id items { id name }
476
+ ```
477
+
478
+ **Arguments** use GraphQL types:
479
+ ```ruby
480
+ args: {operator: :and} # Symbol becomes GraphQL enum
481
+ args: {rules: [...]} # Array becomes GraphQL list
482
+ ```
483
+
484
+ These aren't pure Ruby APIs - they're GraphQL concepts exposed through Ruby syntax.
485
+
486
+ ### Future Direction
487
+
488
+ The abstraction could evolve in two directions:
489
+
490
+ **More abstraction**: Hide even the field selection:
491
+ ```ruby
492
+ client.board.find(123) # Returns a board object with default fields
493
+ client.board.find(123, include: [:items]) # Include related items
494
+ ```
495
+
496
+ **Less abstraction**: Expose an escape hatch for raw GraphQL:
497
+ ```ruby
498
+ client.execute(graphql_query_string)
499
+ ```
500
+
501
+ The current design balances these extremes. It's high-level enough for ease of use but low-level enough to expose GraphQL's power (explicit field selection, complex queries).
502
+
503
+ ## Response Object Design
504
+
505
+ The gem wraps `Net::HTTP::Response` in a custom `Monday::Response` class rather than returning the raw response.
506
+
507
+ ### The Design
508
+
509
+ ```ruby
510
+ class Response
511
+ attr_reader :status, :body, :headers
512
+
513
+ def initialize(response)
514
+ @status = response.code.to_i
515
+ @body = parse_body # Parses JSON
516
+ @headers = parse_headers
517
+ end
518
+
519
+ def success?
520
+ (200..299).cover?(status) && !errors?
521
+ end
522
+ end
523
+ ```
524
+
525
+ ### Why Wrap?
526
+
527
+ **1. Consistent Interface**
528
+
529
+ `Net::HTTP::Response` has quirks:
530
+ - `response.code` is a string ("200"), not an integer
531
+ - `response.body` is raw JSON, not parsed
532
+ - Headers are accessed with `response.each_header`
533
+
534
+ The wrapper provides a cleaner, more predictable interface:
535
+ - `response.status` is always an integer
536
+ - `response.body` is always a parsed hash
537
+ - `response.headers` is a simple hash
538
+
539
+ **2. monday.com Specifics**
540
+
541
+ The `success?` method encodes monday.com-specific knowledge:
542
+
543
+ ```ruby
544
+ def success?
545
+ (200..299).cover?(status) && !errors?
546
+ end
547
+ ```
548
+
549
+ monday.com returns HTTP 200 for GraphQL errors, so HTTP status alone doesn't indicate success. The wrapper checks both HTTP status and response body.
550
+
551
+ **3. Future Evolution**
552
+
553
+ The wrapper provides a stable API even if the underlying HTTP library changes. If the gem switches from `Net::HTTP` to `httparty` or `faraday`, the Response interface can remain the same.
554
+
555
+ **4. Exception Context**
556
+
557
+ All exceptions include the Response object:
558
+
559
+ ```ruby
560
+ exception.response.body
561
+ exception.response.status
562
+ exception.response.headers
563
+ ```
564
+
565
+ This wouldn't work cleanly with raw `Net::HTTP::Response` because it doesn't guarantee a parsed body or integer status.
566
+
567
+ ### Alternative: Return Raw Response
568
+
569
+ The gem could return `Net::HTTP::Response` directly:
570
+
571
+ ```ruby
572
+ http_response = client.board.query(...)
573
+ body = JSON.parse(http_response.body)
574
+ ```
575
+
576
+ **Advantages:**
577
+ - Users can access all `Net::HTTP::Response` methods
578
+ - No abstraction layer
579
+ - Familiar to Ruby developers
580
+
581
+ **Disadvantages:**
582
+ - Users must parse JSON themselves
583
+ - No monday.com-specific success detection
584
+ - Less consistent (status is string vs integer confusion)
585
+ - Tied to Net::HTTP (harder to change HTTP library)
586
+
587
+ ### Trade-offs
588
+
589
+ The wrapper adds a thin abstraction layer, but it significantly improves usability:
590
+
591
+ ```ruby
592
+ # With wrapper
593
+ response = client.board.query(...)
594
+ if response.success?
595
+ boards = response.body["data"]["boards"]
596
+ end
597
+
598
+ # Without wrapper (hypothetical)
599
+ response = client.board.query(...)
600
+ if response.code.to_i.between?(200, 299)
601
+ parsed = JSON.parse(response.body)
602
+ unless parsed["errors"] || parsed["error_code"]
603
+ boards = parsed["data"]["boards"]
604
+ end
605
+ end
606
+ ```
607
+
608
+ The wrapper encapsulates complexity that users would otherwise repeat in every integration.
609
+
610
+ ## The base64 Dependency
611
+
612
+ The gem explicitly depends on the `base64` gem, even though the code never directly requires or uses Base64 encoding. This decision requires explanation.
613
+
614
+ ### The Issue
615
+
616
+ Starting with Ruby 3.4, `base64` was removed from Ruby's default gems. It must be explicitly added as a dependency to Gemfile.
617
+
618
+ The monday_ruby gem uses `Net::HTTP` for HTTP requests. `Net::HTTP` internally requires `base64` for HTTP Basic Authentication, even if the gem doesn't use Basic Auth.
619
+
620
+ Without the explicit dependency, the gem would fail on Ruby 3.4+ with:
621
+ ```
622
+ LoadError: cannot load such file -- base64
623
+ ```
624
+
625
+ ### The Decision
626
+
627
+ Rather than letting users discover this error in production, the gem explicitly declares the dependency:
628
+
629
+ ```ruby
630
+ # In gemspec
631
+ spec.add_dependency "base64", "~> 0.2.0"
632
+ ```
633
+
634
+ ### Why Not Remove Net::HTTP?
635
+
636
+ The gem could switch to an HTTP library that doesn't require `base64`:
637
+ - `httparty`
638
+ - `faraday`
639
+ - `rest-client`
640
+
641
+ However:
642
+ - `Net::HTTP` is in Ruby's standard library (no external dependencies until Ruby 3.4)
643
+ - It's simple and well-understood
644
+ - The gem's HTTP needs are basic (POST requests with JSON)
645
+ - Switching would add dependencies for Ruby < 3.4 users
646
+
647
+ Adding `base64` as a dependency is simpler than changing HTTP libraries.
648
+
649
+ ### Future Considerations
650
+
651
+ As Ruby 3.4+ adoption grows, this decision may be revisited. Options include:
652
+ - Keep the `base64` dependency (current approach)
653
+ - Switch to a different HTTP library
654
+ - Conditionally require `base64` only on Ruby 3.4+
655
+
656
+ For now, explicit dependency on `base64` is the simplest solution that works across all Ruby versions.
657
+
658
+ ## Future Design Considerations
659
+
660
+ Design decisions aren't permanent. As the gem evolves, several areas merit reconsideration.
661
+
662
+ ### 1. Query Caching
663
+
664
+ Currently, every request hits the monday.com API. Future versions could cache responses:
665
+
666
+ ```ruby
667
+ client = Monday::Client.new(token: "...", cache: Redis.new)
668
+ client.board.query(args: {ids: [123]}) # Hits API
669
+ client.board.query(args: {ids: [123]}) # Returns cached response
670
+ ```
671
+
672
+ **Considerations:**
673
+ - Cache invalidation is hard (when does cached data become stale?)
674
+ - monday.com data changes frequently (boards, items updated constantly)
675
+ - Would complicate the simple request-response model
676
+ - Adds dependency on cache backend
677
+
678
+ Caching might be better left to application code using the gem.
679
+
680
+ ### 2. Async/Batch Requests
681
+
682
+ The gem could support batching multiple queries:
683
+
684
+ ```ruby
685
+ client.batch do |batch|
686
+ batch.board.query(args: {ids: [123]})
687
+ batch.item.query(args: {ids: [456]})
688
+ batch.group.query(args: {ids: [789]})
689
+ end # Executes all queries in one HTTP request
690
+ ```
691
+
692
+ GraphQL supports this natively. The gem could expose it.
693
+
694
+ **Considerations:**
695
+ - Batch requests are more complex (partial failures, ordering)
696
+ - monday.com's API may have batch size limits
697
+ - The simple one-method-one-request model would break
698
+ - Testing becomes harder (mocking batch responses)
699
+
700
+ This would be a significant design change requiring careful thought.
701
+
702
+ ### 3. Pagination Helpers
703
+
704
+ The gem exposes monday.com's cursor-based pagination but doesn't provide helpers:
705
+
706
+ ```ruby
707
+ # Current approach
708
+ response = client.board.items_page(board_id: 123, limit: 100)
709
+ items = response.body.dig("data", "boards", 0, "items_page", "items")
710
+ cursor = response.body.dig("data", "boards", 0, "items_page", "cursor")
711
+
712
+ response = client.board.items_page(board_id: 123, cursor: cursor)
713
+ # Repeat...
714
+ ```
715
+
716
+ A pagination helper could simplify this:
717
+
718
+ ```ruby
719
+ # Hypothetical helper
720
+ client.board.items_page(board_id: 123).each_page do |items, cursor|
721
+ process(items)
722
+ break if cursor.nil?
723
+ end
724
+
725
+ # Or automatic pagination
726
+ all_items = client.board.all_items(board_id: 123) # Fetches all pages
727
+ ```
728
+
729
+ **Considerations:**
730
+ - Auto-pagination could make many API calls without users realizing
731
+ - Rate limiting becomes more complex
732
+ - Adds stateful behavior (tracking cursors)
733
+ - Different monday.com resources paginate differently
734
+
735
+ Pagination helpers would need careful design to avoid surprising behavior.
736
+
737
+ ### 4. Response Object Enhancement
738
+
739
+ The Response object could provide convenience methods:
740
+
741
+ ```ruby
742
+ response = client.board.query(args: {ids: [123]})
743
+
744
+ # Current approach
745
+ boards = response.body.dig("data", "boards")
746
+
747
+ # Enhanced approach
748
+ boards = response.data.boards # Method chaining
749
+ boards = response.boards # Even simpler
750
+ ```
751
+
752
+ **Considerations:**
753
+ - Requires understanding monday.com's response structure
754
+ - Different queries return different structures
755
+ - Could hide response complexity (good or bad?)
756
+ - Adds magic (method_missing or dynamic method definition)
757
+
758
+ This would make common cases simpler but could confuse debugging.
759
+
760
+ ### 5. Validation
761
+
762
+ The gem could validate arguments before making requests:
763
+
764
+ ```ruby
765
+ client.board.query(args: {ids: "not an array"})
766
+ # Currently: monday.com API returns error
767
+ # Could: Gem raises ArgumentError immediately
768
+ ```
769
+
770
+ **Considerations:**
771
+ - Requires duplicating monday.com's validation logic
772
+ - monday.com's API evolves (validation rules change)
773
+ - Validation errors vs. API errors (different exception types?)
774
+ - Adds maintenance burden
775
+
776
+ Early validation helps users but couples the gem to monday.com's current API.
777
+
778
+ ## Conclusion
779
+
780
+ The monday_ruby gem's design emerged from specific goals and constraints:
781
+
782
+ - **Client-resource pattern**: Balances organization, discoverability, and flexibility
783
+ - **Dual configuration**: Serves both simple and complex use cases
784
+ - **Exception hierarchy**: Enables semantic error handling
785
+ - **High-level abstraction**: Prioritizes accessibility over GraphQL power
786
+ - **Response wrapper**: Provides consistency and monday.com-specific logic
787
+ - **Explicit dependencies**: Ensures compatibility across Ruby versions
788
+
789
+ These decisions involve trade-offs. The design optimizes for:
790
+ 1. Ease of use for Ruby developers new to monday.com
791
+ 2. Explicit behavior over hidden magic
792
+ 3. Flexibility for advanced users
793
+ 4. Maintainability as monday.com's API evolves
794
+
795
+ Future evolution will balance these goals against emerging use cases and community feedback. Good design isn't about perfect decisions - it's about thoughtful trade-offs that serve the majority of users while remaining open to change.