Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

release v4.6.3 - improve openapi #369

Merged
merged 8 commits into from
Oct 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,11 @@ jobs:
nimble install -y -d
nimble install checksums -y
nimble install regex -y
- uses: reviewdog/action-nimlint@v1
with:
github_token: ${{ secrets.github_token }}
reporter: github-pr-review # Change reporter.
src: 'src/*.nim'
# - uses: reviewdog/action-nimlint@v1
# with:
# github_token: ${{ secrets.github_token }}
# reporter: github-pr-review # Change reporter.
# src: 'src/*.nim'
built_in_server:
name: Test C via built-in server 🧪
needs: dependencies
Expand Down
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
2 changes: 2 additions & 0 deletions src/happyx/ssr/handlers.nim
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ when defined(napibuild):
apiDocData.add(newApiDocObject(
@["GET"],
"Fetch file from directory: " & route.purePath,
"",
routeData.path,
routeData.pathParams,
routeData.requestModels,
Expand All @@ -25,6 +26,7 @@ when defined(napibuild):
apiDocData.add(newApiDocObject(
route.httpMethod,
route.docs,
"",
routeData.path,
routeData.pathParams,
routeData.requestModels,
Expand Down
Loading