activeproject 0.3.0 → 0.5.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 +4 -4
- data/README.md +201 -55
- data/lib/active_project/adapters/base.rb +154 -14
- data/lib/active_project/adapters/basecamp/comments.rb +34 -0
- data/lib/active_project/adapters/basecamp/connection.rb +6 -24
- data/lib/active_project/adapters/basecamp/issues.rb +6 -5
- data/lib/active_project/adapters/basecamp/webhooks.rb +7 -8
- data/lib/active_project/adapters/fizzy/columns.rb +116 -0
- data/lib/active_project/adapters/fizzy/comments.rb +129 -0
- data/lib/active_project/adapters/fizzy/connection.rb +41 -0
- data/lib/active_project/adapters/fizzy/issues.rb +221 -0
- data/lib/active_project/adapters/fizzy/projects.rb +105 -0
- data/lib/active_project/adapters/fizzy_adapter.rb +151 -0
- data/lib/active_project/adapters/github_project/comments.rb +91 -0
- data/lib/active_project/adapters/github_project/connection.rb +58 -0
- data/lib/active_project/adapters/github_project/helpers.rb +100 -0
- data/lib/active_project/adapters/github_project/issues.rb +287 -0
- data/lib/active_project/adapters/github_project/projects.rb +139 -0
- data/lib/active_project/adapters/github_project/webhooks.rb +168 -0
- data/lib/active_project/adapters/github_project.rb +8 -0
- data/lib/active_project/adapters/github_project_adapter.rb +65 -0
- data/lib/active_project/adapters/github_repo/connection.rb +62 -0
- data/lib/active_project/adapters/github_repo/issues.rb +242 -0
- data/lib/active_project/adapters/github_repo/projects.rb +116 -0
- data/lib/active_project/adapters/github_repo/webhooks.rb +354 -0
- data/lib/active_project/adapters/github_repo_adapter.rb +134 -0
- data/lib/active_project/adapters/jira/attribute_normalizer.rb +16 -0
- data/lib/active_project/adapters/jira/comments.rb +41 -0
- data/lib/active_project/adapters/jira/connection.rb +15 -15
- data/lib/active_project/adapters/jira/issues.rb +21 -7
- data/lib/active_project/adapters/jira/projects.rb +3 -1
- data/lib/active_project/adapters/jira/transitions.rb +2 -1
- data/lib/active_project/adapters/jira/webhooks.rb +5 -7
- data/lib/active_project/adapters/jira_adapter.rb +23 -3
- data/lib/active_project/adapters/trello/comments.rb +34 -0
- data/lib/active_project/adapters/trello/connection.rb +12 -9
- data/lib/active_project/adapters/trello/issues.rb +7 -5
- data/lib/active_project/adapters/trello/webhooks.rb +5 -7
- data/lib/active_project/adapters/trello_adapter.rb +5 -3
- data/lib/active_project/association_proxy.rb +3 -2
- data/lib/active_project/configuration.rb +6 -3
- data/lib/active_project/configurations/base_adapter_configuration.rb +102 -0
- data/lib/active_project/configurations/basecamp_configuration.rb +42 -0
- data/lib/active_project/configurations/fizzy_configuration.rb +47 -0
- data/lib/active_project/configurations/github_configuration.rb +57 -0
- data/lib/active_project/configurations/jira_configuration.rb +54 -0
- data/lib/active_project/configurations/trello_configuration.rb +24 -2
- data/lib/active_project/connections/base.rb +35 -0
- data/lib/active_project/connections/graph_ql.rb +83 -0
- data/lib/active_project/connections/http_client.rb +79 -0
- data/lib/active_project/connections/pagination.rb +44 -0
- data/lib/active_project/connections/rest.rb +33 -0
- data/lib/active_project/error_mapper.rb +38 -0
- data/lib/active_project/errors.rb +13 -0
- data/lib/active_project/railtie.rb +1 -3
- data/lib/active_project/resources/base_resource.rb +13 -14
- data/lib/active_project/resources/comment.rb +46 -2
- data/lib/active_project/resources/issue.rb +106 -18
- data/lib/active_project/resources/persistable_resource.rb +47 -0
- data/lib/active_project/resources/project.rb +1 -1
- data/lib/active_project/status_mapper.rb +145 -0
- data/lib/active_project/version.rb +1 -1
- data/lib/active_project/webhook_event.rb +34 -12
- data/lib/activeproject.rb +9 -6
- metadata +74 -16
- data/lib/active_project/adapters/http_client.rb +0 -71
- data/lib/active_project/adapters/pagination.rb +0 -68
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b9726d3cbaf8990833d420df514aeb0e7f283bc443756406f1600e95e73a3f79
|
|
4
|
+
data.tar.gz: f641d36b536206152d300ccc7f054a6ebe0768737eb2a6d658c45a044edd5938
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b86037bbf5ba508e1c7544bdd8aa5787db7a7b340c211029dcb7b4348a45b313b13a8d8e16e90ef083c86a291c332465efafe4e9079a4f8015ff3a4c14850786
|
|
7
|
+
data.tar.gz: 4dd7f59b51e515dae0a57299841573343983160a030adc62c146b9ad8858be262c249cb6b8f2a58e81c8241559a943263bf0b15dc5f31c1b35a86c8604eb6d5e
|
data/README.md
CHANGED
|
@@ -1,50 +1,68 @@
|
|
|
1
1
|
# ActiveProject Gem
|
|
2
2
|
|
|
3
|
-
A standardized Ruby interface for multiple project
|
|
3
|
+
A standardized Ruby interface for multiple project-management APIs
|
|
4
|
+
(Jira, Basecamp, Trello, GitHub Projects, …).
|
|
4
5
|
|
|
5
6
|
## Problem
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
Every platform—Jira, Basecamp, Trello, GitHub—ships its **own** authentication flow, error vocabulary, data model, and workflow quirks.
|
|
9
|
+
Teams end up maintaining a grab-bag of fragile, bespoke clients.
|
|
8
10
|
|
|
9
11
|
## Solution
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
ActiveProject wraps those APIs behind a single, opinionated interface:
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
| Feature | What you get |
|
|
16
|
+
|---------|--------------|
|
|
17
|
+
| **Normalized models** | `Project`, `Issue` (Task/Card/To-do), `Comment`, `User`—same Ruby objects everywhere. |
|
|
18
|
+
| **Standard CRUD** | `issue.close!`, `issue.reopen!`, `project.list_issues`, etc. |
|
|
19
|
+
| **Unified errors** | `AuthenticationError`, `NotFoundError`, `RateLimitError`, … regardless of the backend. |
|
|
20
|
+
| **Co-operative concurrency** | Fiber-based I/O (via [`async`](https://github.com/socketry/async)) for painless parallel fan-out. |
|
|
17
21
|
|
|
18
|
-
## Supported Platforms
|
|
19
22
|
|
|
20
|
-
|
|
23
|
+
## Supported Platforms (initial wave)
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
| Platform | API | Notes |
|
|
26
|
+
|---------------------------|------------|------------------------------|
|
|
27
|
+
| **Jira** (Cloud & Server) | REST v3 | Full issue & project support |
|
|
28
|
+
| **Basecamp** | REST v3+ | Maps To-dos ↔ Issues |
|
|
29
|
+
| **Trello** | REST | Cards ↔ Issues |
|
|
30
|
+
| **GitHub Projects V2** | GraphQL v4 | |
|
|
31
|
+
| **GitHub** | REST v3 | Issues and repositories |
|
|
25
32
|
|
|
26
|
-
|
|
33
|
+
_Planned next_: Asana, Monday.com, Linear, etc.
|
|
27
34
|
|
|
28
35
|
## Core Concepts
|
|
29
36
|
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
37
|
+
* **Project** – Jira Project, Basecamp Project, Trello Board, GitHub ProjectV2, or GitHub Repository.
|
|
38
|
+
* **Issue** – Unified wrapper around Jira Issue, Basecamp To-do, Trello Card, GitHub Issue/PR.
|
|
39
|
+
*GitHub DraftIssues intentionally omitted for now.*
|
|
40
|
+
* **Status** – Normalized to `:open`, `:in_progress`, `:blocked`, `:on_hold`, `:closed`.
|
|
41
|
+
* **Priority** – Normalized to `:low`, `:medium`, `:high` (where supported).
|
|
42
|
+
|
|
43
|
+
---
|
|
34
44
|
|
|
35
45
|
## Architecture
|
|
36
46
|
|
|
37
|
-
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
ActiveProject
|
|
50
|
+
└── Adapters
|
|
51
|
+
├── JiraAdapter
|
|
52
|
+
├── BasecampAdapter
|
|
53
|
+
├── TrelloAdapter
|
|
54
|
+
├── GithubProjectAdapter
|
|
55
|
+
└── GithubAdapter
|
|
56
|
+
|
|
57
|
+
````
|
|
58
|
+
Add a new platform by subclassing and conforming to the common contract.
|
|
59
|
+
---
|
|
38
60
|
|
|
39
61
|
## Planned Features
|
|
40
62
|
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* Standardized error handling and reporting.
|
|
45
|
-
* Webhook support for real-time updates from platforms.
|
|
46
|
-
* Configuration management for API credentials.
|
|
47
|
-
* Utilization of **Mermaid diagrams** to visualize workflows and integration logic within documentation.
|
|
63
|
+
* Webhook helpers for real-time updates
|
|
64
|
+
* Centralised credential/config store
|
|
65
|
+
* Mermaid diagrams for docs & SDK flow-charts
|
|
48
66
|
|
|
49
67
|
## Installation
|
|
50
68
|
|
|
@@ -74,31 +92,29 @@ Configure multiple adapters, optionally with named instances (default is `:prima
|
|
|
74
92
|
|
|
75
93
|
```ruby
|
|
76
94
|
ActiveProject.configure do |config|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
token: ENV.fetch('TRELLO_TOKEN')
|
|
101
|
-
)
|
|
95
|
+
config.add_adapter :jira,
|
|
96
|
+
site_url: ENV["JIRA_SITE_URL"],
|
|
97
|
+
username: ENV["JIRA_USERNAME"],
|
|
98
|
+
api_token: ENV["JIRA_API_TOKEN"]
|
|
99
|
+
|
|
100
|
+
config.add_adapter :basecamp,
|
|
101
|
+
account_id: ENV["BASECAMP_ACCOUNT_ID"],
|
|
102
|
+
access_token: ENV["BASECAMP_ACCESS_TOKEN"]
|
|
103
|
+
|
|
104
|
+
config.add_adapter :trello,
|
|
105
|
+
key: ENV["TRELLO_KEY"],
|
|
106
|
+
token: ENV["TRELLO_TOKEN"]
|
|
107
|
+
|
|
108
|
+
# GitHub Projects – real Issues/PRs only
|
|
109
|
+
config.add_adapter :github_project,
|
|
110
|
+
access_token: ENV["GITHUB_TOKEN"]
|
|
111
|
+
|
|
112
|
+
# GitHub primary instance
|
|
113
|
+
config.add_adapter :github,
|
|
114
|
+
owner: ENV["GITHUB_OWNER"],
|
|
115
|
+
repo: ENV["GITHUB_REPO"],
|
|
116
|
+
access_token: ENV["GITHUB_ACCESS_TOKEN"],
|
|
117
|
+
webhook_secret: ENV["GITHUB_WEBHOOK_SECRET"]
|
|
102
118
|
end
|
|
103
119
|
```
|
|
104
120
|
|
|
@@ -111,6 +127,8 @@ jira_primary = ActiveProject.adapter(:jira) # defaults to :primary
|
|
|
111
127
|
jira_secondary = ActiveProject.adapter(:jira, :secondary)
|
|
112
128
|
basecamp = ActiveProject.adapter(:basecamp) # defaults to :primary
|
|
113
129
|
trello = ActiveProject.adapter(:trello) # defaults to :primary
|
|
130
|
+
github = ActiveProject.adapter(:github) # defaults to :primary
|
|
131
|
+
github_project = ActiveProject.adapter(:github_project) # defaults to :primary
|
|
114
132
|
```
|
|
115
133
|
|
|
116
134
|
### Basic Usage (Jira Example)
|
|
@@ -175,8 +193,7 @@ end
|
|
|
175
193
|
|
|
176
194
|
```ruby
|
|
177
195
|
# Get the configured Basecamp adapter instance
|
|
178
|
-
|
|
179
|
-
basecamp_adapter = ActiveProject::Adapters::BasecampAdapter.new(**basecamp_config)
|
|
196
|
+
basecamp_adapter = ActiveProject.adapter(:basecamp)
|
|
180
197
|
|
|
181
198
|
begin
|
|
182
199
|
# List projects
|
|
@@ -238,15 +255,13 @@ end
|
|
|
238
255
|
|
|
239
256
|
## Asynchronous I/O
|
|
240
257
|
|
|
241
|
-
ActiveProject ships with `async-http` under the hood.
|
|
258
|
+
ActiveProject ships with `async-http` under the hood.
|
|
242
259
|
Enable the non-blocking adapter by setting an ENV var **before** your process boots:
|
|
243
260
|
|
|
244
261
|
```bash
|
|
245
262
|
AP_DEFAULT_ADAPTER=async_http
|
|
246
263
|
```
|
|
247
264
|
|
|
248
|
-
### Parallel fan-out example
|
|
249
|
-
|
|
250
265
|
```ruby
|
|
251
266
|
ActiveProject::Async.run do |task|
|
|
252
267
|
jira = ActiveProject.adapter(:jira)
|
|
@@ -286,6 +301,137 @@ If another gem (e.g. Falcon) already set a scheduler, ActiveProject detects it a
|
|
|
286
301
|
|
|
287
302
|
---
|
|
288
303
|
|
|
304
|
+
### Basic Usage (GitHub Example)
|
|
305
|
+
|
|
306
|
+
```ruby
|
|
307
|
+
# Get the configured GitHub adapter instance
|
|
308
|
+
github_adapter = ActiveProject.adapter(:github)
|
|
309
|
+
|
|
310
|
+
begin
|
|
311
|
+
# With GitHub adapter, a repository is treated as a project.
|
|
312
|
+
# The adapter is configured for a specific repository.
|
|
313
|
+
|
|
314
|
+
# List projects (will return the configured repository)
|
|
315
|
+
projects = github_adapter.list_projects
|
|
316
|
+
puts "Found #{projects.count} GitHub repository."
|
|
317
|
+
repo = projects.first
|
|
318
|
+
|
|
319
|
+
if repo
|
|
320
|
+
puts "Working with repository: #{repo.key} (#{repo.name})"
|
|
321
|
+
|
|
322
|
+
# List issues in the repository
|
|
323
|
+
issues = github_adapter.list_issues(repo.key)
|
|
324
|
+
puts "- Found #{issues.count} issues."
|
|
325
|
+
|
|
326
|
+
# Find a specific issue (replace '1' with a valid issue number)
|
|
327
|
+
# issue_number = 1
|
|
328
|
+
# issue = github_adapter.find_issue(issue_number.to_s)
|
|
329
|
+
# puts "- Found issue: ##{issue.key} - #{issue.title}"
|
|
330
|
+
|
|
331
|
+
# Create a new issue
|
|
332
|
+
puts "Creating a new issue..."
|
|
333
|
+
new_issue_attributes = {
|
|
334
|
+
title: "New issue from ActiveProject Gem #{Time.now}",
|
|
335
|
+
description: "This issue was created via the ActiveProject gem.",
|
|
336
|
+
assignees: [{ name: "username" }] # Optional: Provide GitHub usernames for assignees
|
|
337
|
+
}
|
|
338
|
+
created_issue = github_adapter.create_issue(repo.key, new_issue_attributes)
|
|
339
|
+
puts "- Created issue: ##{created_issue.key} - #{created_issue.title}"
|
|
340
|
+
|
|
341
|
+
# Update the issue
|
|
342
|
+
puts "Updating issue ##{created_issue.key}..."
|
|
343
|
+
updated_issue = github_adapter.update_issue(created_issue.key, { title: "[Updated] #{created_issue.title}" })
|
|
344
|
+
puts "- Updated title: #{updated_issue.title}"
|
|
345
|
+
|
|
346
|
+
# Add a comment
|
|
347
|
+
puts "Adding comment to issue ##{updated_issue.key}..."
|
|
348
|
+
comment = github_adapter.add_comment(updated_issue.key, "This is a comment added via the ActiveProject gem.")
|
|
349
|
+
puts "- Comment added with ID: #{comment.id}"
|
|
350
|
+
|
|
351
|
+
# Close the issue
|
|
352
|
+
puts "Closing issue ##{updated_issue.key}..."
|
|
353
|
+
closed_issue = github_adapter.update_issue(updated_issue.key, { state: "closed" })
|
|
354
|
+
puts "- Issue closed, status: #{closed_issue.status}"
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
rescue ActiveProject::AuthenticationError => e
|
|
358
|
+
puts "Error: GitHub Authentication Failed - #{e.message}"
|
|
359
|
+
rescue ActiveProject::NotFoundError => e
|
|
360
|
+
puts "Error: Resource Not Found - #{e.message}"
|
|
361
|
+
rescue ActiveProject::ValidationError => e
|
|
362
|
+
puts "Error: Validation Failed - #{e.message}"
|
|
363
|
+
rescue ActiveProject::RateLimitError => e
|
|
364
|
+
puts "Error: GitHub Rate Limit Exceeded - #{e.message}"
|
|
365
|
+
rescue ActiveProject::ApiError => e
|
|
366
|
+
puts "Error: GitHub API Error (#{e.status_code}) - #{e.message}"
|
|
367
|
+
rescue => e
|
|
368
|
+
puts "An unexpected error occurred: #{e.message}"
|
|
369
|
+
end
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### Webhook Support (GitHub Example)
|
|
373
|
+
|
|
374
|
+
The GitHub adapter includes support for processing GitHub webhooks, which allows you to receive real-time events when issues are created, updated, commented on, etc.
|
|
375
|
+
|
|
376
|
+
```ruby
|
|
377
|
+
# Configure the adapter with a webhook secret (same secret used in GitHub webhook settings)
|
|
378
|
+
ActiveProject.configure do |config|
|
|
379
|
+
config.add_adapter(:github,
|
|
380
|
+
owner: ENV.fetch('GITHUB_OWNER'),
|
|
381
|
+
repo: ENV.fetch('GITHUB_REPO'),
|
|
382
|
+
access_token: ENV.fetch('GITHUB_ACCESS_TOKEN'),
|
|
383
|
+
webhook_secret: ENV.fetch('GITHUB_WEBHOOK_SECRET', nil)
|
|
384
|
+
)
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
# In your webhook endpoint (e.g., in a Rails controller)
|
|
388
|
+
def github_webhook
|
|
389
|
+
adapter = ActiveProject.adapter(:github)
|
|
390
|
+
|
|
391
|
+
# Get the raw request body and signature header
|
|
392
|
+
request_body = request.raw_post
|
|
393
|
+
signature = request.headers['X-Hub-Signature-256']
|
|
394
|
+
|
|
395
|
+
# Verify the webhook signature (if webhook_secret is configured)
|
|
396
|
+
if adapter.verify_webhook_signature(request_body, signature)
|
|
397
|
+
# Parse the webhook payload into a standardized WebhookEvent
|
|
398
|
+
event_headers = {
|
|
399
|
+
'X-GitHub-Event' => request.headers['X-GitHub-Event'],
|
|
400
|
+
'X-GitHub-Delivery' => request.headers['X-GitHub-Delivery']
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
webhook_event = adapter.parse_webhook(request_body, event_headers)
|
|
404
|
+
|
|
405
|
+
if webhook_event
|
|
406
|
+
# Process different event types
|
|
407
|
+
case webhook_event.type
|
|
408
|
+
when :issue_created
|
|
409
|
+
issue = webhook_event.data[:issue]
|
|
410
|
+
puts "New issue created: ##{issue.key} - #{issue.title}"
|
|
411
|
+
|
|
412
|
+
when :issue_updated, :issue_closed, :issue_reopened
|
|
413
|
+
issue = webhook_event.data[:issue]
|
|
414
|
+
puts "Issue ##{issue.key} was #{webhook_event.type.to_s.sub('issue_', '')}"
|
|
415
|
+
|
|
416
|
+
when :comment_created
|
|
417
|
+
comment = webhook_event.data[:comment]
|
|
418
|
+
issue = webhook_event.data[:issue]
|
|
419
|
+
puts "New comment on issue ##{issue.key}: #{comment.body.truncate(50)}"
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
# The webhook was successfully processed
|
|
423
|
+
head :ok
|
|
424
|
+
else
|
|
425
|
+
# The webhook payload couldn't be parsed
|
|
426
|
+
head :unprocessable_entity
|
|
427
|
+
end
|
|
428
|
+
else
|
|
429
|
+
# The webhook signature verification failed
|
|
430
|
+
head :forbidden
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
```
|
|
434
|
+
|
|
289
435
|
## Development
|
|
290
436
|
|
|
291
437
|
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.
|
|
@@ -1,10 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../status_mapper"
|
|
4
|
+
|
|
3
5
|
module ActiveProject
|
|
4
6
|
module Adapters
|
|
5
7
|
# Base abstract class defining the interface for all adapters.
|
|
6
8
|
# Concrete adapters should inherit from this class and implement its abstract methods.
|
|
7
9
|
class Base
|
|
10
|
+
include ErrorMapper
|
|
11
|
+
|
|
12
|
+
# ─────────────────── Central HTTP-status → exception map ────────────
|
|
13
|
+
rescue_status 401..403, with: ActiveProject::AuthenticationError
|
|
14
|
+
rescue_status 404, with: ActiveProject::NotFoundError
|
|
15
|
+
rescue_status 429, with: ActiveProject::RateLimitError
|
|
16
|
+
rescue_status 400, 422, with: ActiveProject::ValidationError
|
|
17
|
+
|
|
18
|
+
attr_reader :config
|
|
19
|
+
|
|
20
|
+
def initialize(config:)
|
|
21
|
+
@config = config
|
|
22
|
+
@status_mapper = StatusMapper.from_config(adapter_type, config)
|
|
23
|
+
end
|
|
24
|
+
|
|
8
25
|
# Lists projects accessible by the configured credentials.
|
|
9
26
|
# @return [Array<ActiveProject::Project>]
|
|
10
27
|
def list_projects
|
|
@@ -52,8 +69,13 @@ module ActiveProject
|
|
|
52
69
|
|
|
53
70
|
# Finds a specific issue by its ID or key.
|
|
54
71
|
# @param id [String, Integer] The ID or key of the issue.
|
|
55
|
-
# @param context [Hash] Optional context hash
|
|
72
|
+
# @param context [Hash] Optional context hash. Platform-specific requirements:
|
|
73
|
+
# - Basecamp: REQUIRES { project_id: '...' }
|
|
74
|
+
# - Jira: Optional { fields: '...' }
|
|
75
|
+
# - Trello: Optional { fields: '...' }
|
|
76
|
+
# - GitHub: Ignored
|
|
56
77
|
# @return [ActiveProject::Issue, nil] The issue object or nil if not found.
|
|
78
|
+
# @raise [ArgumentError] if required context is missing (platform-specific).
|
|
57
79
|
def find_issue(id, context = {})
|
|
58
80
|
raise NotImplementedError, "#{self.class.name} must implement #find_issue"
|
|
59
81
|
end
|
|
@@ -69,14 +91,30 @@ module ActiveProject
|
|
|
69
91
|
# Updates an existing issue.
|
|
70
92
|
# @param id [String, Integer] The ID or key of the issue to update.
|
|
71
93
|
# @param attributes [Hash] Issue attributes to update.
|
|
72
|
-
# @param context [Hash] Optional context hash
|
|
94
|
+
# @param context [Hash] Optional context hash. Platform-specific requirements:
|
|
95
|
+
# - Basecamp: REQUIRES { project_id: '...' }
|
|
96
|
+
# - Jira: Optional { fields: '...' }
|
|
97
|
+
# - Trello: Optional { fields: '...' }
|
|
98
|
+
# - GitHub: Uses different signature: update_issue(project_id, item_id, attrs)
|
|
73
99
|
# @return [ActiveProject::Issue] The updated issue object.
|
|
100
|
+
# @raise [ArgumentError] if required context is missing (platform-specific).
|
|
101
|
+
# @note GitHub adapter overrides this with update_issue(project_id, item_id, attrs)
|
|
102
|
+
# due to GraphQL API requirements for project-specific field operations.
|
|
74
103
|
def update_issue(id, attributes, context = {})
|
|
75
104
|
raise NotImplementedError, "#{self.class.name} must implement #update_issue"
|
|
76
105
|
end
|
|
77
106
|
|
|
78
|
-
#
|
|
79
|
-
#
|
|
107
|
+
# Deletes an issue from a project.
|
|
108
|
+
# @param id [String, Integer] The ID or key of the issue to delete.
|
|
109
|
+
# @param context [Hash] Optional context hash. Platform-specific requirements:
|
|
110
|
+
# - Basecamp: REQUIRES { project_id: '...' }
|
|
111
|
+
# - Jira: Optional { delete_subtasks: true/false }
|
|
112
|
+
# - Trello: Ignored
|
|
113
|
+
# - GitHub: Uses different signature: delete_issue(project_id, item_id)
|
|
114
|
+
# @return [Boolean] true if deletion was successful.
|
|
115
|
+
# @raise [ArgumentError] if required context is missing (platform-specific).
|
|
116
|
+
# @note GitHub adapter overrides this with delete_issue(project_id, item_id)
|
|
117
|
+
# due to GraphQL API requirements.
|
|
80
118
|
def delete_issue(id, context = {})
|
|
81
119
|
raise NotImplementedError, "The #{self.class.name} adapter does not implement delete_issue"
|
|
82
120
|
end
|
|
@@ -84,21 +122,79 @@ module ActiveProject
|
|
|
84
122
|
# Adds a comment to an issue.
|
|
85
123
|
# @param issue_id [String, Integer] The ID or key of the issue.
|
|
86
124
|
# @param comment_body [String] The text of the comment.
|
|
87
|
-
# @param context [Hash] Optional context hash
|
|
125
|
+
# @param context [Hash] Optional context hash. Platform-specific requirements:
|
|
126
|
+
# - Basecamp: REQUIRES { project_id: '...' }
|
|
127
|
+
# - Jira: Ignored
|
|
128
|
+
# - Trello: Ignored
|
|
129
|
+
# - GitHub: Optional { content_node_id: '...' } for optimization
|
|
88
130
|
# @return [ActiveProject::Comment] The created comment object.
|
|
131
|
+
# @raise [ArgumentError] if required context is missing (platform-specific).
|
|
89
132
|
def add_comment(issue_id, comment_body, context = {})
|
|
90
133
|
raise NotImplementedError, "#{self.class.name} must implement #add_comment"
|
|
91
134
|
end
|
|
92
135
|
|
|
136
|
+
# Updates an existing comment.
|
|
137
|
+
# @param comment_id [String, Integer] The ID of the comment to update.
|
|
138
|
+
# @param body [String] The new comment body text.
|
|
139
|
+
# @param context [Hash] Optional context hash. Platform-specific requirements:
|
|
140
|
+
# - Basecamp: REQUIRES { project_id: '...' }
|
|
141
|
+
# - Jira: REQUIRES { issue_id: '...' }
|
|
142
|
+
# - Trello: Ignored
|
|
143
|
+
# - GitHub: Ignored
|
|
144
|
+
# @return [ActiveProject::Comment] The updated comment object.
|
|
145
|
+
# @raise [NotImplementedError] if the adapter does not support comment updates.
|
|
146
|
+
def update_comment(comment_id, body, context = {})
|
|
147
|
+
raise NotImplementedError, "#{self.class.name} does not support #update_comment"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Deletes a comment.
|
|
151
|
+
# @param comment_id [String, Integer] The ID of the comment to delete.
|
|
152
|
+
# @param context [Hash] Optional context hash. Platform-specific requirements:
|
|
153
|
+
# - Basecamp: REQUIRES { project_id: '...' }
|
|
154
|
+
# - Jira: REQUIRES { issue_id: '...' }
|
|
155
|
+
# - Trello: Ignored
|
|
156
|
+
# - GitHub: Ignored
|
|
157
|
+
# @return [Boolean] true if deletion was successful.
|
|
158
|
+
# @raise [NotImplementedError] if the adapter does not support comment deletion.
|
|
159
|
+
def delete_comment(comment_id, context = {})
|
|
160
|
+
raise NotImplementedError, "#{self.class.name} does not support #delete_comment"
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Checks if the adapter supports webhook processing.
|
|
164
|
+
# @return [Boolean] true if the adapter can process webhooks
|
|
165
|
+
def supports_webhooks?
|
|
166
|
+
respond_to?(:parse_webhook, true) &&
|
|
167
|
+
!method(:parse_webhook).source_location.nil? &&
|
|
168
|
+
method(:parse_webhook).source_location[0] != __FILE__
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Returns the webhook type identifier for this adapter class.
|
|
172
|
+
# This distinguishes between different adapter types for the same platform.
|
|
173
|
+
# @return [Symbol] The webhook type (e.g., :github_repo, :github_project, :jira, :basecamp)
|
|
174
|
+
def self.webhook_type
|
|
175
|
+
# Default implementation extracts from class name
|
|
176
|
+
# GithubRepoAdapter -> :github_repo, BasecampAdapter -> :basecamp
|
|
177
|
+
class_name = name.split("::").last
|
|
178
|
+
class_name.gsub(/Adapter$/, "").gsub(/([a-z])([A-Z])/, '\1_\2').downcase.to_sym
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Instance method that delegates to class method
|
|
182
|
+
# @return [Symbol] The webhook type
|
|
183
|
+
def webhook_type
|
|
184
|
+
self.class.webhook_type
|
|
185
|
+
end
|
|
186
|
+
|
|
93
187
|
# Verifies the signature of an incoming webhook request, if supported by the platform.
|
|
94
|
-
# @param
|
|
95
|
-
# @param
|
|
96
|
-
# @
|
|
97
|
-
# @
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
#
|
|
101
|
-
|
|
188
|
+
# @param request_body [String] The raw request body.
|
|
189
|
+
# @param signature_header [String] The value of the platform-specific signature header.
|
|
190
|
+
# @param webhook_secret [String] Optional webhook secret for verification.
|
|
191
|
+
# @return [Boolean] true if the signature is valid or verification is not supported, false otherwise.
|
|
192
|
+
# @note Override this method in adapter subclasses to implement platform-specific verification.
|
|
193
|
+
def verify_webhook_signature(request_body, signature_header, webhook_secret: nil)
|
|
194
|
+
# Default implementation assumes no verification needed.
|
|
195
|
+
# Adapters supporting verification should override this method.
|
|
196
|
+
return true unless supports_webhooks? # Allow non-webhook flows by default
|
|
197
|
+
false # Adapters must override this method to implement verification
|
|
102
198
|
end
|
|
103
199
|
|
|
104
200
|
# Parses an incoming webhook payload into a standardized WebhookEvent struct.
|
|
@@ -107,7 +203,9 @@ module ActiveProject
|
|
|
107
203
|
# @return [ActiveProject::WebhookEvent, nil] The parsed event object or nil if the payload is irrelevant/unparseable.
|
|
108
204
|
# @raise [NotImplementedError] if webhook parsing is not implemented for the adapter.
|
|
109
205
|
def parse_webhook(request_body, headers = {})
|
|
110
|
-
raise NotImplementedError,
|
|
206
|
+
raise NotImplementedError,
|
|
207
|
+
"#{self.class.name} does not support webhook parsing. " \
|
|
208
|
+
"Webhook support is optional. Check #supports_webhooks? before calling this method."
|
|
111
209
|
end
|
|
112
210
|
|
|
113
211
|
# Retrieves details for the currently authenticated user.
|
|
@@ -124,6 +222,48 @@ module ActiveProject
|
|
|
124
222
|
def connected?
|
|
125
223
|
raise NotImplementedError, "#{self.class.name} must implement #connected?"
|
|
126
224
|
end
|
|
225
|
+
|
|
226
|
+
# Adapters that do **not** support a custom “status” field can simply rely
|
|
227
|
+
# on this default implementation. Adapters that _do_ care (e.g. the
|
|
228
|
+
# GitHub project adapter which knows its single-select options) already
|
|
229
|
+
# override it.
|
|
230
|
+
#
|
|
231
|
+
# @return [Boolean] _true_ if the symbol is safe to pass through.
|
|
232
|
+
def status_known?(project_id, status_sym)
|
|
233
|
+
@status_mapper.status_known?(status_sym, project_id: project_id)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Returns all valid statuses for the given project context.
|
|
237
|
+
# @param project_id [String, Integer] The project context
|
|
238
|
+
# @return [Array<Symbol>] Array of valid status symbols
|
|
239
|
+
def valid_statuses(project_id = nil)
|
|
240
|
+
@status_mapper.valid_statuses(project_id: project_id)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Normalizes a platform-specific status to a standard symbol.
|
|
244
|
+
# @param platform_status [String, Symbol] Platform-specific status
|
|
245
|
+
# @param project_id [String, Integer] Optional project context
|
|
246
|
+
# @return [Symbol] Normalized status symbol
|
|
247
|
+
def normalize_status(platform_status, project_id: nil)
|
|
248
|
+
@status_mapper.normalize_status(platform_status, project_id: project_id)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Converts a normalized status back to platform-specific format.
|
|
252
|
+
# @param normalized_status [Symbol] Normalized status symbol
|
|
253
|
+
# @param project_id [String, Integer] Optional project context
|
|
254
|
+
# @return [String, Symbol] Platform-specific status
|
|
255
|
+
def denormalize_status(normalized_status, project_id: nil)
|
|
256
|
+
@status_mapper.denormalize_status(normalized_status, project_id: project_id)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
protected
|
|
260
|
+
|
|
261
|
+
# Returns the adapter type symbol for status mapping.
|
|
262
|
+
# Override in subclasses if the adapter type differs from class name pattern.
|
|
263
|
+
# @return [Symbol] The adapter type
|
|
264
|
+
def adapter_type
|
|
265
|
+
self.class.name.split("::").last.gsub("Adapter", "").downcase.to_sym
|
|
266
|
+
end
|
|
127
267
|
end
|
|
128
268
|
end
|
|
129
269
|
end
|
|
@@ -21,6 +21,40 @@ module ActiveProject
|
|
|
21
21
|
comment_data = make_request(:post, path, payload)
|
|
22
22
|
map_comment_data(comment_data, todo_id.to_i)
|
|
23
23
|
end
|
|
24
|
+
|
|
25
|
+
# Updates a comment on a To-do in Basecamp.
|
|
26
|
+
# @param comment_id [String, Integer] The ID of the comment.
|
|
27
|
+
# @param body [String] The new comment text (HTML).
|
|
28
|
+
# @param context [Hash] Required context: { project_id: '...' }.
|
|
29
|
+
# @return [ActiveProject::Resources::Comment] The updated comment resource.
|
|
30
|
+
def update_comment(comment_id, body, context = {})
|
|
31
|
+
project_id = context[:project_id]
|
|
32
|
+
unless project_id
|
|
33
|
+
raise ArgumentError,
|
|
34
|
+
"Missing required context: :project_id must be provided for BasecampAdapter#update_comment"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
path = "buckets/#{project_id}/comments/#{comment_id}.json"
|
|
38
|
+
payload = { content: body }.to_json
|
|
39
|
+
comment_data = make_request(:put, path, payload)
|
|
40
|
+
map_comment_data(comment_data, comment_data["parent"]&.dig("id"))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Deletes a comment from a To-do in Basecamp.
|
|
44
|
+
# @param comment_id [String, Integer] The ID of the comment to delete.
|
|
45
|
+
# @param context [Hash] Required context: { project_id: '...' }.
|
|
46
|
+
# @return [Boolean] True if successfully deleted.
|
|
47
|
+
def delete_comment(comment_id, context = {})
|
|
48
|
+
project_id = context[:project_id]
|
|
49
|
+
unless project_id
|
|
50
|
+
raise ArgumentError,
|
|
51
|
+
"Missing required context: :project_id must be provided for BasecampAdapter#delete_comment"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
path = "buckets/#{project_id}/recordings/#{comment_id}/status/trashed.json"
|
|
55
|
+
make_request(:put, path)
|
|
56
|
+
true
|
|
57
|
+
end
|
|
24
58
|
end
|
|
25
59
|
end
|
|
26
60
|
end
|
|
@@ -4,7 +4,7 @@ module ActiveProject
|
|
|
4
4
|
module Adapters
|
|
5
5
|
module Basecamp
|
|
6
6
|
module Connection
|
|
7
|
-
include
|
|
7
|
+
include Connections::Rest
|
|
8
8
|
BASE_URL_TEMPLATE = "https://3.basecampapi.com/%<account_id>s/"
|
|
9
9
|
# Initializes the Basecamp Adapter.
|
|
10
10
|
# @param config [Configurations::BaseAdapterConfiguration] The configuration object for Basecamp.
|
|
@@ -15,38 +15,20 @@ module ActiveProject
|
|
|
15
15
|
unless config.is_a?(ActiveProject::Configurations::BaseAdapterConfiguration)
|
|
16
16
|
raise ArgumentError, "BasecampAdapter requires a BaseAdapterConfiguration object"
|
|
17
17
|
end
|
|
18
|
-
|
|
18
|
+
|
|
19
|
+
super(config: config)
|
|
19
20
|
|
|
20
21
|
account_id = @config.options.fetch(:account_id)
|
|
21
22
|
access_token = @config.options.fetch(:access_token)
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
init_rest(
|
|
24
25
|
base_url: format(BASE_URL_TEMPLATE, account_id: account_id),
|
|
25
26
|
auth_middleware: ->(conn) { conn.request :authorization, :bearer, access_token }
|
|
26
27
|
)
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
raise ArgumentError, "BasecampAdapter configuration requires :account_id and :access_token"
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
@base_url = format(BASE_URL_TEMPLATE, account_id: account_id)
|
|
33
|
-
@connection = initialize_connection
|
|
34
|
-
end
|
|
29
|
+
return if account_id && !account_id.empty? && access_token && !access_token.empty?
|
|
35
30
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
# Initializes the Faraday connection object.
|
|
39
|
-
def initialize_connection
|
|
40
|
-
access_token = @config.options[:access_token]
|
|
41
|
-
|
|
42
|
-
Faraday.new(url: @base_url) do |conn|
|
|
43
|
-
conn.request :authorization, :bearer, access_token
|
|
44
|
-
conn.request :retry
|
|
45
|
-
conn.response :raise_error
|
|
46
|
-
conn.headers["Content-Type"] = "application/json"
|
|
47
|
-
conn.headers["Accept"] = "application/json"
|
|
48
|
-
conn.headers["User-Agent"] = ActiveProject.user_agent
|
|
49
|
-
end
|
|
31
|
+
raise ArgumentError, "BasecampAdapter configuration requires :account_id and :access_token"
|
|
50
32
|
end
|
|
51
33
|
end
|
|
52
34
|
end
|