activeproject 0.1.0 → 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.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +4 -2
  3. data/lib/active_project/adapters/base.rb +9 -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 +158 -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 +150 -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 +61 -485
  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 +133 -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 +59 -377
  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 +19 -1
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveProject
4
+ module Adapters
5
+ module Trello
6
+ module Connection
7
+ BASE_URL = "https://api.trello.com/1/"
8
+ # @raise [ArgumentError] if required configuration options (:api_key, :api_token) are missing.
9
+ def initialize(config:)
10
+ unless config.is_a?(ActiveProject::Configurations::TrelloConfiguration)
11
+ raise ArgumentError, "TrelloAdapter requires a TrelloConfiguration object"
12
+ end
13
+
14
+ @config = config
15
+
16
+ unless @config.api_key && !@config.api_key.empty? && @config.api_token && !@config.api_token.empty?
17
+ raise ArgumentError, "TrelloAdapter configuration requires :api_key and :api_token"
18
+ end
19
+
20
+ @connection = initialize_connection
21
+ end
22
+
23
+ private
24
+
25
+ # Initializes the Faraday connection object.
26
+ def initialize_connection
27
+ Faraday.new(url: BASE_URL) do |conn|
28
+ conn.request :retry
29
+ conn.headers["Accept"] = "application/json"
30
+ conn.response :raise_error
31
+ conn.headers["User-Agent"] = ActiveProject.user_agent
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveProject
4
+ module Adapters
5
+ module Trello
6
+ module Issues
7
+ DEFAULT_FIELDS = %w[id name desc closed idList idBoard due dueComplete idMembers].freeze
8
+
9
+ # Lists Trello cards on a specific board.
10
+ # @param board_id [String] The ID of the Trello board.
11
+ # @param options [Hash] Optional filtering options. Accepts :filter and :fields.
12
+ # @return [Array<ActiveProject::Resources::Issue>]
13
+ def list_issues(board_id, options = {})
14
+ path = "boards/#{board_id}/cards"
15
+
16
+ fields = options[:fields] ? Array(options[:fields]).join(",") : DEFAULT_FIELDS.join(",")
17
+ query = { fields: fields, list: true }
18
+ query[:filter] = options[:filter] if options[:filter]
19
+
20
+ cards_data = make_request(:get, path, nil, query)
21
+ return [] unless cards_data.is_a?(Array)
22
+
23
+ cards_data.map { |card_data| map_card_data(card_data, board_id) }
24
+ end
25
+
26
+ # Finds a specific Card by its ID.
27
+ # @param card_id [String] The ID of the Trello Card.
28
+ # @param context [Hash] Optional context. Accepts :fields for specific field selection.
29
+ # @return [ActiveProject::Resources::Issue]
30
+ def find_issue(card_id, context = {})
31
+ path = "cards/#{card_id}"
32
+
33
+ fields = context[:fields] ? Array(context[:fields]).join(",") : DEFAULT_FIELDS.join(",")
34
+ query = { fields: fields, list: true }
35
+
36
+ card_data = make_request(:get, path, nil, query)
37
+ map_card_data(card_data, card_data["idBoard"])
38
+ end
39
+
40
+ # Creates a new Card in Trello.
41
+ # @param _board_id [String] Ignored (context).
42
+ # @param attributes [Hash] Card attributes. Required: :list_id, :title. Optional: :description, :assignee_ids, :due_on.
43
+ # @return [ActiveProject::Resources::Issue]
44
+ def create_issue(_board_id, attributes)
45
+ list_id = attributes[:list_id]
46
+ title = attributes[:title]
47
+
48
+ unless list_id && title && !title.empty?
49
+ raise ArgumentError, "Missing required attributes for Trello card creation: :list_id, :title"
50
+ end
51
+
52
+ path = "cards"
53
+ query_params = {
54
+ idList: list_id,
55
+ name: title,
56
+ desc: attributes[:description],
57
+ idMembers: attributes[:assignee_ids]&.join(","),
58
+ due: attributes[:due_on]&.iso8601
59
+ }.compact
60
+
61
+ card_data = make_request(:post, path, nil, query_params)
62
+ map_card_data(card_data, card_data["idBoard"])
63
+ end
64
+
65
+ # Updates an existing Card in Trello.
66
+ # @param card_id [String] The ID of the Trello Card.
67
+ # @param attributes [Hash] Attributes to update (e.g., :title, :description, :list_id, :closed, :due_on, :assignee_ids, :status).
68
+ # @param context [Hash] Optional context. Accepts :fields for return data field selection.
69
+ # @return [ActiveProject::Resources::Issue]
70
+ def update_issue(card_id, attributes, context = {})
71
+ update_attributes = attributes.dup
72
+
73
+ if update_attributes.key?(:status)
74
+ target_status = update_attributes.delete(:status)
75
+
76
+ board_id = update_attributes[:board_id] || begin
77
+ find_issue(card_id).project_id
78
+ rescue NotFoundError
79
+ raise NotFoundError, "Trello card with ID '#{card_id}' not found."
80
+ end
81
+
82
+ unless board_id
83
+ raise ApiError, "Could not determine board ID for card '#{card_id}' to perform status mapping."
84
+ end
85
+
86
+ board_mappings = @config.status_mappings[board_id]
87
+ unless board_mappings
88
+ raise ConfigurationError,
89
+ "Trello status mapping not configured for board ID '#{board_id}'. Cannot map status ':#{target_status}'."
90
+ end
91
+
92
+ target_list_id = board_mappings.key(target_status)
93
+
94
+ unless target_list_id
95
+ raise ConfigurationError,
96
+ "Target status ':#{target_status}' not found in configured Trello status mappings for board ID '#{board_id}'."
97
+ end
98
+
99
+ update_attributes[:list_id] = target_list_id
100
+ end
101
+
102
+ path = "cards/#{card_id}"
103
+
104
+ query_params = {}
105
+ query_params[:name] = update_attributes[:title] if update_attributes.key?(:title)
106
+ query_params[:desc] = update_attributes[:description] if update_attributes.key?(:description)
107
+ query_params[:closed] = update_attributes[:closed] if update_attributes.key?(:closed)
108
+ query_params[:idList] = update_attributes[:list_id] if update_attributes.key?(:list_id)
109
+ query_params[:due] = update_attributes[:due_on]&.iso8601 if update_attributes.key?(:due_on)
110
+ query_params[:dueComplete] = update_attributes[:dueComplete] if update_attributes.key?(:dueComplete)
111
+ if update_attributes.key?(:assignee_ids)
112
+ query_params[:idMembers] =
113
+ update_attributes[:assignee_ids]&.join(",")
114
+ end
115
+
116
+ return find_issue(card_id, context) if query_params.empty?
117
+
118
+ card_data = make_request(:put, path, nil, query_params.compact)
119
+ map_card_data(card_data, card_data["idBoard"])
120
+ end
121
+
122
+ # Deletes a Trello card.
123
+ # @param card_id [String] The ID of the Trello Card to delete.
124
+ # @return [Boolean] True if successfully deleted.
125
+ def delete_issue(card_id, **)
126
+ path = "cards/#{card_id}"
127
+ make_request(:delete, path)
128
+ true
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveProject
4
+ module Adapters
5
+ module Trello
6
+ module Lists
7
+ # Creates a new list on a Trello board.
8
+ # @param board_id [String] The ID of the board.
9
+ # @param attributes [Hash] List attributes. Required: :name. Optional: :pos.
10
+ # @return [Hash] The raw data hash of the created list.
11
+ def create_list(board_id, attributes)
12
+ unless attributes[:name] && !attributes[:name].empty?
13
+ raise ArgumentError, "Missing required attribute for Trello list creation: :name"
14
+ end
15
+
16
+ path = "boards/#{board_id}/lists"
17
+ query_params = {
18
+ name: attributes[:name],
19
+ pos: attributes[:pos]
20
+ }.compact
21
+
22
+ make_request(:post, path, nil, query_params)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveProject
4
+ module Adapters
5
+ module Trello
6
+ module Projects
7
+ # Lists Trello boards accessible by the configured token.
8
+ # @return [Array<ActiveProject::Resources::Project>] An array of project resources.
9
+ def list_projects
10
+ path = "members/me/boards"
11
+ query = { fields: "id,name,desc" }
12
+ boards_data = make_request(:get, path, nil, query)
13
+
14
+ return [] unless boards_data.is_a?(Array)
15
+
16
+ boards_data.map do |board_data|
17
+ Resources::Project.new(self,
18
+ id: board_data["id"],
19
+ key: nil,
20
+ name: board_data["name"],
21
+ adapter_source: :trello,
22
+ raw_data: board_data)
23
+ end
24
+ end
25
+
26
+ # Finds a specific Trello Board by its ID.
27
+ # @param board_id [String] The ID of the Trello Board.
28
+ # @return [ActiveProject::Resources::Project]
29
+ def find_project(board_id)
30
+ path = "boards/#{board_id}"
31
+ query = { fields: "id,name,desc" }
32
+ board_data = make_request(:get, path, nil, query)
33
+
34
+ Resources::Project.new(self,
35
+ id: board_data["id"],
36
+ key: nil,
37
+ name: board_data["name"],
38
+ adapter_source: :trello,
39
+ raw_data: board_data)
40
+ end
41
+
42
+ # Creates a new board in Trello.
43
+ # @param attributes [Hash] Board attributes. Required: :name. Optional: :description, :default_lists.
44
+ # @return [ActiveProject::Resources::Project]
45
+ def create_project(attributes)
46
+ unless attributes[:name] && !attributes[:name].empty?
47
+ raise ArgumentError, "Missing required attribute for Trello board creation: :name"
48
+ end
49
+
50
+ path = "boards/"
51
+ query_params = {
52
+ name: attributes[:name],
53
+ desc: attributes[:description],
54
+ defaultLists: attributes.fetch(:default_lists, true)
55
+ }.compact
56
+
57
+ board_data = make_request(:post, path, nil, query_params)
58
+
59
+ Resources::Project.new(self,
60
+ id: board_data["id"],
61
+ key: nil,
62
+ name: board_data["name"],
63
+ adapter_source: :trello,
64
+ raw_data: board_data)
65
+ end
66
+
67
+ # Deletes a board in Trello.
68
+ # WARNING: This is a permanent deletion.
69
+ # @param board_id [String] The ID of the board to delete.
70
+ # @return [Boolean] true if deletion was successful (API returns 200).
71
+ # @raise [NotFoundError] if the board is not found.
72
+ # @raise [AuthenticationError] if credentials lack permission.
73
+ # @raise [ApiError] for other errors.
74
+ def delete_project(board_id)
75
+ path = "/boards/#{board_id}"
76
+ make_request(:delete, path)
77
+ true
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveProject
4
+ module Adapters
5
+ module Trello
6
+ module Webhooks
7
+ # Parses an incoming Trello webhook payload.
8
+ # @param request_body [String] The raw JSON request body.
9
+ # @param headers [Hash] Request headers (unused).
10
+ # @return [ActiveProject::WebhookEvent, nil] Parsed event or nil if unhandled.
11
+ def parse_webhook(request_body, _headers = {})
12
+ payload = begin
13
+ JSON.parse(request_body)
14
+ rescue StandardError
15
+ nil
16
+ end
17
+ return nil unless payload.is_a?(Hash) && payload["action"].is_a?(Hash)
18
+
19
+ action = payload["action"]
20
+ action_type = action["type"]
21
+ actor_data = action["memberCreator"]
22
+ timestamp = begin
23
+ Time.parse(action["date"])
24
+ rescue StandardError
25
+ nil
26
+ end
27
+ board_id = action.dig("data", "board", "id")
28
+ card_data = action.dig("data", "card")
29
+ action.dig("data", "text")
30
+ old_data = action.dig("data", "old")
31
+
32
+ event_type = nil
33
+ object_kind = nil
34
+ event_object_id = nil
35
+ object_key = nil
36
+ changes = nil
37
+ object_data = nil
38
+
39
+ case action_type
40
+ when "createCard"
41
+ event_type = :issue_created
42
+ object_kind = :issue
43
+ event_object_id = card_data["id"]
44
+ object_key = card_data["idShort"]
45
+ when "updateCard"
46
+ event_type = :issue_updated
47
+ object_kind = :issue
48
+ event_object_id = card_data["id"]
49
+ object_key = card_data["idShort"]
50
+ if old_data.is_a?(Hash)
51
+ changes = {}
52
+ old_data.each do |field, old_value|
53
+ new_value = card_data[field]
54
+ changes[field.to_sym] = [ old_value, new_value ]
55
+ end
56
+ end
57
+ when "commentCard"
58
+ event_type = :comment_added
59
+ object_kind = :comment
60
+ event_object_id = action["id"]
61
+ object_key = nil
62
+ when "addMemberToCard", "removeMemberFromCard"
63
+ event_type = :issue_updated
64
+ object_kind = :issue
65
+ event_object_id = card_data["id"]
66
+ object_key = card_data["idShort"]
67
+ changes = { assignees: true }
68
+ else
69
+ return nil
70
+ end
71
+
72
+ WebhookEvent.new(
73
+ event_type: event_type,
74
+ object_kind: object_kind,
75
+ event_object_id: event_object_id,
76
+ object_key: object_key,
77
+ project_id: board_id,
78
+ actor: map_user_data(actor_data),
79
+ timestamp: timestamp,
80
+ adapter_source: :trello,
81
+ changes: changes,
82
+ object_data: object_data,
83
+ raw_data: payload
84
+ )
85
+ rescue JSON::ParserError
86
+ nil
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end