From f3af6c7f2f6f3baf3cf83c2bc3b448198964717f Mon Sep 17 00:00:00 2001 From: Nathan Ortega Date: Sat, 3 Jun 2023 23:06:11 -0400 Subject: [PATCH] feature/routing-functions (#112) 1.) Added support for routing functions 2.) converted the @register macro into a regular function 3.) demo files now use Julia v1.6.6 --- Project.toml | 2 +- README.md | 31 ++++- demo/Manifest.toml | 210 ++++++++++++++--------------- demo/routingfunctions.jl | 28 ++++ docs/src/api.md | 7 + docs/src/index.md | 31 ++++- src/Oxygen.jl | 1 + src/autodoc.jl | 33 ++++- src/core.jl | 246 ++++++++++++++++++---------------- src/util.jl | 9 +- test/crontests.jl | 2 +- test/routingfunctionstests.jl | 108 +++++++++++++++ test/runtests.jl | 1 + 13 files changed, 475 insertions(+), 234 deletions(-) create mode 100644 demo/routingfunctions.jl create mode 100644 test/routingfunctionstests.jl diff --git a/Project.toml b/Project.toml index 61bd6b41..b72e3303 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Oxygen" uuid = "df9a0d86-3283-4920-82dc-4555fc0d1d8b" authors = ["Nathan Ortega "] -version = "1.1.8" +version = "1.1.9" [deps] Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" diff --git a/README.md b/README.md index 431b73e0..d3b56337 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Breathe easy knowing you can quickly spin up a web server with abstractions you' ## Features -- Straightforward routing (`@get`, `@post`, `@put`, `@patch`, `@delete` and `@route` macros) +- Straightforward routing - Auto-generated swagger documentation - Out-of-the-box JSON serialization & deserialization (customizable) - Type definition support for path parameters @@ -88,6 +88,35 @@ end serve() ``` +## Routing Macro & Function Syntax + +There are two primary ways to register your request handlers: the standard routing macros or the routing functions which utilize the do-block syntax. + +For each routing macro, we now have a an equivalent routing function + +```julia +@get -> get() +@post -> post() +@put -> put() +@patch -> patch() +@delete -> delete() +@route -> route() +``` + +The only practical difference between the two is that the routing macros are called during the precompilation +stage, whereas the routing functions are only called when invoked. (The routing macros call the routing functions under the hood) + +```julia +# Routing Macro syntax +@get "/add/{x}/{y}" function(request::HTTP.Request, x::Int, y::Int) + x + y +end + +# Routing Function syntax +get("/add/{x}/{y}") do request::HTTP.Request, x::Int, y::Int + x + y +end +``` ## Path parameters diff --git a/demo/Manifest.toml b/demo/Manifest.toml index 4c731724..cd43405d 100644 --- a/demo/Manifest.toml +++ b/demo/Manifest.toml @@ -1,277 +1,263 @@ # This file is machine-generated - editing it directly is not advised -julia_version = "1.8.5" -manifest_format = "2.0" -project_hash = "a47d9d06639a08f5f79e43adf2cfdf9c0ab10331" - -[[deps.ArgTools]] +[[ArgTools]] uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f" -version = "1.1.1" -[[deps.Artifacts]] +[[Artifacts]] uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" -[[deps.Base64]] +[[Base64]] uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" -[[deps.BitFlags]] +[[BitFlags]] git-tree-sha1 = "43b1a4a8f797c1cddadf60499a8a077d4af2cd2d" uuid = "d1d4a3ce-64b1-5f1a-9ba4-7e7e69966f35" version = "0.1.7" -[[deps.CodecZlib]] +[[CodecZlib]] deps = ["TranscodingStreams", "Zlib_jll"] -git-tree-sha1 = "ded953804d019afa9a3f98981d99b33e3db7b6da" +git-tree-sha1 = "9c209fb7536406834aa938fb149964b985de6c83" uuid = "944b1d66-785c-5afd-91f1-9de20f533193" -version = "0.7.0" +version = "0.7.1" -[[deps.Dates]] +[[ConcurrentUtilities]] +deps = ["Serialization", "Sockets"] +git-tree-sha1 = "96d823b94ba8d187a6d8f0826e731195a74b90e9" +uuid = "f0e56b4a-5159-44fe-b623-3e5288b988bb" +version = "2.2.0" + +[[Dates]] deps = ["Printf"] uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" -[[deps.Downloads]] -deps = ["ArgTools", "FileWatching", "LibCURL", "NetworkOptions"] +[[Downloads]] +deps = ["ArgTools", "LibCURL", "NetworkOptions"] uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6" -version = "1.6.0" - -[[deps.FileWatching]] -uuid = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee" -[[deps.HTTP]] -deps = ["Base64", "CodecZlib", "Dates", "IniFile", "Logging", "LoggingExtras", "MbedTLS", "NetworkOptions", "OpenSSL", "Random", "SimpleBufferStream", "Sockets", "URIs", "UUIDs"] -git-tree-sha1 = "fd9861adba6b9ae4b42582032d0936d456c8602d" +[[HTTP]] +deps = ["Base64", "CodecZlib", "ConcurrentUtilities", "Dates", "Logging", "LoggingExtras", "MbedTLS", "NetworkOptions", "OpenSSL", "Random", "SimpleBufferStream", "Sockets", "URIs", "UUIDs"] +git-tree-sha1 = "41f7dfb2b20e7e8bf64f6b6fae98f4d2df027b06" uuid = "cd3eb016-35fb-5094-929b-558a96fad6f3" -version = "1.6.3" +version = "1.9.4" -[[deps.IniFile]] -git-tree-sha1 = "f550e6e32074c939295eb5ea6de31849ac2c9625" -uuid = "83e8ac13-25f8-5344-8a64-a9f2b223428f" -version = "0.5.1" - -[[deps.InteractiveUtils]] +[[InteractiveUtils]] deps = ["Markdown"] uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" -[[deps.JLLWrappers]] +[[JLLWrappers]] deps = ["Preferences"] git-tree-sha1 = "abc9885a7ca2052a736a600f7fa66209f96506e1" uuid = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210" version = "1.4.1" -[[deps.JSON]] +[[JSON]] deps = ["Dates", "Mmap", "Parsers", "Unicode"] -git-tree-sha1 = "3c837543ddb02250ef42f4738347454f95079d4e" +git-tree-sha1 = "31e996f0a15c7b280ba9f76636b3ff9e2ae58c9a" uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" -version = "0.21.3" +version = "0.21.4" -[[deps.JSON3]] +[[JSON3]] deps = ["Dates", "Mmap", "Parsers", "SnoopPrecompile", "StructTypes", "UUIDs"] git-tree-sha1 = "84b10656a41ef564c39d2d477d7236966d2b5683" uuid = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" version = "1.12.0" -[[deps.JSONSchema]] -deps = ["HTTP", "JSON", "URIs"] -git-tree-sha1 = "8d928db71efdc942f10e751564e6bbea1e600dfe" +[[JSONSchema]] +deps = ["Downloads", "HTTP", "JSON", "URIs"] +git-tree-sha1 = "58cb291b01508293f7a9dc88325bc00d797cf04d" uuid = "7d188eb4-7ad8-530c-ae41-71a32a6d4692" -version = "1.0.1" +version = "1.1.0" -[[deps.LibCURL]] +[[LibCURL]] deps = ["LibCURL_jll", "MozillaCACerts_jll"] uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21" -version = "0.6.3" -[[deps.LibCURL_jll]] +[[LibCURL_jll]] deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll", "Zlib_jll", "nghttp2_jll"] uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0" -version = "7.84.0+0" -[[deps.LibGit2]] +[[LibGit2]] deps = ["Base64", "NetworkOptions", "Printf", "SHA"] uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" -[[deps.LibSSH2_jll]] +[[LibSSH2_jll]] deps = ["Artifacts", "Libdl", "MbedTLS_jll"] uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8" -version = "1.10.2+0" -[[deps.Libdl]] +[[Libdl]] uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" -[[deps.Libiconv_jll]] +[[Libiconv_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] git-tree-sha1 = "c7cb1f5d892775ba13767a87c7ada0b980ea0a71" uuid = "94ce4f54-9a6c-5748-9c1c-f9c7231a4531" version = "1.16.1+2" -[[deps.Logging]] +[[Logging]] uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" -[[deps.LoggingExtras]] +[[LoggingExtras]] deps = ["Dates", "Logging"] git-tree-sha1 = "cedb76b37bc5a6c702ade66be44f831fa23c681e" uuid = "e6f89c97-d47a-5376-807f-9c37f3926c36" version = "1.0.0" -[[deps.MIMEs]] +[[MIMEs]] git-tree-sha1 = "65f28ad4b594aebe22157d6fac869786a255b7eb" uuid = "6c6e2e6c-3030-632d-7369-2d6c69616d65" version = "0.1.4" -[[deps.Markdown]] +[[Markdown]] deps = ["Base64"] uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" -[[deps.MbedTLS]] +[[MbedTLS]] deps = ["Dates", "MbedTLS_jll", "MozillaCACerts_jll", "Random", "Sockets"] git-tree-sha1 = "03a9b9718f5682ecb107ac9f7308991db4ce395b" uuid = "739be429-bea8-5141-9913-cc70e7f3736d" version = "1.1.7" -[[deps.MbedTLS_jll]] +[[MbedTLS_jll]] deps = ["Artifacts", "Libdl"] uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1" -version = "2.28.0+0" -[[deps.Mmap]] +[[Mmap]] uuid = "a63ad114-7e13-5084-954f-fe012c677804" -[[deps.MozillaCACerts_jll]] +[[MozillaCACerts_jll]] uuid = "14a3606d-f60d-562e-9121-12d972cd8159" -version = "2022.2.1" -[[deps.NetworkOptions]] +[[NetworkOptions]] uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908" -version = "1.2.0" -[[deps.OpenSSL]] +[[OpenSSL]] deps = ["BitFlags", "Dates", "MozillaCACerts_jll", "OpenSSL_jll", "Sockets"] -git-tree-sha1 = "df6830e37943c7aaa10023471ca47fb3065cc3c4" +git-tree-sha1 = "51901a49222b09e3743c65b8847687ae5fc78eb2" uuid = "4d8831e6-92b7-49fb-bdf8-b643e874388c" -version = "1.3.2" +version = "1.4.1" -[[deps.OpenSSL_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] -git-tree-sha1 = "f6e9dba33f9f2c44e08a020b0caf6903be540004" +[[OpenSSL_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "6cc6366a14dbe47e5fc8f3cbe2816b1185ef5fc4" uuid = "458c3c95-2e84-50aa-8efc-19380b2a3a95" -version = "1.1.19+0" +version = "3.0.8+0" -[[deps.Parsers]] -deps = ["Dates", "SnoopPrecompile"] -git-tree-sha1 = "6466e524967496866901a78fca3f2e9ea445a559" +[[Parsers]] +deps = ["Dates", "PrecompileTools", "UUIDs"] +git-tree-sha1 = "7302075e5e06da7d000d9bfa055013e3e85578ca" uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" -version = "2.5.2" +version = "2.5.9" -[[deps.Pkg]] +[[Pkg]] deps = ["Artifacts", "Dates", "Downloads", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "Serialization", "TOML", "Tar", "UUIDs", "p7zip_jll"] uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" -version = "1.8.0" -[[deps.Preferences]] +[[PrecompileTools]] +deps = ["Preferences"] +git-tree-sha1 = "259e206946c293698122f63e2b513a7c99a244e8" +uuid = "aea7be01-6a6a-4083-8856-8a6e6704d82a" +version = "1.1.1" + +[[Preferences]] deps = ["TOML"] -git-tree-sha1 = "47e5f437cc0e7ef2ce8406ce1e7e24d44915f88d" +git-tree-sha1 = "7eb1686b4f04b82f96ed7a4ea5890a4f0c7a09f1" uuid = "21216c6a-2e73-6563-6e65-726566657250" -version = "1.3.0" +version = "1.4.0" -[[deps.Printf]] +[[Printf]] deps = ["Unicode"] uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" -[[deps.REPL]] +[[REPL]] deps = ["InteractiveUtils", "Markdown", "Sockets", "Unicode"] uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" -[[deps.Random]] -deps = ["SHA", "Serialization"] +[[Random]] +deps = ["Serialization"] uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" -[[deps.SHA]] +[[SHA]] uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" -version = "0.7.0" -[[deps.Serialization]] +[[Serialization]] uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" -[[deps.SimpleBufferStream]] +[[SimpleBufferStream]] git-tree-sha1 = "874e8867b33a00e784c8a7e4b60afe9e037b74e1" uuid = "777ac1f9-54b0-4bf8-805c-2214025038e7" version = "1.1.0" -[[deps.SnoopPrecompile]] -git-tree-sha1 = "f604441450a3c0569830946e5b33b78c928e1a85" +[[SnoopPrecompile]] +deps = ["Preferences"] +git-tree-sha1 = "e760a70afdcd461cf01a575947738d359234665c" uuid = "66db9d55-30c0-4569-8b51-7e840670fc0c" -version = "1.0.1" +version = "1.0.3" -[[deps.Sockets]] +[[Sockets]] uuid = "6462fe0b-24de-5631-8697-dd941f90decc" -[[deps.StringEncodings]] +[[StringEncodings]] deps = ["Libiconv_jll"] -git-tree-sha1 = "50ccd5ddb00d19392577902f0079267a72c5ab04" +git-tree-sha1 = "33c0da881af3248dafefb939a21694b97cfece76" uuid = "69024149-9ee7-55f6-a4c4-859efe599b68" -version = "0.3.5" +version = "0.3.6" -[[deps.StructTypes]] +[[StructTypes]] deps = ["Dates", "UUIDs"] git-tree-sha1 = "ca4bccb03acf9faaf4137a9abc1881ed1841aa70" uuid = "856f2bd8-1eba-4b0a-8007-ebc267875bd4" version = "1.10.0" -[[deps.SwaggerMarkdown]] +[[SwaggerMarkdown]] deps = ["JSON", "JSONSchema", "Test", "UUIDs", "YAML"] git-tree-sha1 = "ea2513768e67b5ca27bbbab19e34179714a78cab" uuid = "1b6eb727-ad4b-44eb-9669-b9596a6e760f" version = "0.2.1" -[[deps.TOML]] +[[TOML]] deps = ["Dates"] uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76" -version = "1.0.0" -[[deps.Tar]] +[[Tar]] deps = ["ArgTools", "SHA"] uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e" -version = "1.10.1" -[[deps.Test]] +[[Test]] deps = ["InteractiveUtils", "Logging", "Random", "Serialization"] uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" -[[deps.TranscodingStreams]] +[[TranscodingStreams]] deps = ["Random", "Test"] -git-tree-sha1 = "94f38103c984f89cf77c402f2a68dbd870f8165f" +git-tree-sha1 = "9a6ae7ed916312b41236fcef7e0af564ef934769" uuid = "3bb67fe8-82b1-5028-8e26-92a6c54297fa" -version = "0.9.11" +version = "0.9.13" -[[deps.URIs]] -git-tree-sha1 = "ac00576f90d8a259f2c9d823e91d1de3fd44d348" +[[URIs]] +git-tree-sha1 = "074f993b0ca030848b897beff716d93aca60f06a" uuid = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4" -version = "1.4.1" +version = "1.4.2" -[[deps.UUIDs]] +[[UUIDs]] deps = ["Random", "SHA"] uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" -[[deps.Unicode]] +[[Unicode]] uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" -[[deps.YAML]] +[[YAML]] deps = ["Base64", "Dates", "Printf", "StringEncodings"] git-tree-sha1 = "dbc7f1c0012a69486af79c8bcdb31be820670ba2" uuid = "ddb6d928-2868-570f-bddf-ab3f9cf99eb6" version = "0.4.8" -[[deps.Zlib_jll]] +[[Zlib_jll]] deps = ["Libdl"] uuid = "83775a58-1f1d-513f-b197-d71354ab007a" -version = "1.2.12+3" -[[deps.nghttp2_jll]] +[[nghttp2_jll]] deps = ["Artifacts", "Libdl"] uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d" -version = "1.48.0+0" -[[deps.p7zip_jll]] +[[p7zip_jll]] deps = ["Artifacts", "Libdl"] uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0" -version = "17.4.0+0" diff --git a/demo/routingfunctions.jl b/demo/routingfunctions.jl new file mode 100644 index 00000000..68d29dd0 --- /dev/null +++ b/demo/routingfunctions.jl @@ -0,0 +1,28 @@ +module FunctionsRoutingDemo + +include("../src/Oxygen.jl") +using .Oxygen + +using HTTP + + +@get "/" function() + "hello" +end + +get("/add/{x}/{y}") do request::HTTP.Request, x::Int, y::Int + x + y +end + +get("/multiply/{x}/{y}", function(request::HTTP.Request, x::Int, y::Int) + x * y +end) + +put("/put", () -> "put") +patch("/patch", () -> "patch") +delete("/delete", () -> "delete") + +# start the web server +serve() + +end \ No newline at end of file diff --git a/docs/src/api.md b/docs/src/api.md index 4431f564..13a10ea7 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -17,6 +17,13 @@ serveparallel @patch(path, func) @delete(path, func) @route(methods, path, func) + +get(path, func) +post(path, func) +put(path, func) +patch(path, func) +delete(path, func) +route(methods, path, func) ``` ## Mounting Files diff --git a/docs/src/index.md b/docs/src/index.md index b7ba776b..d9c131b4 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -22,7 +22,7 @@ Breathe easy knowing you can quickly spin up a web server with abstractions you' ## Features -- Straightforward routing (`@get`, `@post`, `@put`, `@patch`, `@delete` and `@route` macros) +- Straightforward routing - Auto-generated swagger documentation - Out-of-the-box JSON serialization & deserialization (customizable) - Type definition support for path parameters @@ -88,6 +88,35 @@ end serve() ``` +## Routing Macro & Function Syntax + +There are two primary ways to register your request handlers: the standard routing macros or the routing functions which utilize the do-block syntax. + +For each routing macro, we now have a an equivalent routing function + +```julia +@get -> get() +@post -> post() +@put -> put() +@patch -> patch() +@delete -> delete() +@route -> route() +``` + +The only practical difference between the two is that the routing macros are called during the precompilation +stage, whereas the routing functions are only called when invoked. (The routing macros call the routing functions under the hood) + +```julia +# Routing Macro syntax +@get "/add/{x}/{y}" function(request::HTTP.Request, x::Int, y::Int) + x + y +end + +# Routing Function syntax +get("/add/{x}/{y}") do request::HTTP.Request, x::Int, y::Int + x + y +end +``` ## Path parameters diff --git a/src/Oxygen.jl b/src/Oxygen.jl index ddf5802c..589d0ebb 100644 --- a/src/Oxygen.jl +++ b/src/Oxygen.jl @@ -9,6 +9,7 @@ include("core.jl"); using .Core export @get, @post, @put, @patch, @delete, @route, @cron, @staticfiles, @dynamicfiles, staticfiles, dynamicfiles, + get, post, put, patch, delete, route, serve, serveparallel, terminate, internalrequest, redirect, queryparams, binary, text, json, html, file, configdocs, mergeschema, setschema, getschema, router, diff --git a/src/autodoc.jl b/src/autodoc.jl index 19f7258c..54657eec 100644 --- a/src/autodoc.jl +++ b/src/autodoc.jl @@ -240,13 +240,34 @@ function createrouter(prefix::String, interval::Union{Real, Nothing} = routerinterval, cron::Union{String, Nothing} = routercron) - # combine the current routers prefix with this specfic path - path = !isnothing(path) ? "$(fixpath(prefix))$(fixpath(path))" : fixpath(prefix) - - combinedtags = [tags..., routertags...] - # 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 @@ -271,6 +292,8 @@ function createrouter(prefix::String, end end + combinedtags = [tags..., routertags...] + # register tags if !haskey(taggedroutes, path) taggedroutes[path] = TaggedRoute([httpmethod], combinedtags) diff --git a/src/core.jl b/src/core.jl index 5348bd9d..d4c42edd 100644 --- a/src/core.jl +++ b/src/core.jl @@ -3,6 +3,7 @@ module Core using HTTP using Sockets using JSON3 +using Base include("util.jl"); using .Util include("fileutil.jl"); using .FileUtil @@ -11,6 +12,7 @@ include("autodoc.jl"); using .AutoDoc export @get, @post, @put, @patch, @delete, @route, @cron, @staticfiles, @dynamicfiles, staticfiles, dynamicfiles, + get, post, put, patch, delete, route, start, serve, serveparallel, terminate, internalrequest, file, configdocs, mergeschema, setschema, getschema, router, enabledocs, disabledocs, isdocsenabled, registermountedfolder, @@ -329,7 +331,7 @@ function DefaultSerializer(catch_errors::Bool) end end -### Core Macros ### +### Routing Macros ### """ @get(path::String, func::Function) @@ -375,7 +377,6 @@ macro patch(path, func) end end - """ @delete(path::String, func::Function) @@ -387,7 +388,6 @@ macro delete(path, func) end end - """ @route(methods::Array{String}, path::String, func::Function) @@ -395,135 +395,157 @@ Used to register a function to a specific endpoint to handle mulitiple request t """ macro route(methods, path, func) quote - for method in $methods - @register(method, $(esc(path)), $(esc(func))) - end + route($(esc(methods)), $(esc(path)), $(esc(func))) end end +### Core Routing Functions ### + +function route(methods::Vector{String}, path::Union{String,Function}, func::Function) + for method in methods + register(method, path, func) + end +end + +### Routing Functions ### + +Base.get(path::Union{String,Function}, func::Function) = route(["GET"], path, func) +post(path::Union{String,Function}, func::Function) = route(["POST"], path, func) +put(path::Union{String,Function}, func::Function) = route(["PUT"], path, func) +patch(path::Union{String,Function}, func::Function) = route(["PATCH"], path, func) +delete(path::Union{String,Function}, func::Function) = route(["DELETE"], path, func) + +### Core Routing Functions Support for do..end Syntax ### + +Base.get(func::Function, path::Union{String,Function}) = get(path, func) +post(func::Function, path::Union{String,Function}) = post(path, func) +put(func::Function, path::Union{String,Function}) = put(path, func) +patch(func::Function, path::Union{String,Function}) = patch(path, func) +delete(func::Function, path::Union{String,Function}) = delete(path, func) + +route(func::Function, methods::Vector{String}, path::Union{String,Function}) = route(methods, path, func) """ - @register(httpmethod::String, path::String, func::Function) + register(httpmethod::String, route::String, func::Function) Register a request handler function with a path to the ROUTER """ -macro register(httpmethod, path, func) - return quote - - local method_type = $(esc(httpmethod)) - local route = $(esc(path)) - local action = $(esc(func)) - - # check if path is a callable function (that means it's a router higher-order-function) - if !isempty(methods(route)) - - # This is true when the user passes the router() directly to the path. - # We call the generated function without args so it uses the default args - # from the parent function. - if countargs(route) == 1 - route = route() - end +function register(httpmethod::String, route::Union{String,Function}, func::Function) - # If it's still a function, then that means this is from the 3rd inner function - # defined in the createrouter() function. - if countargs(route) == 2 - route = route(method_type) - end - + # check if path is a callable function (that means it's a router higher-order-function) + if isa(route, Function) + + # This is true when the user passes the router() directly to the path. + # We call the generated function without args so it uses the default args + # from the parent function. + if countargs(route) == 1 + route = route() end - local router = getrouter() - local variableRegex = r"{[a-zA-Z0-9_]+}" - local hasBraces = r"({)|(})" - - # determine if we have parameters defined in our path - local hasPathParams = contains(route, variableRegex) - - # track which index the params are located in - local positions = [] - for (index, value) in enumerate(HTTP.URIs.splitpath(route)) - if contains(value, hasBraces) - # extract the variable name - variable = replace(value, hasBraces => "") |> x -> split(x, ":") |> first - push!(positions, (index, variable)) - end + # If it's still a function, then that means this is from the 3rd inner function + # defined in the createrouter() function. + if countargs(route) == 2 + route = route(httpmethod) end + end - local method = first(methods(action)) - local numfields = method.nargs - - # extract the function handler's field names & types - local fields = [x for x in fieldtypes(method.sig)] - local func_param_names = [String(param) for param in Base.method_argnames(method)[3:end]] - local func_param_types = splice!(Array(fields), 3:numfields) - - # create a map of paramter name to type definition - local func_map = Dict(name => type for (name, type) in zip(func_param_names, func_param_types)) - - # each tuple tracks where the param is refereced (variable, function index, path index) - local param_positions::Array{Tuple{String, Int, Int}} = [] - - # ensure the function params are present inside the path params - for (_, path_param) in positions - hasparam = false - for (_, func_param) in enumerate(func_param_names) - if func_param == path_param - hasparam = true - break - end - end - if !hasparam - throw("Your request handler is missing a parameter: '$path_param' defined in this route: $route") - end + # if the route is still a function, then it's from the 3rd inner function + # defined in the createrouter()function when the 'router()' function is passed directly. + if isa(route, Function) + route = route(httpmethod) + end + + if !isa(route, String) + throw("The `route` parameter is not a String, but is instead a: $(typeof(route))") + end + + router = getrouter() + variableRegex = r"{[a-zA-Z0-9_]+}" + hasBraces = r"({)|(})" + + # determine if we have parameters defined in our path + hasPathParams = contains(route, variableRegex) + + # track which index the params are located in + positions = [] + for (index, value) in enumerate(HTTP.URIs.splitpath(route)) + if contains(value, hasBraces) + # extract the variable name + variable = replace(value, hasBraces => "") |> x -> split(x, ":") |> first + push!(positions, (index, variable)) end + end - # ensure the path params are present inside the function params - for (func_index, func_param) in enumerate(func_param_names) - matched = nothing - for (path_index, path_param) in positions - if func_param == path_param - matched = (func_param, func_index, path_index) - break - end - end - if matched === nothing - throw("Your path is missing a parameter: '$func_param' which needs to be added to this route: $route") - else - push!(param_positions, matched) + method = first(methods(func)) + numfields = method.nargs + + # extract the function handler's field names & types + fields = [x for x in fieldtypes(method.sig)] + func_param_names = [String(param) for param in Base.method_argnames(method)[3:end]] + func_param_types = splice!(Array(fields), 3:numfields) + + # create a map of paramter name to type definition + func_map = Dict(name => type for (name, type) in zip(func_param_names, func_param_types)) + + # each tuple tracks where the param is refereced (variable, function index, path index) + param_positions::Array{Tuple{String, Int, Int}} = [] + + # ensure the function params are present inside the path params + for (_, path_param) in positions + hasparam = false + for (_, func_param) in enumerate(func_param_names) + if func_param == path_param + hasparam = true + break end end + if !hasparam + throw("Your request handler is missing a parameter: '$path_param' defined in this route: $route") + end + end - # strip off any regex patterns attached to our path parameters - registerschema(route, method_type, zip(func_param_names, func_param_types), Base.return_types(action)) - - # case 1.) The request handler is an anonymous function (don't parse out path params) - if numfields <= 1 - local handle = function (req) - action() - end - # case 2.) This route has path params, so we need to parse parameters and pass them to the request handler - elseif hasPathParams && numfields > 2 - local handle = function (req) - # get all path parameters - params = HTTP.getparams(req) - # convert params to their designated type (if applicable) - pathParams = [parseparam(func_map[name], params[name]) for name in func_param_names] - # pass all parameters to handler in the correct order - action(req, pathParams...) + # ensure the path params are present inside the function params + for (func_index, func_param) in enumerate(func_param_names) + matched = nothing + for (path_index, path_param) in positions + if func_param == path_param + matched = (func_param, func_index, path_index) + break end - # case 3.) This function should only get passed the request object + end + if matched === nothing + throw("Your path is missing a parameter: '$func_param' which needs to be added to this route: $route") else - local handle = function (req) - action(req) - end + push!(param_positions, matched) end + end - local requesthandler = function (req) - return handle(req) + # strip off any regex patterns attached to our path parameters + registerschema(route, httpmethod, zip(func_param_names, func_param_types), Base.return_types(func)) + + # case 1.) The request handler is an anonymous function (don't parse out path params) + if numfields <= 1 + handle = function (req) + func() + end + # case 2.) This route has path params, so we need to parse parameters and pass them to the request handler + elseif hasPathParams && numfields > 2 + handle = function (req) + # get all path parameters + params = HTTP.getparams(req) + # convert params to their designated type (if applicable) + pathParams = [parseparam(func_map[name], params[name]) for name in func_param_names] + # pass all parameters to handler in the correct order + func(req, pathParams...) + end + # case 3.) This function should only get passed the request object + else + handle = function (req) + func(req) end + end - HTTP.register!(router, method_type, route, requesthandler) - end + HTTP.register!(router, httpmethod, route, handle) end @@ -534,11 +556,11 @@ function setupswagger() return end - @get docspath function() + @get "$docspath" function() return swaggerhtml() end - @get schemapath function() + @get "$schemapath" function() return getschema() end @@ -581,7 +603,7 @@ function staticfiles(folder::String, mountdir::String="static") registermountedfolder(mountdir) function addroute(currentroute, headers, filepath, registeredpaths; code=200) body = file(filepath) - @get currentroute function(req) + @get "$currentroute" function(req) # return 404 for paths that don't match our files validpath::Bool = get(registeredpaths, req.target, false) return validpath ? HTTP.Response(code, headers , body=body) : HTTP.Response(404) @@ -600,7 +622,7 @@ but files are re-read on each request function dynamicfiles(folder::String, mountdir::String="static") registermountedfolder(mountdir) function addroute(currentroute, headers, filepath, registeredpaths; code = 200) - @get currentroute function(req) + @get "$currentroute" function(req) # return 404 for paths that don't match our files validpath::Bool = get(registeredpaths, req.target, false) return validpath ? HTTP.Response(code, headers , body=file(filepath)) : HTTP.Response(404) diff --git a/src/util.jl b/src/util.jl index 33e51d08..f3f67aa1 100644 --- a/src/util.jl +++ b/src/util.jl @@ -5,7 +5,14 @@ using Dates export countargs, recursive_merge, parseparam, queryparams, html, redirect -# return the number of args inside a function +""" +countargs(func) + +Return the number of arguments of the first method of the function `f`. + +# Arguments +- `f`: The function to get the number of arguments for. +""" function countargs(f::Function) return methods(f) |> first |> x -> x.nargs end diff --git a/test/crontests.jl b/test/crontests.jl index 2da081ec..5e026ae1 100644 --- a/test/crontests.jl +++ b/test/crontests.jl @@ -441,4 +441,4 @@ end close(server) -end +end \ No newline at end of file diff --git a/test/routingfunctionstests.jl b/test/routingfunctionstests.jl new file mode 100644 index 00000000..2a928ccc --- /dev/null +++ b/test/routingfunctionstests.jl @@ -0,0 +1,108 @@ +module RoutingFunctionsTests +using Test +using HTTP +using JSON3 +using StructTypes +using Sockets +using Dates + +include("../src/Oxygen.jl") +using .Oxygen + +##### Setup Routes ##### + +get("/inline/add/{x}/{y}", (request::HTTP.Request, x::Int, y::Int) -> x + y) +get("/add/{x}/{y}") do request::HTTP.Request, x::Int, y::Int + x + y +end + +post("/inline/sub/{x}/{y}", (request::HTTP.Request, x::Int, y::Int) -> x - y) +post("/sub/{x}/{y}") do request::HTTP.Request, x::Int, y::Int + x - y +end + +put("/inline/power/{x}/{y}", (request::HTTP.Request, x::Int, y::Int) -> x ^ y) +put("/power/{x}/{y}") do request::HTTP.Request, x::Int, y::Int + x ^ y +end + +patch("/inline/mulitply/{x}/{y}", (request::HTTP.Request, x::Int, y::Int) -> x * y) +patch("/mulitply/{x}/{y}") do request::HTTP.Request, x::Int, y::Int + x * y +end + +delete("/inline/divide/{x}/{y}", (request::HTTP.Request, x::Int, y::Int) -> x / y) +delete("/divide/{x}/{y}") do request::HTTP.Request, x::Int, y::Int + x / y +end + +route(["GET"], "/inline/route/add/{x}/{y}", (request::HTTP.Request, x::Int, y::Int) -> x + y) +route(["GET"], "/route/add/{x}/{y}") do request::HTTP.Request, x::Int, y::Int + x + y +end + +##### Begin tests ##### + + +@testset "GET routing functions" begin + r = internalrequest(HTTP.Request("GET", "/inline/add/5/4")) + @test r.status == 200 + @test text(r) == "9" + + r = internalrequest(HTTP.Request("GET", "/add/5/4")) + @test r.status == 200 + @test text(r) == "9" + + r = internalrequest(HTTP.Request("GET", "/inline/route/add/5/4")) + @test r.status == 200 + @test text(r) == "9" + + r = internalrequest(HTTP.Request("GET", "/route/add/5/4")) + @test r.status == 200 + @test text(r) == "9" +end + +@testset "POST routing functions" begin + r = internalrequest(HTTP.Request("POST", "/inline/sub/5/4")) + @test r.status == 200 + @test text(r) == "1" + + r = internalrequest(HTTP.Request("POST", "/sub/5/4")) + @test r.status == 200 + @test text(r) == "1" +end + +@testset "PUT routing functions" begin + r = internalrequest(HTTP.Request("PUT", "/inline/power/5/4")) + @test r.status == 200 + @test text(r) == "625" + + r = internalrequest(HTTP.Request("PUT", "/power/5/4")) + @test r.status == 200 + @test text(r) == "625" +end + +@testset "PATCH routing functions" begin + r = internalrequest(HTTP.Request("PATCH", "/inline/mulitply/5/4")) + @test r.status == 200 + @test text(r) == "20" + + r = internalrequest(HTTP.Request("PATCH", "/mulitply/5/4")) + @test r.status == 200 + @test text(r) == "20" +end + +@testset "DELETE routing functions" begin + r = internalrequest(HTTP.Request("DELETE", "/inline/divide/5/4")) + @test r.status == 200 + @test text(r) == "1.25" + + r = internalrequest(HTTP.Request("DELETE", "/divide/5/4")) + @test r.status == 200 + @test text(r) == "1.25" +end + +# clear any routes setup in this file +resetstate() + +end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index d44a2e18..0d265699 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -6,6 +6,7 @@ using StructTypes using Sockets using Dates +include("routingfunctionstests.jl") include("bodyparsertests.jl") include("crontests.jl")