Skip to content

Instantly share code, notes, and snippets.

@ZoomTen
Last active February 15, 2025 14:28
Show Gist options
  • Save ZoomTen/2c11a63a2fe736c02a5e6502c84bf6fa to your computer and use it in GitHub Desktop.
Save ZoomTen/2c11a63a2fe736c02a5e6502c84bf6fa to your computer and use it in GitHub Desktop.
a router
proc route(reqType: string, path: string): void =
proc notFound(
reqType: string, pathOnly: string, getParams: string, pathParams: StringTableRef
): void =
stderr.writeLine "default"
proc methodNotAllowed(
reqType: string, pathOnly: string, getParams: string, pathParams: StringTableRef
): void =
stderr.writeLine "method not allowed"
var pathParams = newStringTable()
## Calculate where the path should split
var startPos_536874292 = -1
for i in 0 ..< len(path):
case path[i]
of '?', '&':
startPos_536874292 = i
break
else:
discard
let pathOnly = block:
var p =
if startPos_536874292 > -1:
path[0 ..< startPos_536874292]
else:
path
## Clean up trailing slash
if len(p) > 1 and p[^1] == '/':
p[0 ..^ 2]
else:
p
let getParams = block:
if startPos_536874292 > -1:
path[(startPos_536874292 + 1) ..^ 1]
else:
""
case pathOnly
of "/":
case reqType
of "GET":
stderr.writeLine "root"
else:
methodNotAllowed(reqType, pathOnly, getParams, pathParams)
of "/static/something":
case reqType
of "GET":
stderr.writeLine "static"
else:
methodNotAllowed(reqType, pathOnly, getParams, pathParams)
else:
## dynamic path fallback
const rexp_536874285 =
re2"(?P<Route0>/uploads/(?P<Route0_id>[^/]+?))|(?P<Route1>/(?P<Route1_id>[^/]+?)/(?P<Route1_test>[^/]+?))"
var m_536874286: RegexMatch2
if match(pathOnly, rexp_536874285, m_536874286):
let gn_536874287 = groupNames(m_536874286)
var matchedRoute_536874288 = ""
for i_536874289 in 0 ..< 2:
let routeNameCandidate_536874290 = "Route" & $i_536874289
if m_536874286.group(routeNameCandidate_536874290) != reNonCapture:
matchedRoute_536874288 = routeNameCandidate_536874290
break
if len(matchedRoute_536874288) > 1:
for i_536874291 in gn_536874287:
if i_536874291.startsWith(matchedRoute_536874288) and
len(i_536874291) > len(matchedRoute_536874288):
pathParams[i_536874291[(len(matchedRoute_536874288) + 1) ..^ 1]] =
pathOnly[m_536874286.group(i_536874291)]
case matchedRoute_536874288
of "Route0":
case reqType
of "GET":
stderr.writeLine "id => " & pathParams.getOrDefault("id")
else:
## invalid method
methodNotAllowed(reqType, pathOnly, getParams, pathParams)
of "Route1":
case reqType
of "GET":
stderr.writeLine "id => " & pathParams.getOrDefault("id")
stderr.writeLine "test => " & pathParams.getOrDefault("test")
else:
## invalid method
methodNotAllowed(reqType, pathOnly, getParams, pathParams)
else:
## something went wrong
notFound(reqType, pathOnly, getParams, pathParams)
else:
## nothing REALLY matched
notFound(reqType, pathOnly, getParams, pathParams)
else:
## nothing matched
notFound(reqType, pathOnly, getParams, pathParams)
requires "nim >= 2.0.10"
requires "regex == 0.26.1"
# benchmarking
requires "benchy == 0.0.1"
import std/macros
import std/tables
import std/strtabs
export strtabs
import std/strutils
export strutils
import regex
export regex
type RouterHttpMethods = enum
Get = "GET"
Head = "HEAD"
Post = "POST"
Put = "PUT"
Delete = "DELETE"
Options = "OPTIONS"
Patch = "PATCH"
macro makeRouter*(name: string, body: untyped) =
##[
Creates a router proc of the following form:
```nim
proc routerProcName(reqType: string; path: string): void
```
Where `name` will replace `routerProcName`. The `reqType` that
this new proc will accept is the following (all **uppercase**):
1. GET
2. HEAD
3. POST
4. PUT
5. DELETE
6. OPTIONS
7. PATCH
When defining a router, you will define a path for the reqType
using **lowercase**, e.g. to define `GET` for `/` and `PUT` for `/obj`, you do:
```nim
makeRouter("test"):
get "/":
# your stuff here
discard
put "/obj":
# ditto
discard
default:
# ...
discard
```
You must define a `default` path, when the route for a URL is not
found.
You may optionally define a `methodNotAllowed` path, for when
the URL is accessed not through one of the defined methods.
You can also define "dynamic" paths that capture URL parameters
surrounded in braces. The URL parameters will be made available
via the `pathParams` variable as a string, like this:
```nim
makeRouter("test2"):
get "/posts/{postId}":
let id = pathParams.getOrDefault("postId")
echo "got post ID: " & id
```
Other variables made available to you include `pathOnly` and `getParams`.
When the path is `/test/abc?id=10&def=qwerty`:
* `pathOnly` = `/test/abc`
* `getParams` = `id=10&def=qwerty`
]##
runnableExamples:
var output: string = ""
makeRouter("routeThisPhrhrht"):
get "/":
output = "got /"
get "/upload":
output = "loaded /upload"
post "/upload":
output =
"uploaded something" & (
if len(getParams) > 0:
" with params: " & getParams
else:
""
)
default:
output = "unknown"
methodNotAllowed:
output = "whoops!"
routeThisPhrhrht("GET", "/")
assert output == "got /"
routeThisPhrhrht("GET", "/upload")
assert output == "loaded /upload"
routeThisPhrhrht("POST", "/upload")
assert output == "uploaded something"
routeThisPhrhrht("POST", "/upload?aaaaaa=1231223&&&&11rrrdsaop3r")
assert output == "uploaded something with params: aaaaaa=1231223&&&&11rrrdsaop3r"
routeThisPhrhrht("GET", "/eogijoergjioerajgieog")
assert output == "unknown"
routeThisPhrhrht("PUT", "/upload")
assert output == "whoops!"
var staticRoutes: Table[string, seq[(RouterHttpMethods, NimNode)]]
var dynamicRoutes: Table[string, seq[(RouterHttpMethods, NimNode)]]
var defaultRoutine = newEmptyNode()
var methodNotAllowedRoutine = newEmptyNode()
# Perform AST analysis
for b in body:
case b.kind
of nnkCommand: # command(ident<method>, strlit, stmtlist)
#[
Statements of the form:
get "/":
<statements>
post "/upload":
<statements>
]#
let
methodName = b[0]
url = b[1]
list = b[2]
newTup = (parseEnum[RouterHttpMethods](methodName.strVal.toUpperAscii()), list)
var isDynamicRoute = false
for i in url.strVal:
if i == '{':
isDynamicRoute = true
break
if isDynamicRoute:
if not (url.strVal in staticRoutes):
dynamicRoutes[url.strVal] = @[newTup]
else:
dynamicRoutes[url.strVal].add(newTup)
else: # is static route
if not (url.strVal in staticRoutes):
staticRoutes[url.strVal] = @[newTup]
else:
staticRoutes[url.strVal].add(newTup)
of nnkCall: # call(ident"others", stmtlist)
#[
Statements of the form:
default:
<statements>
]#
case b[0].strVal
of "default":
defaultRoutine = b[1]
of "methodNotAllowed":
methodNotAllowedRoutine = b[1]
else:
discard
when defined(routerMacroDbg):
debugEcho "============= STATIC ROUTES ============="
for k, v in pairs(staticRoutes):
debugEcho k & ":"
for m in v:
let (mtd, node) = m
debugEcho " " & $mtd & " => " & node.repr.repr.replace("\n", "")
debugEcho "============= DYNAMIC ROUTES ============="
for k, v in pairs(dynamicRoutes):
debugEcho k & ":"
for m in v:
let (mtd, node) = m
debugEcho " " & $mtd & " => " & node.repr.repr.replace("\n", "")
# Generate a regex pattern from all the dynamic routes
var genRegex = ""
if len(dynamicRoutes) > 0:
var counter = 0
const paramConverter = re2"""\{(\w+)\}"""
for url in keys(dynamicRoutes):
# Add a case for the dynamic route
genRegex &= "(?P<Route"
genRegex &= $counter
genRegex &= ">"
genRegex &=
url.replace(
paramConverter,
(
proc(m: RegexMatch2, s: string): string =
# turn "{id}" into "(?P<RouteXX_id>[^/]+?)"
"(?P<Route" & $counter & "_" & s[m.group(0)] & ">[^/]+?)"
),
)
genRegex &= ")|"
inc(counter)
# cut the final OR symbol
genRegex = genRegex[0 ..< (len(genRegex) - 1)]
when defined(routerMacroDbg):
debugEcho "============= GENERATED REGEX PATTERN ============="
debugEcho genRegex
# Generate default handler proc
var defaultProc = newProc(
ident("notFound"),
[
ident("void"),
newIdentDefs(ident("reqType"), ident("string")),
newIdentDefs(ident("pathOnly"), ident("string")),
newIdentDefs(ident("getParams"), ident("string")),
newIdentDefs(ident("pathParams"), ident("StringTableRef")),
],
defaultRoutine,
)
let defaultProcCall = newCall(
ident("notFound"),
ident("reqType"),
ident("pathOnly"),
ident("getParams"),
ident("pathParams"),
)
# Generate invalid method proc, if any
var methodNotAllowedProc = block:
if methodNotAllowedRoutine.kind != nnkEmpty:
newProc(
ident("methodNotAllowed"),
[
ident("void"),
newIdentDefs(ident("reqType"), ident("string")),
newIdentDefs(ident("pathOnly"), ident("string")),
newIdentDefs(ident("getParams"), ident("string")),
newIdentDefs(ident("pathParams"), ident("StringTableRef")),
],
methodNotAllowedRoutine,
)
else:
newEmptyNode()
let methodNotAllowedCall = newCall(
ident("methodNotAllowed"),
ident("reqType"),
ident("pathOnly"),
ident("getParams"),
ident("pathParams"),
)
# Build switch statement for static routes
var routeSwitch = nnkCaseStmt.newTree(ident("pathOnly"))
for routeName, routeBody in pairs(staticRoutes):
var rqSwitch = nnkCaseStmt.newTree(ident("reqType"))
for i in routeBody:
let (rqKind, rqRoutine) = i
rqSwitch.add(nnkOfBranch.newTree(newStrLitNode($rqKind), rqRoutine))
rqSwitch.add(
nnkElse.newTree(
if methodNotAllowedRoutine.kind != nnkEmpty:
methodNotAllowedCall
else:
defaultProcCall
)
)
routeSwitch.add(
nnkOfBranch.newTree(newStrLitNode(routeName), newStmtList(rqSwitch))
)
var dynamicRouteRoutine = newStmtList(newCommentStmtNode("dynamic path fallback"))
if len(genRegex) > 0:
# Dynamic route code path
let regexConstName = genSym(nskConst, "rexp")
let regexMatchHolderName = genSym(nskVar, "m")
# Add the big regex to be compiled
dynamicRouteRoutine.add(
nnkConstSection.newTree(
nnkConstDef.newTree(
regexConstName,
newEmptyNode(),
nnkCallStrLit.newTree(ident("re2"), newLit(genRegex)),
)
)
)
# Prepare regex match holder variable
dynamicRouteRoutine.add(
nnkVarSection.newTree(newIdentDefs(regexMatchHolderName, ident("RegexMatch2")))
)
# Prepare the "something matched" arm
var caseOfMatchContents = newStmtList()
let caseOfMatch = nnkElifBranch.newTree(
newCall(ident("match"), ident("pathOnly"), regexConstName, regexMatchHolderName),
caseOfMatchContents,
)
block somethingMatched:
let groupNameHolderName = genSym(nskLet, "gn")
let matchedRouteHolderName = genSym(nskVar, "matchedRoute")
caseOfMatchContents.add:
nnkLetSection.newTree(
nnkIdentDefs.newTree(
groupNameHolderName,
newEmptyNode(),
nnkCall.newTree(newIdentNode("groupNames"), regexMatchHolderName),
)
)
caseOfMatchContents.add:
nnkVarSection.newTree(
nnkIdentDefs.newTree(matchedRouteHolderName, newEmptyNode(), newLit(""))
)
let tmpIterName1 = genSym(nskForVar, "i")
let routeNameCandidateName = genSym(nskLet, "routeNameCandidate")
caseOfMatchContents.add:
nnkForStmt.newTree(
tmpIterName1,
nnkInfix.newTree(newIdentNode("..<"), newLit(0), newLit(len(dynamicRoutes))),
nnkStmtList.newTree(
nnkLetSection.newTree(
nnkIdentDefs.newTree(
routeNameCandidateName,
newEmptyNode(),
nnkInfix.newTree(
newIdentNode("&"),
newLit("Route"),
nnkPrefix.newTree(newIdentNode("$"), tmpIterName1),
),
)
),
nnkIfStmt.newTree(
nnkElifBranch.newTree(
nnkInfix.newTree(
newIdentNode("!="),
nnkCall.newTree(
nnkDotExpr.newTree(regexMatchHolderName, newIdentNode("group")),
routeNameCandidateName,
),
newIdentNode("reNonCapture"),
),
nnkStmtList.newTree(
nnkAsgn.newTree(matchedRouteHolderName, routeNameCandidateName),
nnkBreakStmt.newTree(newEmptyNode()),
),
)
),
),
)
var matchFoundStatements = newStmtList()
caseOfMatchContents.add:
nnkIfStmt.newTree(
nnkElifBranch.newTree(
nnkInfix.newTree(
newIdentNode(">"),
nnkCall.newTree(newIdentNode("len"), matchedRouteHolderName),
newLit(1),
),
matchFoundStatements,
),
nnkElse.newTree(
nnkStmtList.newTree(
newCommentStmtNode("nothing REALLY matched"), defaultProcCall
)
),
)
# Inject captured parameters
let tmpIterName2 = genSym(nskForVar, "i")
matchFoundStatements.add:
nnkForStmt.newTree(
tmpIterName2,
groupNameHolderName,
nnkStmtList.newTree(
nnkIfStmt.newTree(
nnkElifBranch.newTree(
nnkInfix.newTree(
newIdentNode("and"),
nnkCall.newTree(
nnkDotExpr.newTree(tmpIterName2, newIdentNode("startsWith")),
matchedRouteHolderName,
),
nnkInfix.newTree(
newIdentNode(">"),
nnkCall.newTree(newIdentNode("len"), tmpIterName2),
nnkCall.newTree(newIdentNode("len"), matchedRouteHolderName),
),
),
nnkStmtList.newTree(
nnkAsgn.newTree(
nnkBracketExpr.newTree(
newIdentNode("pathParams"),
nnkBracketExpr.newTree(
tmpIterName2,
nnkInfix.newTree(
newIdentNode("..^"),
nnkPar.newTree(
nnkInfix.newTree(
newIdentNode("+"),
nnkCall.newTree(
newIdentNode("len"), matchedRouteHolderName
),
newLit(1),
)
),
newLit(1),
),
),
),
nnkBracketExpr.newTree(
newIdentNode("pathOnly"),
nnkCall.newTree(
nnkDotExpr.newTree(regexMatchHolderName, newIdentNode("group")),
tmpIterName2,
),
),
)
),
)
)
),
)
let matchFoundCaseStmts = nnkCaseStmt.newTree(matchedRouteHolderName)
var counter = 0
for k, v in pairs(dynamicRoutes):
var matchFoundMtdCases = nnkCaseStmt.newTree(ident("reqType"))
matchFoundCaseStmts.add:
nnkOfBranch.newTree(newLit("Route" & $counter), matchFoundMtdCases)
for endpoint in v:
let (rq, content) = endpoint
matchFoundMtdCases.add:
nnkOfBranch.newTree(newLit($rq), content)
matchFoundMtdCases.add:
nnkElse.newTree(
newStmtList(
newCommentStmtNode("invalid method"),
(
if methodNotAllowedRoutine.kind != nnkEmpty:
methodNotAllowedCall
else:
defaultProcCall
),
)
)
inc(counter)
matchFoundCaseStmts.add:
nnkElse.newTree(
newStmtList(newCommentStmtNode("something went wrong"), defaultProcCall)
)
matchFoundStatements.add(matchFoundCaseStmts)
# Prepare the "nothing matched" arm
let caseElseMatch = nnkElse.newTree(
newStmtList(newCommentStmtNode("nothing matched"), defaultProcCall)
)
dynamicRouteRoutine.add(nnkIfStmt.newTree(caseOfMatch, caseElseMatch))
else:
dynamicRouteRoutine.add(defaultProcCall)
routeSwitch.add(nnkElse.newTree(dynamicRouteRoutine))
# build the part where the path gets split
let splitPath = block:
let startPosName = genSym(nskVar, "startPos")
nnkStmtList.newTree(
newCommentStmtNode("Calculate where the path should split"),
nnkVarSection.newTree(
nnkIdentDefs.newTree(startPosName, newEmptyNode(), newLit(-1))
),
nnkForStmt.newTree(
newIdentNode("i"),
nnkInfix.newTree(
newIdentNode("..<"),
newLit(0),
nnkCall.newTree(newIdentNode("len"), newIdentNode("path")),
),
nnkStmtList.newTree(
nnkCaseStmt.newTree(
nnkBracketExpr.newTree(newIdentNode("path"), newIdentNode("i")),
nnkOfBranch.newTree(
newLit('?'),
newLit('&'),
nnkStmtList.newTree(
nnkAsgn.newTree(startPosName, newIdentNode("i")),
nnkBreakStmt.newTree(newEmptyNode()),
),
),
nnkElse.newTree(nnkStmtList.newTree(nnkDiscardStmt.newTree(newEmptyNode()))),
)
),
),
nnkLetSection.newTree(
nnkIdentDefs.newTree(
newIdentNode("pathOnly"),
newEmptyNode(),
nnkBlockStmt.newTree(
newEmptyNode(),
nnkStmtList.newTree(
nnkVarSection.newTree(
nnkIdentDefs.newTree(
newIdentNode("p"),
newEmptyNode(),
nnkIfExpr.newTree(
nnkElifExpr.newTree(
nnkInfix.newTree(newIdentNode(">"), startPosName, newLit(-1)),
nnkStmtList.newTree(
nnkBracketExpr.newTree(
newIdentNode("path"),
nnkInfix.newTree(newIdentNode("..<"), newLit(0), startPosName),
)
),
),
nnkElseExpr.newTree(nnkStmtList.newTree(newIdentNode("path"))),
),
)
),
newCommentStmtNode("Clean up trailing slash"),
nnkIfStmt.newTree(
nnkElifBranch.newTree(
nnkInfix.newTree(
newIdentNode("and"),
nnkInfix.newTree(
newIdentNode(">"),
nnkCall.newTree(newIdentNode("len"), newIdentNode("p")),
newLit(1),
),
nnkInfix.newTree(
newIdentNode("=="),
nnkBracketExpr.newTree(
newIdentNode("p"),
nnkPrefix.newTree(newIdentNode("^"), newLit(1)),
),
newLit('/'),
),
),
nnkStmtList.newTree(
nnkBracketExpr.newTree(
newIdentNode("p"),
nnkInfix.newTree(newIdentNode("..^"), newLit(0), newLit(2)),
)
),
),
nnkElse.newTree(nnkStmtList.newTree(newIdentNode("p"))),
),
),
),
)
),
nnkLetSection.newTree(
nnkIdentDefs.newTree(
newIdentNode("getParams"),
newEmptyNode(),
nnkBlockStmt.newTree(
newEmptyNode(),
nnkStmtList.newTree(
nnkIfStmt.newTree(
nnkElifBranch.newTree(
nnkInfix.newTree(newIdentNode(">"), startPosName, newLit(-1)),
nnkStmtList.newTree(
nnkBracketExpr.newTree(
newIdentNode("path"),
nnkInfix.newTree(
newIdentNode("..^"),
nnkPar.newTree(
nnkInfix.newTree(newIdentNode("+"), startPosName, newLit(1))
),
newLit(1),
),
)
),
),
nnkElse.newTree(nnkStmtList.newTree(newLit(""))),
)
),
),
)
),
)
let pathParamsDefine = nnkVarSection.newTree(
nnkIdentDefs.newTree(
ident("pathParams"), newEmptyNode(), newCall(ident("newStringTable"))
)
)
# step 4. build the final router proc
let builtBody = newStmtList(
newProc(
ident(name.strVal),
[
ident("void"),
newIdentDefs(ident("reqType"), ident("string")),
newIdentDefs(ident("path"), ident("string")),
],
newStmtList(
defaultProc, methodNotAllowedProc, pathParamsDefine, splitPath, routeSwitch
),
)
)
when defined(routerMacroDbg):
debugEcho "======BUILT========="
debugEcho builtBody.repr
builtBody
when isMainModule:
import benchy
makeRouter "route":
get "/":
stderr.writeLine "root"
get "/static/something":
stderr.writeLine "static"
get "/{id}/{test}":
stderr.writeLine "id => " & pathParams.getOrDefault("id")
stderr.writeLine "test => " & pathParams.getOrDefault("test")
get "/uploads/{id}":
stderr.writeLine "id => " & pathParams.getOrDefault("id")
default:
stderr.writeLine "default"
methodNotAllowed:
stderr.writeLine "method not allowed"
timeIt "1":
route("GET", "/")
timeIt "2":
route("GET", "/wejfpoewjfpewhr23")
timeIt "3":
route("GET", "/194/1333")
timeIt "4":
route("GET", "/uploads/1111")
timeIt "5":
route("GET", "/uploads/ajndoiq?ekioffiqf&&&klengkleg=&&&Y")
# dumpTree:
# var capturedPathParams = newStringTable()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment