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

Simplify http extensions with adjustable Context signature #1881

Open
dzikoysk opened this issue May 8, 2023 · 12 comments
Open

Simplify http extensions with adjustable Context signature #1881

dzikoysk opened this issue May 8, 2023 · 12 comments

Comments

@dzikoysk
Copy link
Member

dzikoysk commented May 8, 2023

Each Javalin project I know, that does slightly more than regular hello world app, reaches a point where it needs some extra utility methods on Context. As long as it's not an issue for Kotlin users, due to extension methods, it's pretty painful in regular Java projects. Atm Javalin doesn't really address this area and imo that's pretty sad as it affects daily experience & code that is forced to use XXXUtils classes / random methods in a domain that shouldn't really care about the http access api.

My proposal to resolve this issue is to enhance Handler's signature (and any associated api) with <C : Context> type, so users are allowed to provide extra type-safe API for their apps that fits the general flow of execution & does not require monkey-like patching to cover that hole.

How it could look like? Custom context type should be declared at config level to declare consistent C across entire instance:

Javalin
    .create<CustomContext>(config -> {
        // [...] cfg
        // return/builder-like config/config.customContextFactory, but it'd be nice to somehow enforce it on type level 
        return (req, res) -> CustomContext(req, res)
    })
    .get("/*", customContext -> customContext.monke())
    .start()

The custom context may look like this:

class CustomAsContext extends JavalinServletContext {
  // [...] custom methods
  public void monke() { System.out.println("🐒"); }
}

Such feature would also help with issues like that:

So we could always recommend it as a fallback solution without a need to constantly increase amount of methods provided by the base Context.

@zugazagoitia
Copy link
Member

zugazagoitia commented May 8, 2023

How would this context look in WS/SSE? Using a generic type at the app level limits expansion to http. They're only wrappers on top of the regular context but they could be extended too IMO.

@dzikoysk
Copy link
Member Author

dzikoysk commented May 8, 2023

SSE is a syntax sugar on top of the Context, so C would be propagated there as well.

WS is atm excluded from this proposal, especially that we had a conversation on Discord with @tipsy where I've proposed to move websockets out of the core implementation. It's heavily bounded to Jetty, so I think it could function as JettyWebSockets plugin or sth like that. Anyway, that part is probably controversial, so I don't really want to discuss it here as it's not that related. Ofc such feature could be a part of WS API, but it should be discussed in standalone issue as well - WS impls are usually different than http routes, so I'd say it'd just provide fewer use-cases.

@zugazagoitia
Copy link
Member

I understand. I really like the change!

@tipsy
Copy link
Member

tipsy commented May 14, 2023

Previously brought up in #1020

@dzikoysk
Copy link
Member Author

I'd like to remove this from 6.x wishlist. I think that this is a relatively big change, and 6.x already has several quite huge improvements over 5.x + new routing API already allows it at some point.

I'd keep it open, so we can still gather some extra feedback & link issues that could be addressed by that.

@tipsy
Copy link
Member

tipsy commented Sep 10, 2023

Sounds good, I agree!

@unldenis
Copy link

unldenis commented Dec 12, 2023

In production I have a backend based on Javalin where I have been working on it for a year and a half.
Meanwhile, congratulations to the framework.
For various reasons I/we chose to use Java instead of Kotlin :/.

Basically I created a framework with simple annotations for http handlers.
Needing additional methods I had to create a class by wrapping the Context, the framework then instantiates this class at each call, which will then be used in the annotated methods.
(I want to specify, I don't use reflection but I create a class at startup using javax.tools.JavaCompiler, simpler to maintain than ASM., therefore without performance overhead from this point of view).

I read that you removed it from the 6.x wishlist, can I contribute to this proposal anyway?

@dzikoysk
Copy link
Member Author

dzikoysk commented Dec 12, 2023

Extensible context would be quite a huge change, because we'd need to propagate context as a generic type across all signatures that are currently using it. We have quite a lot of changes in 6.x, so we've decided to reconsider it for 7.x

Speaking about the alternative solution, lately I've proposed another enhancement, called component/hooks system, that partly addressed this problem:

As long as team decided to not implement it in the requested way for now, context related part of this request is refined in these 2 new PRs:

There's also an ongoing discussion on our Discord server. If you think you can add some valuable input to the discussion, feel free to comment & suggest some improvements :)

@tipsy
Copy link
Member

tipsy commented Dec 12, 2023

I'll write a short explanation of how #2068 works. The PR allows you to extend a ContextPlugin class, which has a createExtension method that you have to override.

It would be used like this:

// creating a plugin (this is done by the plugin author)
class Plugin(userConfig : Consumer<Config>) : ContextPlugin<Plugin.Config, Plugin.Extension>(userConfig, Config()) {
    class Config(var directory: String = "...")
    class Extension(val context: Context, private val plugin: Plugin) {
        fun render(path: String) {
            context.html(plugin.directory() + path)
        }
    }
    fun directory() = pluginConfig.directory
    override fun createExtension(context: Context): Extension = Extension(context, this)
}
// registering the plugin
Javalin.create(config -> {
    config.registerPlugin(new Plugin(it -> it.directory = "src/test/resources"));
});
// using the plugin
app.get("/", ctx -> ctx.with(Plugin.class).render("/template.tpl"));

It would be great with some feedback. Would this approach help solve your issue?

@unldenis
Copy link

unldenis commented Dec 17, 2023

It would be great with some feedback. Would this approach help solve your issue?
As long as team decided to not implement it in the requested way for now, context related part of this request is refined in these 2 new PRs:

After all, if I didn't misunderstand, you wrap the context and so we both do the same thing in the end.
The thing is, I'd like to avoid doing this twice for request.
I'm trying to figure out how to do it.

@unldenis
Copy link

unldenis commented Dec 17, 2023

Could this be the only example where perhaps inheritance is the only solution?
Have the functionality of JavalinServletContext but add more. And to instantiate it just call, for example.

fun main() {
    val app : Javalin<MyCustomContext> = Javalin
    .withContext<MyCustomContext>({ (cfg, req, res) -> MyCustomContext(cfg, req, res})
    .create()
    .start(7000)
    app.get("/") { ctx -> ctx.result("Hello World") }
}

class MyCustomConfig : JavalinServletContext ... 

@tipsy
Copy link
Member

tipsy commented Dec 17, 2023

That's definitely a possibility, but won't be included in 6x. We have released a change similar to what was described above, you can read about it here: https://javalin.io/plugins/how-to#creating-a-plugin-with-config-and-extension

You can try it out in 6.0.0-beta.4: https://central.sonatype.com/artifact/io.javalin/javalin/6.0.0-beta.4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: No status
Development

No branches or pull requests

4 participants