Skip to content

Commit

Permalink
improve openapi documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
Ethosa committed Oct 26, 2024
1 parent d2782b5 commit f33f606
Show file tree
Hide file tree
Showing 6 changed files with 364 additions and 258 deletions.
2 changes: 1 addition & 1 deletion happyx.nimble
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

description = "Macro-oriented asynchronous web-framework written with ♥"
author = "HapticX"
version = "4.6.2"
version = "4.6.3"
license = "MIT"
srcDir = "src"
installExt = @["nim"]
Expand Down
2 changes: 1 addition & 1 deletion src/happyx/core/constants.nim
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ const
# Framework version
HpxMajor* = 4
HpxMinor* = 6
HpxPatch* = 2
HpxPatch* = 3
HpxVersion* = $HpxMajor & "." & $HpxMinor & "." & $HpxPatch


Expand Down
48 changes: 37 additions & 11 deletions src/happyx/ssr/docs/api_doc_template.nim
Original file line number Diff line number Diff line change
Expand Up @@ -144,10 +144,10 @@ const IndexApiDocPageTemplate* = fmt"""
/>
</svg>
</div>
<div id="httpMethod_{{{{httpMethod}}}}" class="flex flex-col gap-6 lg:gap-4 xl:gap-2 h-fit transition-all duration-300">
<div id="httpMethod_{{{{httpMethod}}}}" class="flex flex-col gap-6 lg:gap-4 xl:gap-2 max-h-[1000vh] transition-all duration-300">
{{% for req in data %}}
<div class="flex flex-col w-fit border-[2px] border-[{Fore}]/25 dark:border-[{ForeDark}]/25 rounded-md">
<div class="flex p-1 bg-[{BackCode}] dark:bg-[{BackCodeDark}] font-mono px-4 py-1 rounded-md font-semibold">
<div class="flex p-1 bg-[{BackCode}] dark:bg-[{BackCodeDark}] font-mono px-4 py-1 rounded-t-md font-semibold">
<p class="flex mr-4 {AccentColor} cursor-pointer select-none">
{{% if req.httpMethod.len == 0 %}}
ANY <!-- HTTP Method -->
Expand Down Expand Up @@ -190,7 +190,15 @@ const IndexApiDocPageTemplate* = fmt"""
%}}
<tr class="{{{{color}}}} py-1">
<td class="px-2">{{{{param.name}}}}</td>
<td class="px-2 {AccentColor} font-mono">{{{{param.paramType}}}}</td>
<td class="px-2 {AccentColor} font-mono">
{{% if modelsData.hasKey(param.paramType.replace("enum::", "")) %}}
<a href="#Model_{{{{ param.paramType.replace("enum::", "") }}}}">
{{{{ param.paramType.replace("enum::", "") }}}}
</a>
{{% else %}}
{{{{ param.paramType.replace("enum::", "") }}}}
{{% endif %}}
</td>
<td class="px-2 {AccentColor} font-mono">{{{{param.defaultValue}}}}</td>
<td class="text-center align-middle px-2">
{{% if param.optional %}}{{% else %}}{{% endif %}}
Expand Down Expand Up @@ -275,7 +283,7 @@ const IndexApiDocPageTemplate* = fmt"""
<div id="Model_{{{{ key }}}}" class="flex flex-col justify-between items-center px-4 py-2 rounded-md border-[2px] border-[{Fore}]/25 dark:border-[{ForeDark}]/25">
<p class="text-3xl lg:text-xl xl:text-lg font-semibold">{{{{ key }}}}</p>
{{% for field in fields.keys() %}}
<div class="text-xl lg:text-lg xl:text-base flex gap-8 lg:gap-4 xl:gap-2 justify-between w-full">
<div class="text-xl lg:text-lg xl:text-base flex gap-8 lg:gap-6 xl:gap-4 justify-between w-full">
<p>{{{{ field }}}}</p>
{{% if modelsData.hasKey(fields[field]) %}}
<p class="font-mono font-black {RequestModelColor}">
Expand All @@ -295,7 +303,7 @@ const IndexApiDocPageTemplate* = fmt"""
<div class="w-48 h-48 py-12">&nbsp;</div>
</div>
<div class="text-3xl lg:text-xl xl:text-base fixed bottom-0 flex flex-col justify-center items-center w-full bg-[{BackCode}] dark:bg-[{BackCodeDark}] py-8">
<div class="text-3xl lg:text-xl xl:text-base fixed bottom-0 flex flex-col justify-center items-center w-full bg-[{BackCode}] dark:bg-[{BackCodeDark}] py-6">
<p>
Made with
<a href="https://github.com/HapticX/happyx" class="{Link}">
Expand All @@ -305,6 +313,20 @@ const IndexApiDocPageTemplate* = fmt"""
</div>
</div>
<script>
function removeHash() {{
var scrollV, scrollH, loc = window.location;
if (history.pushState)
history.pushState("", document.title, loc.pathname + loc.search);
else {{
// Prevent scrolling by storing the page's current scroll offset
scrollV = document.body.scrollTop;
scrollH = document.body.scrollLeft;
loc.hash = "";
// Restore the scroll offset, should be flicker free
document.body.scrollTop = scrollV;
document.body.scrollLeft = scrollH;
}}
}}
function changeHash() {{// clean
let elements = document.querySelectorAll("[id]");
elements.forEach((e) => {{
Expand All @@ -315,6 +337,10 @@ const IndexApiDocPageTemplate* = fmt"""
let elem = document.getElementById(id);
if (elem) {{
elem.classList.add("highlight-animation");
const _t = setTimeout(() => {{
removeHash();
clearTimeout(_t);
}}, 1000);
}}
}}
Expand All @@ -331,27 +357,27 @@ const IndexApiDocPageTemplate* = fmt"""
// show
arw.classList.remove("rotate-90");
arw.classList.add("rotate-0");
section.classList.remove("h-0");
section.classList.remove("max-h-0");
section.classList.remove("opacity-0");
section.classList.add("h-fit");
section.classList.add("max-h-[1000vh]");
section.classList.add("opacity-100");
}} else {{
// hide
arw.classList.remove("rotate-0");
arw.classList.add("rotate-90");
section.classList.remove("h-fit");
section.classList.remove("max-h-[1000vh]");
section.classList.remove("opacity-100");
section.classList.add("h-0");
section.classList.add("max-h-0");
section.classList.add("opacity-0");
}}
}} else {{
toggled[identifier] = true;
// hide
arw.classList.remove("rotate-0");
arw.classList.add("rotate-90");
section.classList.remove("h-fit");
section.classList.remove("max-h-[1000vh]");
section.classList.remove("opacity-100");
section.classList.add("h-0");
section.classList.add("max-h-0");
section.classList.add("opacity-0");
}}
}}
Expand Down
89 changes: 66 additions & 23 deletions src/happyx/ssr/docs/autodocs.nim
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,16 @@ when nimvm:
type
ApiDocObject* = object
description*: string
src*: string
path*: string
httpMethod*: seq[string]
pathParams*: seq[PathParamObj]
models*: seq[RequestModelObj]

proc newApiDocObject*(httpMethod: seq[string], description, path: string, pathParams: seq[PathParamObj],
proc newApiDocObject*(httpMethod: seq[string], description, src, path: string, pathParams: seq[PathParamObj],
models: seq[RequestModelObj]): ApiDocObject =
ApiDocObject(httpMethod: httpMethod, description: description, path: path,
pathParams: pathParams, models: models)
src: src, pathParams: pathParams, models: models)


proc fetchPathParams*(route: var string): tuple[pathParams, models: NimNode] =
Expand Down Expand Up @@ -63,6 +64,10 @@ proc fetchPathParams*(route: var string): tuple[pathParams, models: NimNode] =
re2"\{([a-zA-Z][a-zA-Z0-9_]*)\??(:(bool|int|float|string|path|word|/[\s\S]+?/|enum\(\w+\)))?(\[m\])?(=(\S+?))?\}",
"{$1}"
)
route = route.replace(
re2"\$([a-zA-Z][a-zA-Z0-9_]*)\??(:(bool|int|float|string|path|word|/[\s\S]+?/|enum\(\w+\)))?(\[m\])?(=(\S+?))?",
"{$1}"
)
route = route.replace(re2"\[([a-zA-Z][a-zA-Z0-9_]*):([a-zA-Z][a-zA-Z0-9_]*)(\[m\])?(:[a-zA-Z\\-]+)?\]", "")

(newCall("@", params), newCall("@", models))
Expand All @@ -85,6 +90,7 @@ proc fetchModelFields*(): NimNode =
ident"initTable", ident"string", ident"StringTableRef"
))


proc genApiDoc*(body: var NimNode): NimNode =
## Returns API route
var
Expand All @@ -97,31 +103,39 @@ proc genApiDoc*(body: var NimNode): NimNode =
## HTTP Method
var
description = ""
src: seq[string] = @[]
pathParam = $i[1]
(params, models) = fetchPathParams(pathParam)
for statement in i[2]:
if statement.kind == nnkCommentStmt:
description &= $statement & "\n"
else:
src.add($statement.toStrLit)
docsData.add(newCall(
"newApiDocObject",
newCall("@", bracket(newLit(($i[0].toStrLit).toUpper()))), # HTTP Method
newLit(description), # Description
newLit(src.join("\n")), # Source code
newLit(pathParam), # Path
params, models
))
elif i[0].kind == nnkStrLit and i.len == 2 and i[1].kind == nnkStmtList:
## HTTP Method
var
description = ""
src: seq[string] = @[]
pathParam = $i[0]
(params, models) = fetchPathParams(pathParam)
for statement in i[1]:
if statement.kind == nnkCommentStmt:
description &= $statement & "\n"
else:
src.add($statement.toStrLit)
docsData.add(newCall(
"newApiDocObject",
newCall("@", bracket(newLit"")), # HTTP Method
newLit(description), # Description
newLit(src.join("\n")), # Source code
newLit(pathParam), # Path
params, models
))
Expand Down Expand Up @@ -251,7 +265,10 @@ proc openApiDocs*(docsData: NimNode): NimNode =
for k, v in modelFields.pairs():
let table = newNimNode(nnkTableConstr)
for s in v.children:
table.add(newColonExpr(s[0], s[1]))
if s.len == 3:
table.add(newColonExpr(s[0], bracket(s[1], s[2])))
else:
table.add(newColonExpr(s[0], s[1]))
modelsTable.add(
newNimNode(nnkExprColonExpr).add(
newLit(k), table
Expand Down Expand Up @@ -283,7 +300,7 @@ proc openApiDocs*(docsData: NimNode): NimNode =
return parseFile("openapi.json")
else:
result = %*{
"openapi": "3.1.0",
"openapi": "3.1.1",
"swagger": "2.0",
"info": {"title": "HappyX OpenAPI Docs", "version": "1.0.0"},
"paths": {},
Expand All @@ -307,10 +324,17 @@ proc openApiDocs*(docsData: NimNode): NimNode =
for k, v in modelsData.pairs:
var schema = %*{
"type": "object",
"required": [],
"properties": {}
}
for name, value in v.pairs:
let strValue = value.getStr
let strValue =
if value.kind == JArray:
value[0].getStr
else:
value.getStr
if value.kind == JArray:
schema["required"].add(%name)
# atomic types
case strValue
of "int8", "int16", "int32":
Expand Down Expand Up @@ -346,8 +370,38 @@ proc openApiDocs*(docsData: NimNode): NimNode =
"description": decscription,
"parameters": [],
"requestBody": {},
"responses": {}
"responses": {
"200": {
"description": "",
"content": {}
}
}
}

for m in route.src.findAll(re2"\bstatusCode\b\s*=\s*(\d+)(\s*#+\s*([^\n]+))?"):
let
statusCode = route.src[m.group(0)]
description = route.src[m.group(2)]
if statusCode != "200":
pathData["responses"][statusCode] = %*{
"description": description,
"headers": {},
"content": {}
}

# echo route.srcd

# Params
for p in route.pathParams:
let param = %*{
"name": p.name,
"required": not p.optional,
"in": "path",
"schema": {
"type": p.paramType
}
}
pathData["parameters"].add(param)

if route.description.find(
re2"@openapi\s*\{((\s*\w+\s*[^\n]+|\s*@(params|responses)\s*\{[^\}]+?}\s*)+)\s*\}",
Expand All @@ -357,17 +411,6 @@ proc openApiDocs*(docsData: NimNode): NimNode =
# Additional data
for m in text.findAll(re2"(?m)^\s*(\w[\w\d_]*)\s*=\s*([^\n]+)$"):
pathData[text[m.group(0)]] = %text[m.group(1)]
# Params
for p in route.pathParams:
let param = %*{
"name": p.name,
"required": not p.optional,
"in": "path",
"schema": {
"type": p.paramType
}
}
pathData["parameters"].add(param)

var paramMatches: RegexMatch2
if text.find(re2"@params\s*{((\s*\w[\w\d]*\!?\s*(:\s*\w+)?[^\n]+)+)\s*}", paramMatches):
Expand Down Expand Up @@ -403,7 +446,7 @@ proc openApiDocs*(docsData: NimNode): NimNode =
pathData["parameters"].add(param)

for m in route.models:
echo m
# echo m
let schema = %*{
"schema": {
"$ref": "#/components/schemas/" & m.typeName
Expand All @@ -414,20 +457,20 @@ proc openApiDocs*(docsData: NimNode): NimNode =
}
}
}
case m.target
of "JSON":
case m.target.toLower()
of "json":
pathData["requestBody"]["content"] = %{
"application/json": schema
}
of "XML":
of "xml":
pathData["requestBody"]["content"] = %{
"application/xml": schema
}
of "Form-Data":
of "formdata", "form-data":
pathData["requestBody"]["content"] = %{
"multipart/form-data": schema
}
of "x-www-form-urlencoded":
of "x-www-form-urlencoded", "urlencoded":
pathData["requestBody"]["content"] = %{
"application/x-www-form-urlencoded": schema
}
Expand Down
Loading

0 comments on commit f33f606

Please sign in to comment.