Skip to content

Conversation

chickenchickenlove
Copy link
Contributor

Motivation:

  • Currently, armeria supports only 1-depth context paths. Sometimes, user want deeper context paths than 1-depth when they use contextPaths().

Modifications:

  • Make ContextPathServiceBuilder tree to support nested context paths.
  • Add public functions.
    • before() is for going back previous node.
    • contextPath() is for adding context paths and making child ContextPathServiceBuilder
  • Add package-private and private functions.
    • parent(): To give Child contextPathServiceBuilder parent object. because of this, child can return parent object when and() is called.
    • virtualHostbuilder(): to relay their context via ContextPathServiceBuilder tree.
    • mergeContextPaths(): to merge previous context paths and current context paths.

Example

Result:

sb.contextPath("/rest")
      .contextPath("/catalog")
          .service("/product", new GetProductService())
          .service("/products", new ProductsHandler())
          .before()
      .contextPath("/cart")
          .contextPath("/foo")
               .contextPath("/bar")
                    .service("/checkout", new CheckoutService());
                    .and()
  .contextPath("/gql")
      .service("/catalog", new GraphQLService());

/rest/catalog/product => getProductService
/rest/catalog/products => productsHandler
/rest/cart/foo/bar/checkout => checkoutService
/gql/catalog => GraphQLService

@chickenchickenlove
Copy link
Contributor Author

I would like to receive feedback on whether this PR is heading in the right direction, as it is currently a draft.

After fix direction, I will write unit tests and Java docs based on the BaseContextPathsTest code.

When you have time, please take a look 🙇‍♂️

@ikhoon
Copy link
Contributor

ikhoon commented Aug 8, 2024

I didn't see that a fluent style is good for nested context paths in terms of API design. Users may find it difficult to know the current depth and parent trees and children trees.

How about taking a lambda expression as a second parameter to leverage Kotlin trailing lamdas?
https://kotlinlang.org/docs/lambdas.html#passing-trailing-lambdas

I imagined the following style API. We used contextPath() at the top level but I am not sure if the naming contextPath is also good for the nested routes.

Server
  .builder()
  .contextPath("/rest") {
     contextPath("/catalog") {
        service("/product", new GetProductService())
        service("/products", new ProductsHandler())
     }
  }
  .contextPath("/cart") {
     contextPath("/foo") {
        contextPath("/bar") {
           service("/checkout", new CheckoutService())
        }
     }
  }
  .contextPath("/gql") {
      service("/catalog", new GraphQLService())
   }

Related work: https://ktor.io/docs/server-routing.html#nested_routes

@chickenchickenlove
Copy link
Contributor Author

@ikhoon nim, Thanks for your guidelines.
I didn't know about KTOR. i will take a look and follow how KTOR supports nested context path.

@chickenchickenlove
Copy link
Contributor Author

chickenchickenlove commented Aug 25, 2024

@ikhoon nim, i change code style from fluent style to nested lambda expression.
For example, we can use feature of nested context like this.

// Without virtual host
        sb.baseContextPath("/api")
          .contextPath("/context-path/a1", "/context-path/a2")
          .nestedContext()
          .contextPaths(Set.of("/b1", "/b2"), ctx1 -> ctx1  // ctx1 scope start (lambda function)
                  .annotatedService(new Object() { ... })
                  .service("/my-service", new HttpService() { ... })
                  .contextPaths(Set.of("/c1", "/c2"), ctx2 -> ctx2 // ctx2 scope start (lambda function)
                          .service("/my-service1", new HttpService() { ... })
                          .contextPaths(Set.of(...), ctx3 -> ctx3  // ctx3 scope start (lambda function)
                                   .service("/...", new HttpService() { ... })
                                   ) // ctx3 scope end.
                           ) // ctx2 scope end.                   
           ) // ctx1 scope end.
          .contextPaths(Set.of("/b3", "/b4"), ctx11 -> ctx11
                  .service("/my-service", new HttpService() { ... });

// With virtual host.
       sb.virtualHost("foo.com")
          .contextPath("/virtual-foo")
          .nestedContext()
          .contextPaths(Set.of("/a1", "/a2"), ctx -> ctx
                  .service("/my-service1", new HttpService() { ... }));

To get detail code, you can refer to this java code.
Also, i create pytest code to test whether endpoint is valid, corresponding to java code above.

Java does not support lambda functions as robustly as Kotlin does.
IMHO, it would be difficult to implement the API in the same way perfectly as you imagined 🤔.
Like Kotlin trailing lamdas, i introduce lambda function as second parameter. (as you recommended!)
lambda functions represents the context scope and incorporate a bit of a Fluent API style within it.

It may not be exactly the same as the API you imagined, but since we can represent the scope within a lambda function, I believe it will be more user-friendly than my initial commit. also, java cannot perfectly match to Kotlin trailing lambdas, i guess 😢.

When you have time, please take another look. 🙇‍♂️

@github-actions github-actions bot added the Stale label Sep 25, 2024
@chickenchickenlove
Copy link
Contributor Author

@ikhoon nim, gently ping.
When you have time, please take a look 🙇‍♂️
Or would you prefer to not proceed with this PR any further, in which case I'd be happy to close this PR.

@github-actions github-actions bot removed the Stale label Apr 29, 2025
@ikhoon
Copy link
Contributor

ikhoon commented Apr 29, 2025

 sb.baseContextPath("/api")
   .contextPath("/context-path/a1", "/context-path/a2")
   .nestedContext()
   .contextPaths(Set.of("/b1", "/b2"), ctx1 -> ctx1  // ctx1 scope start (lambda function)

Is it possible to build nested context paths without using .nestedContext()? I would like the API to be designed in a form close to a DSL.

@chickenchickenlove
Copy link
Contributor Author

@ikhoon nim, sure.
I fixed the codes.
Two styles are supported.

I updated example codes and API test codes..

Style1

It starts with contextPath(Iterable<String> paths).

      sb.baseContextPath("/api")
        .contextPath("/context-path/a1", "/context-path/a2")
        .contextPaths(Set.of("/b1", "/b2"), ctx1 -> ctx1  // ctx1 scope start (lambda function)
                .annotatedService(new Object() { ... })
                .service("/my-service", new HttpService() { ... })
                .contextPaths(Set.of("/c1", "/c2"), ctx2 -> ctx2 // ctx2 scope start (lambda function)
                        .service("/my-service1", new HttpService() { ... })
                        .contextPaths(Set.of(...), ctx3 -> ctx3  // ctx3 scope start (lambda function)
                                 .service("/...", new HttpService() { ... })
                                 ) // ctx3 scope end.
                       ) // ctx2 scope end.                   
         ) // ctx1 scope end.
        .contextPaths(Set.of("/b3", "/b4"), ctx11 -> ctx11
                .service("/my-service", new HttpService() { ... });

Style2

It starts with contextPath(Iterable<String> paths, Consumer<ContextPathServicesBuilder> context).

      sb.baseContextPath("/api")
        .contextPath(Set.of("/b1", "/b2"), ctx1 -> ctx1  // ctx1 scope start (lambda function)
                .annotatedService(new Object() { ... })
                .service("/my-service", new HttpService() { ... })
                .contextPaths(Set.of("/c1", "/c2"), ctx2 -> ctx2 // ctx2 scope start (lambda function)
                        .service("/my-service1", new HttpService() { ... })
                        .contextPaths(Set.of(...), ctx3 -> ctx3  // ctx3 scope start (lambda function)
                                 .service("/...", new HttpService() { ... })
                                 ) // ctx3 scope end.
                       ) // ctx2 scope end.                   
         ) // ctx1 scope end.
        .contextPaths(Set.of("/b3", "/b4"), ctx11 -> ctx11
                .service("/my-service", new HttpService() { ... });

When you have time, PTAL 🙇‍♂️

@github-actions github-actions bot added the Stale label Jun 1, 2025
@ikhoon
Copy link
Contributor

ikhoon commented Jun 19, 2025

Could you explain the difference between contextPath() and contextPaths()? The method names alone don't make it clear.

@github-actions github-actions bot removed the Stale label Jun 22, 2025
@chickenchickenlove
Copy link
Contributor Author

chickenchickenlove commented Jul 19, 2025

@ikhoon nim, sorry to late.
I updated interface.

For ContextBuilder

      sb.baseContextPath("/api")
        .toContextBuilder()
        .contextPath(Set.of("/b1", "/b2"), ctx1 -> ctx1  // ctx1 scope start (lambda function)
                .annotatedService(new Object() { ... })
                .service("/my-service", new HttpService() { ... })
                .contextPath(Set.of("/c1", "/c2"), ctx2 -> ctx2 // ctx2 scope start (lambda function)
                        .service("/my-service1", new HttpService() { ... })
                        .contextPath(Set.of(...), ctx3 -> ctx3  // ctx3 scope start (lambda function)
                                 .service("/...", new HttpService() { ... })
                                 ) // ctx3 scope end.
                       ) // ctx2 scope end.                   
         ) // ctx1 scope end.
        .contextPath(Set.of("/b3", "/b4"), ctx11 -> ctx11
                .service("/my-service", new HttpService() { ... });

Currently, baseContextPath() returns ServerBuilder.
I don't want to break backward compatiablity, so I added new function toContextBuilder() to retrieve ContextPathServicesBuilder.

Then, we can use contextPath() for nested path.

For VirtuaHostlBuilder

      sb.virtualHost("foo.com")
          .contextPath(Set.of("/b1", "/b2"), ctx1 -> ctx1  // ctx1 scope start (lambda function)
                .annotatedService(new Object() { ... })
                .service("/my-service", new HttpService() { ... })
                .contextPath(Set.of("/c1", "/c2"), ctx2 -> ctx2 // ctx2 scope start (lambda function)
                        .service("/my-service1", new HttpService() { ... })
                        .contextPath(Set.of(...), ctx3 -> ctx3  // ctx3 scope start (lambda function)
                                 .service("/...", new HttpService() { ... })
                                 ) // ctx3 scope end.
                       ) // ctx2 scope end.                   
         ) // ctx1 scope end.
          .contextPath(Set.of("/b3", "/b4"), ctx11 -> ctx11
                  .service("/my-service", new HttpService() { ... });

When you have time, Please take another look. 🙇‍♂️

@github-actions github-actions bot added the Stale label Aug 19, 2025
Copy link

codecov bot commented Aug 19, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 0.00%. Comparing base (3596b3b) to head (6a1b4ee).
⚠️ Report is 374 commits behind head on main.

Additional details and impacted files
@@             Coverage Diff              @@
##               main   #5846       +/-   ##
============================================
- Coverage     74.78%       0   -74.79%     
============================================
  Files          1877       0     -1877     
  Lines         79199       0    -79199     
  Branches      10212       0    -10212     
============================================
- Hits          59228       0    -59228     
+ Misses        15168       0    -15168     
+ Partials       4803       0     -4803     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions github-actions bot removed the Stale label Aug 20, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Nested context paths
2 participants