OxygenFramework / Oxygen.jl

💨 A breath of fresh air for programming web apps in Julia
https://oxygenframework.github.io/Oxygen.jl/
MIT License
409 stars 25 forks source link

Using Oxygen in a package #115

Closed JanisErdmanis closed 1 year ago

JanisErdmanis commented 1 year ago

I love the idea of writing HTTP request handlers and simultaneously registering them to a router. I would also love to try to use Swagger to document my service currently written in bare HTTP.jl, as can be seen here. However, I have stumbled upon an issue where my handlers are not registered when it's imported from the module.

For example, let's consider a module:

module Example

using Oxygen
using HTTP

get("/") do request::HTTP.Request
    return "hello world!"
end

serve() = Oxygen.serve(port=8081)

end

Using this module as import Example; Example.serve() would serve a blank page at /.

Interestingly, I found that if I import the module as a file include("src/Example.jl"); Example.serve(), the handler works as expected. Also, I found that moving the handler to the __init__ fixes the issue; however, that is not a viable solution as that would either make __init__ function unbearably long or would make code less local in comparison with direct usage of HTTP.

ndortega commented 1 year ago

Hi @JanisErdmanis, No worries, that's actually intended behavior.

In Julia, importing a module does not run all the code defined inside it at runtime. Instead, it only loads the module into memory and makes its exported functions and variables available for use in the current scope.

If you want to run all the code defined inside a module, you can use the include() function. This function reads and evaluates the contents of a file as if they were entered directly into the REPL or script.

You can read more about this here: https://docs.julialang.org/en/v1/manual/code-loading/

So when integrating Oxygen into a custom module, you'll need to wrap the code that registers routes with some function. You can use the __init__ function to have it called automatically or create your own function that would need to get called manually. Alternatively, you can load your custom module with include() like you're already doing here: https://github.com/PeaceFounder/PeaceFounder.jl/blob/master/src/PeaceFounder.jl#L24

This line is what executes all the code in your module and registers your routes automatically. If you take a similar approach with oxygen, it should work as expected.


Below is a brief example of how you could get this to work in your package. Break out the api operations into its own module and then include it, which will register all your routes.

image

JanisErdmanis commented 1 year ago

This still does not work as I do load PeaceFounder as a package. However, I just found that I can use @eval at the __init__ block to include my service module:

module Example

function __init__()
    fname = (@__DIR__) * "/Service.jl"
    @eval include($fname)
end

serve() = Service.serve()

end 

and that works as expected. Could there be any downsides to using this approach?

Also, thanks for pointing out to code-loading section. That has made me confused lately.

ndortega commented 1 year ago

Honestly, I like that solution you came up with. It's short, clear, and is scalable ( it should automatically execute any other code referenced from that module ).

The only downside I'd be wary of is any weird precompilation issues from using eval in your package. Granted, this should be ok since you're only evaluating your own code that's tracked in git. This usually becomes an issue when people try to evaluate stuff that can change during run-time.

Here's the specific use case and discussion I found: https://discourse.julialang.org/t/precompilation-init-and-eval/70188

On a side note, the following package isn't using this new approach we're talking about, but it could still be helpful to see a package that uses Oxygen called RemoteHPC by Louis Ponet: https://github.com/louisponet/RemoteHPC.jl/blob/master/src/api.jl#L38

JanisErdmanis commented 1 year ago

I am honestly hesitant to use @eval + include approach I proposed earlier. The reason is that execution at the runtime is nonessential when compared with using HTTP directly. In my opinion, there is a missing Julia feature but I am not sure what it is.

Another approach I am considering is to include the Oxygen as a local module:

module OxygenExample

include(Base.find_package("Oxygen"))
using .Oxygen

using HTTP

get("/") do request::HTTP.Request
    return "hello world!"
end

serve() = Oxygen.serve(port=8081)

end 

which works fine, as expected. The only downside I see is that I need to manually add Oxygen's dependencies for that to work, but I expect that could be automated within a function.

ndortega commented 1 year ago

That's a pretty wild approach. I had no idea we could add packages as local modules in Julia!

Although, I would caution you against going that route unless you absolutely had to. If I ever have to add a new dependency and publish it - it could break your package and require you to republish your app with the new dependency. I personally wouldn't want my package to be coupled so tightly to another package.

Granted, you could get around this by explicitly adding strict version rules in the compatibility section of your Project.toml file. This would prevent any potential breaking changes from getting introduced at the cost of your users not having access to the latest updates from Oxygen (until you manually update your compatibility section).

But at the end of the day, It's your package and you should choose whatever approach best meets your requirements. I'm just thankful that you see Oxygen as a viable alternative to HTTP.jl and am excited to see what you can do with it! Let me know if you have any other questions or feature requests. Package authors are the power users of our little community and I want to make sure to support them in any way that I can.