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.
@@ -0,0 +1,212 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FizzyApiClient
4
+ module Resources
5
+ module Cards
6
+ def cards(page: nil, auto_paginate: false, account_slug: nil, **filters)
7
+ params = build_card_filters(filters)
8
+ paginate("/cards", params: params, account_slug: account_slug, auto_paginate: auto_paginate, page: page)
9
+ end
10
+
11
+ def card(card_number, account_slug: nil)
12
+ slug = effective_account_slug(account_slug)
13
+ response = connection.get("/#{slug}/cards/#{card_number}")
14
+ response.body
15
+ end
16
+
17
+ def create_card(board_id:, title:, description: nil, status: nil, tag_ids: nil,
18
+ created_at: nil, last_active_at: nil, image: nil, account_slug: nil)
19
+ slug = effective_account_slug(account_slug)
20
+
21
+ payload = { title: title }
22
+ payload[:description] = description unless description.nil?
23
+ payload[:status] = status unless status.nil?
24
+ payload[:tag_ids] = tag_ids unless tag_ids.nil?
25
+ payload[:created_at] = created_at unless created_at.nil?
26
+ payload[:last_active_at] = last_active_at unless last_active_at.nil?
27
+
28
+ if image
29
+ response = create_card_with_image(slug, board_id, payload, image)
30
+ else
31
+ response = connection.post("/#{slug}/boards/#{board_id}/cards", body: { card: payload })
32
+ end
33
+
34
+ response = connection.follow_location(response)
35
+ response.body
36
+ end
37
+
38
+ def update_card(card_number, title: nil, description: nil, status: nil, tag_ids: nil,
39
+ last_active_at: nil, image: nil, account_slug: nil)
40
+ slug = effective_account_slug(account_slug)
41
+
42
+ payload = {}
43
+ payload[:title] = title unless title.nil?
44
+ payload[:description] = description unless description.nil?
45
+ payload[:status] = status unless status.nil?
46
+ payload[:tag_ids] = tag_ids unless tag_ids.nil?
47
+ payload[:last_active_at] = last_active_at unless last_active_at.nil?
48
+
49
+ if image
50
+ response = update_card_with_image(slug, card_number, payload, image)
51
+ else
52
+ response = connection.put("/#{slug}/cards/#{card_number}", body: { card: payload })
53
+ end
54
+
55
+ response.no_content? ? nil : response.body
56
+ end
57
+
58
+ def delete_card(card_number, account_slug: nil)
59
+ slug = effective_account_slug(account_slug)
60
+ response = connection.delete("/#{slug}/cards/#{card_number}")
61
+ response.no_content? ? nil : response.body
62
+ end
63
+
64
+ def close_card(card_number, account_slug: nil)
65
+ slug = effective_account_slug(account_slug)
66
+ response = connection.post("/#{slug}/cards/#{card_number}/closure")
67
+ response.no_content? ? nil : response.body
68
+ end
69
+
70
+ def reopen_card(card_number, account_slug: nil)
71
+ slug = effective_account_slug(account_slug)
72
+ response = connection.delete("/#{slug}/cards/#{card_number}/closure")
73
+ response.no_content? ? nil : response.body
74
+ end
75
+
76
+ def postpone_card(card_number, account_slug: nil)
77
+ slug = effective_account_slug(account_slug)
78
+ response = connection.post("/#{slug}/cards/#{card_number}/not_now")
79
+ response.no_content? ? nil : response.body
80
+ end
81
+
82
+ alias not_now_card postpone_card
83
+
84
+ def triage_card(card_number, column_id:, account_slug: nil)
85
+ slug = effective_account_slug(account_slug)
86
+ response = connection.post("/#{slug}/cards/#{card_number}/triage",
87
+ body: { column_id: column_id })
88
+ response.no_content? ? nil : response.body
89
+ end
90
+
91
+ def untriage_card(card_number, account_slug: nil)
92
+ slug = effective_account_slug(account_slug)
93
+ response = connection.delete("/#{slug}/cards/#{card_number}/triage")
94
+ response.no_content? ? nil : response.body
95
+ end
96
+
97
+ def toggle_assignment(card_number, assignee_id:, account_slug: nil)
98
+ slug = effective_account_slug(account_slug)
99
+ response = connection.post("/#{slug}/cards/#{card_number}/assignments",
100
+ body: { assignee_id: assignee_id })
101
+ response.no_content? ? nil : response.body
102
+ end
103
+
104
+ def toggle_tag(card_number, tag_title:, account_slug: nil)
105
+ slug = effective_account_slug(account_slug)
106
+ response = connection.post("/#{slug}/cards/#{card_number}/taggings",
107
+ body: { tag_title: tag_title })
108
+ response.no_content? ? nil : response.body
109
+ end
110
+
111
+ def watch_card(card_number, account_slug: nil)
112
+ slug = effective_account_slug(account_slug)
113
+ response = connection.post("/#{slug}/cards/#{card_number}/watch")
114
+ response.no_content? ? nil : response.body
115
+ end
116
+
117
+ def unwatch_card(card_number, account_slug: nil)
118
+ slug = effective_account_slug(account_slug)
119
+ response = connection.delete("/#{slug}/cards/#{card_number}/watch")
120
+ response.no_content? ? nil : response.body
121
+ end
122
+
123
+ # Makes a card a "golden ticket" (highlighted/pinned card)
124
+ def gild_card(card_number, account_slug: nil)
125
+ slug = effective_account_slug(account_slug)
126
+ response = connection.post("/#{slug}/cards/#{card_number}/goldness")
127
+ response.no_content? ? nil : response.body
128
+ end
129
+
130
+ # Removes "golden ticket" status from a card
131
+ def ungild_card(card_number, account_slug: nil)
132
+ slug = effective_account_slug(account_slug)
133
+ response = connection.delete("/#{slug}/cards/#{card_number}/goldness")
134
+ response.no_content? ? nil : response.body
135
+ end
136
+
137
+ private
138
+
139
+ def build_card_filters(filters)
140
+ params = {}
141
+
142
+ # Array filters - encode with []
143
+ %i[board_ids tag_ids assignee_ids creator_ids closer_ids card_ids terms].each do |key|
144
+ params[key] = filters[key] if filters[key]
145
+ end
146
+
147
+ # Scalar filters
148
+ %i[indexed_by sorted_by assignment_status creation closure].each do |key|
149
+ params[key] = filters[key] if filters[key]
150
+ end
151
+
152
+ params
153
+ end
154
+
155
+ def create_card_with_image(slug, board_id, payload, image)
156
+ fields = build_multipart_fields("card", payload)
157
+ files = { "card[image]" => build_file_info(image) }
158
+
159
+ connection.post_multipart("/#{slug}/boards/#{board_id}/cards", fields: fields, files: files)
160
+ end
161
+
162
+ def update_card_with_image(slug, card_number, payload, image)
163
+ fields = build_multipart_fields("card", payload)
164
+ files = { "card[image]" => build_file_info(image) }
165
+
166
+ connection.put_multipart("/#{slug}/cards/#{card_number}", fields: fields, files: files)
167
+ end
168
+
169
+ def build_multipart_fields(prefix, payload)
170
+ fields = {}
171
+ payload.each do |key, value|
172
+ if value.is_a?(Array)
173
+ value.each_with_index do |v, i|
174
+ fields["#{prefix}[#{key}][]"] = v.to_s
175
+ end
176
+ else
177
+ fields["#{prefix}[#{key}]"] = value.to_s
178
+ end
179
+ end
180
+ fields
181
+ end
182
+
183
+ def build_file_info(file)
184
+ if file.is_a?(String)
185
+ # File path
186
+ {
187
+ filename: File.basename(file),
188
+ content: File.read(file),
189
+ content_type: guess_content_type(file)
190
+ }
191
+ else
192
+ # IO object
193
+ {
194
+ filename: file.respond_to?(:path) ? File.basename(file.path) : "upload",
195
+ content: file.read,
196
+ content_type: "application/octet-stream"
197
+ }
198
+ end
199
+ end
200
+
201
+ def guess_content_type(path)
202
+ case File.extname(path).downcase
203
+ when ".png" then "image/png"
204
+ when ".jpg", ".jpeg" then "image/jpeg"
205
+ when ".gif" then "image/gif"
206
+ when ".webp" then "image/webp"
207
+ else "application/octet-stream"
208
+ end
209
+ end
210
+ end
211
+ end
212
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FizzyApiClient
4
+ module Resources
5
+ module Columns
6
+ def columns(board_id, account_slug: nil)
7
+ slug = effective_account_slug(account_slug)
8
+ response = connection.get("/#{slug}/boards/#{board_id}/columns")
9
+ response.body
10
+ end
11
+
12
+ def column(board_id, column_id, account_slug: nil)
13
+ slug = effective_account_slug(account_slug)
14
+ response = connection.get("/#{slug}/boards/#{board_id}/columns/#{column_id}")
15
+ response.body
16
+ end
17
+
18
+ # Creates a column on a board.
19
+ #
20
+ # @param board_id [String] the board ID
21
+ # @param name [String] the column name
22
+ # @param color [Symbol, String, nil] the column color - accepts named colors
23
+ # (:blue, :gray, :tan, :yellow, :lime, :aqua, :violet, :purple, :pink)
24
+ # or CSS variable strings
25
+ # @param account_slug [String, nil] optional account slug override
26
+ # @return [Hash] the created column
27
+ #
28
+ # @example Create with named color
29
+ # client.create_column(board_id: 'board_1', name: 'Review', color: :lime)
30
+ #
31
+ # @example Create with CSS variable (still supported)
32
+ # client.create_column(board_id: 'board_1', name: 'Review', color: 'var(--color-card-4)')
33
+ #
34
+ def create_column(board_id:, name:, color: nil, account_slug: nil)
35
+ slug = effective_account_slug(account_slug)
36
+
37
+ payload = { name: name }
38
+ payload[:color] = Colors.resolve(color) unless color.nil?
39
+
40
+ response = connection.post("/#{slug}/boards/#{board_id}/columns", body: { column: payload })
41
+ response = connection.follow_location(response)
42
+ response.body
43
+ end
44
+
45
+ # Updates a column.
46
+ #
47
+ # @param board_id [String] the board ID
48
+ # @param column_id [String] the column ID
49
+ # @param name [String, nil] the new column name
50
+ # @param color [Symbol, String, nil] the new column color - accepts named colors
51
+ # (:blue, :gray, :tan, :yellow, :lime, :aqua, :violet, :purple, :pink)
52
+ # or CSS variable strings
53
+ # @param account_slug [String, nil] optional account slug override
54
+ # @return [Hash, nil] the updated column or nil if 204 response
55
+ #
56
+ # @example Update with named color
57
+ # client.update_column('board_id', 'col_id', color: :purple)
58
+ #
59
+ def update_column(board_id, column_id, name: nil, color: nil, account_slug: nil)
60
+ slug = effective_account_slug(account_slug)
61
+
62
+ payload = {}
63
+ payload[:name] = name unless name.nil?
64
+ payload[:color] = Colors.resolve(color) unless color.nil?
65
+
66
+ response = connection.put("/#{slug}/boards/#{board_id}/columns/#{column_id}",
67
+ body: { column: payload })
68
+ response.no_content? ? nil : response.body
69
+ end
70
+
71
+ def delete_column(board_id, column_id, account_slug: nil)
72
+ slug = effective_account_slug(account_slug)
73
+ response = connection.delete("/#{slug}/boards/#{board_id}/columns/#{column_id}")
74
+ response.no_content? ? nil : response.body
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FizzyApiClient
4
+ module Resources
5
+ module Comments
6
+ def comments(card_number, account_slug: nil)
7
+ slug = effective_account_slug(account_slug)
8
+ response = connection.get("/#{slug}/cards/#{card_number}/comments")
9
+ response.body
10
+ end
11
+
12
+ def comment(card_number, comment_id, account_slug: nil)
13
+ slug = effective_account_slug(account_slug)
14
+ response = connection.get("/#{slug}/cards/#{card_number}/comments/#{comment_id}")
15
+ response.body
16
+ end
17
+
18
+ def create_comment(card_number, body:, created_at: nil, account_slug: nil)
19
+ slug = effective_account_slug(account_slug)
20
+
21
+ payload = { body: body }
22
+ payload[:created_at] = created_at unless created_at.nil?
23
+
24
+ response = connection.post("/#{slug}/cards/#{card_number}/comments",
25
+ body: { comment: payload })
26
+ response = connection.follow_location(response)
27
+ response.body
28
+ end
29
+
30
+ def update_comment(card_number, comment_id, body:, account_slug: nil)
31
+ slug = effective_account_slug(account_slug)
32
+
33
+ response = connection.put("/#{slug}/cards/#{card_number}/comments/#{comment_id}",
34
+ body: { comment: { body: body } })
35
+ response.no_content? ? nil : response.body
36
+ end
37
+
38
+ def delete_comment(card_number, comment_id, account_slug: nil)
39
+ slug = effective_account_slug(account_slug)
40
+ response = connection.delete("/#{slug}/cards/#{card_number}/comments/#{comment_id}")
41
+ response.no_content? ? nil : response.body
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FizzyApiClient
4
+ module Resources
5
+ module DirectUploads
6
+ def create_direct_upload(filename:, byte_size:, checksum:, content_type:, account_slug: nil)
7
+ slug = effective_account_slug(account_slug)
8
+
9
+ payload = {
10
+ filename: filename,
11
+ byte_size: byte_size,
12
+ checksum: checksum,
13
+ content_type: content_type
14
+ }
15
+
16
+ response = connection.post("/#{slug}/rails/active_storage/direct_uploads",
17
+ body: { blob: payload })
18
+ response.body
19
+ end
20
+
21
+ def upload_file(file_path_or_io, filename: nil, content_type: nil, account_slug: nil)
22
+ file_content, file_name, file_size = read_file_info(file_path_or_io, filename)
23
+ checksum = calculate_checksum(file_content)
24
+ content_type ||= "application/octet-stream"
25
+
26
+ # Step 1: Create direct upload
27
+ result = create_direct_upload(
28
+ filename: file_name,
29
+ byte_size: file_size,
30
+ checksum: checksum,
31
+ content_type: content_type,
32
+ account_slug: account_slug
33
+ )
34
+
35
+ # Step 2: Upload to storage URL
36
+ upload_url = result["direct_upload"]["url"]
37
+ upload_headers = result["direct_upload"]["headers"] || {}
38
+
39
+ put_to_external_url(upload_url, file_content, upload_headers)
40
+
41
+ # Step 3: Return signed_id
42
+ result["signed_id"]
43
+ end
44
+
45
+ private
46
+
47
+ def read_file_info(file_path_or_io, filename)
48
+ if file_path_or_io.is_a?(String)
49
+ content = File.binread(file_path_or_io)
50
+ name = filename || File.basename(file_path_or_io)
51
+ size = content.bytesize
52
+ else
53
+ content = file_path_or_io.read
54
+ name = filename || (file_path_or_io.respond_to?(:path) ? File.basename(file_path_or_io.path) : "upload")
55
+ size = content.bytesize
56
+ end
57
+
58
+ [content, name, size]
59
+ end
60
+
61
+ def calculate_checksum(content)
62
+ Base64.strict_encode64(Digest::MD5.digest(content))
63
+ end
64
+
65
+ def put_to_external_url(url, body, headers)
66
+ uri = URI.parse(url)
67
+
68
+ http = Net::HTTP.new(uri.host, uri.port)
69
+ http.use_ssl = uri.scheme == "https"
70
+
71
+ request = Net::HTTP::Put.new(uri.request_uri)
72
+ headers.each { |key, value| request[key] = value }
73
+ request.body = body
74
+
75
+ response = http.request(request)
76
+
77
+ unless response.is_a?(Net::HTTPSuccess)
78
+ raise ApiError.new(
79
+ message: "Failed to upload file to storage",
80
+ status: response.code.to_i
81
+ )
82
+ end
83
+
84
+ response
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FizzyApiClient
4
+ module Resources
5
+ module Identity
6
+ def identity
7
+ response = connection.get("/my/identity")
8
+ response.body
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FizzyApiClient
4
+ module Resources
5
+ module Notifications
6
+ def notifications(page: nil, auto_paginate: false, account_slug: nil)
7
+ paginate("/notifications", account_slug: account_slug, auto_paginate: auto_paginate, page: page)
8
+ end
9
+
10
+ def mark_notification_read(notification_id, account_slug: nil)
11
+ slug = effective_account_slug(account_slug)
12
+ response = connection.post("/#{slug}/notifications/#{notification_id}/reading")
13
+ response.no_content? ? nil : response.body
14
+ end
15
+
16
+ def mark_notification_unread(notification_id, account_slug: nil)
17
+ slug = effective_account_slug(account_slug)
18
+ response = connection.delete("/#{slug}/notifications/#{notification_id}/reading")
19
+ response.no_content? ? nil : response.body
20
+ end
21
+
22
+ def mark_all_notifications_read(account_slug: nil)
23
+ slug = effective_account_slug(account_slug)
24
+ response = connection.post("/#{slug}/notifications/bulk_reading")
25
+ response.no_content? ? nil : response.body
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FizzyApiClient
4
+ module Resources
5
+ module Reactions
6
+ def reactions(card_number, comment_id, account_slug: nil)
7
+ slug = effective_account_slug(account_slug)
8
+ response = connection.get("/#{slug}/cards/#{card_number}/comments/#{comment_id}/reactions")
9
+ response.body
10
+ end
11
+
12
+ def add_reaction(card_number, comment_id, content:, account_slug: nil)
13
+ slug = effective_account_slug(account_slug)
14
+ response = connection.post(
15
+ "/#{slug}/cards/#{card_number}/comments/#{comment_id}/reactions",
16
+ body: { reaction: { content: content } }
17
+ )
18
+ response = connection.follow_location(response)
19
+ response.body
20
+ end
21
+
22
+ def remove_reaction(card_number, comment_id, reaction_id, account_slug: nil)
23
+ slug = effective_account_slug(account_slug)
24
+ response = connection.delete(
25
+ "/#{slug}/cards/#{card_number}/comments/#{comment_id}/reactions/#{reaction_id}"
26
+ )
27
+ response.no_content? ? nil : response.body
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FizzyApiClient
4
+ module Resources
5
+ module Steps
6
+ def step(card_number, step_id, account_slug: nil)
7
+ slug = effective_account_slug(account_slug)
8
+ response = connection.get("/#{slug}/cards/#{card_number}/steps/#{step_id}")
9
+ response.body
10
+ end
11
+
12
+ def create_step(card_number, content:, completed: nil, account_slug: nil)
13
+ slug = effective_account_slug(account_slug)
14
+
15
+ payload = { content: content }
16
+ payload[:completed] = completed unless completed.nil?
17
+
18
+ response = connection.post("/#{slug}/cards/#{card_number}/steps",
19
+ body: { step: payload })
20
+ response = connection.follow_location(response)
21
+ response.body
22
+ end
23
+
24
+ def update_step(card_number, step_id, content: nil, completed: nil, account_slug: nil)
25
+ slug = effective_account_slug(account_slug)
26
+
27
+ payload = {}
28
+ payload[:content] = content unless content.nil?
29
+ payload[:completed] = completed unless completed.nil?
30
+
31
+ response = connection.put("/#{slug}/cards/#{card_number}/steps/#{step_id}",
32
+ body: { step: payload })
33
+ response.no_content? ? nil : response.body
34
+ end
35
+
36
+ def complete_step(card_number, step_id, account_slug: nil)
37
+ update_step(card_number, step_id, completed: true, account_slug: account_slug)
38
+ end
39
+
40
+ def incomplete_step(card_number, step_id, account_slug: nil)
41
+ update_step(card_number, step_id, completed: false, account_slug: account_slug)
42
+ end
43
+
44
+ def delete_step(card_number, step_id, account_slug: nil)
45
+ slug = effective_account_slug(account_slug)
46
+ response = connection.delete("/#{slug}/cards/#{card_number}/steps/#{step_id}")
47
+ response.no_content? ? nil : response.body
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FizzyApiClient
4
+ module Resources
5
+ module Tags
6
+ def tags(page: nil, auto_paginate: false, account_slug: nil)
7
+ paginate("/tags", account_slug: account_slug, auto_paginate: auto_paginate, page: page)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FizzyApiClient
4
+ module Resources
5
+ module Users
6
+ def users(page: nil, auto_paginate: false, account_slug: nil)
7
+ paginate("/users", account_slug: account_slug, auto_paginate: auto_paginate, page: page)
8
+ end
9
+
10
+ def user(user_id, account_slug: nil)
11
+ slug = effective_account_slug(account_slug)
12
+ response = connection.get("/#{slug}/users/#{user_id}")
13
+ response.body
14
+ end
15
+
16
+ def update_user(user_id, name: nil, avatar: nil, account_slug: nil)
17
+ slug = effective_account_slug(account_slug)
18
+
19
+ if avatar
20
+ response = update_user_with_avatar(slug, user_id, name, avatar)
21
+ else
22
+ payload = {}
23
+ payload[:name] = name unless name.nil?
24
+
25
+ response = connection.put("/#{slug}/users/#{user_id}", body: { user: payload })
26
+ end
27
+
28
+ response.no_content? ? nil : response.body
29
+ end
30
+
31
+ def deactivate_user(user_id, account_slug: nil)
32
+ slug = effective_account_slug(account_slug)
33
+ response = connection.delete("/#{slug}/users/#{user_id}")
34
+ response.no_content? ? nil : response.body
35
+ end
36
+
37
+ private
38
+
39
+ def update_user_with_avatar(slug, user_id, name, avatar)
40
+ fields = {}
41
+ fields["user[name]"] = name if name
42
+
43
+ files = { "user[avatar]" => build_user_file_info(avatar) }
44
+
45
+ connection.put_multipart("/#{slug}/users/#{user_id}", fields: fields, files: files)
46
+ end
47
+
48
+ def build_user_file_info(file)
49
+ if file.is_a?(String)
50
+ {
51
+ filename: File.basename(file),
52
+ content: File.read(file),
53
+ content_type: guess_user_content_type(file)
54
+ }
55
+ else
56
+ {
57
+ filename: file.respond_to?(:path) ? File.basename(file.path) : "avatar",
58
+ content: file.read,
59
+ content_type: "application/octet-stream"
60
+ }
61
+ end
62
+ end
63
+
64
+ def guess_user_content_type(path)
65
+ case File.extname(path).downcase
66
+ when ".png" then "image/png"
67
+ when ".jpg", ".jpeg" then "image/jpeg"
68
+ when ".gif" then "image/gif"
69
+ when ".webp" then "image/webp"
70
+ else "application/octet-stream"
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end