activeproject 0.1.0 → 0.1.1

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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +4 -2
  3. data/lib/active_project/adapters/base.rb +3 -7
  4. data/lib/active_project/adapters/basecamp/comments.rb +27 -0
  5. data/lib/active_project/adapters/basecamp/connection.rb +49 -0
  6. data/lib/active_project/adapters/basecamp/issues.rb +139 -0
  7. data/lib/active_project/adapters/basecamp/lists.rb +54 -0
  8. data/lib/active_project/adapters/basecamp/projects.rb +110 -0
  9. data/lib/active_project/adapters/basecamp/webhooks.rb +73 -0
  10. data/lib/active_project/adapters/basecamp_adapter.rb +46 -437
  11. data/lib/active_project/adapters/jira/comments.rb +28 -0
  12. data/lib/active_project/adapters/jira/connection.rb +47 -0
  13. data/lib/active_project/adapters/jira/issues.rb +132 -0
  14. data/lib/active_project/adapters/jira/projects.rb +100 -0
  15. data/lib/active_project/adapters/jira/transitions.rb +68 -0
  16. data/lib/active_project/adapters/jira/webhooks.rb +89 -0
  17. data/lib/active_project/adapters/jira_adapter.rb +57 -483
  18. data/lib/active_project/adapters/trello/comments.rb +21 -0
  19. data/lib/active_project/adapters/trello/connection.rb +37 -0
  20. data/lib/active_project/adapters/trello/issues.rb +117 -0
  21. data/lib/active_project/adapters/trello/lists.rb +27 -0
  22. data/lib/active_project/adapters/trello/projects.rb +82 -0
  23. data/lib/active_project/adapters/trello/webhooks.rb +91 -0
  24. data/lib/active_project/adapters/trello_adapter.rb +54 -376
  25. data/lib/active_project/association_proxy.rb +9 -2
  26. data/lib/active_project/configuration.rb +1 -3
  27. data/lib/active_project/configurations/trello_configuration.rb +1 -3
  28. data/lib/active_project/resource_factory.rb +20 -10
  29. data/lib/active_project/resources/issue.rb +0 -3
  30. data/lib/active_project/resources/project.rb +0 -1
  31. data/lib/active_project/version.rb +3 -1
  32. data/lib/activeproject.rb +2 -2
  33. metadata +20 -5
@@ -13,8 +13,6 @@ module ActiveProject
13
13
  # Implements the interface defined in ActiveProject::Adapters::Base.
14
14
  # API Docs: https://developer.atlassian.com/cloud/trello/rest/
15
15
  class TrelloAdapter < Base
16
- BASE_URL = "https://api.trello.com/1/"
17
-
18
16
  # Computes the expected Trello webhook signature.
19
17
  # @param callback_url [String] The exact URL registered for the webhook.
20
18
  # @param response_body [String] The raw response body received from Trello.
@@ -28,22 +26,12 @@ module ActiveProject
28
26
 
29
27
  attr_reader :config
30
28
 
31
- # Initializes the Trello Adapter.
32
- # @param config [Configurations::TrelloConfiguration] The configuration object for Trello.
33
- # @raise [ArgumentError] if required configuration options (:api_key, :api_token) are missing.
34
- def initialize(config:)
35
- unless config.is_a?(ActiveProject::Configurations::TrelloConfiguration)
36
- raise ArgumentError, "TrelloAdapter requires a TrelloConfiguration object"
37
- end
38
- @config = config
39
-
40
- unless @config.api_key && !@config.api_key.empty? && @config.api_token && !@config.api_token.empty?
41
- raise ArgumentError, "TrelloAdapter configuration requires :api_key and :api_token"
42
- end
43
-
44
- @connection = initialize_connection
45
- end
46
-
29
+ include Trello::Connection
30
+ include Trello::Projects
31
+ include Trello::Issues
32
+ include Trello::Comments
33
+ include Trello::Lists
34
+ include Trello::Webhooks
47
35
 
48
36
  # --- Resource Factories ---
49
37
 
@@ -59,318 +47,6 @@ module ActiveProject
59
47
  ResourceFactory.new(adapter: self, resource_class: Resources::Issue)
60
48
  end
61
49
 
62
- # --- Implementation of Base methods ---
63
-
64
- # Lists Trello boards accessible by the configured token.
65
- # @return [Array<ActiveProject::Resources::Project>] An array of project resources.
66
- def list_projects
67
- path = "members/me/boards"
68
- query = { fields: "id,name,desc" }
69
- boards_data = make_request(:get, path, nil, query)
70
-
71
- return [] unless boards_data.is_a?(Array)
72
-
73
- boards_data.map do |board_data|
74
- Resources::Project.new(self, # Pass adapter instance
75
- id: board_data["id"],
76
- key: nil,
77
- name: board_data["name"],
78
- adapter_source: :trello,
79
- raw_data: board_data
80
- )
81
- end
82
- end
83
-
84
- # Finds a specific Trello Board by its ID.
85
- # @param board_id [String] The ID of the Trello Board.
86
- # @return [ActiveProject::Resources::Project] The project resource.
87
- def find_project(board_id)
88
- path = "boards/#{board_id}"
89
- query = { fields: "id,name,desc" }
90
- board_data = make_request(:get, path, nil, query)
91
-
92
- Resources::Project.new(self, # Pass adapter instance
93
- id: board_data["id"],
94
- key: nil,
95
- name: board_data["name"],
96
- adapter_source: :trello,
97
- raw_data: board_data
98
- )
99
- end
100
-
101
- # Creates a new board in Trello.
102
- # @param attributes [Hash] Board attributes. Required: :name. Optional: :description, :default_lists.
103
- # @return [ActiveProject::Resources::Project] The created project resource.
104
- def create_project(attributes)
105
- unless attributes[:name] && !attributes[:name].empty?
106
- raise ArgumentError, "Missing required attribute for Trello board creation: :name"
107
- end
108
-
109
- path = "boards/"
110
- query_params = {
111
- name: attributes[:name],
112
- desc: attributes[:description],
113
- defaultLists: attributes.fetch(:default_lists, true) # Default to creating lists
114
- # Add other board options here if needed (e.g., idOrganization)
115
- }.compact
116
-
117
- board_data = make_request(:post, path, nil, query_params)
118
-
119
- Resources::Project.new(self, # Pass adapter instance
120
- id: board_data["id"],
121
- key: nil,
122
- name: board_data["name"],
123
- adapter_source: :trello,
124
- raw_data: board_data
125
- )
126
- end
127
-
128
- # Creates a new list on a Trello board.
129
- # @param board_id [String] The ID of the board.
130
- # @param attributes [Hash] List attributes. Required: :name. Optional: :pos.
131
- # @return [Hash] The raw data hash of the created list.
132
- def create_list(board_id, attributes)
133
- unless attributes[:name] && !attributes[:name].empty?
134
- raise ArgumentError, "Missing required attribute for Trello list creation: :name"
135
- end
136
-
137
- path = "boards/#{board_id}/lists"
138
- query_params = {
139
- name: attributes[:name],
140
- pos: attributes[:pos]
141
- }.compact
142
-
143
- make_request(:post, path, nil, query_params)
144
- end
145
-
146
- # Deletes a board in Trello.
147
- # WARNING: This is a permanent deletion.
148
- # @param board_id [String] The ID of the board to delete.
149
- # @return [Boolean] true if deletion was successful (API returns 200).
150
- # @raise [NotFoundError] if the board is not found.
151
- # @raise [AuthenticationError] if credentials lack permission.
152
- # @raise [ApiError] for other errors.
153
- def delete_project(board_id)
154
- path = "/boards/#{board_id}"
155
- make_request(:delete, path) # DELETE returns 200 OK on success
156
- true # Return true if make_request doesn't raise an error
157
- end
158
-
159
-
160
-
161
-
162
- # Lists Trello cards on a specific board.
163
- # @param board_id [String] The ID of the Trello board.
164
- # @param options [Hash] Optional filtering options.
165
- # @return [Array<ActiveProject::Resources::Issue>] An array of issue resources.
166
- def list_issues(board_id, options = {})
167
- path = "boards/#{board_id}/cards"
168
- # Fetch idMembers and list name for potential name mapping fallback
169
- query = { fields: "id,name,desc,closed,idList,idBoard,due,dueComplete,idMembers", list: true }
170
- query[:filter] = options[:filter] if options[:filter]
171
-
172
- cards_data = make_request(:get, path, nil, query)
173
- return [] unless cards_data.is_a?(Array)
174
-
175
- cards_data.map { |card_data| map_card_data(card_data, board_id) }
176
- end
177
-
178
- # Finds a specific Card by its ID.
179
- # @param card_id [String] The ID of the Trello Card.
180
- # @param context [Hash] Optional context (ignored).
181
- # @return [ActiveProject::Resources::Issue] The issue resource.
182
- def find_issue(card_id, context = {})
183
- path = "cards/#{card_id}"
184
- # Fetch idMembers and list name for potential name mapping fallback
185
- query = { fields: "id,name,desc,closed,idList,idBoard,due,dueComplete,idMembers", list: true }
186
- card_data = make_request(:get, path, nil, query)
187
- map_card_data(card_data, card_data["idBoard"])
188
- end
189
-
190
- # Creates a new Card in Trello.
191
- # @param _board_id [String] Ignored (context).
192
- # @param attributes [Hash] Card attributes. Required: :list_id, :title. Optional: :description, :assignee_ids, :due_on.
193
- # @return [ActiveProject::Resources::Issue] The created issue resource.
194
- def create_issue(_board_id, attributes)
195
- list_id = attributes[:list_id]
196
- title = attributes[:title]
197
-
198
- unless list_id && title && !title.empty?
199
- raise ArgumentError, "Missing required attributes for Trello card creation: :list_id, :title"
200
- end
201
-
202
- path = "cards"
203
- query_params = {
204
- idList: list_id,
205
- name: title,
206
- desc: attributes[:description],
207
- # Use assignee_ids (expects an array of Trello member IDs)
208
- idMembers: attributes[:assignee_ids]&.join(","),
209
- due: attributes[:due_on]&.iso8601
210
- }.compact
211
-
212
- card_data = make_request(:post, path, nil, query_params)
213
- map_card_data(card_data, card_data["idBoard"])
214
- end
215
-
216
- # Updates an existing Card in Trello.
217
- # @param card_id [String] The ID of the Trello Card.
218
- # @param attributes [Hash] Attributes to update (e.g., :title, :description, :list_id, :closed, :due_on, :assignee_ids, :status).
219
- # @param context [Hash] Optional context (ignored).
220
- # @return [ActiveProject::Resources::Issue] The updated issue resource.
221
- def update_issue(card_id, attributes, context = {})
222
- # Make a mutable copy of attributes
223
- update_attributes = attributes.dup
224
-
225
- # Handle :status mapping to :list_id
226
- if update_attributes.key?(:status)
227
- target_status = update_attributes.delete(:status) # Remove status key
228
-
229
- # Fetch board_id efficiently if not already known
230
- # We need the board_id to look up the correct status mapping
231
- board_id = update_attributes[:board_id] || begin
232
- find_issue(card_id).project_id # Fetch the issue to get its board_id
233
- rescue NotFoundError
234
- # Re-raise NotFoundError if the card itself doesn't exist
235
- raise NotFoundError, "Trello card with ID '#{card_id}' not found."
236
- end
237
-
238
- unless board_id
239
- # This should theoretically not happen if find_issue succeeded or board_id was passed
240
- raise ApiError, "Could not determine board ID for card '#{card_id}' to perform status mapping."
241
- end
242
-
243
- # Use stored config for status mappings
244
- board_mappings = @config.status_mappings[board_id]
245
- unless board_mappings
246
- raise ConfigurationError, "Trello status mapping not configured for board ID '#{board_id}'. Cannot map status ':#{target_status}'."
247
- end
248
-
249
- # Find the target list ID by looking up the status symbol in the board's mappings.
250
- # We iterate through the mappings hash { list_id => status_symbol }
251
- target_list_id = board_mappings.key(target_status)
252
-
253
- unless target_list_id
254
- raise ConfigurationError, "Target status ':#{target_status}' not found in configured Trello status mappings for board ID '#{board_id}'."
255
- end
256
-
257
- # Add the resolved list_id to the attributes to be updated
258
- update_attributes[:list_id] = target_list_id
259
- end
260
-
261
-
262
- path = "cards/#{card_id}"
263
-
264
- # Build query parameters from the potentially modified update_attributes
265
- query_params = {}
266
- query_params[:name] = update_attributes[:title] if update_attributes.key?(:title)
267
- query_params[:desc] = update_attributes[:description] if update_attributes.key?(:description)
268
- query_params[:closed] = update_attributes[:closed] if update_attributes.key?(:closed)
269
- query_params[:idList] = update_attributes[:list_id] if update_attributes.key?(:list_id) # Use the mapped list_id if status was provided
270
- query_params[:due] = update_attributes[:due_on]&.iso8601 if update_attributes.key?(:due_on)
271
- query_params[:dueComplete] = update_attributes[:dueComplete] if update_attributes.key?(:dueComplete)
272
- # Use assignee_ids (expects an array of Trello member IDs)
273
- query_params[:idMembers] = update_attributes[:assignee_ids]&.join(",") if update_attributes.key?(:assignee_ids)
274
-
275
- # If after processing :status, there are no actual changes, just return the current issue state
276
- return find_issue(card_id, context) if query_params.empty?
277
-
278
- # Make the PUT request to update the card
279
- card_data = make_request(:put, path, nil, query_params.compact)
280
-
281
- # Return the updated issue resource, mapped with potentially new status
282
- map_card_data(card_data, card_data["idBoard"])
283
- end
284
-
285
- # Adds a comment to a Card in Trello.
286
- # @param card_id [String] The ID of the Trello Card.
287
- # @param comment_body [String] The comment text (Markdown).
288
- # @param context [Hash] Optional context (ignored).
289
- # @return [ActiveProject::Resources::Comment] The created comment resource.
290
- def add_comment(card_id, comment_body, context = {})
291
- path = "cards/#{card_id}/actions/comments"
292
- query_params = { text: comment_body }
293
- comment_data = make_request(:post, path, nil, query_params)
294
- map_comment_action_data(comment_data, card_id)
295
- end
296
-
297
- # Parses an incoming Trello webhook payload.
298
- # @param request_body [String] The raw JSON request body.
299
- # @param headers [Hash] Request headers (unused).
300
- # @return [ActiveProject::WebhookEvent, nil] Parsed event or nil if unhandled.
301
- def parse_webhook(request_body, headers = {})
302
- payload = JSON.parse(request_body) rescue nil
303
- return nil unless payload.is_a?(Hash) && payload["action"].is_a?(Hash)
304
-
305
- action = payload["action"]
306
- action_type = action["type"]
307
- actor_data = action.dig("memberCreator")
308
- timestamp = Time.parse(action["date"]) rescue nil
309
- board_id = action.dig("data", "board", "id")
310
- card_data = action.dig("data", "card")
311
- comment_text = action.dig("data", "text")
312
- old_data = action.dig("data", "old") # For updateCard events
313
-
314
- event_type = nil
315
- object_kind = nil
316
- event_object_id = nil
317
- object_key = nil
318
- changes = nil
319
- object_data = nil
320
-
321
- case action_type
322
- when "createCard"
323
- event_type = :issue_created
324
- object_kind = :issue
325
- event_object_id = card_data["id"]
326
- object_key = card_data["idShort"]
327
- when "updateCard"
328
- event_type = :issue_updated
329
- object_kind = :issue
330
- event_object_id = card_data["id"]
331
- object_key = card_data["idShort"]
332
- # Parse changes for updateCard
333
- if old_data.is_a?(Hash)
334
- changes = {}
335
- old_data.each do |field, old_value|
336
- # Find the corresponding new value in the card data if possible
337
- new_value = card_data[field]
338
- changes[field.to_sym] = [ old_value, new_value ]
339
- end
340
- end
341
- when "commentCard"
342
- event_type = :comment_added
343
- object_kind = :comment
344
- event_object_id = action["id"] # Action ID is comment ID
345
- object_key = nil
346
- when "addMemberToCard", "removeMemberFromCard"
347
- event_type = :issue_updated
348
- object_kind = :issue
349
- event_object_id = card_data["id"]
350
- object_key = card_data["idShort"]
351
- changes = { assignees: true } # Indicate assignees changed, specific diff not easily available
352
- else
353
- return nil # Unhandled action type
354
- end
355
-
356
- WebhookEvent.new(
357
- event_type: event_type,
358
- object_kind: object_kind,
359
- event_object_id: event_object_id,
360
- object_key: object_key,
361
- project_id: board_id,
362
- actor: map_user_data(actor_data), # Use helper
363
- timestamp: timestamp,
364
- adapter_source: :trello,
365
- changes: changes,
366
- object_data: object_data, # Keep nil for now
367
- raw_data: payload
368
- )
369
- rescue JSON::ParserError
370
- nil # Ignore unparseable payloads
371
- end
372
-
373
-
374
50
  # Retrieves details for the currently authenticated user.
375
51
  # @return [ActiveProject::Resources::User] The user object.
376
52
  # @raise [ActiveProject::AuthenticationError] if authentication fails.
@@ -390,18 +66,9 @@ module ActiveProject
390
66
  false
391
67
  end
392
68
 
393
-
394
69
  private
395
70
 
396
71
  # Initializes the Faraday connection object.
397
- def initialize_connection
398
- Faraday.new(url: BASE_URL) do |conn|
399
- conn.request :retry
400
- conn.headers["Accept"] = "application/json"
401
- conn.response :raise_error
402
- conn.headers["User-Agent"] = ActiveProject.user_agent
403
- end
404
- end
405
72
 
406
73
  # Helper method for making requests.
407
74
  def make_request(method, path, body = nil, query_params = {})
@@ -417,6 +84,7 @@ module ActiveProject
417
84
  end
418
85
 
419
86
  return nil if response.status == 204 || response.body.empty?
87
+
420
88
  JSON.parse(response.body)
421
89
  rescue Faraday::Error => e
422
90
  handle_faraday_error(e)
@@ -440,11 +108,14 @@ module ActiveProject
440
108
  when 400, 422
441
109
  if status == 400 && message&.include?("invalid id")
442
110
  raise NotFoundError, "Trello resource not found (Status: 400, Message: #{message})"
443
- else
444
- raise ValidationError.new("Trello validation failed (Status: #{status}): #{message}", status_code: status, response_body: body)
445
111
  end
112
+
113
+ raise ValidationError.new("Trello validation failed (Status: #{status}): #{message}", status_code: status,
114
+ response_body: body)
115
+
446
116
  else
447
- raise ApiError.new("Trello API error (Status: #{status || 'N/A'}): #{message}", original_error: error, status_code: status, response_body: body)
117
+ raise ApiError.new("Trello API error (Status: #{status || 'N/A'}): #{message}", original_error: error,
118
+ status_code: status, response_body: body)
448
119
  end
449
120
  end
450
121
 
@@ -461,48 +132,54 @@ module ActiveProject
461
132
  status = board_mappings[list_id]
462
133
  # Fallback: Try mapping by List Name if ID mapping failed and list data is present
463
134
  elsif board_mappings && card_data["list"] && board_mappings.key?(card_data["list"]["name"])
464
- status = board_mappings[card_data["list"]["name"]]
135
+ status = board_mappings[card_data["list"]["name"]]
465
136
  end
466
137
 
467
138
  # Override status if the card is archived (closed)
468
139
  status = :closed if card_data["closed"]
469
140
 
470
- created_timestamp = Time.at(card_data["id"][0..7].to_i(16)) rescue nil
471
- due_on = card_data["due"] ? Date.parse(card_data["due"]) : nil rescue nil
141
+ created_timestamp = begin
142
+ Time.at(card_data["id"][0..7].to_i(16))
143
+ rescue StandardError
144
+ nil
145
+ end
146
+ due_on = begin
147
+ card_data["due"] ? Date.parse(card_data["due"]) : nil
148
+ rescue StandardError
149
+ nil
150
+ end
472
151
 
473
152
  Resources::Issue.new(self, # Pass adapter instance
474
- id: card_data["id"],
475
- key: nil,
476
- title: card_data["name"],
477
- description: card_data["desc"],
478
- status: status, # Use the determined status
479
- assignees: map_member_ids_to_users(card_data["idMembers"]), # Use new method
480
- reporter: nil, # Trello cards don't have a distinct reporter
481
- project_id: board_id,
482
- created_at: created_timestamp,
483
- updated_at: nil, # Trello API doesn't provide a standard updated_at for cards easily
484
- due_on: due_on,
485
- priority: nil, # Trello doesn't have priority
486
- adapter_source: :trello,
487
- raw_data: card_data
488
- )
153
+ id: card_data["id"],
154
+ key: nil,
155
+ title: card_data["name"],
156
+ description: card_data["desc"],
157
+ status: status, # Use the determined status
158
+ assignees: map_member_ids_to_users(card_data["idMembers"]), # Use new method
159
+ reporter: nil, # Trello cards don't have a distinct reporter
160
+ project_id: board_id,
161
+ created_at: created_timestamp,
162
+ updated_at: nil, # Trello API doesn't provide a standard updated_at for cards easily
163
+ due_on: due_on,
164
+ priority: nil, # Trello doesn't have priority
165
+ adapter_source: :trello,
166
+ raw_data: card_data)
489
167
  end
490
168
 
491
169
  # Maps raw Trello comment action data hash to a Comment resource.
492
170
  def map_comment_action_data(action_data, card_id)
493
- author_data = action_data.dig("memberCreator")
171
+ author_data = action_data["memberCreator"]
494
172
  comment_text = action_data.dig("data", "text")
495
173
 
496
174
  Resources::Comment.new(self, # Pass adapter instance
497
- id: action_data["id"],
498
- body: comment_text,
499
- author: map_user_data(author_data), # Use user mapping
500
- created_at: action_data["date"] ? Time.parse(action_data["date"]) : nil,
501
- updated_at: nil,
502
- issue_id: card_id,
503
- adapter_source: :trello,
504
- raw_data: action_data
505
- )
175
+ id: action_data["id"],
176
+ body: comment_text,
177
+ author: map_user_data(author_data), # Use user mapping
178
+ created_at: action_data["date"] ? Time.parse(action_data["date"]) : nil,
179
+ updated_at: nil,
180
+ issue_id: card_id,
181
+ adapter_source: :trello,
182
+ raw_data: action_data)
506
183
  end
507
184
 
508
185
  # Maps an array of Trello member IDs to an array of User resources.
@@ -511,6 +188,7 @@ module ActiveProject
511
188
  # @return [Array<Resources::User>]
512
189
  def map_member_ids_to_users(member_ids)
513
190
  return [] unless member_ids.is_a?(Array)
191
+
514
192
  member_ids.map do |id|
515
193
  Resources::User.new(self, id: id, adapter_source: :trello, raw_data: { id: id })
516
194
  end
@@ -521,13 +199,13 @@ module ActiveProject
521
199
  # @return [Resources::User, nil]
522
200
  def map_user_data(member_data)
523
201
  return nil unless member_data && member_data["id"]
202
+
524
203
  Resources::User.new(self, # Pass adapter instance
525
- id: member_data["id"],
526
- name: member_data["fullName"], # Trello uses fullName
527
- email: nil, # Trello API often doesn't provide email directly here
528
- adapter_source: :trello,
529
- raw_data: member_data
530
- )
204
+ id: member_data["id"],
205
+ name: member_data["fullName"], # Trello uses fullName
206
+ email: nil, # Trello API often doesn't provide email directly here
207
+ adapter_source: :trello,
208
+ raw_data: member_data)
531
209
  end
532
210
  end
533
211
  end
@@ -32,6 +32,7 @@ module ActiveProject
32
32
  # Ensure owner.id is accessed correctly
33
33
  owner_id = @owner.respond_to?(:id) ? @owner.id : nil
34
34
  raise "Owner object #{@owner.inspect} does not have an ID for association call." unless owner_id
35
+
35
36
  @adapter.send(list_method, owner_id, options)
36
37
  end
37
38
 
@@ -79,6 +80,8 @@ module ActiveProject
79
80
  @target_resource_class.new(@adapter, merged_attrs)
80
81
  end
81
82
 
83
+ alias new build
84
+
82
85
  # Creates and saves a new associated resource.
83
86
  # Example: project.issues.create(title: 'New', list_id: '...')
84
87
  # @param attributes [Hash] Attributes for the new resource.
@@ -86,12 +89,13 @@ module ActiveProject
86
89
  # @raise [NotImplementedError] Currently raises because #save is not fully implemented on resource.
87
90
  def create(attributes = {})
88
91
  create_method = determine_create_method
89
- context = determine_context # Get owner context
92
+ determine_context # Get owner context
90
93
  # Pass owner ID/context first, then attributes
91
94
  owner_id = @owner.respond_to?(:id) ? @owner.id : nil
92
95
  raise "Owner object #{@owner.inspect} does not have an ID for association call." unless owner_id
96
+
93
97
  @adapter.send(create_method, owner_id, attributes)
94
- # Note: This currently returns the result from the adapter directly.
98
+ # NOTE: This currently returns the result from the adapter directly.
95
99
  # A full implementation would likely build and then save, or re-fetch.
96
100
  end
97
101
 
@@ -113,6 +117,7 @@ module ActiveProject
113
117
  unless @adapter.respond_to?(method_name)
114
118
  raise NotImplementedError, "#{@adapter.class.name} does not implement ##{method_name}"
115
119
  end
120
+
116
121
  method_name
117
122
  end
118
123
 
@@ -125,6 +130,7 @@ module ActiveProject
125
130
  unless @adapter.respond_to?(method_name)
126
131
  raise NotImplementedError, "#{@adapter.class.name} does not implement ##{method_name}"
127
132
  end
133
+
128
134
  method_name
129
135
  end
130
136
 
@@ -136,6 +142,7 @@ module ActiveProject
136
142
  unless @adapter.respond_to?(method_name)
137
143
  raise NotImplementedError, "#{@adapter.class.name} does not implement ##{method_name}"
138
144
  end
145
+
139
146
  method_name
140
147
  end
141
148
  end
@@ -29,9 +29,7 @@ module ActiveProject
29
29
  # @param options [Hash] Configuration options for the adapter (e.g., site, api_key, token).
30
30
  # @yield [BaseAdapterConfiguration] Yields an adapter-specific configuration object if a block is given.
31
31
  def add_adapter(adapter_type, instance_name = :primary, options = {}, &block)
32
- unless adapter_type.is_a?(Symbol)
33
- raise ArgumentError, "Adapter type must be a Symbol (e.g., :basecamp)"
34
- end
32
+ raise ArgumentError, "Adapter type must be a Symbol (e.g., :basecamp)" unless adapter_type.is_a?(Symbol)
35
33
 
36
34
  # Handle the case where instance_name is actually the options hash
37
35
  if instance_name.is_a?(Hash) && options.empty?
@@ -20,9 +20,7 @@ module ActiveProject
20
20
 
21
21
  def freeze
22
22
  # Ensure nested hashes are also frozen
23
- @status_mappings.each do |board_id, mappings|
24
- mappings.freeze
25
- end
23
+ @status_mappings.each_value(&:freeze)
26
24
  @status_mappings.freeze
27
25
  super
28
26
  end
@@ -23,13 +23,11 @@ module ActiveProject
23
23
  # Call adapter method with appropriate arguments
24
24
  if primary_arg
25
25
  @adapter.send(list_method, primary_arg, options)
26
- else
26
+ elsif @adapter.method(list_method).arity.zero?
27
27
  # Handle case where list method might not take options (like list_projects)
28
- if @adapter.method(list_method).arity == 0
29
- @adapter.send(list_method)
30
- else
31
- @adapter.send(list_method, options)
32
- end
28
+ @adapter.send(list_method)
29
+ else
30
+ @adapter.send(list_method, options)
33
31
  end
34
32
  end
35
33
 
@@ -89,7 +87,7 @@ module ActiveProject
89
87
  def create(attributes = {})
90
88
  # Determine the correct adapter create method based on resource type
91
89
  create_method = determine_create_method
92
- # Note: Assumes create methods on adapters take attributes hash directly
90
+ # NOTE: Assumes create methods on adapters take attributes hash directly
93
91
  # Context like project_id needs to be part of the attributes hash if required by adapter
94
92
  @adapter.send(create_method, attributes)
95
93
  # A full implementation would likely involve build then save:
@@ -106,7 +104,11 @@ module ActiveProject
106
104
  when "ActiveProject::Resources::Issue" then :list_issues
107
105
  else raise "Cannot determine list method for #{@resource_class.name}"
108
106
  end
109
- raise NotImplementedError, "#{@adapter.class.name} does not implement ##{method_name}" unless @adapter.respond_to?(method_name)
107
+ unless @adapter.respond_to?(method_name)
108
+ raise NotImplementedError,
109
+ "#{@adapter.class.name} does not implement ##{method_name}"
110
+ end
111
+
110
112
  method_name
111
113
  end
112
114
 
@@ -116,14 +118,22 @@ module ActiveProject
116
118
  when "ActiveProject::Resources::Issue" then :find_issue
117
119
  else raise "Cannot determine find method for #{@resource_class.name}"
118
120
  end
119
- raise NotImplementedError, "#{@adapter.class.name} does not implement ##{method_name}" unless @adapter.respond_to?(method_name)
121
+ unless @adapter.respond_to?(method_name)
122
+ raise NotImplementedError,
123
+ "#{@adapter.class.name} does not implement ##{method_name}"
124
+ end
125
+
120
126
  method_name
121
127
  end
122
128
 
123
129
  def determine_create_method
124
130
  singular_name = @resource_class.name.split("::").last.downcase.to_sym
125
131
  method_name = :"create_#{singular_name}"
126
- raise NotImplementedError, "#{@adapter.class.name} does not implement ##{method_name}" unless @adapter.respond_to?(method_name)
132
+ unless @adapter.respond_to?(method_name)
133
+ raise NotImplementedError,
134
+ "#{@adapter.class.name} does not implement ##{method_name}"
135
+ end
136
+
127
137
  method_name
128
138
  end
129
139
  end
@@ -7,8 +7,6 @@ module ActiveProject
7
7
  def_members :id, :key, :title, :description, :status, :assignees,
8
8
  :reporter, :project_id, :created_at, :updated_at, :due_on,
9
9
  :priority, :adapter_source
10
- # raw_data and adapter are inherited from BaseResource
11
-
12
10
 
13
11
  # Saves the issue (creates if new, updates if existing).
14
12
  # Placeholder - Full implementation requires attribute tracking and adapter delegation.
@@ -28,7 +26,6 @@ module ActiveProject
28
26
  raise NotImplementedError, "#update not yet implemented for #{self.class.name}"
29
27
  end
30
28
 
31
-
32
29
  # Returns an association proxy for accessing comments on this issue.
33
30
  # @return [AssociationProxy<Resources::Comment>]
34
31
  def comments
@@ -7,7 +7,6 @@ module ActiveProject
7
7
  def_members :id, :key, :name, :adapter_source
8
8
  # raw_data and adapter are inherited from BaseResource
9
9
 
10
-
11
10
  # Returns an association proxy for accessing issues within this project.
12
11
  # @return [AssociationProxy<Resources::Issue>]
13
12
  def issues