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