wip-ruby 0.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.
- checksums.yaml +7 -0
- data/.simplecov +36 -0
- data/CHANGELOG.md +41 -0
- data/LICENSE.txt +21 -0
- data/README.md +846 -0
- data/Rakefile +45 -0
- data/lib/wip/client.rb +84 -0
- data/lib/wip/configuration.rb +89 -0
- data/lib/wip/error.rb +76 -0
- data/lib/wip/http_client.rb +221 -0
- data/lib/wip/models/base.rb +160 -0
- data/lib/wip/models/collection.rb +81 -0
- data/lib/wip/models/comment.rb +28 -0
- data/lib/wip/models/concerns/reactable.rb +25 -0
- data/lib/wip/models/project.rb +48 -0
- data/lib/wip/models/reaction.rb +32 -0
- data/lib/wip/models/todo.rb +57 -0
- data/lib/wip/models/user.rb +51 -0
- data/lib/wip/resources/base.rb +80 -0
- data/lib/wip/resources/comments.rb +93 -0
- data/lib/wip/resources/projects.rb +42 -0
- data/lib/wip/resources/reactions.rb +74 -0
- data/lib/wip/resources/todos.rb +47 -0
- data/lib/wip/resources/uploads.rb +111 -0
- data/lib/wip/resources/users.rb +61 -0
- data/lib/wip/resources/viewer.rb +52 -0
- data/lib/wip/version.rb +5 -0
- data/lib/wip-ruby.rb +1 -0
- data/lib/wip.rb +51 -0
- data/sig/wip/ruby.rbs +6 -0
- data/test_examples.rb +435 -0
- metadata +119 -0
data/README.md
ADDED
|
@@ -0,0 +1,846 @@
|
|
|
1
|
+
# 🚧 `wip-ruby` – WIP.co API wrapper for Ruby
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/rb/wip-ruby) [](https://github.com/rameerez/wip-ruby/actions)
|
|
4
|
+
|
|
5
|
+
> [!TIP]
|
|
6
|
+
> **🚀 Ship your next Rails app 10x faster!** I've built **[RailsFast](https://railsfast.com/?ref=wip-ruby)**, a production-ready Rails boilerplate template that comes with everything you need to launch a software business in days, not weeks. Go [check it out](https://railsfast.com/?ref=wip-ruby)!
|
|
7
|
+
|
|
8
|
+
`wip-ruby` is a Ruby wrapper for the [WIP.co](https://wip.co) API. WIP is a community of makers and indie hackers who share what they're working on. This gem provides an simple interface for interacting with the WIP API from Ruby and Rails applications.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
Add to your Gemfile:
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
gem 'wip-ruby'
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Then install:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
bundle install
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Or install directly:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
gem install wip-ruby
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Configuration
|
|
31
|
+
|
|
32
|
+
### Rails Configuration
|
|
33
|
+
|
|
34
|
+
Create an initializer at `config/initializers/wip.rb`:
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
# config/initializers/wip.rb
|
|
38
|
+
|
|
39
|
+
Wip.configure do |config|
|
|
40
|
+
# Required: Your WIP API key
|
|
41
|
+
config.api_key = Rails.application.credentials.dig(Rails.env.to_sym, :wip, :api_key)
|
|
42
|
+
|
|
43
|
+
# Optional: API base URL (default: "https://api.wip.co")
|
|
44
|
+
config.base_url = "https://api.wip.co"
|
|
45
|
+
|
|
46
|
+
# Optional: Request timeout in seconds (default: 10, range: 1-60)
|
|
47
|
+
config.timeout = 10
|
|
48
|
+
|
|
49
|
+
# Optional: Number of retries for failed requests (default: 2, range: 0-5)
|
|
50
|
+
config.max_retries = 2
|
|
51
|
+
|
|
52
|
+
# Optional: Logger for debugging (default: nil)
|
|
53
|
+
config.logger = Rails.logger
|
|
54
|
+
end
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Plain Ruby Configuration
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
require 'wip-ruby'
|
|
61
|
+
|
|
62
|
+
Wip.configure do |config|
|
|
63
|
+
config.api_key = ENV['WIP_API_KEY']
|
|
64
|
+
end
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Configuration Options
|
|
68
|
+
|
|
69
|
+
| Option | Type | Default | Description |
|
|
70
|
+
|--------|------|---------|-------------|
|
|
71
|
+
| `api_key` | String | *required* | Your WIP API key |
|
|
72
|
+
| `base_url` | String | `"https://api.wip.co"` | API endpoint URL |
|
|
73
|
+
| `timeout` | Integer | `10` | Request timeout (1-60 seconds) |
|
|
74
|
+
| `max_retries` | Integer | `2` | Retry attempts for transient failures (0-5) |
|
|
75
|
+
| `logger` | Logger | `nil` | Logger instance for debugging |
|
|
76
|
+
|
|
77
|
+
## Quick Start
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
# Create a client
|
|
81
|
+
client = Wip::Client.new
|
|
82
|
+
|
|
83
|
+
# Get your profile
|
|
84
|
+
me = client.viewer.me
|
|
85
|
+
puts "Hello, #{me.full_name}!"
|
|
86
|
+
puts "Current streak: #{me.streak} days"
|
|
87
|
+
|
|
88
|
+
# Create a todo
|
|
89
|
+
todo = client.todos.create(body: "Shipped a new feature! #myproject")
|
|
90
|
+
puts "Created: #{todo.url}"
|
|
91
|
+
|
|
92
|
+
# List your recent todos
|
|
93
|
+
client.viewer.todos(limit: 5).each do |todo|
|
|
94
|
+
puts "- #{todo.body}"
|
|
95
|
+
end
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Resources
|
|
99
|
+
|
|
100
|
+
### Viewer (Authenticated User)
|
|
101
|
+
|
|
102
|
+
The Viewer resource provides access to the authenticated user's data.
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
# Get your profile
|
|
106
|
+
me = client.viewer.me
|
|
107
|
+
puts me.username # => "marc"
|
|
108
|
+
puts me.full_name # => "Marc Kohlbrugge"
|
|
109
|
+
puts me.streak # => 365
|
|
110
|
+
puts me.best_streak # => 500
|
|
111
|
+
puts me.todos_count # => 1234
|
|
112
|
+
puts me.on_streak? # => true
|
|
113
|
+
puts me.avatar_url # => "https://..." (medium size)
|
|
114
|
+
puts me.avatar_url(:large) # => "https://..." (large size)
|
|
115
|
+
|
|
116
|
+
# List your todos
|
|
117
|
+
todos = client.viewer.todos
|
|
118
|
+
todos = client.viewer.todos(limit: 10)
|
|
119
|
+
todos = client.viewer.todos(since: "2024-01-01")
|
|
120
|
+
todos = client.viewer.todos(since: Date.today - 7, before: Date.today)
|
|
121
|
+
|
|
122
|
+
# List your projects
|
|
123
|
+
projects = client.viewer.projects
|
|
124
|
+
projects = client.viewer.projects(limit: 5)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Todos
|
|
128
|
+
|
|
129
|
+
Create and retrieve todos.
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
# Create a todo
|
|
133
|
+
todo = client.todos.create(body: "Launched my new app! #myapp")
|
|
134
|
+
puts todo.id
|
|
135
|
+
puts todo.body
|
|
136
|
+
puts todo.url
|
|
137
|
+
puts todo.created_at
|
|
138
|
+
|
|
139
|
+
# Create a todo with attachments
|
|
140
|
+
signed_id = client.uploads.upload("screenshot.png")
|
|
141
|
+
todo = client.todos.create(
|
|
142
|
+
body: "Check out this screenshot!",
|
|
143
|
+
attachments: [signed_id]
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Get a specific todo
|
|
147
|
+
todo = client.todos.find("todo_123")
|
|
148
|
+
puts todo.body
|
|
149
|
+
puts todo.reactions_count
|
|
150
|
+
puts todo.comments_count
|
|
151
|
+
|
|
152
|
+
# Check todo properties
|
|
153
|
+
puts "Has attachments" if todo.attachments?
|
|
154
|
+
puts "In projects: #{todo.projects.map(&:name).join(', ')}" if todo.projects?
|
|
155
|
+
puts "Has comments" if todo.comments?
|
|
156
|
+
puts "Has reactions" if todo.reactions?
|
|
157
|
+
|
|
158
|
+
# Check viewer interaction
|
|
159
|
+
puts "You reacted" if todo.reacted_by_viewer?
|
|
160
|
+
puts "You commented" if todo.commented_by_viewer?
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
> **Note:** To list todos, use the appropriate resource:
|
|
164
|
+
> - `client.viewer.todos` - Your own todos
|
|
165
|
+
> - `client.users.todos("username")` - A user's todos
|
|
166
|
+
> - `client.projects.todos("project_id")` - A project's todos
|
|
167
|
+
|
|
168
|
+
### Users
|
|
169
|
+
|
|
170
|
+
Access user profiles and their public data.
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
# Get a user's profile
|
|
174
|
+
user = client.users.find("marc")
|
|
175
|
+
puts user.username
|
|
176
|
+
puts user.full_name
|
|
177
|
+
puts user.streak
|
|
178
|
+
puts user.best_streak
|
|
179
|
+
puts user.todos_count
|
|
180
|
+
puts user.time_zone
|
|
181
|
+
puts user.url
|
|
182
|
+
puts user.avatar_url(:small)
|
|
183
|
+
puts user.avatar_url(:medium)
|
|
184
|
+
puts user.avatar_url(:large)
|
|
185
|
+
|
|
186
|
+
# Check user properties
|
|
187
|
+
puts user.on_streak? # Currently streaking?
|
|
188
|
+
puts user.protected? # Protected account?
|
|
189
|
+
puts user.streaking? # Actively streaking?
|
|
190
|
+
|
|
191
|
+
# List a user's projects
|
|
192
|
+
projects = client.users.projects("marc")
|
|
193
|
+
projects = client.users.projects("marc", limit: 10)
|
|
194
|
+
|
|
195
|
+
# List a user's todos
|
|
196
|
+
todos = client.users.todos("marc")
|
|
197
|
+
todos = client.users.todos("marc", limit: 20, since: "2024-01-01")
|
|
198
|
+
todos = client.users.todos("marc",
|
|
199
|
+
since: Time.now - 86400 * 30, # Last 30 days
|
|
200
|
+
before: Time.now
|
|
201
|
+
)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Projects
|
|
205
|
+
|
|
206
|
+
Retrieve projects and their todos.
|
|
207
|
+
|
|
208
|
+
```ruby
|
|
209
|
+
# Get a project
|
|
210
|
+
project = client.projects.find("project_123")
|
|
211
|
+
puts project.name
|
|
212
|
+
puts project.slug
|
|
213
|
+
puts project.hashtag
|
|
214
|
+
puts project.pitch
|
|
215
|
+
puts project.description
|
|
216
|
+
puts project.website_url
|
|
217
|
+
puts project.url
|
|
218
|
+
puts project.created_at
|
|
219
|
+
puts project.updated_at
|
|
220
|
+
|
|
221
|
+
# Check project properties
|
|
222
|
+
puts project.protected? # Protected project?
|
|
223
|
+
puts project.archived? # Archived project?
|
|
224
|
+
puts project.logo? # Has a logo?
|
|
225
|
+
puts project.team_project? # Multiple makers?
|
|
226
|
+
|
|
227
|
+
# Get project logo
|
|
228
|
+
puts project.logo_url(:small)
|
|
229
|
+
puts project.logo_url(:medium)
|
|
230
|
+
puts project.logo_url(:large)
|
|
231
|
+
|
|
232
|
+
# Get project owner
|
|
233
|
+
owner = project.owner
|
|
234
|
+
puts "Owner: #{owner.full_name}"
|
|
235
|
+
|
|
236
|
+
# Get all project makers
|
|
237
|
+
project.makers.each do |maker|
|
|
238
|
+
puts "- #{maker.full_name} (@#{maker.username})"
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# List project todos
|
|
242
|
+
todos = client.projects.todos("project_123")
|
|
243
|
+
todos = client.projects.todos("project_123",
|
|
244
|
+
limit: 25,
|
|
245
|
+
since: "2024-01-01",
|
|
246
|
+
before: "2024-12-31"
|
|
247
|
+
)
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Comments
|
|
251
|
+
|
|
252
|
+
Add, update, and delete comments on todos.
|
|
253
|
+
|
|
254
|
+
```ruby
|
|
255
|
+
# List comments for a todo
|
|
256
|
+
comments = client.comments.for_todo("todo_123")
|
|
257
|
+
comments = client.comments.for_todo("todo_123", limit: 10)
|
|
258
|
+
|
|
259
|
+
comments.each do |comment|
|
|
260
|
+
puts "#{comment.creator.username}: #{comment.body}"
|
|
261
|
+
puts " Reactions: #{comment.reactions_count}"
|
|
262
|
+
puts " Created: #{comment.created_at}"
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Create a comment
|
|
266
|
+
comment = client.comments.create(
|
|
267
|
+
commentable_type: "Todo",
|
|
268
|
+
commentable_id: "todo_123",
|
|
269
|
+
body: "Great work on this!"
|
|
270
|
+
)
|
|
271
|
+
puts "Comment created: #{comment.id}"
|
|
272
|
+
|
|
273
|
+
# Update a comment (your own comments only)
|
|
274
|
+
updated = client.comments.update("comment_123", body: "Updated comment text")
|
|
275
|
+
puts "Updated at: #{updated.updated_at}"
|
|
276
|
+
|
|
277
|
+
# Delete a comment (your own comments only)
|
|
278
|
+
client.comments.delete("comment_123")
|
|
279
|
+
|
|
280
|
+
# Check if you've reacted to a comment
|
|
281
|
+
puts "You liked this" if comment.reacted_by_viewer?
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### Reactions
|
|
285
|
+
|
|
286
|
+
Add and remove reactions (likes) on todos and comments.
|
|
287
|
+
|
|
288
|
+
```ruby
|
|
289
|
+
# React to a todo (convenience method)
|
|
290
|
+
reaction = client.reactions.react_to_todo("todo_123")
|
|
291
|
+
puts "Reacted by: #{reaction.reactor.username}"
|
|
292
|
+
|
|
293
|
+
# React to a comment (convenience method)
|
|
294
|
+
reaction = client.reactions.react_to_comment("comment_123")
|
|
295
|
+
|
|
296
|
+
# React using the generic method
|
|
297
|
+
reaction = client.reactions.create(
|
|
298
|
+
reactable_type: "Todo", # "Todo" or "Comment"
|
|
299
|
+
reactable_id: "todo_123"
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
# Check reaction properties
|
|
303
|
+
puts reaction.on_todo? # => true
|
|
304
|
+
puts reaction.on_comment? # => false
|
|
305
|
+
puts reaction.reactable_type # => "Todo"
|
|
306
|
+
puts reaction.reactable_id # => "todo_123"
|
|
307
|
+
puts reaction.created_at
|
|
308
|
+
|
|
309
|
+
# Remove a reaction (your own reactions only)
|
|
310
|
+
client.reactions.delete("reaction_123")
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### File Uploads
|
|
314
|
+
|
|
315
|
+
Upload files to attach to todos.
|
|
316
|
+
|
|
317
|
+
```ruby
|
|
318
|
+
# Simple upload (recommended)
|
|
319
|
+
signed_id = client.uploads.upload("path/to/screenshot.png")
|
|
320
|
+
|
|
321
|
+
# Create todo with the uploaded file
|
|
322
|
+
todo = client.todos.create(
|
|
323
|
+
body: "Check out this screenshot!",
|
|
324
|
+
attachments: [signed_id]
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
# Upload multiple files
|
|
328
|
+
signed_ids = [
|
|
329
|
+
client.uploads.upload("screenshot1.png"),
|
|
330
|
+
client.uploads.upload("screenshot2.png")
|
|
331
|
+
]
|
|
332
|
+
todo = client.todos.create(
|
|
333
|
+
body: "Multiple screenshots attached!",
|
|
334
|
+
attachments: signed_ids
|
|
335
|
+
)
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
#### Advanced Upload (Manual Process)
|
|
339
|
+
|
|
340
|
+
For more control over the upload process:
|
|
341
|
+
|
|
342
|
+
```ruby
|
|
343
|
+
require 'digest'
|
|
344
|
+
|
|
345
|
+
file_path = "path/to/file.png"
|
|
346
|
+
|
|
347
|
+
# Step 1: Request pre-signed upload URL
|
|
348
|
+
credentials = client.uploads.request_upload_url(
|
|
349
|
+
filename: File.basename(file_path),
|
|
350
|
+
byte_size: File.size(file_path),
|
|
351
|
+
checksum: Digest::MD5.file(file_path).base64digest,
|
|
352
|
+
content_type: "image/png"
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
# The credentials hash contains:
|
|
356
|
+
# - url: Pre-signed upload URL
|
|
357
|
+
# - method: HTTP method to use (usually "PUT")
|
|
358
|
+
# - headers: Headers to include in upload request
|
|
359
|
+
# - signed_id: ID to reference the upload
|
|
360
|
+
# - key: Storage key
|
|
361
|
+
|
|
362
|
+
# Step 2: Upload the file
|
|
363
|
+
success = client.uploads.upload_file(
|
|
364
|
+
url: credentials["url"],
|
|
365
|
+
headers: credentials["headers"],
|
|
366
|
+
file_path: file_path,
|
|
367
|
+
method: credentials["method"] # Optional, defaults to PUT
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
# Step 3: Use the signed_id in your todo
|
|
371
|
+
if success
|
|
372
|
+
todo = client.todos.create(
|
|
373
|
+
body: "Uploaded!",
|
|
374
|
+
attachments: [credentials["signed_id"]]
|
|
375
|
+
)
|
|
376
|
+
end
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
## Models
|
|
380
|
+
|
|
381
|
+
### User Model
|
|
382
|
+
|
|
383
|
+
Represents a WIP user.
|
|
384
|
+
|
|
385
|
+
| Attribute | Type | Description |
|
|
386
|
+
|-----------|------|-------------|
|
|
387
|
+
| `id` | String | Unique identifier |
|
|
388
|
+
| `username` | String | Username handle |
|
|
389
|
+
| `first_name` | String | First name |
|
|
390
|
+
| `last_name` | String | Last name |
|
|
391
|
+
| `streak` | Integer | Current streak in days |
|
|
392
|
+
| `best_streak` | Integer | All-time best streak |
|
|
393
|
+
| `todos_count` | Integer | Total completed todos |
|
|
394
|
+
| `time_zone` | String | User's time zone |
|
|
395
|
+
| `url` | String | Profile URL on WIP |
|
|
396
|
+
| `avatar` | Hash | Avatar URLs (small, medium, large) |
|
|
397
|
+
| `protected` | Boolean | Whether account is protected |
|
|
398
|
+
| `streaking` | Boolean | Whether currently streaking |
|
|
399
|
+
| `created_at` | Time | Account creation date |
|
|
400
|
+
| `updated_at` | Time | Last update date |
|
|
401
|
+
|
|
402
|
+
**Helper Methods:**
|
|
403
|
+
|
|
404
|
+
```ruby
|
|
405
|
+
user.full_name # "First Last"
|
|
406
|
+
user.avatar_url(:size) # Avatar URL for :small, :medium, or :large
|
|
407
|
+
user.on_streak? # Currently on an active streak?
|
|
408
|
+
user.todos? # Has completed any todos?
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
### Todo Model
|
|
412
|
+
|
|
413
|
+
Represents a completed todo.
|
|
414
|
+
|
|
415
|
+
| Attribute | Type | Description |
|
|
416
|
+
|-----------|------|-------------|
|
|
417
|
+
| `id` | String | Unique identifier |
|
|
418
|
+
| `body` | String | Todo content |
|
|
419
|
+
| `url` | String | Todo URL on WIP |
|
|
420
|
+
| `creator_id` | String | Creator's user ID |
|
|
421
|
+
| `attachments` | Array | Attached files |
|
|
422
|
+
| `projects` | Array\<Project\> | Associated projects |
|
|
423
|
+
| `reactions_count` | Integer | Number of reactions |
|
|
424
|
+
| `comments_count` | Integer | Number of comments |
|
|
425
|
+
| `viewer` | Hash | Viewer's interactions |
|
|
426
|
+
| `created_at` | Time | Creation date |
|
|
427
|
+
| `updated_at` | Time | Last update date |
|
|
428
|
+
|
|
429
|
+
**Helper Methods:**
|
|
430
|
+
|
|
431
|
+
```ruby
|
|
432
|
+
todo.attachments? # Has attachments?
|
|
433
|
+
todo.projects? # Belongs to projects?
|
|
434
|
+
todo.comments? # Has comments?
|
|
435
|
+
todo.reactions? # Has reactions?
|
|
436
|
+
todo.viewer_reactions # Your reactions on this todo
|
|
437
|
+
todo.viewer_comments # Your comments on this todo
|
|
438
|
+
todo.reacted_by_viewer? # Have you reacted?
|
|
439
|
+
todo.commented_by_viewer? # Have you commented?
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
### Project Model
|
|
443
|
+
|
|
444
|
+
Represents a WIP project.
|
|
445
|
+
|
|
446
|
+
| Attribute | Type | Description |
|
|
447
|
+
|-----------|------|-------------|
|
|
448
|
+
| `id` | String | Unique identifier |
|
|
449
|
+
| `slug` | String | URL slug |
|
|
450
|
+
| `name` | String | Project name |
|
|
451
|
+
| `hashtag` | String | Project hashtag |
|
|
452
|
+
| `pitch` | String | Short description |
|
|
453
|
+
| `description` | String | Full description |
|
|
454
|
+
| `website_url` | String | Project website |
|
|
455
|
+
| `url` | String | Project URL on WIP |
|
|
456
|
+
| `logo` | Hash | Logo URLs (small, medium, large) |
|
|
457
|
+
| `owner` | User | Project owner |
|
|
458
|
+
| `makers` | Array\<User\> | All project makers |
|
|
459
|
+
| `protected` | Boolean | Whether project is protected |
|
|
460
|
+
| `archived` | Boolean | Whether project is archived |
|
|
461
|
+
| `created_at` | Time | Creation date |
|
|
462
|
+
| `updated_at` | Time | Last update date |
|
|
463
|
+
|
|
464
|
+
**Helper Methods:**
|
|
465
|
+
|
|
466
|
+
```ruby
|
|
467
|
+
project.logo? # Has a logo?
|
|
468
|
+
project.logo_url(:size) # Logo URL for :small, :medium, or :large
|
|
469
|
+
project.team_project? # Has multiple makers?
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
### Comment Model
|
|
473
|
+
|
|
474
|
+
Represents a comment on a todo.
|
|
475
|
+
|
|
476
|
+
| Attribute | Type | Description |
|
|
477
|
+
|-----------|------|-------------|
|
|
478
|
+
| `id` | String | Unique identifier |
|
|
479
|
+
| `body` | String | Comment content |
|
|
480
|
+
| `url` | String | Comment URL |
|
|
481
|
+
| `creator` | User | Comment author |
|
|
482
|
+
| `reactions_count` | Integer | Number of reactions |
|
|
483
|
+
| `viewer` | Hash | Viewer's interactions |
|
|
484
|
+
| `created_at` | Time | Creation date |
|
|
485
|
+
| `updated_at` | Time | Last update date |
|
|
486
|
+
|
|
487
|
+
**Helper Methods:**
|
|
488
|
+
|
|
489
|
+
```ruby
|
|
490
|
+
comment.reactions? # Has reactions?
|
|
491
|
+
comment.viewer_reactions # Your reactions on this comment
|
|
492
|
+
comment.reacted_by_viewer? # Have you reacted?
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
### Reaction Model
|
|
496
|
+
|
|
497
|
+
Represents a reaction (like) on a todo or comment.
|
|
498
|
+
|
|
499
|
+
| Attribute | Type | Description |
|
|
500
|
+
|-----------|------|-------------|
|
|
501
|
+
| `id` | String | Unique identifier |
|
|
502
|
+
| `reactable_type` | String | Type of resource ("Todo" or "Comment") |
|
|
503
|
+
| `reactable_id` | String | ID of the resource |
|
|
504
|
+
| `reactor` | User | User who reacted |
|
|
505
|
+
| `created_at` | Time | Reaction date |
|
|
506
|
+
|
|
507
|
+
**Helper Methods:**
|
|
508
|
+
|
|
509
|
+
```ruby
|
|
510
|
+
reaction.on_todo? # Is this on a todo?
|
|
511
|
+
reaction.on_comment? # Is this on a comment?
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
### Collection Model
|
|
515
|
+
|
|
516
|
+
Represents a paginated collection of items.
|
|
517
|
+
|
|
518
|
+
| Attribute | Type | Description |
|
|
519
|
+
|-----------|------|-------------|
|
|
520
|
+
| `data` | Array | Items in the collection |
|
|
521
|
+
| `has_more` | Boolean | Whether more items exist |
|
|
522
|
+
| `total_count` | Integer | Total number of items |
|
|
523
|
+
|
|
524
|
+
**Methods:**
|
|
525
|
+
|
|
526
|
+
```ruby
|
|
527
|
+
collection.each { |item| ... } # Iterate items (Enumerable)
|
|
528
|
+
collection.size # Number of items in this page
|
|
529
|
+
collection.length # Alias for size
|
|
530
|
+
collection.count # Alias for size
|
|
531
|
+
collection.empty? # Is collection empty?
|
|
532
|
+
collection.first # First item
|
|
533
|
+
collection.last # Last item
|
|
534
|
+
collection.last_id # ID of last item (for pagination)
|
|
535
|
+
collection.has_more # Are there more pages?
|
|
536
|
+
collection.total_count # Total items across all pages
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
## Pagination
|
|
540
|
+
|
|
541
|
+
All list endpoints support cursor-based pagination.
|
|
542
|
+
|
|
543
|
+
```ruby
|
|
544
|
+
# Get first page (default: 25 items)
|
|
545
|
+
todos = client.users.todos("marc")
|
|
546
|
+
puts "Page 1: #{todos.size} items"
|
|
547
|
+
puts "Total: #{todos.total_count}"
|
|
548
|
+
puts "Has more: #{todos.has_more}"
|
|
549
|
+
|
|
550
|
+
# Get next page
|
|
551
|
+
if todos.has_more
|
|
552
|
+
next_page = client.users.todos("marc",
|
|
553
|
+
starting_after: todos.last_id
|
|
554
|
+
)
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
# Custom page size
|
|
558
|
+
todos = client.users.todos("marc", limit: 50)
|
|
559
|
+
|
|
560
|
+
# Iterate through all pages
|
|
561
|
+
def fetch_all_todos(client, username)
|
|
562
|
+
all_todos = []
|
|
563
|
+
cursor = nil
|
|
564
|
+
|
|
565
|
+
loop do
|
|
566
|
+
page = client.users.todos(username,
|
|
567
|
+
limit: 100,
|
|
568
|
+
starting_after: cursor
|
|
569
|
+
)
|
|
570
|
+
all_todos.concat(page.data)
|
|
571
|
+
break unless page.has_more
|
|
572
|
+
cursor = page.last_id
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
all_todos
|
|
576
|
+
end
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
## Date Filtering
|
|
580
|
+
|
|
581
|
+
Todo endpoints support date filtering with flexible input formats.
|
|
582
|
+
|
|
583
|
+
```ruby
|
|
584
|
+
# Using Time objects
|
|
585
|
+
todos = client.viewer.todos(
|
|
586
|
+
since: Time.now - 86400 * 7, # 7 days ago
|
|
587
|
+
before: Time.now
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
# Using Date objects
|
|
591
|
+
todos = client.viewer.todos(
|
|
592
|
+
since: Date.today - 30,
|
|
593
|
+
before: Date.today
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
# Using ISO 8601 strings
|
|
597
|
+
todos = client.viewer.todos(
|
|
598
|
+
since: "2024-01-01",
|
|
599
|
+
before: "2024-12-31"
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
# Using partial dates
|
|
603
|
+
todos = client.viewer.todos(since: "2024") # Year only
|
|
604
|
+
todos = client.viewer.todos(since: "2024-06") # Year and month
|
|
605
|
+
|
|
606
|
+
# Using Unix timestamps
|
|
607
|
+
todos = client.viewer.todos(
|
|
608
|
+
since: 1704067200, # 2024-01-01 00:00:00 UTC
|
|
609
|
+
before: 1735689600 # 2025-01-01 00:00:00 UTC
|
|
610
|
+
)
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
**Supported Formats:**
|
|
614
|
+
|
|
615
|
+
| Type | Example |
|
|
616
|
+
|------|---------|
|
|
617
|
+
| `Time` | `Time.now`, `Time.parse("2024-01-01")` |
|
|
618
|
+
| `Date` | `Date.today`, `Date.new(2024, 1, 1)` |
|
|
619
|
+
| String (ISO 8601) | `"2024-01-01T12:00:00Z"` |
|
|
620
|
+
| String (YYYY-MM-DD) | `"2024-01-01"` |
|
|
621
|
+
| String (YYYY-MM) | `"2024-01"` |
|
|
622
|
+
| String (YYYY) | `"2024"` |
|
|
623
|
+
| Integer (Unix timestamp) | `1704067200` |
|
|
624
|
+
|
|
625
|
+
> **Note:** The gem uses `before` instead of `until` (which is a Ruby reserved keyword). It's automatically mapped to the API's `until` parameter.
|
|
626
|
+
|
|
627
|
+
## Error Handling
|
|
628
|
+
|
|
629
|
+
The gem provides specific error classes for different failure scenarios.
|
|
630
|
+
|
|
631
|
+
```ruby
|
|
632
|
+
begin
|
|
633
|
+
client.todos.create(body: "New todo!")
|
|
634
|
+
rescue Wip::Error::ConfigurationError => e
|
|
635
|
+
# Invalid configuration (missing API key, etc.)
|
|
636
|
+
puts "Config error: #{e.message}"
|
|
637
|
+
|
|
638
|
+
rescue Wip::Error::UnauthorizedError => e
|
|
639
|
+
# Invalid API key (HTTP 401)
|
|
640
|
+
puts "Auth failed: #{e.message}"
|
|
641
|
+
|
|
642
|
+
rescue Wip::Error::ForbiddenError => e
|
|
643
|
+
# Access denied (HTTP 403)
|
|
644
|
+
puts "Forbidden: #{e.message}"
|
|
645
|
+
|
|
646
|
+
rescue Wip::Error::NotFoundError => e
|
|
647
|
+
# Resource not found (HTTP 404)
|
|
648
|
+
puts "Not found: #{e.message}"
|
|
649
|
+
|
|
650
|
+
rescue Wip::Error::ValidationError => e
|
|
651
|
+
# Invalid request data (HTTP 400, 422)
|
|
652
|
+
puts "Validation error: #{e.message}"
|
|
653
|
+
puts "Details: #{e.response_data}"
|
|
654
|
+
|
|
655
|
+
rescue Wip::Error::RateLimitError => e
|
|
656
|
+
# Too many requests (HTTP 429)
|
|
657
|
+
puts "Rate limited: #{e.message}"
|
|
658
|
+
puts "Status code: #{e.status_code}"
|
|
659
|
+
|
|
660
|
+
rescue Wip::Error::ServerError => e
|
|
661
|
+
# WIP server error (HTTP 5xx)
|
|
662
|
+
puts "Server error: #{e.message}"
|
|
663
|
+
|
|
664
|
+
rescue Wip::Error::TimeoutError => e
|
|
665
|
+
# Request timed out
|
|
666
|
+
puts "Timeout: #{e.message}"
|
|
667
|
+
|
|
668
|
+
rescue Wip::Error::ConnectionError => e
|
|
669
|
+
# Network connection failed
|
|
670
|
+
puts "Connection error: #{e.message}"
|
|
671
|
+
|
|
672
|
+
rescue Wip::Error::UploadError => e
|
|
673
|
+
# File upload failed
|
|
674
|
+
puts "Upload error: #{e.message}"
|
|
675
|
+
|
|
676
|
+
rescue Wip::Error::NetworkError => e
|
|
677
|
+
# Other network errors
|
|
678
|
+
puts "Network error: #{e.message}"
|
|
679
|
+
|
|
680
|
+
rescue Wip::Error => e
|
|
681
|
+
# Catch-all for any WIP error
|
|
682
|
+
puts "Error: #{e.message}"
|
|
683
|
+
puts "Status: #{e.status_code}" if e.status_code
|
|
684
|
+
end
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
### Error Class Hierarchy
|
|
688
|
+
|
|
689
|
+
```
|
|
690
|
+
Wip::Error
|
|
691
|
+
├── ConfigurationError # Invalid configuration
|
|
692
|
+
├── NetworkError # Network-related errors
|
|
693
|
+
│ ├── TimeoutError # Request timeout
|
|
694
|
+
│ └── ConnectionError # Connection failed
|
|
695
|
+
├── APIError # API response errors
|
|
696
|
+
│ ├── UnauthorizedError # 401 Unauthorized
|
|
697
|
+
│ ├── ForbiddenError # 403 Forbidden
|
|
698
|
+
│ ├── NotFoundError # 404 Not Found
|
|
699
|
+
│ ├── ValidationError # 400, 422 Validation errors
|
|
700
|
+
│ ├── RateLimitError # 429 Too Many Requests
|
|
701
|
+
│ └── ServerError # 500, 502, 503, 504
|
|
702
|
+
└── UploadError # File upload failures
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
### Error Properties
|
|
706
|
+
|
|
707
|
+
All errors include:
|
|
708
|
+
|
|
709
|
+
```ruby
|
|
710
|
+
error.message # Human-readable error message
|
|
711
|
+
error.status_code # HTTP status code (if applicable)
|
|
712
|
+
error.response_data # Raw response body (if applicable)
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
## Retry Behavior
|
|
716
|
+
|
|
717
|
+
The gem automatically retries failed requests for transient errors.
|
|
718
|
+
|
|
719
|
+
### Default Behavior
|
|
720
|
+
|
|
721
|
+
- **Max retries:** 2 (configurable from 0-5)
|
|
722
|
+
- **Backoff:** Exponential with jitter (50ms base, 2x multiplier)
|
|
723
|
+
- **Retried methods:** GET, PUT, PATCH, DELETE, HEAD, OPTIONS (idempotent methods only)
|
|
724
|
+
- **Retried errors:** Timeouts, connection errors, and specific HTTP status codes
|
|
725
|
+
|
|
726
|
+
### Retried Status Codes
|
|
727
|
+
|
|
728
|
+
| Code | Description |
|
|
729
|
+
|------|-------------|
|
|
730
|
+
| 408 | Request Timeout |
|
|
731
|
+
| 429 | Too Many Requests (rate limiting) |
|
|
732
|
+
| 500 | Internal Server Error |
|
|
733
|
+
| 502 | Bad Gateway |
|
|
734
|
+
| 503 | Service Unavailable |
|
|
735
|
+
| 504 | Gateway Timeout |
|
|
736
|
+
|
|
737
|
+
### Configuration
|
|
738
|
+
|
|
739
|
+
```ruby
|
|
740
|
+
Wip.configure do |config|
|
|
741
|
+
config.max_retries = 3 # 0 to disable retries, max 5
|
|
742
|
+
end
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
### Logging Retries
|
|
746
|
+
|
|
747
|
+
When logging is enabled, retry attempts are logged:
|
|
748
|
+
|
|
749
|
+
```
|
|
750
|
+
[Wip] Retry 1 for GET /v1/users/marc: Faraday::TimeoutError (waiting 0.05s)
|
|
751
|
+
[Wip] Retry 2 for GET /v1/users/marc: Faraday::TimeoutError (waiting 0.12s)
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
## Logging
|
|
755
|
+
|
|
756
|
+
Enable debug logging to troubleshoot API requests.
|
|
757
|
+
|
|
758
|
+
```ruby
|
|
759
|
+
Wip.configure do |config|
|
|
760
|
+
config.logger = Logger.new($stdout)
|
|
761
|
+
# Or in Rails:
|
|
762
|
+
config.logger = Rails.logger
|
|
763
|
+
end
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
The logger must respond to `debug`, `info`, `warn`, and `error` methods.
|
|
767
|
+
|
|
768
|
+
### Log Output
|
|
769
|
+
|
|
770
|
+
When enabled, logs include:
|
|
771
|
+
- Request method and URL
|
|
772
|
+
- Request headers and body
|
|
773
|
+
- Response status and body
|
|
774
|
+
- Retry attempts with timing
|
|
775
|
+
|
|
776
|
+
## API Reference
|
|
777
|
+
|
|
778
|
+
### Complete Endpoint Mapping
|
|
779
|
+
|
|
780
|
+
| Method | Endpoint | Gem Method |
|
|
781
|
+
|--------|----------|------------|
|
|
782
|
+
| GET | `/v1/users/me` | `client.viewer.me` |
|
|
783
|
+
| GET | `/v1/users/me/todos` | `client.viewer.todos(...)` |
|
|
784
|
+
| GET | `/v1/users/me/projects` | `client.viewer.projects(...)` |
|
|
785
|
+
| GET | `/v1/users/{username}` | `client.users.find(username)` |
|
|
786
|
+
| GET | `/v1/users/{username}/todos` | `client.users.todos(username, ...)` |
|
|
787
|
+
| GET | `/v1/users/{username}/projects` | `client.users.projects(username, ...)` |
|
|
788
|
+
| GET | `/v1/todos/{id}` | `client.todos.find(id)` |
|
|
789
|
+
| POST | `/v1/todos` | `client.todos.create(...)` |
|
|
790
|
+
| GET | `/v1/todos/{id}/comments` | `client.comments.for_todo(id, ...)` |
|
|
791
|
+
| GET | `/v1/projects/{id}` | `client.projects.find(id)` |
|
|
792
|
+
| GET | `/v1/projects/{id}/todos` | `client.projects.todos(id, ...)` |
|
|
793
|
+
| POST | `/v1/comments` | `client.comments.create(...)` |
|
|
794
|
+
| PATCH | `/v1/comments/{id}` | `client.comments.update(id, ...)` |
|
|
795
|
+
| DELETE | `/v1/comments/{id}` | `client.comments.delete(id)` |
|
|
796
|
+
| POST | `/v1/reactions` | `client.reactions.create(...)` |
|
|
797
|
+
| DELETE | `/v1/reactions/{id}` | `client.reactions.delete(id)` |
|
|
798
|
+
| POST | `/v1/uploads` | `client.uploads.request_upload_url(...)` |
|
|
799
|
+
|
|
800
|
+
### Convenience Methods
|
|
801
|
+
|
|
802
|
+
| Method | Description |
|
|
803
|
+
|--------|-------------|
|
|
804
|
+
| `client.uploads.upload(file_path)` | One-step file upload |
|
|
805
|
+
| `client.reactions.react_to_todo(id)` | React to a todo |
|
|
806
|
+
| `client.reactions.react_to_comment(id)` | React to a comment |
|
|
807
|
+
|
|
808
|
+
## Requirements
|
|
809
|
+
|
|
810
|
+
- **Ruby:** >= 3.1.0
|
|
811
|
+
- **Dependencies:**
|
|
812
|
+
- `faraday` (~> 2.12) - HTTP client
|
|
813
|
+
- `faraday-retry` (~> 2.2) - Retry middleware
|
|
814
|
+
- `mime-types` (~> 3.5) - File type detection
|
|
815
|
+
|
|
816
|
+
## Development
|
|
817
|
+
|
|
818
|
+
After checking out the repo:
|
|
819
|
+
|
|
820
|
+
```bash
|
|
821
|
+
# Install dependencies
|
|
822
|
+
bin/setup
|
|
823
|
+
|
|
824
|
+
# Run tests
|
|
825
|
+
rake spec
|
|
826
|
+
|
|
827
|
+
# Interactive console
|
|
828
|
+
bin/console
|
|
829
|
+
|
|
830
|
+
# Install locally
|
|
831
|
+
bundle exec rake install
|
|
832
|
+
```
|
|
833
|
+
|
|
834
|
+
## Contributing
|
|
835
|
+
|
|
836
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/rameerez/wip-ruby.
|
|
837
|
+
|
|
838
|
+
**Code of Conduct:** Just be nice and make your mom proud of what you do and post online.
|
|
839
|
+
|
|
840
|
+
## License
|
|
841
|
+
|
|
842
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
843
|
+
|
|
844
|
+
---
|
|
845
|
+
|
|
846
|
+
**Official API Documentation:** [wip.apidocumentation.com](https://wip.apidocumentation.com/api-specs) | [OpenAPI Spec](https://api.wip.co/v1/openapi.yml)
|