Skip to content

refactor(gateway): extract source plugin trait and resolve_common()#161

Open
enilsen16 wants to merge 1 commit into
TrogonStack:mainfrom
enilsen16:plugin-system-dev
Open

refactor(gateway): extract source plugin trait and resolve_common()#161
enilsen16 wants to merge 1 commit into
TrogonStack:mainfrom
enilsen16:plugin-system-dev

Conversation

@enilsen16
Copy link
Copy Markdown

refactor(gateway): extract source plugin trait and common config validation

Eliminates ~400 lines of duplication across http.rs, streams.rs, and
the 10 resolve_* functions in config.rs by introducing:

  • A gateway-internal SourcePlugin trait with enum dispatch (unit
    structs + monomorphised entry points, zero vtables) replacing
    the 9-arm if-let chains in http.rs and streams.rs.

  • A shared resolve_common() helper in config.rs extracting the
    four repeated validation blocks (subject_prefix, stream_name,
    nats_ack_timeout, stream_max_age) that appeared in every
    per-source resolver.

  • Nine thin wrapper impls (GithubPlugin, SlackPlugin, etc.) that
    delegate to the existing source crate routers and provision
    fns. No source crate code or dependency changes needed.

Discord is intentionally excluded from SourcePlugin: its
webhook-less primary path is handled by the gateway runner
spawned in main.rs, which stays untouched.

All 606+ workspace tests pass unchanged.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 14, 2026

Review Change Stack

Warning

Rate limit exceeded

@enilsen16 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 54 minutes and 51 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 43505de4-d585-4d32-9a09-96d803773d52

📥 Commits

Reviewing files that changed from the base of the PR and between a9ebf3e and dc07fc2.

📒 Files selected for processing (5)
  • rsworkspace/crates/trogon-gateway/src/config.rs
  • rsworkspace/crates/trogon-gateway/src/http.rs
  • rsworkspace/crates/trogon-gateway/src/main.rs
  • rsworkspace/crates/trogon-gateway/src/source_plugin.rs
  • rsworkspace/crates/trogon-gateway/src/streams.rs

Walkthrough

This PR implements a gateway-internal plugin abstraction to replace per-source conditional nesting and duplicated NATS field validation. A new SourcePlugin trait standardizes provisioning and webhook mounting; http.rs and streams.rs delegate to centralized dispatch functions; and config.rs consolidates repeated field parsing into a shared resolve_common() helper.

Changes

Plugin System Implementation

Layer / File(s) Summary
Design Specification
PLUGIN_SYSTEM_PLAN.md
Documents the refactor strategy: replacing a marketplace approach with a minimal gateway-internal abstraction using a SourcePlugin trait, enum/macro dispatch (no Box<dyn>), and per-source wrapper structs delegating to existing trogon_source_* crate functions. Specifies goals, non-goals, phased rollout, definition of done, and anti-patterns to avoid.
SourcePlugin Trait and Dispatch Layer
rsworkspace/crates/trogon-gateway/src/source_plugin.rs
Introduces SourcePlugin trait with id, is_enabled, provision, and routes methods; generic provision_one and mount_one helpers; implementations for nine concrete sources (GitHub, Slack, Telegram, Twitter, GitLab, Incident.io, Linear, Notion, Sentry); and exports provision_all and mount_all dispatch entry points.
NATS Field Validation Consolidation
rsworkspace/crates/trogon-gateway/src/config.rs
Introduces ResolvedCommon struct and resolve_common() helper to centralize validation and conversion of subject_prefix, stream_name, nats_ack_timeout_secs, and stream_max_age_secs. Updates all nine resolve_* functions (github, discord, slack, telegram, twitter, gitlab, linear, incidentio, notion, sentry) to call resolve_common and populate per-source config structs from returned common values.
Gateway Integration of Dispatch Layer
rsworkspace/crates/trogon-gateway/src/http.rs, streams.rs, main.rs
mount_sources delegates all source routing to source_plugin::mount_all; provision delegates non-Discord sources to source_plugin::provision_all after Discord-only provision; source_plugin module declared in main.rs. Removes inline per-source conditionals and per-source tracing::info logging.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • TrogonStack/trogonai#139: Modifies the same gateway routing paths (http.rs mount_sources and streams.rs provision) to add Microsoft Teams integration alongside the main PR's abstraction refactor.
  • TrogonStack/trogonai#156: Refactors gateway source integration routing and provisioning around enabled source configuration, with overlapping changes to http.rs and streams.rs flow patterns.

Poem

🐰 A plugin for each source, not a chain of nested checks,
We've woven common validation, fixed the config's deck,
From GitHub's hooks to Sentry's noise, they all now dance the same,
One abstraction to dispatch them all—the plugin way's the game! 🎭

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.70% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: extracting a source plugin trait and introducing a resolve_common() helper function.
Description check ✅ Passed The description clearly explains the refactoring work: eliminating duplication via a SourcePlugin trait, resolve_common() helper, and wrapper implementations, with Discord intentionally excluded.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@enilsen16 enilsen16 marked this pull request as ready for review May 14, 2026 20:58
@cursor
Copy link
Copy Markdown

cursor Bot commented May 14, 2026

PR Summary

Medium Risk
Touches gateway startup paths for routing and JetStream provisioning across all webhook sources; behavior should be equivalent but mistakes could silently skip mounting/provisioning for a source.

Overview
Refactors source setup to eliminate repeated per-source if let chains by introducing a gateway-internal SourcePlugin abstraction (source_plugin.rs) with mount_all and provision_all dispatchers used by http.rs and streams.rs.

Extracts repeated NATS-related config parsing/validation (subject_prefix, stream_name, nats_ack_timeout_secs, stream_max_age_secs) into resolve_common() in config.rs, and updates each resolve_* to reuse it while keeping source-specific secret/token validation and Sentry’s ack-timeout cap. Discord remains handled separately (gateway runner + stream provisioning).

Reviewed by Cursor Bugbot for commit dc07fc2. Bugbot is set up for automated code reviews on this repo. Configure here.

…dation

Eliminates ~400 lines of duplication across http.rs, streams.rs, and
the 10 resolve_* functions in config.rs by introducing:

- A gateway-internal SourcePlugin trait with enum dispatch (unit
  structs + monomorphised entry points, zero vtables) replacing
  the 9-arm if-let chains in http.rs and streams.rs.

- A shared resolve_common() helper in config.rs extracting the
  four repeated validation blocks (subject_prefix, stream_name,
  nats_ack_timeout, stream_max_age) that appeared in every
  per-source resolver.

- Nine thin wrapper impls (GithubPlugin, SlackPlugin, etc.) that
  delegate to the existing source crate routers and provision
  fns. No source crate code or dependency changes needed.

Discord is intentionally excluded from SourcePlugin: its
webhook-less primary path is handled by the gateway runner
spawned in main.rs, which stays untouched.

All 606+ workspace tests pass unchanged.
@enilsen16 enilsen16 force-pushed the plugin-system-dev branch from a9ebf3e to dc07fc2 Compare May 14, 2026 21:00
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
rsworkspace/crates/trogon-gateway/src/source_plugin.rs (1)

435-435: 💤 Low value

Remove unnecessary clone on the final mount_one call.

The last call to mount_one clones publisher, but since this is the final use, you can move it instead. This avoids an unnecessary clone operation.

♻️ Proposed fix
-    mount_one(&SentryPlugin, router, publisher.clone(), resolved)
+    mount_one(&SentryPlugin, router, publisher, resolved)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rsworkspace/crates/trogon-gateway/src/source_plugin.rs` at line 435, The
final call to mount_one(&SentryPlugin, router, publisher.clone(), resolved)
performs an unnecessary clone of publisher; change this call to move the
publisher (mount_one(&SentryPlugin, router, publisher, resolved)) since this is
the last use—ensure no subsequent code expects publisher afterwards, and if
earlier mounts needed clones keep those as-is (only remove .clone() on the final
mount_one invocation).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@rsworkspace/crates/trogon-gateway/src/config.rs`:
- Around line 481-531: The resolve_common function bundles four distinct
conversions (subject_prefix via NatsToken::new, stream_name via NatsToken::new,
nats_ack_timeout via NonZeroDuration::from_secs, and stream_max_age via
StreamMaxAge::from_secs) and should be split into small per-field resolver
functions (e.g., resolve_subject_prefix, resolve_stream_name,
resolve_nats_ack_timeout, resolve_stream_max_age) that each return Result<... ,
ConfigValidationError> or Option<...> and push errors into the shared errors
vec; then have resolve_common (or the callers of ResolvedCommon) compose those
per-field resolvers to construct ResolvedCommon so validation is isolated by
type and easier to test/maintain.

---

Nitpick comments:
In `@rsworkspace/crates/trogon-gateway/src/source_plugin.rs`:
- Line 435: The final call to mount_one(&SentryPlugin, router,
publisher.clone(), resolved) performs an unnecessary clone of publisher; change
this call to move the publisher (mount_one(&SentryPlugin, router, publisher,
resolved)) since this is the last use—ensure no subsequent code expects
publisher afterwards, and if earlier mounts needed clones keep those as-is (only
remove .clone() on the final mount_one invocation).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d13776bb-abb5-4e2f-b89b-a838d2651d70

📥 Commits

Reviewing files that changed from the base of the PR and between 0f13a50 and a9ebf3e.

📒 Files selected for processing (6)
  • PLUGIN_SYSTEM_PLAN.md
  • rsworkspace/crates/trogon-gateway/src/config.rs
  • rsworkspace/crates/trogon-gateway/src/http.rs
  • rsworkspace/crates/trogon-gateway/src/main.rs
  • rsworkspace/crates/trogon-gateway/src/source_plugin.rs
  • rsworkspace/crates/trogon-gateway/src/streams.rs

Comment on lines +481 to 531
fn resolve_common(
source: &'static str,
subject_prefix: String,
stream_name: String,
nats_ack_timeout_secs: u64,
stream_max_age_secs: u64,
errors: &mut Vec<ConfigValidationError>,
) -> Option<ResolvedCommon> {
let subject_prefix = match NatsToken::new(subject_prefix) {
Ok(t) => t,
Err(e) => {
errors.push(ConfigValidationError::invalid_subject_token(
"github",
source,
"subject_prefix",
e,
));
return None;
}
};

let stream_name = match NatsToken::new(section.stream_name) {
let stream_name = match NatsToken::new(stream_name) {
Ok(t) => t,
Err(e) => {
errors.push(ConfigValidationError::invalid_subject_token("github", "stream_name", e));
errors.push(ConfigValidationError::invalid_subject_token(source, "stream_name", e));
return None;
}
};

let nats_ack_timeout = match NonZeroDuration::from_secs(section.nats_ack_timeout_secs) {
let nats_ack_timeout = match NonZeroDuration::from_secs(nats_ack_timeout_secs) {
Ok(d) => d,
Err(err) => {
errors.push(ConfigValidationError::invalid("github", "nats_ack_timeout_secs", err));
errors.push(ConfigValidationError::invalid(source, "nats_ack_timeout_secs", err));
return None;
}
};

let stream_max_age = match StreamMaxAge::from_secs(section.stream_max_age_secs) {
let stream_max_age = match StreamMaxAge::from_secs(stream_max_age_secs) {
Ok(age) => age,
Err(err) => {
errors.push(ConfigValidationError::invalid("github", "stream_max_age_secs", err));
errors.push(ConfigValidationError::invalid(source, "stream_max_age_secs", err));
return None;
}
};

Some(trogon_source_github::GithubConfig {
webhook_secret,
Some(ResolvedCommon {
subject_prefix,
stream_name,
stream_max_age,
nats_ack_timeout,
})
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Split resolve_common into per-field resolvers to keep validation boundaries type-focused.

This helper bundles four unrelated validations/conversions into one aggregate resolver. Consider small per-field resolvers (or per-type constructors at module boundaries) and compose them in each resolve_* function to keep type validation isolated.

As per coding guidelines, "Validate per-type, not per-aggregate: avoid validating unrelated fields together in a single constructor."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rsworkspace/crates/trogon-gateway/src/config.rs` around lines 481 - 531, The
resolve_common function bundles four distinct conversions (subject_prefix via
NatsToken::new, stream_name via NatsToken::new, nats_ack_timeout via
NonZeroDuration::from_secs, and stream_max_age via StreamMaxAge::from_secs) and
should be split into small per-field resolver functions (e.g.,
resolve_subject_prefix, resolve_stream_name, resolve_nats_ack_timeout,
resolve_stream_max_age) that each return Result<... , ConfigValidationError> or
Option<...> and push errors into the shared errors vec; then have resolve_common
(or the callers of ResolvedCommon) compose those per-field resolvers to
construct ResolvedCommon so validation is isolated by type and easier to
test/maintain.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant