Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Improved pattern parsing. Added better halt().

  • Loading branch information...
commit 275837f0606e203f5207473402127078ec4ab4b2 1 parent b706c75
Dominik Picheta authored May 04, 2012

Showing 2 changed files with 191 additions and 104 deletions. Show diff stats Hide diff stats

  1. 103  jester.nim
  2. 192  patterns.nim
103  jester.nim
... ...
@@ -1,16 +1,21 @@
1  
-import httpserver, sockets, strtabs, re, htmlgen, tables, parseutils
  1
+import httpserver, sockets, strtabs, re, htmlgen, tables, parseutils, os
2 2
 
3 3
 import patterns, errorpages
4 4
 
5 5
 from cgi import decodeData, ECgi
6 6
 
7 7
 type
8  
-  TCallbackRet = tuple[code: THttpCode, headers: PStringTable, content: string]
  8
+  TCallbackRet = tuple[action: TCallbackAction, code: THttpCode, 
  9
+                       headers: PStringTable, content: string]
9 10
   TCallback = proc (request: TRequest): TCallbackRet
10 11
 
11 12
   TJester = object
12 13
     s: TServer
13 14
     routes*: seq[tuple[m: PMatch, c: TCallback]]
  15
+    options: TOptions
  16
+
  17
+  TOptions = object
  18
+    staticDir: string # By default ./public
14 19
 
15 20
   TRegexMatch = tuple[compiled: TRegex, original: string]
16 21
   
@@ -35,10 +40,17 @@ type
35 40
     Http404 = "404 Not Found",
36 41
     Http502 = "502 Bad Gateway"
37 42
 
  43
+  TCallbackAction = enum
  44
+    TCActionSend, TCActionPass, TCActionHalt
  45
+
38 46
 const jesterVer = "0.1.0"
39 47
 
  48
+proc initOptions(j: var TJester) =
  49
+  j.options.staticDir = getAppDir() / "public"
  50
+
40 51
 var j: TJester
41 52
 j.routes = @[]
  53
+j.initOptions()
42 54
 
43 55
 when not defined(writeStatusContent):
44 56
   proc writeStatusContent(c: TSocket, status, content: string, headers: PStringTable) =
@@ -61,8 +73,18 @@ proc handleHTTPRequest(client: TSocket, path, query: string) =
61 73
     echo("[Warning] Incorrect query. Got: ", query)
62 74
 
63 75
   template routeReq(): stmt =
64  
-    let (code, headers, content) = route.c(req)
65  
-    client.writeStatusContent($code, content, headers)
  76
+    let (action, code, headers, content) = route.c(req)
  77
+    case action
  78
+    of TCActionSend:
  79
+      client.writeStatusContent($code, content, headers)
  80
+      matched = true
  81
+      break
  82
+    of TCActionPass:
  83
+      matched = false
  84
+    of TCActionHalt:
  85
+      matched = true
  86
+      client.writeStatusContent($code, content, headers)
  87
+      break
66 88
 
67 89
   var matched = false
68 90
   var req: TRequest
@@ -74,8 +96,7 @@ proc handleHTTPRequest(client: TSocket, path, query: string) =
74 96
       if path =~ route.m.regexMatch.compiled:
75 97
         req.matches = matches
76 98
         routeReq()
77  
-        matched = true
78  
-        break
  99
+
79 100
     of MSpecial:
80 101
       let (match, params) = route.m.pattern.match(path)
81 102
       #echo(path, " =@ ", route.m.pattern, " | ", match, " ", params)
@@ -83,11 +104,18 @@ proc handleHTTPRequest(client: TSocket, path, query: string) =
83 104
         for key, val in params:
84 105
           req.params[key] = val
85 106
         routeReq()
86  
-        matched = true
87  
-        break
  107
+
88 108
   if not matched:
89  
-    client.writeStatusContent($Http404, error($Http404, jesterVer), 
90  
-                              {"Content-type": "text/html"}.newStringTable)
  109
+    # Find static file.
  110
+    # TODO: Caching.
  111
+    if existsFile(j.options.staticDir / path):
  112
+      var file = readFile(j.options.staticDir / path)
  113
+      # TODO: Mimetypes
  114
+      client.writeStatusContent($Http200, file, 
  115
+                               {"Content-type": "text/plain"}.newStringTable)
  116
+    else:
  117
+      client.writeStatusContent($Http404, error($Http404, jesterVer), 
  118
+                                {"Content-type": "text/html"}.newStringTable)
91 119
   
92 120
   client.close()
93 121
   
@@ -103,8 +131,8 @@ proc regex*(s: string, flags = {reExtended, reStudy}): TRegexMatch =
103 131
   result = (re(s, flags), s)
104 132
 
105 133
 template setDefaultResp(): stmt =
106  
-  bind error, jesterVer
107  
-  result = (Http502, {"Content-Type": "text/html"}.newStringTable, 
  134
+  bind error, jesterVer, TCActionSend
  135
+  result = (TCActionSend, Http502, {"Content-Type": "text/html"}.newStringTable, 
108 136
             error($Http502, jesterVer))
109 137
 
110 138
 template get*(path: string, body: stmt): stmt =
@@ -120,29 +148,64 @@ template get*(path: string, body: stmt): stmt =
120 148
                             setDefaultResp()
121 149
                             body)))
122 150
 
123  
-template getRe*(path: TRegexMatch, body: stmt): stmt =
  151
+template getRe*(rePath: TRegexMatch, body: stmt): stmt =
124 152
   block:
125 153
     bind j, PMatch, TRequest, TCallbackRet, setDefaultResp
126 154
     var match: PMatch
127 155
     new(match)
128 156
     match.typ = MRegex
129  
-    match.regexMatch = path
  157
+    match.regexMatch = rePath
130 158
     j.routes.add((match, (proc (request: TRequest): TCallbackRet =
131 159
                             setDefaultResp()
132 160
                             body)))
133 161
 
134  
-template resp*(v: tuple[code: THttpCode, 
135  
-                       headers: openarray[tuple[key, value: string]],
136  
-                       content: string]): stmt =
137  
-  return (v[0], v[1].newStringTable, v[2])
  162
+template resp*(code: THttpCode, 
  163
+               headers: openarray[tuple[key, value: string]],
  164
+               content: string): stmt =
  165
+  return (TCActionSend, v[0], v[1].newStringTable, v[2])
138 166
 
139 167
 template resp*(content: string): stmt =
140 168
   ## Responds 
141  
-  return (Http200, {"Content-Type": "text/html"}.newStringTable, content)
  169
+  bind TCActionSend
  170
+  return (TCActionSend, Http200,
  171
+          {"Content-Type": "text/html"}.newStringTable, content)
  172
+
  173
+template pass*(): stmt =
  174
+  bind TCActionPass
  175
+  return (TCActionPass, Http404, nil, "")
  176
+
  177
+template halt*(code: THttpCode,
  178
+               headers: openarray[tuple[key, value: string]],
  179
+               content: string): stmt =
  180
+  bind TCActionHalt
  181
+  return (TCActionHalt, code, headers.newStringTable, content)
  182
+
  183
+template halt*(): stmt =
  184
+  bind error, jesterVer
  185
+  halt(Http404, {:}, error($Http404, jesterVer))
  186
+
  187
+template halt*(code: THttpCode): stmt = 
  188
+  bind error, jesterVer
  189
+  halt(code, {:}, error($code, jesterVer))
  190
+
  191
+template halt*(content: string): stmt =
  192
+  halt(Http404, {:}, content)
  193
+
  194
+template halt*(code: THttpCode, content: string): stmt =
  195
+  halt(code, {:}, content)
142 196
 
143 197
 template `@`*(s: string): expr =
144 198
   ## Retrieves the parameter ``s`` from ``request.params``. ``""`` will be
145 199
   ## returned if parameter doesn't exist.
146 200
   request.params[s]
147 201
   
148  
-  
  202
+proc `staticDir=`*(dir: string) =
  203
+  ## Sets the directory in which Jester will look for static files. It is
  204
+  ## ``./public`` by default.
  205
+  ##
  206
+  ## The files will be served like so:
  207
+  ## 
  208
+  ## ./public/css/style.css -> http://example.com/css/style.css
  209
+  ## 
  210
+  ## (``./public`` is not included in the final URL)
  211
+  j.options.staticDir = dir
192  patterns.nim
... ...
@@ -1,98 +1,117 @@
1  
-import tables, parseutils, strtabs
  1
+import parseutils, strtabs
2 2
 type
3  
-  TSPatternType = enum
4  
-    TSPNamed, TSPNamedOptional, TSPOptionalChar
5  
-  TPattern* = object
6  
-    original: string
7  
-    filtered: string ## No @whatever
8  
-    fields: TTable[int, seq[tuple[name: string, typ: TSPatternType]]]
9  
-    required: int ## Number of TSPNamed
10  
-
11  
-proc `$`*(p: TPattern): string = return p.original
  3
+  TNodeType = enum
  4
+    TNodeText, TNodeField
  5
+  TNode = object
  6
+    typ: TNodeType
  7
+    text: string
  8
+    optional: bool
  9
+  
  10
+  TPattern* = seq[TNode]
12 11
 
  12
+#/show/@id/?
13 13
 proc parsePattern*(pattern: string): TPattern =
14  
-  template addKey(key, value: expr): stmt =
15  
-    if not result.fields.hasKey(key):
16  
-      result.fields.add(key, @[value])
17  
-    else:
18  
-      result.fields.mget(key).add(value)
19  
-  result.required = 0
20  
-  result.original = pattern
21  
-  result.filtered = ""
22  
-  result.fields = initTable[int, seq[tuple[name: string, typ: TSPatternType]]]()
  14
+  result = @[]
  15
+  template addNode(result: var TPattern, theT: TNodeType, theText: string,
  16
+                   isOptional: bool): stmt =
  17
+    block:
  18
+      var newNode: TNode
  19
+      newNode.typ = theT
  20
+      newNode.text = theText
  21
+      newNode.optional = isOptional
  22
+      result.add(newNode)
  23
+  
23 24
   var i = 0
24  
-  while pattern.len() > i:
  25
+  var text = ""
  26
+  while i < pattern.len():
25 27
     case pattern[i]
26  
-    of '\\':
27  
-      if i+1 <= pattern.len-1 and pattern[i+1] in {'@', '?', '\\'}:
28  
-        result.filtered.add(pattern[i+1])
29  
-        inc(i, 2) # Skip \ and whatever the character is after.
30  
-      else:
31  
-        result.filtered.add('\\')
32  
-        inc(i) # Skip \
33  
-    of '?':
34  
-      let c = result.filtered[result.filtered.len()-1]
35  
-      result.filtered.setLen(result.filtered.len()-1) # Truncate string.
36  
-      addKey(result.filtered.len, ($c, TSPOptionalChar))
37  
-      inc(i) # Skip ?
38 28
     of '@':
  29
+      # Add the stored text.
  30
+      if text != "":
  31
+        result.addNode(TNodeText, text, false)
  32
+        text = ""
  33
+      # Parse named parameter.
39 34
       inc(i) # Skip @
40  
-      var fvar = ""
41  
-      i += pattern.parseUntil(fvar, {'/', '?'}, i)
  35
+      var nparam = ""
  36
+      i += pattern.parseUntil(nparam, {'/', '?'}, i)
42 37
       var optional = pattern[i] == '?'
43  
-      if pattern[i] == '?': inc(i) # Skip the ?
44  
-      # Don't skip /, let it be added to filtered.
45  
-      addKey(result.filtered.len, 
46  
-          (fvar, if optional: TSPNamedOptional else: TSPNamed))
47  
-      if not optional: result.required.inc()
  38
+      result.addNode(TNodeField, nparam, optional)
  39
+      if pattern[i] == '?': inc(i) # Only skip ?. / should not be skipped.
  40
+    of '?':
  41
+      var optionalChar = text[text.len-1]
  42
+      setLen(text, text.len-1) # Truncate ``text``.
  43
+      # Add the stored text.
  44
+      if text != "":
  45
+        result.addNode(TNodeText, text, false)
  46
+        text = ""
  47
+      # Add optional char.
  48
+      inc(i) # Skip ?
  49
+      result.addNode(TNodeText, $optionalChar, true)
  50
+    of '\\':
  51
+      inc i # Skip \
  52
+      if pattern[i] notin {'?', '@', '\\'}:
  53
+        raise newException(EInvalidValue, 
  54
+                "This character does not require escaping: " & pattern[i])
  55
+      text.add(pattern[i])
  56
+      inc i # Skip ``pattern[i]``
  57
+      
  58
+      
  59
+      
48 60
     else:
49  
-      result.filtered.add(pattern[i])
  61
+      text.add(pattern[i])
50 62
       inc(i)
  63
+  
  64
+  if text != "":
  65
+    result.addNode(TNodeText, text, false)
  66
+
  67
+proc findNextText(pattern: TPattern, i: int, toNode: var TNode): bool =
  68
+  ## Finds the next TNodeText in the pattern, starts looking from ``i``.
  69
+  result = false
  70
+  for n in i..pattern.len()-1:
  71
+    if pattern[n].typ == TNodeText:
  72
+      toNode = pattern[n]
  73
+      return true
  74
+
  75
+proc check(n: TNode, s: string, i: int): bool =
  76
+  let cutTo = (n.text.len-1)+i
  77
+  if cutTo > s.len-1: return false
  78
+  return s.substr(i, cutTo) == n.text
51 79
 
52 80
 proc match*(pattern: TPattern, s: string): tuple[matched: bool, params: PStringTable] =
53  
-  result.params = {:}.newStringTable()
  81
+  var i = 0 # Location in ``s``.
  82
+
54 83
   result.matched = true
55  
-  var i = 0
56  
-  var fi = 0 # Filtered counter
57  
-  var requiredDone = 0
58  
-  var fieldsToDo = pattern.fields
59  
-  
60  
-  while true:
61  
-    if s.len() <= i:
62  
-      # Check to see if there are any more TSPNamed
63  
-      assert(not (requiredDone > pattern.required))
64  
-      if requiredDone < pattern.required:
65  
-        result.matched = false
66  
-      
67  
-      break
  84
+  result.params = {:}.newStringTable()
68 85
   
69  
-    if fieldsToDo.hasKey(fi):
70  
-      for field in fieldsToDo[fi]:
71  
-        let (name, typ) = field
72  
-        case typ
73  
-        of TSPNamed, TSPNamedOptional:
74  
-          var stopChar = '/' # The char to stop consuming at
75  
-          if pattern.filtered.len-1 >= fi:
76  
-            stopChar = pattern.filtered[fi]
77  
-
78  
-          var matchNamed = ""
79  
-          i += s.parseUntil(matchNamed, stopChar, i)
80  
-          result.params[name] = matchNamed
81  
-          if typ == TSPNamed: requiredDone.inc()
82  
-
83  
-        of TSPOptionalChar:
84  
-          if s[i] == name[0]:
85  
-            inc(i) # Skip this optional char.
86  
-      
87  
-      fieldsToDo.del(fi)
88  
-    else:
89  
-      if not (fi <= pattern.filtered.len()-1 and pattern.filtered[fi] == s[i]):
  86
+  for ncount, node in pattern:
  87
+    case node.typ
  88
+    of TNodeText:
  89
+      if node.optional:
  90
+        if check(node, s, i):
  91
+          inc(i, node.text.len) # Skip over this optional character.
  92
+        else:
  93
+          # If it's not there, we have nothing to do. It's optional after all.
  94
+      else:
  95
+        if check(node, s, i):
  96
+          inc(i, node.text.len) # Skip over this
  97
+        else:
  98
+          # No match.
  99
+          result.matched = false
  100
+          return
  101
+    of TNodeField:
  102
+      var nextTxtNode: TNode
  103
+      var stopChar = '/'
  104
+      if findNextText(pattern, ncount, nextTxtNode):
  105
+        stopChar = nextTxtNode.text[0]
  106
+      var matchNamed = ""
  107
+      i += s.parseUntil(matchNamed, stopChar, i)
  108
+      if matchNamed != "":
  109
+        result.params[node.text] = matchNamed
  110
+      elif matchNamed == "" and not node.optional:
90 111
         result.matched = false
91 112
         return
92  
-      inc(i)
93  
-      inc(fi)
94 113
 
95  
-  if pattern.filtered.len != fi:
  114
+  if s.len != i:
96 115
     result.matched = false
97 116
 
98 117
 when isMainModule:
@@ -100,12 +119,17 @@ when isMainModule:
100 119
   doAssert match(f, "/show/12/test/hallo/").matched
101 120
   doAssert match(f, "/show/2131726/test/jjjuuwąąss").matched
102 121
   doAssert(not match(f, "/").matched)
103  
-  doAssert(match(f, "/show//test//").matched)
  122
+  doAssert(not match(f, "/show//test//").matched)
  123
+  doAssert(match(f, "/show/asd/test//").matched)
104 124
   doAssert(not match(f, "/show/asd/asd/test/jjj/").matched)
105 125
   doAssert(match(f, "/show/@łę¶ŧ←/test/asd/").params["id"] == "@łę¶ŧ←")
106 126
   
107  
-  echo(f.original)
108  
-  echo(f.filtered)
109  
-  echo(f.fields)
110  
-  let m = match(f, "/show/12/test/hallo/")
111  
-  echo(m)
  127
+  let f2 = parsePattern("/test42/somefile.?@ext?/?")
  128
+  doAssert(match(f2, "/test42/somefile/").params["ext"] == "")
  129
+  doAssert(match(f2, "/test42/somefile.txt").params["ext"] == "txt")
  130
+  doAssert(match(f2, "/test42/somefile.txt/").params["ext"] == "txt")
  131
+  
  132
+  let f3 = parsePattern(r"/test32/\@\\\??")
  133
+  doAssert(match(f3, r"/test32/@\").matched)
  134
+  doAssert(not match(f3, r"/test32/@\\").matched)
  135
+  doAssert(match(f3, r"/test32/@\?").matched)

0 notes on commit 275837f

Please sign in to comment.
Something went wrong with that request. Please try again.