Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support nested context paths. #5846

Open
wants to merge 8 commits into
base: main
Choose a base branch
from

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. 🙇‍♂️

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