Repository: HapticX/happyx (language: python)

Issue:
Support route decorators for mounts
### Discussed in https://github.com/HapticX/happyx/discussions/332

<div type='discussions-op-text'>

<sup>Originally posted by **jerrygzy** August 25, 2024</sup>
after some time, I have found two approaches.

`method 1`,  this method does not use app object, which means, it's hard to pass app object to render function.
```nim
import happyx

proc renderIndex(): string =
  return " default client"

proc renderLogin(): string =
  return " login client"

mount Clients:
  "/":
    renderIndex()
  "/login":
    renderLogin()

serve "127.0.0.1", 5000:
  # Mounts
  mount "" -> Clients
  # Pages (docs)
  get "/{title:string}":
    req.answerHtml render(title)
  # Static Files
  staticDir "/download" ->  "public" 


```


`method 2`, this method use app object, but can only work under [setup:] which does not work with Decorator
```nim
# define app object
type
    MyApp* = object
        pwa: bool  
        name: string

# Object Method Mount
proc doMount(app: MyApp) =
  "/" -> get:
    " $#  | default client" % app.name
  "/login" -> get:
    " $#  | login client" % app.name

serve "0.0.0.0", 80:
  setup:
    # Create app 
    let app = MyApp(pwa:false, name: "happyx")
    # Mount
    app.doMount()

  # Pages (docs)
  get "/{title:string}":
    req.answerHtml render(title)
  # Static Files
  staticDir "/download" ->  "public" 

```

Note: for Decorator, `method 1` only apply the last route, `method 2` does not work

```nim
proc myCustomDecorator*(httpMethods: seq[string], path: string, statementList: NimNode, arguments: seq[NimNode]) = 
  # This decorator will add
  #   echo "Hello from {path}"
  # as leading statement in route at compile-time
  statementList.insert(0, newCall("echo", newLit("Hello from " & path)))


# Register our decorator
static:
  regDecorator("OurDecorator", myCustomDecorator)
```

`method 1`  only apply the last route
```nim
...
@OurDecorator
mount "" -> Clients
...
```
`method 2`  does not work
```nim
...
    @OurDecorator # this does not work
    app.doMount()
...
```


</div>
Support route decorators for mounts
### Discussed in https://github.com/HapticX/happyx/discussions/332

<div type='discussions-op-text'>

<sup>Originally posted by **jerrygzy** August 25, 2024</sup>
after some time, I have found two approaches.

`method 1`,  this method does not use app object, which means, it's hard to pass app object to render function.
```nim
import happyx

proc renderIndex(): string =
  return " default client"

proc renderLogin(): string =
  return " login client"

mount Clients:
  "/":
    renderIndex()
  "/login":
    renderLogin()

serve "127.0.0.1", 5000:
  # Mounts
  mount "" -> Clients
  # Pages (docs)
  get "/{title:string}":
    req.answerHtml render(title)
  # Static Files
  staticDir "/download" ->  "public" 


```


`method 2`, this method use app object, but can only work under [setup:] which does not work with Decorator
```nim
# define app object
type
    MyApp* = object
        pwa: bool  
        name: string

# Object Method Mount
proc doMount(app: MyApp) =
  "/" -> get:
    " $#  | default client" % app.name
  "/login" -> get:
    " $#  | login client" % app.name

serve "0.0.0.0", 80:
  setup:
    # Create app 
    let app = MyApp(pwa:false, name: "happyx")
    # Mount
    app.doMount()

  # Pages (docs)
  get "/{title:string}":
    req.answerHtml render(title)
  # Static Files
  staticDir "/download" ->  "public" 

```

Note: for Decorator, `method 1` only apply the last route, `method 2` does not work

```nim
proc myCustomDecorator*(httpMethods: seq[string], path: string, statementList: NimNode, arguments: seq[NimNode]) = 
  # This decorator will add
  #   echo "Hello from {path}"
  # as leading statement in route at compile-time
  statementList.insert(0, newCall("echo", newLit("Hello from " & path)))


# Register our decorator
static:
  regDecorator("OurDecorator", myCustomDecorator)
```

`method 1`  only apply the last route
```nim
...
@OurDecorator
mount "" -> Clients
...
```
`method 2`  does not work
```nim
...
    @OurDecorator # this does not work
    app.doMount()
...
```


</div>


Discussion / hints:

Fix patch:
diff --git a/happyx.nimble b/happyx.nimble
index 5dfb2aac..eb9d955a 100755
--- a/happyx.nimble
+++ b/happyx.nimble
@@ -2,7 +2,7 @@
 
 description = "Macro-oriented asynchronous web-framework written with ♥"
 author = "HapticX"
-version = "4.4.4"
+version = "4.5.0"
 license = "MIT"
 srcDir = "src"
 installExt = @["nim"]
diff --git a/src/happyx/core/constants.nim b/src/happyx/core/constants.nim
index ed8b4498..7e27d476 100755
--- a/src/happyx/core/constants.nim
+++ b/src/happyx/core/constants.nim
@@ -98,8 +98,8 @@ const
   nim_2_0_0* = (NimMajor, NimMinor, NimPatch) >= (2, 0, 0)
   # Framework version
   HpxMajor* = 4
-  HpxMinor* = 4
-  HpxPatch* = 4
+  HpxMinor* = 5
+  HpxPatch* = 0
   HpxVersion* = $HpxMajor & "." & $HpxMinor & "." & $HpxPatch
 
 
diff --git a/src/happyx/private/macro_utils.nim b/src/happyx/private/macro_utils.nim
index e163d3fa..3297b065 100644
--- a/src/happyx/private/macro_utils.nim
+++ b/src/happyx/private/macro_utils.nim
@@ -142,6 +142,18 @@ proc formatNode*(node: NimNode): NimNode =
     node
 
 
+proc liveviewParam*(name: string): NimNode =
+  newNimNode(nnkWhenStmt).add(newNimNode(nnkElifBranch).add(
+    newCall("declared", ident(name)),
+    ident(name)
+  ), newNimNode(nnkElifBranch).add(
+    newCall("declared", newDotExpr(ident"self", ident(name))),
+    newDotExpr(ident"self", ident(name))
+  ), newNimNode(nnkElse).add(
+    newLit""
+  ))
+
+
 proc useComponent*(statement: NimNode, inCycle, inComponent: bool,
                    cycleTmpVar: string, compTmpVar: NimNode, cycleVars: var seq[NimNode],
                    returnTagRef: bool = true, constructor: bool = false,
@@ -229,69 +241,13 @@ proc useComponent*(statement: NimNode, inCycle, inComponent: bool,
   objConstr.add(stringId)
   when not defined(js) and enableLiveViews:
     objConstr.add(
-      newNimNode(nnkWhenStmt).add(newNimNode(nnkElifBranch).add(
-        newCall("declared", ident"urlPath"),
-        ident"urlPath"
-      ), newNimNode(nnkElifBranch).add(
-        newCall("declared", newDotExpr(ident"self", ident"urlPath")),
-        newDotExpr(ident"self", ident"urlPath")
-      ), newNimNode(nnkElse).add(
-        newLit""
-      )),
-      newNimNode(nnkWhenStmt).add(newNimNode(nnkElifBranch).add(
-        newCall("declared", ident"hostname"),
-        ident"hostname"
-      ), newNimNode(nnkElifBranch).add(
-        newCall("declared", newDotExpr(ident"self", ident"hostname")),
-        newDotExpr(ident"self", ident"hostname")
-      ), newNimNode(nnkElse).add(
-        newLit""
-      )),
-      newNimNode(nnkWhenStmt).add(newNimNode(nnkElifBranch).add(
-        newCall("declared", ident"query"),
-        ident"query"
-      ), newNimNode(nnkElifBranch).add(
-        newCall("declared", newDotExpr(ident"self", ident"query")),
-        newDotExpr(ident"self", ident"query")
-      ), newNimNode(nnkElse).add(
-        newLit""
-      )),
-      newNimNode(nnkWhenStmt).add(newNimNode(nnkElifBranch).add(
-        newCall("declared", ident"queryArr"),
-        ident"queryArr"
-      ), newNimNode(nnkElifBranch).add(
-        newCall("declared", newDotExpr(ident"self", ident"queryArr")),
-        newDotExpr(ident"self", ident"queryArr")
-      ), newNimNode(nnkElse).add(
-        newLit""
-      )),
-      newNimNode(nnkWhenStmt).add(newNimNode(nnkElifBranch).add(
-        newCall("declared", ident"reqMethod"),
-        ident"reqMethod"
-      ), newNimNode(nnkElifBranch).add(
-        newCall("declared", newDotExpr(ident"self", ident"reqMethod")),
-        newDotExpr(ident"self", ident"reqMethod")
-      ), newNimNode(nnkElse).add(
-        newLit""
-      )),
-      newNimNode(nnkWhenStmt).add(newNimNode(nnkElifBranch).add(
-        newCall("declared", ident"inCookies"),
-        ident"inCookies"
-      ), newNimNode(nnkElifBranch).add(
-        newCall("declared", newDotExpr(ident"self", ident"inCookies")),
-        newDotExpr(ident"self", ident"inCookies")
-      ), newNimNode(nnkElse).add(
-        newLit""
-      )),
-      newNimNode(nnkWhenStmt).add(newNimNode(nnkElifBranch).add(
-        newCall("declared", ident"headers"),
-        ident"headers"
-      ), newNimNode(nnkElifBranch).add(
-        newCall("declared", newDotExpr(ident"self", ident"headers")),
-        newDotExpr(ident"self", ident"headers")
-      ), newNimNode(nnkElse).add(
-        newLit""
-      )),
+      liveviewParam("urlPath"),
+      liveviewParam("hostname"),
+      liveviewParam("query"),
+      liveviewParam("queryArr"),
+      liveviewParam("reqMethod"),
+      liveviewParam("inCookies"),
+      liveviewParam("headers"),
     )
   if statement[1].kind == nnkCall:
     for i in 1..<statement[1].len:
diff --git a/src/happyx/routing/mounting.nim b/src/happyx/routing/mounting.nim
index 499cf963..de2e9d29 100644
--- a/src/happyx/routing/mounting.nim
+++ b/src/happyx/routing/mounting.nim
@@ -21,7 +21,8 @@ import
   std/macrocache,
   std/strformat,
   # HappyX
-  ../core/[exceptions]
+  ../core/[exceptions],
+  ./decorators
 
 when not declared(CacheTable.hasKey):
   import ../private/macro_utils
@@ -35,10 +36,22 @@ proc findAndReplaceMount*(body: NimNode) =
   ## 
   ## Don't use it in product
   ## 
-  var offset =  0
+  var
+    offset = 0
+    nextRouteDecorators: seq[NimNode] = @[]
   for i in 0..<body.len:
     let idx = i+offset
-    if body[idx].kind == nnkCommand and body[idx][0] == ident"mount":
+    # Decorators
+    if body[idx].kind == nnkPrefix and $body[idx][0] == "@" and body[idx][1].kind == nnkIdent:
+      # @Decorator
+      nextRouteDecorators.add(body[idx].copy())
+    # @Decorator()
+    elif body[idx].kind == nnkCall and body[idx][0].kind == nnkPrefix and $body[idx][0][0] == "@" and body[idx].len == 1:
+      nextRouteDecorators.add(body[idx].copy())
+    # @Decorator(arg1, arg2, ...)
+    elif body[idx].kind == nnkCall and body[idx][0].kind == nnkPrefix and $body[idx][0][0] == "@" and body[idx].len > 1:
+      nextRouteDecorators.add(body[idx].copy())
+    elif body[idx].kind == nnkCommand and body[idx][0] == ident"mount":
       if body[idx][1].kind == nnkInfix and body[idx][1][0] == ident"->":
         # handle mount
         let
@@ -63,10 +76,16 @@ proc findAndReplaceMount*(body: NimNode) =
             elif statement[1].kind in [nnkStrLit, nnkTripleStrLit]:
               statement[1] = newLit($route & $statement[1])
           # Add mount routes
+          for decorator in nextRouteDecorators:
+            inc offset
+            body.insert(i, decorator)
           if (statement.kind in [nnkCall, nnkCommand] and $statement[0] != "mount") or
              (statement.kind == nnkPrefix and $statement[0] == "@"):
             inc offset
             body.insert(i, statement)
+        nextRouteDecorators = @[]
+      else:
+        nextRouteDecorators = @[]
 
 
 macro mount*(mountName, body: untyped): untyped =
diff --git a/src/happyx/spa/components.nim b/src/happyx/spa/components.nim
index fb4f31ee..1d528fb7 100644
--- a/src/happyx/spa/components.nim
+++ b/src/happyx/spa/components.nim
@@ -390,81 +390,25 @@ macro component*(name, body: untyped): untyped =
     initObjConstr.add(
       newColonExpr(
         ident"urlPath",
-        newNimNode(nnkWhenStmt).add(newNimNode(nnkElifBranch).add(
-          newCall("declared", ident"urlPath"),
-          ident"urlPath"
-        ), newNimNode(nnkElifBranch).add(
-          newCall("declared", newDotExpr(ident"self", ident"urlPath")),
-          newDotExpr(ident"self", ident"urlPath")
-        ), newNimNode(nnkElse).add(
-          newLit""
-        ))
+        liveviewParam("urlPath")
       ), newColonExpr(
         ident"hostname",
-        newNimNode(nnkWhenStmt).add(newNimNode(nnkElifBranch).add(
-          newCall("declared", ident"hostname"),
-          ident"hostname"
-        ), newNimNode(nnkElifBranch).add(
-          newCall("declared", newDotExpr(ident"self", ident"hostname")),
-          newDotExpr(ident"self", ident"hostname")
-        ), newNimNode(nnkElse).add(
-          newLit""
-        ))
+        liveviewParam("hostname")
       ), newColonExpr(
         ident"query",
-        newNimNode(nnkWhenStmt).add(newNimNode(nnkElifBranch).add(
-          newCall("declared", ident"query"),
-          ident"query"
-        ), newNimNode(nnkElifBranch).add(
-          newCall("declared", newDotExpr(ident"self", ident"query")),
-          newDotExpr(ident"self", ident"query")
-        ), newNimNode(nnkElse).add(
-          newLit""
-        ))
+        liveviewParam("query")
       ), newColonExpr(
         ident"queryArr",
-        newNimNode(nnkWhenStmt).add(newNimNode(nnkElifBranch).add(
-          newCall("declared", ident"queryArr"),
-          ident"queryArr"
-        ), newNimNode(nnkElifBranch).add(
-          newCall("declared", newDotExpr(ident"self", ident"queryArr")),
-          newDotExpr(ident"self", ident"queryArr")
-        ), newNimNode(nnkElse).add(
-          newLit""
-        ))
+        liveviewParam("queryArr")
       ), newColonExpr(
         ident"reqMethod",
-        newNimNode(nnkWhenStmt).add(newNimNode(nnkElifBranch).add(
-          newCall("declared", ident"reqMethod"),
-          ident"reqMethod"
-        ), newNimNode(nnkElifBranch).add(
-          newCall("declared", newDotExpr(ident"self", ident"reqMethod")),
-          newDotExpr(ident"self", ident"reqMethod")
-        ), newNimNode(nnkElse).add(
-          newLit""
-        ))
+        liveviewParam("reqMethod")
       ), newColonExpr(
         ident"inCookies",
-        newNimNode(nnkWhenStmt).add(newNimNode(nnkElifBranch).add(
-          newCall("declared", ident"inCookies"),
-          ident"inCookies"
-        ), newNimNode(nnkElifBranch).add(
-          newCall("declared", newDotExpr(ident"self", ident"inCookies")),
-          newDotExpr(ident"self", ident"inCookies")
-        ), newNimNode(nnkElse).add(
-          newLit""
-        ))
+        liveviewParam("inCookies")
       ), newColonExpr(
         ident"headers",
-        newNimNode(nnkWhenStmt).add(newNimNode(nnkElifBranch).add(
-          newCall("declared", ident"headers"),
-          ident"headers"
-        ), newNimNode(nnkElifBranch).add(
-          newCall("declared", newDotExpr(ident"self", ident"headers")),
-          newDotExpr(ident"self", ident"headers")
-        ), newNimNode(nnkElse).add(
-          newLit""
-        ))
+        liveviewParam("headers")
       ),
     )
   initProc[2] = generics.copy()
diff --git a/src/happyx/spa/renderer.nim b/src/happyx/spa/renderer.nim
index 27f38f62..61468ea4 100644
--- a/src/happyx/spa/renderer.nim
+++ b/src/happyx/spa/renderer.nim
@@ -149,8 +149,10 @@ when enableDefaultComponents:
     var components* = newTable[cstring, BaseComponent]()
   else:
     var
-      requestResult* = newTable[string, string]()
-      componentsResult* = newTable[string, string]()
+      requestResult* {.threadvar.}: TableRef[string, string]
+      componentsResult* {.threadvar.}: TableRef[string, string]
+    requestResult = newTable[string, string]()
+    componentsResult = newTable[string, string]()
 
 
 when defined(js):
@@ -213,12 +215,13 @@ else:
     requestResult[host] = "route:" & path
   proc injectJs*(host, script: string) =
     requestResult[host] = "script:" & fmt"<script>{script}</script>"
+  proc js*(host, script: string) =
+    requestResult[host] = "script:" & fmt"<script>{script}</script>"
   proc route*(comp: BaseComponent, path: string) =
     componentsResult[comp.uniqCompId] = "route:" & path
   proc js*(comp: BaseComponent, script: string) =
     componentsResult[comp.uniqCompId] = "script:" & fmt"<script>{script}</script>"
   proc rerender*(query, queryArr, reqMethod, inCookies, headers: auto, hostname, urlPath: string) =
-    echo "rerender for " & hostname
     requestResult[hostname] = "rerender:" & liveviewRoutes[urlPath](
       query, queryArr, reqMethod, inCookies, headers, nil
     ).children[1].ugly()
diff --git a/src/happyx/spa/tag.nim b/src/happyx/spa/tag.nim
index c0385f21..a2b23eee 100644
--- a/src/happyx/spa/tag.nim
+++ b/src/happyx/spa/tag.nim
@@ -441,7 +441,10 @@ proc tagFromString*(source: string): TagRef {.inline.} =
   let xmlNode = parseHtml(source)
   result = initTag("div", @[], true)
   result.xmlTree2Tag(nil, xmlNode)
-  result = result.children[0].children[0].TagRef
+  when defined(js):
+    result = result.children[0].children[0].TagRef
+  else:
+    result = result.children[0].children[0]
 
 
 proc addArg*(self: TagRef, arg: string) =
@@ -467,11 +470,13 @@ proc addArgIter*(self: TagRef, arg: string) =
   ## See also `addArg function #addArg,TagRef,string`_
   when defined(js):
     self.setAttribute(cstring(arg), "")
+    for i in self.children:
+      i.TagRef.addArgIter(arg)
   else:
     if self.args.len == 0:
       self.args.add(arg)
-  for i in self.children:
-    i.TagRef.addArgIter(arg)
+    for i in self.children:
+      i.addArgIter(arg)
 
 
 proc toSeqIter*(self: TagRef): seq[TagRef] =
@@ -485,7 +490,7 @@ proc toSeqIter*(self: TagRef): seq[TagRef] =
         result.add(i)
   else:
     for child in self.children:
-      for i in child.TagRef.toSeqIter:
+      for i in child.toSeqIter:
         result.add(i)
   return result
 
@@ -506,7 +511,10 @@ func lvl*(self: TagRef): int =
   var tag = self
   when defined(js):
     while not tag.parentElement.isNil:
-      tag = tag.parentElement.TagRef
+      when defined(js):
+        tag = tag.parentElement.TagRef
+      else:
+        tag = tag.parentElement
       if not tag.onlyChildren:
         inc result
   else:
@@ -527,7 +535,10 @@ func `[]`*(self: TagRef, attrName: string): string =
 
 func `[]`*(self: TagRef, index: int): TagRef =
   ## Returns tag by index
-  self.children[index].TagRef
+  when defined(js):
+    self.children[index].TagRef
+  else:
+    self.children[index]
 
 
 func `[]=`*(self: TagRef, attrName: string, attrValue: string) =
diff --git a/src/happyx/ssr/form_data.nim b/src/happyx/ssr/form_data.nim
index 8e5eb6b4..36309df6 100644
--- a/src/happyx/ssr/form_data.nim
+++ b/src/happyx/ssr/form_data.nim
@@ -37,9 +37,7 @@ type FormDataItem* = object
 proc parseFormData*(formData: string): (StringTableRef, TableRef[string, FormDataItem]) =
   ## Parses `form-data` into `StringTableRef`
   result = (newStringTable(), newTable[string, FormDataItem]())
-  let
-    lineSeparator = "\r\n"
-    lines = formData.split("\r\n")
+  let lines = formData.split("\r\n")
   var
     key = ""
     data = ""
diff --git a/src/happyx/ssr/request_models.nim b/src/happyx/ssr/request_models.nim
index a1dada98..d6fadc72 100644
--- a/src/happyx/ssr/request_models.nim
+++ b/src/happyx/ssr/request_models.nim
@@ -84,7 +84,6 @@ import
   # stdlib
   std/macros,
   std/macrocache,
-  std/strtabs,
   std/strutils,
   std/strformat,
   # Happyx
diff --git a/src/happyx/ssr/server.nim b/src/happyx/ssr/server.nim
index 33ef571f..fc091e84 100755
--- a/src/happyx/ssr/server.nim
+++ b/src/happyx/ssr/server.nim
@@ -758,6 +758,9 @@ macro routes*(server: Server, body: untyped = newStmtList()): untyped =
               ), statement[2]
             ))
         nextRouteDecorators = @[]
+      # mount ...
+      elif statement.kind == nnkCommand and statement[0] == ident"mount" and statement[1].kind == nnkInfix and statement[1][0] == ident"->":
+        nextRouteDecorators = @[]
       # reqMethod "/...":
       #   ...
       elif statement[0].kind == nnkIdent and statement[0] != ident"mount" and statement[1].kind in {nnkStrLit, nnkTripleStrLit, nnkInfix}:
diff --git a/src/happyx/ssr/types.nim b/src/happyx/ssr/types.nim
index 55f0b491..70017d9f 100644
--- a/src/happyx/ssr/types.nim
+++ b/src/happyx/ssr/types.nim
@@ -1,7 +1,7 @@
 import
   std/logging,
   std/tables,
-  ../spa/[tag, renderer],
+  ../spa/renderer,
   ../core/constants
 
 when enableHttpx:
diff --git a/src/happyx/ssr/websockets.nim b/src/happyx/ssr/websockets.nim
index 51c5b6f4..2f5c5633 100644
--- a/src/happyx/ssr/websockets.nim
+++ b/src/happyx/ssr/websockets.nim
@@ -1,7 +1,10 @@
 import
   std/macros,
-  ../core/constants,
-  ../private/macro_utils
+  ../core/constants
+
+
+when enableHttpBeast:
+  import ../private/macro_utils
 
 
 proc handleWebsockets*(wsClosedConnection: NimNode): tuple[wsStmtList, insertWsList: NimNode] =


Test patch:
diff --git a/tests/testc6.nim b/tests/testc6.nim
index e92da8a2..989f2afc 100644
--- a/tests/testc6.nim
+++ b/tests/testc6.nim
@@ -26,10 +26,21 @@ decorator HelloWorld:
     statementList[0].del(statementList[0].len-1)
 
 
+mount Test:
+  get "/":
+    echo 1
+  
+  get "/test":
+    echo 1
+
+
 serve("127.0.0.1", 5000):
   "/":
     return "Hello, world!"
   
+  @HelloWorld(1, 2, 3, req)
+  mount "/test" -> Test
+  
   @HelloWorld(1, 2, 3, req)
   "/test-deco":
     return "Hello, world!"