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

feature/app-context-support #237

Merged
merged 6 commits into from
Jan 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -886,7 +886,7 @@ using Mustache
using Oxygen

# Load the Mustache template from a file and create a render function
render = mustache("./templates/greeting.txt", from_file=false)
render = mustache("./templates/greeting.txt", from_file=true)

@get "/mustache/file" function()
data = Dict("name" => "Chris")
Expand Down
12 changes: 7 additions & 5 deletions src/Oxygen.jl
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ include("instances.jl"); using .Instances
include("extensions/load.jl");

import HTTP: Request, Response, Stream, WebSocket, queryparams
using .Core: Context, History, Server, Nullable
using .Core: ServerContext, History, Server, Nullable
using .Core: GET, POST, PUT, DELETE, PATCH

const CONTEXT = Ref{Context}(Context())
const CONTEXT = Ref{ServerContext}(ServerContext())

import Base: get
include("methods.jl")
Expand All @@ -26,10 +26,10 @@ include("deprecated.jl")
macro oxidise()
quote
import Oxygen
import Oxygen: PACKAGE_DIR, Context, Nullable
import Oxygen: PACKAGE_DIR, ServerContext, Nullable
import Oxygen: GET, POST, PUT, DELETE, PATCH, STREAM, WEBSOCKET

const CONTEXT = Ref{Context}(Context(; mod=$(__module__)))
const CONTEXT = Ref{ServerContext}(ServerContext(; mod=$(__module__)))
include(joinpath(PACKAGE_DIR, "methods.jl"))

nothing; # to hide last definition
Expand All @@ -53,5 +53,7 @@ export @oxidise, @get, @post, @put, @patch, @delete, @route,
starttasks, stoptasks, cleartasks,
startcronjobs, stopcronjobs, clearcronjobs,
# Common HTTP Types
Request, Response, Stream, WebSocket, queryparams
Request, Response, Stream, WebSocket, queryparams,
# Context Types and methods
Context, context
end
2 changes: 1 addition & 1 deletion src/autodoc.jl
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ using RelocatableFolders

using ..Util: html, recursive_merge
using ..Constants
using ..AppContext: Context, Documenation
using ..AppContext: ServerContext, Documenation
using ..Types: TaggedRoute, TaskDefinition, CronDefinition, Nullable, Param, isrequired
using ..Extractors: isextractor, extracttype, isreqparam
using ..Reflection: splitdef
Expand Down
13 changes: 7 additions & 6 deletions src/context.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ using HTTP
using HTTP: Server, Router
using ..Types

export Context, CronContext, TasksContext, Documenation, EagerReviseService, Service, history, wait, close, isopen
export ServerContext, CronContext, TasksContext, Documenation, EagerReviseService, Service, history, wait, close, isopen

function defaultSchema() :: Dict
Dict(
Expand Down Expand Up @@ -61,12 +61,13 @@ end
middleware_cache_lock :: ReentrantLock = ReentrantLock()
end

@kwdef struct Context
@kwdef struct ServerContext
service :: Service = Service()
docs :: Documenation = Documenation()
cron :: CronContext = CronContext()
tasks :: TasksContext = TasksContext()
mod :: Nullable{Module} = nothing
app_context :: Ref{Any} = Ref{Any}(missing) # This stores a reference to an Context{T} object
end

Base.isopen(service::Service) = !isnothing(service.server[]) && isopen(service.server[])
Expand All @@ -79,12 +80,12 @@ end

# @eval begin
# """
# Context(ctx::Context; kwargs...)
# ServerContext(ctx::ServerContext; kwargs...)

# Create a new `Context` object by copying an existing one and optionally overriding some of its fields with keyword arguments.
# Create a new `ServerContext` object by copying an existing one and optionally overriding some of its fields with keyword arguments.
# """
# function Context(ctx::Context; $([Expr(:kw ,k, :(ctx.$k)) for k in fieldnames(Context)]...))
# return Context($(fieldnames(Context)...))
# function ServerContext(ctx::ServerContext; $([Expr(:kw ,k, :(ctx.$k)) for k in fieldnames(ServerContext)]...))
# return ServerContext($(fieldnames(ServerContext)...))
# end
# end

Expand Down
98 changes: 59 additions & 39 deletions src/core.jl
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ function serverwelcome(external_url::String, docs::Bool, metrics::Bool, parallel
end

struct ReviseHandler
ctx::Context
ctx::ServerContext
end

function (handler::ReviseHandler)(handle)
Expand All @@ -87,7 +87,7 @@ end

Start the webserver with your own custom request handler
"""
function serve(ctx::Context;
function serve(ctx::ServerContext;
middleware = [],
handler = stream_handler,
host = "127.0.0.1",
Expand All @@ -103,9 +103,14 @@ function serve(ctx::Context;
docs_path = "/docs",
schema_path = "/schema",
external_url = nothing,
context = missing,
revise = :none, # :none, :lazy, :eager
kwargs...) :: Server

if !ismissing(context)
ctx.app_context[] = Context(context)
end

# set the external url if it's passed
if !isnothing(external_url)
ctx.service.external_url[] = external_url
Expand Down Expand Up @@ -197,7 +202,7 @@ end

stops the webserver immediately
"""
function terminate(context::Context)
function terminate(context::ServerContext)
if isopen(context.service)
# stop background cron jobs
stopcronjobs(context.cron)
Expand All @@ -209,14 +214,17 @@ function terminate(context::Context)

# stop server
close(context.service)

# Set the external url to nothing when the server is terminated
context.service.external_url[] = nothing
end
end


"""
Register all cron jobs defined through our router() HOF
"""
function registercronjobs(ctx::Context)
function registercronjobs(ctx::ServerContext)
for job in ctx.cron.job_definitions
path, httpmethod, expression = job.path, job.httpmethod, job.expression
cron(ctx.cron.registered_jobs, expression, path, () -> internalrequest(ctx, HTTP.Request(httpmethod, path)))
Expand All @@ -226,7 +234,7 @@ end
"""
Register all repeat tasks defined through our router() HOF
"""
function registertasks(ctx::Context)
function registertasks(ctx::ServerContext)
for task_def in ctx.tasks.task_definitions
path, httpmethod, interval = task_def.path, task_def.httpmethod, task_def.interval
task(ctx.tasks.registered_tasks, interval, path, () -> internalrequest(ctx, HTTP.Request(httpmethod, path)))
Expand Down Expand Up @@ -288,7 +296,7 @@ Compose the user & internally defined middleware functions together. Practically
users to 'chain' middleware functions like `serve(handler1, handler2, handler3)` when starting their
application and have them execute in the order they were passed (left to right) for each incoming request
"""
function setupmiddleware(ctx::Context; middleware::Vector=[], docs::Bool=true, metrics::Bool=true, serialize::Bool=true, catch_errors::Bool=true, show_errors=true)::Function
function setupmiddleware(ctx::ServerContext; middleware::Vector=[], docs::Bool=true, metrics::Bool=true, serialize::Bool=true, catch_errors::Bool=true, show_errors=true)::Function

# determine if we have any special router or route-specific middleware
custom_middleware = if !isempty(ctx.service.custommiddleware)
Expand Down Expand Up @@ -320,7 +328,7 @@ end
"""
Internal helper function to launch the server in a consistent way
"""
function startserver(ctx::Context; host, port, show_banner=false, docs=false, metrics=false, parallel=false, async=false, kwargs, start)::Server
function startserver(ctx::ServerContext; host, port, show_banner=false, docs=false, metrics=false, parallel=false, async=false, kwargs, start)::Server

docs && setupdocs(ctx)
metrics && setupmetrics(ctx)
Expand Down Expand Up @@ -376,7 +384,7 @@ end
Directly call one of our other endpoints registered with the router, using your own middleware
and bypassing any globally defined middleware
"""
function internalrequest(ctx::Context, req::HTTP.Request; middleware::Vector=[], metrics::Bool=false, serialize::Bool=true, catch_errors=true)::HTTP.Response
function internalrequest(ctx::ServerContext, req::HTTP.Request; middleware::Vector=[], metrics::Bool=false, serialize::Bool=true, catch_errors=true)::HTTP.Response
req.context[:ip] = "INTERNAL" # label internal requests
return req |> setupmiddleware(ctx; middleware, metrics, serialize, catch_errors)
end
Expand Down Expand Up @@ -516,8 +524,14 @@ function parse_func_params(route::String, func::Function)
body_params = []

for param in info.args
# case 1: it's an extractor type
if param.type <: Extractor

# case 1: it's an Context type it will be injected by the framework (so we skip it)
if param.type <: Context
continue

# case 2: it's an extractor type
elseif param.type <: Extractor

innner_type = param.type |> extracttype
# push the variables from the struct into the params array
if param.type <: Path
Expand All @@ -534,12 +548,12 @@ function parse_func_params(route::String, func::Function)
push!(body_params, param)
end

# case 2: It's a path parameter
# case 3: It's a path parameter
elseif param.name in route_params
push!(pathnames, param.name)
push!(path_params, param)

# Case 3: It's a query parameter
# Case 4: It's a query parameter
else
push!(querynames, param.name)
push!(query_params, param)
Expand Down Expand Up @@ -568,13 +582,12 @@ function parse_func_params(route::String, func::Function)
end



"""
register(ctx::Context, httpmethod::String, route::String, func::Function)
register(ctx::ServerContext, httpmethod::String, route::String, func::Function)

Register a request handler function with a path to the ROUTER
"""
function register(ctx::Context, httpmethod::String, route::Union{String,Function}, func::Function)
function register(ctx::ServerContext, httpmethod::String, route::Union{String,Function}, func::Function)
# Parse & validate path parameters
route = parse_route(httpmethod, route)
func_details = parse_func_params(route, func)
Expand All @@ -593,29 +606,29 @@ function register(ctx::Context, httpmethod::String, route::Union{String,Function
end

# Register the route with the router
registerhandler(ctx.service.router, httpmethod, route, func, func_details)
registerhandler(ctx, ctx.service.router, httpmethod, route, func, func_details)
end


"""
This alternaive registers a route wihout generating any documentation for it. Used primarily for internal routes like
This registers a route wihout generating any documentation for it. Used primarily for internal routes like
docs and metrics
"""
function register(router::Router, httpmethod::String, route::Union{String,Function}, func::Function)
function register_internal(ctx::ServerContext, router::Router, httpmethod::String, route::Union{String,Function}, func::Function)
# Parse & validate path parameters
route = parse_route(httpmethod, route)
func_details = parse_func_params(route, func)

# Register the route with the router
registerhandler(router, httpmethod, route, func, func_details)
registerhandler(ctx, router, httpmethod, route, func, func_details)
end


"""
Given an incoming request, parse out each argument and prepare it to get passed to the
corresponding handler.
"""
function extract_params(req::HTTP.Request, func_details)::Vector{Any}
function extract_params(ctx::ServerContext, req::HTTP.Request, func_details)::Vector{Any}
info = func_details.info
pathparams = func_details.pathnames
queryparams = func_details.querynames
Expand All @@ -626,7 +639,10 @@ function extract_params(req::HTTP.Request, func_details)::Vector{Any}
for param in info.sig
name = string(param.name)

if param.type <: Extractor
if param.type <: Context
push!(parameters, ctx.app_context[])

elseif param.type <: Extractor
push!(parameters, extract(param, lazy_req))

elseif param.name in pathparams
Expand All @@ -649,7 +665,7 @@ function extract_params(req::HTTP.Request, func_details)::Vector{Any}
end


function registerhandler(router::Router, httpmethod::String, route::String, func::Function, func_details::NamedTuple)
function registerhandler(ctx::ServerContext, router::Router, httpmethod::String, route::String, func::Function, func_details::NamedTuple)

# Get information about the function's arguments
method = first(methods(func))
Expand All @@ -671,7 +687,7 @@ function registerhandler(router::Router, httpmethod::String, route::String, func
end
else
handle = function (req::HTTP.Request)
path_parameters = extract_params(req, func_details)
path_parameters = extract_params(ctx, req, func_details)
func_handle(req, func; pathParams=path_parameters)
end
end
Expand All @@ -681,25 +697,25 @@ function registerhandler(router::Router, httpmethod::String, route::String, func
HTTP.register!(router, resolved_httpmethod, route, handle)
end

function setupdocs(ctx::Context)
setupdocs(ctx.docs.router[], ctx.docs.schema, ctx.docs.docspath[], ctx.docs.schemapath[])
function setupdocs(ctx::ServerContext)
setupdocs(ctx, ctx.docs.router[], ctx.docs.schema, ctx.docs.docspath[], ctx.docs.schemapath[])
end

# add the swagger and swagger/schema routes
function setupdocs(router::Router, schema::Dict, docspath::String, schemapath::String)
function setupdocs(ctx::ServerContext, router::Router, schema::Dict, docspath::String, schemapath::String)
full_schema = "$docspath$schemapath"
register(router, "GET", "$docspath", () -> swaggerhtml(full_schema, docspath))
register(router, "GET", "$docspath/swagger", () -> swaggerhtml(full_schema, docspath))
register(router, "GET", "$docspath/redoc", () -> redochtml(full_schema, docspath))
register(router, "GET", full_schema, () -> schema)
register_internal(ctx, router, "GET", "$docspath", () -> swaggerhtml(full_schema, docspath))
register_internal(ctx, router, "GET", "$docspath/swagger", () -> swaggerhtml(full_schema, docspath))
register_internal(ctx, router, "GET", "$docspath/redoc", () -> redochtml(full_schema, docspath))
register_internal(ctx, router, "GET", full_schema, () -> schema)
end

function setupmetrics(context::Context)
setupmetrics(context.docs.router[], context.service.history, context.docs.docspath[], context.service.history_lock)
function setupmetrics(context::ServerContext)
setupmetrics(context, context.docs.router[], context.service.history, context.docs.docspath[], context.service.history_lock)
end

# add the swagger and swagger/schema routes
function setupmetrics(router::Router, history::History, docspath::String, history_lock::ReentrantLock)
function setupmetrics(ctx::ServerContext, router::Router, history::History, docspath::String, history_lock::ReentrantLock)

# This allows us to customize the path to the metrics dashboard
function loadfile(filepath)::String
Expand All @@ -713,7 +729,7 @@ function setupmetrics(router::Router, history::History, docspath::String, histor
end
end

staticfiles(router, "$DATA_PATH/dashboard", "$docspath/metrics"; loadfile=loadfile)
staticfiles(ctx, router, "$DATA_PATH/dashboard", "$docspath/metrics"; loadfile=loadfile)

# Create a thread-safe copy of the history object and it's internal data
function safe_get_transactions(history::History)::Vector{HTTPTransaction}
Expand Down Expand Up @@ -745,7 +761,7 @@ function setupmetrics(router::Router, history::History, docspath::String, histor

end

register(router, GET, "$docspath/metrics/data/{window}/{latest}", innermetrics)
register_internal(ctx, router, GET, "$docspath/metrics/data/{window}/{latest}", innermetrics)
end


Expand All @@ -755,7 +771,9 @@ end
Mount all files inside the /static folder (or user defined mount point).
The `headers` array will get applied to all mounted files
"""
function staticfiles(router::HTTP.Router,
function staticfiles(
ctx::ServerContext,
router::HTTP.Router,
folder::String,
mountdir::String="static";
headers::Vector=[],
Expand All @@ -767,7 +785,7 @@ function staticfiles(router::HTTP.Router,
end
function addroute(currentroute, filepath)
resp = file(filepath; loadfile=loadfile, headers=headers)
register(router, GET, currentroute, () -> resp)
register_internal(ctx, router, GET, currentroute, () -> resp)
end
mountfolder(folder, mountdir, addroute)
end
Expand All @@ -779,7 +797,9 @@ end
Mount all files inside the /static folder (or user defined mount point),
but files are re-read on each request. The `headers` array will get applied to all mounted files
"""
function dynamicfiles(router::Router,
function dynamicfiles(
ctx::ServerContext,
router::Router,
folder::String,
mountdir::String="static";
headers::Vector=[],
Expand All @@ -790,7 +810,7 @@ function dynamicfiles(router::Router,
mountdir = mountdir[2:end]
end
function addroute(currentroute, filepath)
register(router, GET, currentroute, () -> file(filepath; loadfile=loadfile, headers=headers))
register_internal(ctx, router, GET, currentroute, () -> file(filepath; loadfile=loadfile, headers=headers))
end
mountfolder(folder, mountdir, addroute)
end
Expand Down
Loading
Loading