activeproject 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8f3323a59c15ce296050ec1c7b231416d2cac1aa6f78fe133c8d7d2fd3ebb639
4
+ data.tar.gz: 0f60e5b29d28e3adca3ce7e26df63929ed3c0d4b898887213823cd284efa365a
5
+ SHA512:
6
+ metadata.gz: 756f9f83b0f443ceb6d426514d79c7c7cca5c8f6b410ab6fbad9469426354735d706d0b00be102a1f36ec4dcb4a7ee04d8b110c6aaaeda3ac8ccce2b61695623
7
+ data.tar.gz: 5397faf8bae3c5fe4ab86a98360de55c92237270806ac3372cdf7cff08742fa857ccff9f4a139998116ce366cd321df5afab6d81368a78970822de853d43c68c
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright Abdelkader Boudih
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,304 @@
1
+ # ActiveProject Gem
2
+
3
+ A standardized Ruby interface for multiple project management APIs (Jira, Basecamp, Trello, etc.).
4
+
5
+ ## Problem
6
+
7
+ Integrating with various project management platforms like Jira, Basecamp, and Trello often requires writing separate, complex clients for each API. Developers face challenges in handling different authentication methods, error formats, data structures, and workflow concepts across these platforms.
8
+
9
+ ## Solution
10
+
11
+ The ActiveProject gem aims to solve this by providing a unified, opinionated interface built on the **Adapter pattern**. It abstracts away the complexities of individual APIs, offering:
12
+
13
+ * **Normalized Data Models:** Common Ruby objects for core concepts like `Project`, `Task` (Issue/Card/To-do), `Comment`, and `User`.
14
+ * **Standardized Operations:** Consistent methods for creating, reading, updating, and transitioning tasks (e.g., `task.close!`, `task.reopen!`).
15
+ * **Unified Error Handling:** A common set of exceptions (`AuthenticationError`, `NotFoundError`, `RateLimitError`, etc.) regardless of the underlying platform.
16
+
17
+ ## Supported Platforms
18
+
19
+ The initial focus is on integrating with platforms primarily via their **REST APIs**:
20
+
21
+ * **Jira (Cloud & Server):** REST API (v2/v3)
22
+ * **Basecamp (v3+):** REST API
23
+ * **Trello:** REST API
24
+
25
+ Future integrations might include platforms like Asana (REST), Monday.com (GraphQL), and Linear (GraphQL). For GraphQL-based APIs, the adapter will encapsulate the query logic, maintaining a consistent interface for the gem user.
26
+
27
+ ## Core Concepts
28
+
29
+ * **Project:** Represents a Jira Project, Basecamp Project, or Trello Board.
30
+ * **Task:** A unified representation of a Jira Issue, Basecamp To-do, or Trello Card. Includes normalized fields like `title`, `description`, `assignees`, `status`, and `priority`.
31
+ * **Status Normalization:** Maps platform-specific statuses (Jira statuses, Basecamp completion, Trello lists) to a common set like `:open`, `:in_progress`, `:closed`.
32
+ * **Priority Normalization:** Maps priorities (where available, like in Jira) to a standard scale (e.g., `:low`, `:medium`, `:high`).
33
+
34
+ ## Architecture
35
+
36
+ The gem uses an **Adapter pattern**, with specific adapters (`Adapters::JiraAdapter`, `Adapters::BasecampAdapter`, etc.) implementing a common interface. This allows for easy extension to new platforms.
37
+
38
+ ## Planned Features
39
+
40
+ * CRUD operations for Projects and Tasks.
41
+ * Unified status transitions.
42
+ * Comment management.
43
+ * Standardized error handling and reporting.
44
+ * Webhook support for real-time updates from platforms.
45
+ * Configuration management for API credentials.
46
+ * Utilization of **Mermaid diagrams** to visualize workflows and integration logic within documentation.
47
+
48
+ ## Installation
49
+
50
+ Add this line to your application's Gemfile:
51
+
52
+ ```ruby
53
+ gem 'activeproject'
54
+ ```
55
+
56
+ And then execute:
57
+
58
+ ```bash
59
+ $ bundle install
60
+ ```
61
+
62
+ Or install it yourself as:
63
+
64
+ ```bash
65
+ $ gem install activeproject
66
+ ```
67
+
68
+ ## Usage
69
+
70
+ ### Configuration
71
+
72
+ Configure the adapters in an initializer (e.g., `config/initializers/active_project.rb`):
73
+
74
+ ```ruby
75
+ ActiveProject.configure do |config|
76
+ # Configure Jira Adapter
77
+ config.add_adapter(:jira,
78
+ site_url: ENV.fetch('JIRA_SITE_URL'),
79
+ username: ENV.fetch('JIRA_USERNAME'), # Your Jira email
80
+ api_token: ENV.fetch('JIRA_API_TOKEN')
81
+ )
82
+
83
+
84
+ # Configure Basecamp Adapter
85
+ config.add_adapter(:basecamp,
86
+ account_id: ENV.fetch('BASECAMP_ACCOUNT_ID'),
87
+ access_token: ENV.fetch('BASECAMP_ACCESS_TOKEN')
88
+ )
89
+
90
+ # Configure other adapters later (e.g., :trello, :basecamp)
91
+ # config.add_adapter(:trello, key: '...', token: '...')
92
+ end
93
+ ```
94
+
95
+ ### Basic Usage (Jira Example)
96
+
97
+ ```ruby
98
+ # Get the configured Jira adapter instance
99
+ jira_adapter = ActiveProject.adapter(:jira)
100
+
101
+ begin
102
+ # List projects
103
+ projects = jira_adapter.list_projects
104
+ puts "Found #{projects.count} projects."
105
+ first_project = projects.first
106
+
107
+ if first_project
108
+ puts "Listing issues for project: #{first_project.key}"
109
+ # List issues in the first project
110
+ issues = jira_adapter.list_issues(first_project.key, max_results: 5)
111
+ puts "- Found #{issues.count} issues (showing max 5)."
112
+
113
+ # Find a specific issue (replace 'PROJ-1' with a valid key)
114
+ # issue_key_to_find = 'PROJ-1'
115
+ # issue = jira_adapter.find_issue(issue_key_to_find)
116
+ # puts "- Found issue: #{issue.key} - #{issue.title}"
117
+
118
+ # Create a new issue
119
+ puts "Creating a new issue..."
120
+ new_issue_attributes = {
121
+ project: { key: first_project.key },
122
+ summary: "New task from ActiveProject Gem #{Time.now}",
123
+ issue_type: { name: 'Task' }, # Ensure 'Task' is a valid issue type name
124
+ description: "This issue was created via the ActiveProject gem."
125
+ }
126
+ created_issue = jira_adapter.create_issue(first_project.key, new_issue_attributes)
127
+ puts "- Created issue: #{created_issue.key} - #{created_issue.title}"
128
+
129
+ # Update the issue
130
+ puts "Updating issue #{created_issue.key}..."
131
+ updated_issue = jira_adapter.update_issue(created_issue.key, { summary: "[Updated] #{created_issue.title}" })
132
+ puts "- Updated summary: #{updated_issue.title}"
133
+
134
+ # Add a comment
135
+ puts "Adding comment to issue #{updated_issue.key}..."
136
+ comment = jira_adapter.add_comment(updated_issue.key, "This is a comment added via the ActiveProject gem.")
137
+ puts "- Comment added with ID: #{comment.id}"
138
+ end
139
+
140
+ rescue ActiveProject::AuthenticationError => e
141
+ puts "Error: Jira Authentication Failed - #{e.message}"
142
+ rescue ActiveProject::NotFoundError => e
143
+ puts "Error: Resource Not Found - #{e.message}"
144
+ rescue ActiveProject::ValidationError => e
145
+ puts "Error: Validation Failed - #{e.message} (Details: #{e.errors})"
146
+ rescue ActiveProject::ApiError => e
147
+ puts "Error: Jira API Error (#{e.status_code}) - #{e.message}"
148
+ rescue => e
149
+ puts "An unexpected error occurred: #{e.message}"
150
+ end
151
+ ```
152
+
153
+ ### Basic Usage (Basecamp Example)
154
+
155
+ ```ruby
156
+ # Get the configured Basecamp adapter instance
157
+ basecamp_config = ActiveProject.configuration.adapter_config(:basecamp)
158
+ basecamp_adapter = ActiveProject::Adapters::BasecampAdapter.new(**basecamp_config)
159
+
160
+ begin
161
+ # List projects
162
+ projects = basecamp_adapter.list_projects
163
+ puts "Found #{projects.count} Basecamp projects."
164
+ first_project = projects.first
165
+
166
+ if first_project
167
+ puts "Listing issues (To-dos) for project: #{first_project.name} (ID: #{first_project.id})"
168
+ # List issues (To-dos) in the first project
169
+ # Note: This lists across all to-do lists in the project
170
+ issues = basecamp_adapter.list_issues(first_project.id)
171
+ puts "- Found #{issues.count} To-dos."
172
+
173
+ # Create a new issue (To-do)
174
+ # IMPORTANT: You need a valid todolist_id for the target project.
175
+ # You might need another API call to find a todolist_id first.
176
+ # todolist_id_for_test = 1234567 # Replace with a real ID
177
+ # puts "Creating a new To-do..."
178
+ # new_issue_attributes = {
179
+ # todolist_id: todolist_id_for_test,
180
+ # title: "New BC To-do from ActiveProject Gem #{Time.now}",
181
+ # description: "<em>HTML description</em> for the to-do."
182
+ # }
183
+ # created_issue = basecamp_adapter.create_issue(first_project.id, new_issue_attributes)
184
+ # puts "- Created To-do: #{created_issue.id} - #{created_issue.title}"
185
+
186
+ # --- Operations requiring project_id context (Currently raise NotImplementedError) ---
187
+ # puts "Finding, updating, and commenting require project_id context and are currently not directly usable via the base interface."
188
+ #
189
+ # # Find a specific issue (To-do) - Requires project_id context
190
+ # # todo_id_to_find = created_issue.id
191
+ # # issue = basecamp_adapter.find_issue(todo_id_to_find) # Raises NotImplementedError
192
+ #
193
+ # # Update the issue (To-do) - Requires project_id context
194
+ # # updated_issue = basecamp_adapter.update_issue(created_issue.id, { title: "[Updated] #{created_issue.title}" }) # Raises NotImplementedError
195
+ #
196
+ # # Add a comment - Requires project_id context
197
+ # # comment = basecamp_adapter.add_comment(created_issue.id, "This is an <b>HTML</b> comment.") # Raises NotImplementedError
198
+
199
+ end
200
+
201
+ rescue ActiveProject::AuthenticationError => e
202
+ puts "Error: Basecamp Authentication Failed - #{e.message}"
203
+ rescue ActiveProject::NotFoundError => e
204
+ puts "Error: Basecamp Resource Not Found - #{e.message}"
205
+ rescue ActiveProject::ValidationError => e
206
+ puts "Error: Basecamp Validation Failed - #{e.message}"
207
+ rescue ActiveProject::RateLimitError => e
208
+ puts "Error: Basecamp Rate Limit Exceeded - #{e.message}"
209
+ rescue ActiveProject::ApiError => e
210
+ puts "Error: Basecamp API Error (#{e.status_code}) - #{e.message}"
211
+ rescue NotImplementedError => e
212
+ puts "Error: Method requires project_id context which is not yet implemented: #{e.message}"
213
+ rescue => e
214
+ puts "An unexpected error occurred: #{e.message}"
215
+ end
216
+ ```
217
+
218
+
219
+
220
+ ### Webhook Handling
221
+
222
+ The gem provides helpers for parsing webhook payloads and verifying signatures (where applicable), but you need to implement your own webhook receiver endpoint (e.g., a Rails controller action).
223
+
224
+ ```ruby
225
+ # Example Rails Controller Action
226
+ class WebhooksController < ApplicationController
227
+ # Disable CSRF protection for webhook endpoints
228
+ skip_before_action :verify_authenticity_token
229
+
230
+ def jira_webhook
231
+ adapter = ActiveProject.adapter(:jira)
232
+ request_body = request.body.read
233
+
234
+ # Verification (if applicable and implemented for your Jira setup)
235
+ # signature = request.headers['X-Jira-Signature'] # Example header
236
+ # unless adapter.verify_webhook_signature(request_body, signature)
237
+ # render plain: 'Invalid signature', status: :unauthorized
238
+ # return
239
+ # end
240
+
241
+ # Parse the event
242
+ event = adapter.parse_webhook(request_body, request.headers)
243
+
244
+ if event
245
+ puts "Received Jira Event: #{event.event_type} for #{event.object_kind} #{event.object_key || event.object_id}"
246
+ # Process the event (e.g., queue a background job)
247
+ # handle_event(event)
248
+ else
249
+ puts "Received unhandled or unparseable Jira webhook"
250
+ end
251
+
252
+ head :ok # Respond to Jira quickly
253
+ end
254
+
255
+ def trello_webhook
256
+ adapter = ActiveProject.adapter(:trello)
257
+ request_body = request.body.read
258
+ signature = request.headers['X-Trello-Webhook'] # Trello signature header
259
+ callback_url = request.original_url # The URL Trello sent the webhook to
260
+ trello_api_secret = ENV.fetch('TRELLO_API_SECRET') # You need your secret
261
+
262
+ # Verification (Manual comparison using the helper)
263
+ expected_signature = ActiveProject::Adapters::TrelloAdapter.compute_webhook_signature(
264
+ callback_url,
265
+ request_body,
266
+ trello_api_secret
267
+ )
268
+
269
+ unless ActiveSupport::SecurityUtils.secure_compare(signature, expected_signature)
270
+ render plain: 'Invalid Trello signature', status: :unauthorized
271
+ return
272
+ end
273
+
274
+ # Parse the event
275
+ event = adapter.parse_webhook(request_body)
276
+
277
+ if event
278
+ puts "Received Trello Event: #{event.event_type} for #{event.object_kind} #{event.object_id}"
279
+ # Process the event
280
+ else
281
+ puts "Received unhandled or unparseable Trello webhook"
282
+ end
283
+
284
+ head :ok
285
+ end
286
+
287
+ # Add similar actions for other adapters like Basecamp
288
+
289
+ end
290
+ ```
291
+
292
+ ## Development
293
+
294
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
295
+
296
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `lib/active_project/version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [RubyGems.org](https://rubygems.org).
297
+
298
+ ## Contributing
299
+
300
+ Bug reports and pull requests are welcome on GitHub at https://github.com/seuros/activeproject.
301
+
302
+ ## License
303
+
304
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,3 @@
1
+ require "bundler/setup"
2
+
3
+ require "bundler/gem_tasks"
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveProject
4
+ module Adapters
5
+ # Base abstract class defining the interface for all adapters.
6
+ # Concrete adapters should inherit from this class and implement its abstract methods.
7
+ class Base
8
+ # Lists projects accessible by the configured credentials.
9
+ # @return [Array<ActiveProject::Project>]
10
+ def list_projects
11
+ raise NotImplementedError, "#{self.class.name} must implement #list_projects"
12
+ end
13
+
14
+ # Finds a specific project by its ID or key.
15
+ # @param id [String, Integer] The ID or key of the project.
16
+ # @return [ActiveProject::Project, nil] The project object or nil if not found.
17
+ def find_project(id)
18
+ raise NotImplementedError, "#{self.class.name} must implement #find_project"
19
+ end
20
+
21
+
22
+ # Creates a new project.
23
+ # @param attributes [Hash] Project attributes (platform-specific).
24
+ # @return [ActiveProject::Project] The created project object.
25
+ def create_project(attributes)
26
+ raise NotImplementedError, "#{self.class.name} must implement #create_project"
27
+ end
28
+
29
+ # Creates a new list/container within a project (e.g., Trello List, Basecamp Todolist).
30
+ # Not applicable to all platforms (e.g., Jira statuses are managed differently).
31
+ # @param project_id [String, Integer] The ID or key of the parent project.
32
+ # @param attributes [Hash] List attributes (platform-specific, e.g., :name).
33
+ # @return [Hash] A hash representing the created list (platform-specific structure).
34
+ def create_list(project_id, attributes)
35
+ raise NotImplementedError, "#{self.class.name} does not support #create_list or must implement #create_list"
36
+ end
37
+
38
+
39
+ # Deletes a project. Use with caution.
40
+ # @param project_id [String, Integer] The ID or key of the project to delete.
41
+ # @return [Boolean] true if deletion was successful (or accepted), false otherwise.
42
+ # @raise [NotImplementedError] if deletion is not supported or implemented.
43
+ def delete_project(project_id)
44
+ raise NotImplementedError, "#{self.class.name} does not support #delete_project or must implement it"
45
+ end
46
+
47
+
48
+ # Lists issues within a specific project.
49
+ # @param project_id [String, Integer] The ID or key of the project.
50
+ # @param options [Hash] Optional filtering/pagination options.
51
+ # @return [Array<ActiveProject::Issue>]
52
+ def list_issues(project_id, options = {})
53
+ raise NotImplementedError, "#{self.class.name} must implement #list_issues"
54
+ end
55
+
56
+ # Finds a specific issue by its ID or key.
57
+ # @param id [String, Integer] The ID or key of the issue.
58
+ # @param context [Hash] Optional context hash (e.g., { project_id: '...' } for Basecamp).
59
+ # @return [ActiveProject::Issue, nil] The issue object or nil if not found.
60
+ def find_issue(id, context = {})
61
+ raise NotImplementedError, "#{self.class.name} must implement #find_issue"
62
+ end
63
+
64
+ # Creates a new issue.
65
+ # @param project_id [String, Integer] The ID or key of the project to create the issue in.
66
+ # @param attributes [Hash] Issue attributes (e.g., title, description).
67
+ # @return [ActiveProject::Issue] The created issue object.
68
+ def create_issue(project_id, attributes)
69
+ raise NotImplementedError, "#{self.class.name} must implement #create_issue"
70
+ end
71
+
72
+ # Updates an existing issue.
73
+ # @param id [String, Integer] The ID or key of the issue to update.
74
+ # @param attributes [Hash] Issue attributes to update.
75
+ # @param context [Hash] Optional context hash (e.g., { project_id: '...' } for Basecamp).
76
+ # @return [ActiveProject::Issue] The updated issue object.
77
+ def update_issue(id, attributes, context = {})
78
+ raise NotImplementedError, "#{self.class.name} must implement #update_issue"
79
+ end
80
+
81
+ # Adds a comment to an issue.
82
+ # @param issue_id [String, Integer] The ID or key of the issue.
83
+ # @param comment_body [String] The text of the comment.
84
+ # @param context [Hash] Optional context hash (e.g., { project_id: '...' } for Basecamp).
85
+ # @return [ActiveProject::Comment] The created comment object.
86
+ def add_comment(issue_id, comment_body, context = {})
87
+ raise NotImplementedError, "#{self.class.name} must implement #add_comment"
88
+ end
89
+
90
+ # Verifies the signature of an incoming webhook request, if supported by the platform.
91
+ # @param request_body [String] The raw request body.
92
+ # @param signature_header [String] The value of the platform-specific signature header (e.g., 'X-Trello-Webhook').
93
+ # @return [Boolean] true if the signature is valid or verification is not supported/needed, false otherwise.
94
+ # @raise [NotImplementedError] if verification is applicable but not implemented by a subclass.
95
+ def verify_webhook_signature(request_body, signature_header)
96
+ # Default implementation assumes no verification needed or supported.
97
+ # Adapters supporting verification should override this.
98
+ true
99
+ end
100
+
101
+ # Parses an incoming webhook payload into a standardized WebhookEvent struct.
102
+ # @param request_body [String] The raw request body.
103
+ # @param headers [Hash] Optional hash of request headers (may be needed for event type detection).
104
+ # @return [ActiveProject::WebhookEvent, nil] The parsed event object or nil if the payload is irrelevant/unparseable.
105
+ # @raise [NotImplementedError] if webhook parsing is not implemented for the adapter.
106
+ def parse_webhook(request_body, headers = {})
107
+ raise NotImplementedError, "#{self.class.name} must implement #parse_webhook"
108
+ end
109
+
110
+
111
+ # Retrieves details for the currently authenticated user.
112
+ # @return [ActiveProject::Resources::User] The user object.
113
+ # @raise [ActiveProject::AuthenticationError] if authentication fails.
114
+ # @raise [ActiveProject::ApiError] for other API-related errors.
115
+ def get_current_user
116
+ raise NotImplementedError, "#{self.class.name} must implement #get_current_user"
117
+ end
118
+
119
+ # Checks if the adapter can successfully authenticate and connect to the service.
120
+ # Typically calls #get_current_user internally and catches authentication errors.
121
+ # @return [Boolean] true if connection is successful, false otherwise.
122
+ def connected?
123
+ raise NotImplementedError, "#{self.class.name} must implement #connected?"
124
+ end
125
+
126
+
127
+ # Placeholder comments for data structures (to be defined elsewhere)
128
+ # Example:
129
+ # Project = Struct.new(:id, :key, :name, :adapter_source, keyword_init: true)
130
+ # Issue = Struct.new(:id, :key, :title, :description, :status, :assignee, :project_id, :adapter_source, keyword_init: true)
131
+ # Comment = Struct.new(:id, :body, :author, :created_at, :issue_id, :adapter_source, keyword_init: true)
132
+ end
133
+ end
134
+ end