Open blackmius opened 5 days ago
I like the idea 🚀
Would likely need either a pragma for parameters or have some kind of convention for documenting routes (Cause sadly there is no way to attach doc comments to a parameter) e.g.
"/posts" -> post(body {.help: "Contains the post data blah blah".} : Json[Post]):
# ...
"/posts" -> post(body: Json[Post]):
## Comment describing the route
##
## * body: This would get parsed and attached to the body
Probably deserves it's own issue but I always had an idea in the back of my mind for typed return types. Since I don't want to break compatibility with anything I'd have to squeeze it into the current DSL like (this isn't final, just playing around)
"/posts" -> post(body): string:
# .. create post
return newPost.id
Having that should give us ability for return types
Small thing I thought of, exceptions. Since the compiler tracks them via {.raises: [].}
we could have them automatically listed
while i thinking of how to add supporting for return types i think come with such design
let router = newRouter()
type MyEnum = enum
A
B
C
router.get("/items/:item_id") do (item_id: MyEnum): Json =
# GET /items?q=heh&short=true
router.get("/items") do (q: string, short: bool): Json =
return %{"hello": "world"}
router.post("/items") do (item: Json[Item]): Response =
Response(text="heh", headers={"blabla": "dsf"})
# maybe without specifying that it is json it should search up in different places
# depending on request Content-Type
# if application/json -> parse json
# if xxx-form -> parse form multipart data
# either try to search for query parameters
# and somehow add plugins for parsing other types
router.post("/items") do (item: Item): Response =
Response(text="heh", headers={"blabla": "dsf"})
router.post("/header_test") do (myHeader: Header) =
discard
router.post("/:path") do (path: Path): File =
File(path)
router.post("/stream") do (): Stream =
Stream
# and for addition this can support router merging
let subRoute = newRouter()
router.mount("/sub", subRoute)
the macro used here
macro get(router: Router, path: string, handler: untyped): untyped
macro post(router: Router, path: string, handler: untyped): untyped
macro put(router: Router, path: string, handler: untyped): untyped
about pragmas for comments they can be also be in types definitions
import std/macros
import std/sets
type Obj = object
name: string
help: string
fields: seq[(string, string)]
type Objects = seq[Obj]
proc parseTypeDeep(x: NimNode, visited: HashSet[string], objs: var Objects) =
if x.kind == nnkTypeDef:
var objFields = newSeq[(string, string)]()
var fields: NimNode
if x[2].kind == nnkRefTy:
fields = x[2][0][2]
else:
fields = x[2][2]
for field in fields:
if field[1].kind == nnkSym:
let fieldType = field[1].getImpl
if fieldType.kind == nnkTypeDef:
parseTypeDeep(fieldType, visited, objs)
else:
# TODO: generics
echo field[1].getTypeImpl.repr
objFields.add((field[0].repr, field[1].repr))
var help: string
var name: string
if x[0].kind == nnkPragmaExpr:
name = x[0][0].repr
for pragma in x[0][1]:
if pragma[0].repr == "help":
help = $pragma[1]
else:
name = x[0].repr
objs.add Obj(name: name, help: help, fields: objFields)
template help(s: string) {.pragma.}
macro parseType(x: typed) =
var info = newSeq[Obj]()
parseTypeDeep(x.getImpl, initHashSet[string](), info)
echo info
type
Author = object
Post {.help: "contains post data".} = object
id {.help: "Post Identificator".}: int
author {.help: "Post author".}: Author
content {.help: "contains post text".}: string
parseType(Post)
@[(name: "Author", help: "", fields: @[]), (name: "Post", help: "contains post data", fields: @[("id {.help: \"Post Identificator\".}", "int"), ("author {.help: \"Post author\".}", "Author"), ("content {.help: \"contains post text\".}", "string")])]
unfortunately nim ast doesnt contain comments for types definitions, but it may be parsed manually using lineInfo and reading source files
proc countIndent(line: string): int =
for c in line:
if c != ' ':
break
result += 1
macro readObj(n: typed) =
let x = n.getImpl
echo x.lineInfoObj.repr
let li = x.lineInfoObj
let lines = readFile(li.filename).split("\n")
let startIdent = countIndent(lines[li.line-1])
var i = 0
while i < lines.len - li.line and countIndent(lines[li.line+i]) > startIdent:
i += 1
echo lines[li.line-1..li.line+i-1]
maybe in future it will need integration with lib like this https://github.com/captainbland/nim-validation/tree/master
proc swagerizePath(path: string): string =
result = newStringOfCap(path.len+path.count(':')*2)
var searchSlash = false;
for c in path:
if c == '/' and searchSlash:
result &= '}'
searchSlash = false
if c == ':':
result &= '{'
searchSlash = true
else:
result &= c
if searchSlash:
result &= '}'
proc dumpInfo*(info: HandlerInfo): NimNode =
let path = swagerizePath(info.path)
let verbs = info.verbs
var preParams = newSeq[JsonNode]()
let pathParameters = block:
var params: seq[string]
let nodes = info.path.toNodes()
for node in nodes:
if node.kind in {Param, Greedy} and node.val != "":
params &= node.val
params
var prebody: JsonNode = %*{"content": {}}
for p in info.params:
# not support
# {p} -> get(p: Option[int])
# option path
# {p} -> get(p: Path[int])
# direct specifying that its path
if p.name in pathParameters:
preParams &= %*{
"name": p.name,
"in": "path",
"schema": {"type": p.kind.repr},
"description": "",
"required": true
}
elif p.kind.kind == nnkBracketExpr:
if $(p.kind[0]) in @["Query", "Header", "Cookie"]:
var inn: string
if $(p.kind[0]) == "Query":
# not supported content
# objects as form or json
inn = "query"
if $(p.kind[0]) == "Header":
inn = "header"
if $(p.kind[0]) == "Cookie":
inn = "cookie"
var t: string = p.kind[1].repr
var required: bool = true
if p.kind[1].kind == nnkBracketExpr and $(p.kind[1][0]) == "Option":
t = p.kind[1][1].repr
required = false
preParams &= %*{
"name": p.name,
"in": inn,
"schema": {"type": t},
"description": "",
"required": required
}
if $(p.kind[0]) in @["Json"]:
prebody["content"]["application/json"] = %*{
"schema": {
"$ref": "#/components/schemas/" & p.kind[1].repr
}
}
let params = $ %preParams
let body = $prebody
return quote do:
if not mikeRoutesInfo.contains(`path`):
mikeRoutesInfo[`path`] = JsonNode(kind: JObject)
for verb in `verbs`:
var spec = JsonNode(kind: JObject)
mikeRoutesInfo[`path`][($verb).toLowerAscii()] = spec
spec["tags"] = %*{}
spec["parameters"] = parseJson(`params`)
spec["requestBody"] = parseJson(`body`)
var
mikeRouter = Router[Route]()
errorHandlers: Table[cstring, AsyncHandler]
mikeRoutesInfo* = JsonNode(kind: JObject)
macro `->`*(path: static[string], info: untyped, body: untyped): untyped =
let info = getHandlerInfo(path, info, body)
let handlerProc = createAsyncHandler(body, info.path, info.params)
let infoJson = dumpInfo(info)
result = genAst(path = info.path, verbs = info.verbs, pos = info.pos, handlerProc, infoJson=infoJson):
addHandler(path, verbs, pos, handlerProc)
infoJson
i managed to generate first version which generates openapi spec for my toy example https://github.com/blackmius/realworld-mike
i see there one library for jester https://github.com/ThomasTJdev/jester2swagger/tree/main but it is mainly parsing source files using regexes (which is not as reliable as analyzing ast) but i dont know how to connect macros in such way that swagger enhancement could be optional
i mean all it needs is to receive HandlerInfo at compile time and inject some ast back (to make data available in runtime)
maybe make first some sort of plugin system and then make plugin for gather data for swagger
var transformers {.compileTime.} = newSeq[proc(x:NimNode): NimNode]()
macro test(x: untyped): untyped =
echo transformers.len
result = transformers[0](x)
for t in transformers[1..^1]:
result = t(result)
static:
transformers.add(proc(x: NimNode): NimNode=
return x)
static:
transformers.add(proc(x: NimNode): NimNode=
return x)
test echo "a"
Sorry for the delayed response work has been busy, this is some cool stuff though. Hopefully I have time this weekend to properly look at integrating your code.
Shouldn't be too difficult to just store more information in a macrocache list/table, then a user can just place before the end of their routes something like
"/swagger" -> get:
return renderSwagger()
For comments yeah having pragmas is going to have to be the way. I recently was writing something and wanted comments so was going to make a PR to Nim to expose the comments that is stored in nodes but it doesn't work in IC so I didn't put effort into it (Not that IC is really available :P), maybe NIR will change that limitation.
For annotating the parameters, I'd probably go down a route more like
type
SwaggerInfo = object
kind: # Enum of path, query, header, cookie
required: bool
schema: # Schema object
# ...
proc swaggerRouteInfo(x: typedesc[SomeType]): SwaggerInfo
That way custom types can change how it gets converted.
Like the thing you mentioned about parsing depending on Content-Type
, make a quick issue to separately track that #52
what about adding swagger routes?
i see it is possible with saving handlerInfo in dsl.nim
and few things to generate better doc: