-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
1.) refactored middleware building logic 2.) added caching to middleware logic 3.) moved middleware ordering tests into its own file
- Loading branch information
Showing
11 changed files
with
269 additions
and
225 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
module AutoDoc | ||
module AutoDoc | ||
using HTTP | ||
using Dates | ||
using DataStructures | ||
|
@@ -10,13 +10,7 @@ using ..Constants | |
using ..AppContext: Context, Documenation | ||
using ..Types: TaggedRoute, TaskDefinition, CronDefinition, Nullable | ||
|
||
export registerschema, | ||
swaggerhtml, redochtml, getschemapath, configdocs, mergeschema, setschema, | ||
getrepeatasks, hasmiddleware, compose, resetstatevariables | ||
|
||
const SWAGGER_VERSION = "[email protected]" | ||
const REDOC_VERSION = "[email protected]" | ||
|
||
export registerschema, swaggerhtml, redochtml, mergeschema | ||
|
||
""" | ||
mergeschema(route::String, customschema::Dict) | ||
|
@@ -35,190 +29,27 @@ Merge the top-level autogenerated schema with a custom schema | |
""" | ||
function mergeschema(schema::Dict, customschema::Dict) | ||
updated_schema = recursive_merge(schema, customschema) | ||
merge!(schema, updated_schema) | ||
end | ||
|
||
|
||
""" | ||
returns true if we have any special middleware (router or route specific) | ||
""" | ||
function hasmiddleware(custommiddleware::Dict{String, Tuple})::Bool | ||
return !isempty(custommiddleware) | ||
end | ||
|
||
|
||
""" | ||
This function dynamically determines which middleware functions to apply to a request at runtime. | ||
If router or route specific middleware is defined, then it's used instead of the globally defined | ||
middleware. | ||
""" | ||
function compose(router, appmiddleware, custommiddleware) | ||
return function(handler) | ||
return function(req::HTTP.Request) | ||
innerhandler, path, params = HTTP.Handlers.gethandler(router, req) | ||
# Check if the current request matches one of our predefined routes | ||
if innerhandler !== nothing | ||
|
||
# always initialize with the next handler function | ||
layers::Vector{Function} = [ handler ] | ||
|
||
# lookup the middleware for this path | ||
routermiddleware, routemiddleware = get(custommiddleware, "$(req.method)|$path", (nothing, nothing)) | ||
|
||
# calculate the checks ahead of time | ||
hasrouter = !isnothing(routermiddleware) | ||
hasroute = !isnothing(routemiddleware) | ||
|
||
# case 1: no middleware is defined at any level -> use global middleware | ||
if !hasrouter && !hasroute | ||
append!(layers, reverse(appmiddleware)) | ||
|
||
# case 2: if route level is empty -> don't add any middleware | ||
elseif hasroute && isempty(routemiddleware) | ||
return req |> reduce(|>, layers) | ||
|
||
# case 3: if router level is empty -> only register route level middleware | ||
elseif hasrouter && isempty(routermiddleware) | ||
hasroute && append!(layers, reverse(routemiddleware)) | ||
|
||
# case 4: router & route level is defined -> combine global, router, and route middleware | ||
elseif hasrouter && hasroute | ||
append!(layers, reverse([appmiddleware..., routermiddleware..., routemiddleware...])) | ||
|
||
# case 5: only router level is defined -> combine global and router middleware | ||
elseif hasrouter && !hasroute | ||
append!(layers, reverse([appmiddleware..., routermiddleware...])) | ||
|
||
# case 6: only route level is defined -> combine global + route level middleware | ||
elseif !hasrouter && hasroute | ||
append!(layers, reverse([appmiddleware..., routemiddleware...])) | ||
end | ||
|
||
# combine all the middleware functions together | ||
return req |> reduce(|>, layers) | ||
end | ||
return handler(req) | ||
end | ||
end | ||
end | ||
|
||
""" | ||
This functions assists registering routes with a specific prefix. | ||
You can optionally assign tags either at the prefix and/or route level which | ||
are used to group and organize the autogenerated documentation | ||
""" | ||
#function router(taggedroutes::Dict{String, TaggedRoute}, custommiddleware::Dict{String, Tuple}, repeattasks::Vector, prefix::String = ""; | ||
function router(ctx::Context, prefix::String = ""; | ||
tags::Vector{String} = Vector{String}(), | ||
middleware::Nullable{Vector} = nothing, | ||
interval::Nullable{Real} = nothing, | ||
cron::Nullable{String} = nothing) | ||
|
||
return createrouter(ctx, prefix, tags, middleware, interval, cron) | ||
end | ||
|
||
function createrouter(ctx::Context, prefix::String, | ||
routertags::Vector{String}, | ||
routermiddleware::Nullable{Vector}, | ||
routerinterval::Nullable{Real}, | ||
routercron::Nullable{String} = nothing) | ||
|
||
# appends a "/" character to the given string if doesn't have one. | ||
function fixpath(path::String) | ||
path = String(strip(path)) | ||
if !isnothing(path) && !isempty(path) && path !== "/" | ||
return startswith(path, "/") ? path : "/$path" | ||
end | ||
return "" | ||
end | ||
|
||
# This function takes input from the user next to the request handler | ||
return function(path = nothing; | ||
tags::Vector{String} = Vector{String}(), | ||
middleware::Nullable{Vector} = nothing, | ||
interval::Nullable{Real} = routerinterval, | ||
cron::Nullable{String} = routercron) | ||
|
||
# this is called inside the @register macro (only it knows the exact httpmethod associated with each path) | ||
return function(httpmethod::String) | ||
|
||
""" | ||
This scenario can happen when the user passes a router object directly like so: | ||
@get router("/math/power/{a}/{b}") function (req::HTTP.Request, a::Float64, b::Float64) | ||
return a ^ b | ||
end | ||
Under normal circumstances, the function returned by the router call is used when registering routes. | ||
However, in this specific case, the call to router returns a higher-order function (HOF) that's nested one | ||
layer deeper than expected. | ||
Due to the way we call these functions to derive the path for the currently registered route, | ||
the path argument can sometimes be mistakenly set to the HTTP method (e.g., "GET", "POST"). | ||
This can lead to the path getting concatenated with the HTTP method string. | ||
To account for this specific use case, we've added a check in the inner function to verify whether | ||
path matches the current passed in httpmethod. If it does, we assume that path has been incorrectly | ||
set to the HTTP method, and we update path to use the router prefix instead. | ||
""" | ||
if path === httpmethod | ||
path = prefix | ||
else | ||
# combine the current routers prefix with this specfic path | ||
path = !isnothing(path) ? "$(fixpath(prefix))$(fixpath(path))" : fixpath(prefix) | ||
end | ||
|
||
if !(isnothing(routermiddleware) && isnothing(middleware)) | ||
# add both router & route-sepecific middleware | ||
ctx.service.custommiddleware["$httpmethod|$path"] = (routermiddleware, middleware) | ||
end | ||
|
||
# register interval for this route | ||
if !isnothing(interval) && interval >= 0.0 | ||
task = TaskDefinition(path, httpmethod, interval) | ||
push!(ctx.tasks.task_definitions, task) | ||
end | ||
|
||
# register cron expression for this route | ||
if !isnothing(cron) && !isempty(cron) | ||
job = CronDefinition(path, httpmethod, cron) | ||
push!(ctx.cron.job_definitions, job) | ||
end | ||
|
||
combinedtags = [tags..., routertags...] | ||
|
||
# register tags | ||
if !haskey(ctx.docs.taggedroutes, path) | ||
ctx.docs.taggedroutes[path] = TaggedRoute([httpmethod], combinedtags) | ||
else | ||
combinedmethods = vcat(httpmethod, ctx.docs.taggedroutes[path].httpmethods) | ||
ctx.docs.taggedroutes[path] = TaggedRoute(combinedmethods, combinedtags) | ||
end | ||
|
||
#return path | ||
return path | ||
end | ||
end | ||
merge!(schema, updated_schema) | ||
end | ||
|
||
|
||
""" | ||
Returns the openapi equivalent of each Julia type | ||
""" | ||
function gettype(type::Type) :: String | ||
function gettype(type::Type)::String | ||
if type <: Bool | ||
return "boolean" | ||
elseif type <: AbstractFloat | ||
return "number" | ||
elseif type <: Integer | ||
elseif type <: Integer | ||
return "integer" | ||
elseif type <: AbstractVector | ||
return "array" | ||
elseif type <: String || type == Date || type == DateTime | ||
return "string" | ||
elseif isstructtype(type) | ||
return "object" | ||
else | ||
else | ||
return "string" | ||
end | ||
end | ||
|
@@ -227,20 +58,20 @@ end | |
Returns the specific format type for a given parameter | ||
ex.) DateTime(2022,1,1) => "date-time" | ||
""" | ||
function getformat(type::Type) :: Nullable{String} | ||
function getformat(type::Type)::Nullable{String} | ||
if type <: AbstractFloat | ||
if type == Float32 | ||
return "float" | ||
elseif type == Float64 | ||
return "double" | ||
end | ||
elseif type <: Integer | ||
elseif type <: Integer | ||
if type == Int32 | ||
return "int32" | ||
elseif type == Int64 | ||
return "int64" | ||
end | ||
elseif type == Date | ||
elseif type == Date | ||
return "date" | ||
elseif type == DateTime | ||
return "date-time" | ||
|
@@ -249,7 +80,6 @@ function getformat(type::Type) :: Nullable{String} | |
end | ||
|
||
|
||
|
||
""" | ||
Used to generate & register schema related for a specific endpoint | ||
""" | ||
|
@@ -258,9 +88,9 @@ function registerschema(docs::Documenation, path::String, httpmethod::String, pa | |
params = [] | ||
for (name, type) in parameters | ||
format = getformat(type) | ||
param = Dict( | ||
param = Dict( | ||
"in" => "path", | ||
"name" => "$name", | ||
"name" => "$name", | ||
"required" => true, | ||
"schema" => Dict( | ||
"type" => gettype(type) | ||
|
@@ -274,8 +104,8 @@ function registerschema(docs::Documenation, path::String, httpmethod::String, pa | |
|
||
# lookup if this route has any registered tags | ||
if haskey(docs.taggedroutes, path) && httpmethod in docs.taggedroutes[path].httpmethods | ||
tags = docs.taggedroutes[path].tags | ||
else | ||
tags = docs.taggedroutes[path].tags | ||
else | ||
tags = [] | ||
end | ||
|
||
|
@@ -331,21 +161,23 @@ function registerschema(docs::Documenation, path::String, httpmethod::String, pa | |
mergeschema(docs.schema, cleanedpath, route) | ||
end | ||
|
||
|
||
""" | ||
Read in a static file from the /data folder | ||
""" | ||
function readstaticfile(filepath::String) :: String | ||
function readstaticfile(filepath::String)::String | ||
path = joinpath(DATA_PATH, filepath) | ||
return read(path, String) | ||
end | ||
|
||
function redochtml(schemapath::String, docspath::String) :: HTTP.Response | ||
|
||
function redochtml(schemapath::String, docspath::String)::HTTP.Response | ||
redocjs = readstaticfile("$REDOC_VERSION/redoc.standalone.js") | ||
|
||
html(""" | ||
<!DOCTYPE html> | ||
<html lang="en"> | ||
<head> | ||
<title>Docs</title> | ||
<meta charset="utf-8"/> | ||
|
@@ -358,15 +190,16 @@ function redochtml(schemapath::String, docspath::String) :: HTTP.Response | |
<redoc spec-url="$schemapath"></redoc> | ||
<script>$redocjs</script> | ||
</body> | ||
</html> | ||
""") | ||
end | ||
|
||
|
||
""" | ||
Return HTML page to render the autogenerated docs | ||
""" | ||
function swaggerhtml(schemapath::String, docspath::String) :: HTTP.Response | ||
function swaggerhtml(schemapath::String, docspath::String)::HTTP.Response | ||
|
||
# load static content files | ||
swaggerjs = readstaticfile("$SWAGGER_VERSION/swagger-ui-bundle.js") | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,7 +6,8 @@ export PACKAGE_DIR, DATA_PATH, | |
GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, CONNECT, TRACE, | ||
HTTP_METHODS, | ||
WEBSOCKET, STREAM, | ||
SPECIAL_METHODS, METHOD_ALIASES, TYPE_ALIASES | ||
SPECIAL_METHODS, METHOD_ALIASES, TYPE_ALIASES, | ||
SWAGGER_VERSION, REDOC_VERSION | ||
|
||
# Generate a reliable path to our package directory | ||
const PACKAGE_DIR = @path @__DIR__ | ||
|
@@ -45,4 +46,7 @@ const TYPE_ALIASES = Dict{String, Type}( | |
STREAM => HTTP.Streams.Stream | ||
) | ||
|
||
const SWAGGER_VERSION = "[email protected]" | ||
const REDOC_VERSION = "[email protected]" | ||
|
||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.