Skip to content

Feature/routing separate from plugin - #68

Merged
bamsemats merged 3 commits into
mainfrom
feature/routing-separate-from-plugin
Feb 17, 2026
Merged

Feature/routing separate from plugin#68
bamsemats merged 3 commits into
mainfrom
feature/routing-separate-from-plugin

Conversation

@bamsemats

@bamsemats bamsemats commented Feb 16, 2026

Copy link
Copy Markdown

Closes #61

For the current scope, routing inside Plugin.handle() works, but it couples routing and endpoint logic.

Introducing a lightweight Router would improve separation of concerns, testability, and extensibility without adding much complexity.
I intend to solve this by introducing a simple Router/RouteHandler abstraction, not a full framework.

  • Refactor plugin handling by introducing Router abstraction;
  • added SimpleRouter implementation.
  • Updated pipeline and tests to support new routing system.

Summary by CodeRabbit

Release Notes

  • New Features

    • Application now supports routing requests to different handlers based on request paths, including exact matches and wildcard patterns.
    • Requests to unmatched paths are handled with a dedicated fallback handler.
  • Chores

    • Updated build plugin to the latest version.
  • Tests

    • Added comprehensive test coverage for the new request routing functionality, including path matching and fallback scenarios.

…SimpleRouter` implementation. Updated pipeline and tests to support new routing system.
…SimpleRouter` implementation. Updated pipeline and tests to support new routing system.
@coderabbitai

coderabbitai Bot commented Feb 16, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Replaces direct Plugin wiring with a Router abstraction. Adds Router and SimpleRouter, updates Pipeline and FilterChainImpl to use routers, updates App to register path-to-plugin mappings, and adjusts tests. Also bumps spotless-maven-plugin version in pom.xml.

Changes

Cohort / File(s) Summary
Dependency
pom.xml
Bumped com.diffplug.spotless:spotless-maven-plugin from 2.43.03.2.1.
Router API & Impl
src/main/java/org/juv25d/router/Router.java, src/main/java/org/juv25d/router/SimpleRouter.java
Add Router interface and SimpleRouter implementation: path->Plugin registry, exact and wildcard matching, NotFound fallback, registerPlugin and resolve APIs.
Pipeline
src/main/java/org/juv25d/Pipeline.java
Replace Plugin field/methods with Router equivalents: setPlugin/getPluginsetRouter/getRouter; pass router into filter chain construction.
Filter Chain
src/main/java/org/juv25d/filter/FilterChainImpl.java
Constructor and field now accept Router; doFilter delegates to router.resolve(request).handle(...) instead of a single plugin.
Application Setup
src/main/java/org/juv25d/App.java
Configure SimpleRouter with path mappings (e.g., "/", "/*"StaticFilesPlugin, "/notfound"NotFoundPlugin) and assign router to pipeline; keep global filters.
Tests Updated
src/test/java/org/juv25d/...
Tests updated to create SimpleRouter, register plugins, and use setRouter in place of setPlugin across PipelineTest, FilterChainImplTest, GlobalFilterTests, RouteFilterTests.
New Router Tests
src/test/java/org/juv25d/router/SimpleRouterTest.java
Adds comprehensive unit tests for exact match, wildcard matching, specificity ordering, root handling, and NotFound fallback.

Sequence Diagram(s)

sequenceDiagram
  participant Client as Client
  participant Pipeline as Pipeline
  participant Chain as FilterChainImpl
  participant Router as SimpleRouter
  participant Plugin as Plugin

  Client->>Pipeline: send(HttpRequest)
  Pipeline->>Chain: createFilterChain(request, response)
  Chain->>Chain: applyFilters(request,response)
  alt filters continue
    Chain->>Router: resolve(request)
    Router-->>Chain: Plugin
    Chain->>Plugin: handle(request,response)
  else short-circuit by filter
    Chain--xClient: response returned
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • fmazmz
  • kristina0x7
  • annikaholmqvist94

Poem

🐰
I hopped through codepaths, mapped each way,
Exact then wildcard — I show the way,
Plugins now found where routes are bound,
NotFound waits when none are found,
Hooray — a tidy hop and play! 🌷

🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.90% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feature/routing separate from plugin' clearly and directly describes the main architectural change: introducing a Router abstraction to separate routing from Plugin responsibilities.
Linked Issues check ✅ Passed The PR fully addresses issue #61 by introducing Router interface, SimpleRouter implementation, and refactoring all components to use the new routing abstraction instead of Plugin.handle() for routing.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing the Router abstraction and refactoring plugin handling. The spotless-maven-plugin version update is a minor dependency maintenance change not outside scope.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/routing-separate-from-plugin

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.

@bamsemats bamsemats self-assigned this Feb 16, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/org/juv25d/Pipeline.java (1)

43-65: ⚠️ Potential issue | 🟡 Minor

router may be null when createChain is called.

If setRouter has not been called before createChain, router is null and will be passed to FilterChainImpl, likely causing an NPE when a request is handled. Consider adding a defensive check or initializing the field with a default router.

Option A: Fail fast with a clear message
+        if (router == null) {
+            throw new IllegalStateException("Router has not been configured");
+        }
         return new FilterChainImpl(filters, router);
🤖 Fix all issues with AI agents
In `@src/main/java/org/juv25d/App.java`:
- Around line 3-6: The file contains duplicate import statements for the
IpFilter and LoggingFilter classes; in App.java remove the redundant lines so
each of the two imports appears only once (leave a single import for
org.juv25d.filter.IpFilter and a single import for
org.juv25d.filter.LoggingFilter), ensuring the App class still compiles and no
duplicate import warnings remain.

In `@src/main/java/org/juv25d/Pipeline.java`:
- Line 21: The router field in Pipeline must be made volatile to ensure
visibility across threads; update the declaration of the Router field (router)
to be volatile and keep existing accesses in setRouter and createChain unchanged
so writes in setRouter are visible to reads in createChain; this aligns router
with other volatile/shared state (e.g., sortedGlobalFilters) for thread-safety
in class Pipeline.

In `@src/main/java/org/juv25d/router/Router.java`:
- Around line 15-18: The Javadoc for Router.resolve incorrectly allows null
while FilterChainImpl.doFilter assumes a non-null Plugin, risking NPE; tighten
the contract by updating Router.resolve's Javadoc to state it never returns
null, add a non-null annotation (e.g., `@Nonnull` or `@NotNull`) to the method
signature, and ensure all implementations (e.g., SimpleRouter) never return null
(return a default/no-op Plugin or throw a well-documented exception instead);
alternatively, if you prefer guarding at call site, add a null-check in
FilterChainImpl.doFilter before calling handle and handle the missing plugin
case explicitly.

In `@src/main/java/org/juv25d/router/SimpleRouter.java`:
- Around line 53-58: The wildcard-match loop in SimpleRouter (iterating over
routes and returning the first entry whose key endsWith("/*") and matches the
request path) is nondeterministic because routes is a HashMap; update the
implementation to deterministically pick the most specific wildcard: either
change the routes field to a LinkedHashMap to preserve registration order (so
insertion order wins) or, better, collect matching wildcard keys in the method
that does lookup and choose the longest matching prefix (registeredPath with the
greatest length) before returning its Plugin; adjust the code in the
SimpleRouter method that performs the loop and the routes declaration
accordingly (use LinkedHashMap<> or add a comparison by
registeredPath.length()).
🧹 Nitpick comments (6)
src/test/java/org/juv25d/filter/RouteFilterTests.java (2)

23-27: Router setup registers plugin only at "/", but test requests hit other paths.

The NoOpPlugin is registered at "/", but execute(pipeline, "/api/test") and execute(pipeline, "/home") won't match that route — they'll fall through to NotFoundPlugin. This works today because the tests only assert on filter execution, not on the response. However, registering a catch-all "/*" route would better express the intent and avoid confusion if these tests are later extended to validate response content.

Also applies to: 39-43


80-83: NoOpPlugin inner class is duplicated across test files.

This same NoOpPlugin inner class also appears in GlobalFilterTests. Consider extracting it to a shared test utility to reduce duplication.

src/test/java/org/juv25d/filter/GlobalFilterTests.java (1)

23-27: Same observation: plugin registered at "/" but request path is "/anything".

The global filter test will route to NotFoundPlugin since "/anything" doesn't match "/". This is fine for verifying filter execution, but consider using "/*" for the catch-all registration to better match the test intent.

src/main/java/org/juv25d/App.java (1)

34-37: Consider reusing a single StaticFilesPlugin instance.

Two separate StaticFilesPlugin instances are created for "/" and "/*". Since StaticFilesPlugin appears stateless, a single shared instance would suffice.

Suggested change
+        StaticFilesPlugin staticFiles = new StaticFilesPlugin();
         SimpleRouter router = new SimpleRouter();
-        router.registerPlugin("/", new StaticFilesPlugin());
-        router.registerPlugin("/*", new StaticFilesPlugin());
+        router.registerPlugin("/", staticFiles);
+        router.registerPlugin("/*", staticFiles);
         router.registerPlugin("/notfound", new NotFoundPlugin());
src/test/java/org/juv25d/router/SimpleRouterTest.java (1)

19-26: notFoundPlugin field is initialized but never referenced in assertions.

The field on line 19 is set up in @BeforeEach but all not-found assertions use instanceof NotFoundPlugin checks instead. Consider removing it to avoid confusion.

src/main/java/org/juv25d/router/SimpleRouter.java (1)

46-49: Minor: double HashMap lookup can be simplified.

containsKey followed by get performs two lookups. Use get with a null check instead.

Suggested change
-        if (routes.containsKey(path)) {
-            return routes.get(path);
+        Plugin exactMatch = routes.get(path);
+        if (exactMatch != null) {
+            return exactMatch;
         }

Comment thread src/main/java/org/juv25d/App.java Outdated
Comment thread src/main/java/org/juv25d/Pipeline.java Outdated
Comment thread src/main/java/org/juv25d/router/Router.java
Comment thread src/main/java/org/juv25d/router/SimpleRouter.java Outdated
…logic; add test coverage for specific and wildcard match scenarios.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
src/main/java/org/juv25d/router/SimpleRouter.java (1)

56-67: Minor: wildcard stream is rebuilt on every request.

For a hot path, materializing and sorting the wildcard entries on each resolve() call is wasteful. If route registration is done at startup, consider pre-sorting the wildcard routes once (e.g., in a separate List maintained at registration time). Not urgent at current scale.

src/test/java/org/juv25d/router/SimpleRouterTest.java (1)

19-26: Unused field: notFoundPlugin is never referenced in assertions.

The field is created on line 19/26 but all not-found assertions use instanceof NotFoundPlugin (which is correct, since the router creates its own instance). Remove the dead field to reduce noise.

♻️ Proposed fix
     private Plugin mockPluginA;
     private Plugin mockPluginB;
-    private Plugin notFoundPlugin;
 
     `@BeforeEach`
     void setUp() {
         router = new SimpleRouter();
         mockPluginA = mock(Plugin.class);
         mockPluginB = mock(Plugin.class);
-        notFoundPlugin = new NotFoundPlugin(); // Assuming NotFoundPlugin is a concrete class
     }

@lindaeskilsson

Copy link
Copy Markdown

Nice refactor! 👍
I found the flow easier to understand now that routing isn’t inside the plugin anymore. Approved!

@mattknatt mattknatt left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Looks good, felt easier to understand the routing logic! Nice work.

One thing to maybe be considered in the test-file is to make a helper method that returns a request for the requested path, something like "

private HttpRequest requestFor(String path) {
return new HttpRequest("GET", path, null, "HTTP/1.1", Map.of(), new byte[0], "UNKNOWN");
}

"
Just to not have to write the full request in each test. But not a necessary change, so looks good to merge!

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.

Should routing be separated from Plugin?

3 participants