├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── config.nims ├── nimboost.nimble ├── src └── boost │ ├── data │ ├── impl │ │ └── nodeiterator.nim │ ├── memory.nim │ ├── props.nim │ ├── rbtree.nim │ ├── rbtreem.nim │ └── stackm.nim │ ├── formatters.nim │ ├── http │ ├── asyncchunkedstream.nim │ ├── asynchttpserver.nim │ ├── httpcommon.nim │ ├── impl │ │ ├── errorpages.nim │ │ ├── patterns.nim │ │ └── utils.nim │ ├── jester.nim │ └── multipart.nim │ ├── io │ └── asyncstreams.nim │ ├── jsonserialize.nim │ ├── limits.nim │ ├── parsers.nim │ ├── richstring.nim │ ├── typeclasses.nim │ ├── types.nim │ └── typeutils.nim ├── tests └── boost │ ├── data │ ├── test_memory.nim │ ├── test_props.nim │ ├── test_rbtree.nim │ ├── test_rbtreem.nim │ └── test_stackm.nim │ ├── http │ ├── impl │ │ └── run_test_server.nim │ ├── test_asyncchunkedstream.nim │ ├── test_asynchttpserver.nim │ ├── test_asynchttpserver_internals.nim │ ├── test_httpcommon.nim │ ├── test_jester.nim │ └── test_multipart.nim │ ├── io │ └── test_asyncstreams.nim │ ├── test_all.nim │ ├── test_formatters.nim │ ├── test_limits.nim │ ├── test_parsers.nim │ ├── test_richstring.nim │ ├── test_typeclasses.nim │ ├── test_typeutils.nim │ └── test_typeutils_int.nim └── utils └── index_html.nim /.gitignore: -------------------------------------------------------------------------------- 1 | nimcache/ 2 | build 3 | bin 4 | docs 5 | nimsuggest.log 6 | .#*.* 7 | index.html -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: c 2 | 3 | env: 4 | # Nim versions to test against 5 | - CHOOSENIM_CHOOSE_VERSION=devel 6 | - CHOOSENIM_CHOOSE_VERSION=0.19.4 7 | - CHOOSENIM_CHOOSE_VERSION=0.19.0 8 | 9 | install: 10 | - curl https://nim-lang.org/choosenim/init.sh -sSf | sh -s -- -y 11 | - export PATH=~/.nimble/bin:$PATH 12 | - nimble update 13 | - nimble install -d -y 14 | 15 | before_script: 16 | - set -e 17 | - export PATH=~/.nimble/bin:$PATH 18 | - export CHOOSENIM_NO_ANALYTICS=1 19 | script: 20 | - nim test 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Anatoly Galiulin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nimboost [![nimble](https://raw.githubusercontent.com/yglukhov/nimble-tag/master/nimble.png)](https://github.com/yglukhov/nimble-tag) 2 | 3 | [![Build Status](https://travis-ci.org/vegansk/nimboost.svg?branch=master)](https://travis-ci.org/vegansk/nimboost) 4 | 5 | Additions to the Nim's standard library, like boost for C++ :-). The documentation can be found [here](http://vegansk.github.io/nimboost/). 6 | -------------------------------------------------------------------------------- /config.nims: -------------------------------------------------------------------------------- 1 | srcdir = "src" 2 | 3 | import ospaths, strutils, sequtils 4 | 5 | type Target {.pure.} = enum JS, C 6 | 7 | template dep(task: untyped): untyped = 8 | selfExec astToStr(task) 9 | 10 | template deps(a, b: untyped): untyped = 11 | dep(a) 12 | dep(b) 13 | 14 | proc buildBase(debug: bool, bin: string, src: string, target: Target) = 15 | let baseBinPath = thisDir() / bin 16 | case target 17 | of Target.C: 18 | switch("out", baseBinPath.toExe) 19 | of Target.JS: 20 | switch("out", baseBinPath & ".js") 21 | 22 | --nimcache: build 23 | if not debug: 24 | --forceBuild 25 | --define: release 26 | --opt: size 27 | else: 28 | --define: debug 29 | # --reportconceptfailures: on 30 | # --define: exportPrivate 31 | --debuginfo 32 | --debugger: native 33 | --linedir: on 34 | --stacktrace: on 35 | --linetrace: on 36 | --verbosity: 1 37 | 38 | --path: src 39 | --path: srcdir 40 | 41 | case target 42 | of Target.C: 43 | --threads: on 44 | setCommand "c", src 45 | of Target.JS: 46 | switch("d", "nodeJs") 47 | setCommand "js", src 48 | 49 | proc test(name: string, target: Target) = 50 | if not dirExists "bin": 51 | mkDir "bin" 52 | --run 53 | --define:insideTheTest 54 | --define:testing 55 | let fName = name.splitPath[1] 56 | buildBase true, joinPath("bin", fName), joinPath("tests/boost", name), target 57 | 58 | proc getVersion: string = 59 | const PREFIX = "version: \"" 60 | for line in (staticExec "nimble dump").splitLines: 61 | if line.startsWith(PREFIX): 62 | return line[PREFIX.len..^2] 63 | quit "Can't read version from project's configuration" 64 | 65 | task version, "Show project version": 66 | echo getVersion() 67 | 68 | task test_c, "Run all tests (C)": 69 | test "test_all", Target.C 70 | 71 | task test_js, "Run all tests (JS)": 72 | test "test_all", Target.JS 73 | 74 | task test, "Run all tests": 75 | deps test_c, test_js 76 | setCommand "nop" 77 | 78 | import ./utils/index_html 79 | 80 | proc listFilesRec(dir: string): seq[string] = 81 | result = newSeq[string]() 82 | withDir dir: 83 | result.add(".".listFiles.mapIt(it.splitPath[1])) 84 | for d in ".".listDirs: 85 | if d == "impl": continue 86 | result.add(listFilesRec(d).mapIt(d.splitPath[1] / it)) 87 | 88 | proc genIndex: auto = 89 | result = newSeq[(string, seq[string])]() 90 | withDir "docs": 91 | for d in ".".listDirs: 92 | result.add((d.splitPath[1], listFilesRec(d))) 93 | echo result 94 | 95 | task docs, "Build documentation": 96 | mkDir "docs" / getVersion() 97 | const modules = [ 98 | "limits.nim", 99 | "parsers.nim", 100 | "typeutils.nim", 101 | "typeclasses.nim", 102 | "types.nim", 103 | "formatters.nim", 104 | "richstring.nim", 105 | 106 | "data/props.nim", 107 | "data/rbtree.nim", 108 | "data/rbtreem.nim", 109 | "data/stackm.nim", 110 | 111 | "io/asyncstreams.nim", 112 | 113 | "http/asynchttpserver.nim", 114 | "http/jester.nim", 115 | "http/httpcommon.nim", 116 | "http/multipart.nim", 117 | ] 118 | 119 | for m in modules: 120 | let (d, f, _) = m.splitFile 121 | let dir = "docs" / getVersion() / "boost" / d 122 | if not dirExists(dir): 123 | mkDir(dir) 124 | exec "nim doc --out:" & joinPath(dir, f & ".html") & " " & joinPath("src", "boost", m) 125 | 126 | writeFile "index.html", index_html(genIndex()) 127 | 128 | task test_asynchttpserver, "Test asynchttpserver": 129 | test "http/test_asynchttpserver", Target.C 130 | 131 | task test_asynchttpserver_internals, "Test asynchttpserver (internals)": 132 | test "http/test_asynchttpserver_internals", Target.C 133 | 134 | task test_jester, "Test jester": 135 | test "http/test_jester", Target.C 136 | 137 | task test_httpcommon, "Test http common utils": 138 | test "http/test_httpcommon", Target.C 139 | 140 | task test_props, "Test props": 141 | test "data/test_props", Target.C 142 | 143 | task test_asyncstreams, "Test asyncstreams": 144 | test "io/test_asyncstreams", Target.C 145 | 146 | task test_asyncstreams_no_net, "Test asyncstreams without networking": 147 | --d: noNet 148 | test "io/test_asyncstreams", Target.C 149 | 150 | task test_asyncchunkedstream, "Test AsyncChunkedStream": 151 | test "http/test_asyncchunkedstream", Target.C 152 | 153 | task test_multipart, "Test multipart": 154 | test "http/test_multipart", Target.C 155 | 156 | task test_memory, "Test memory": 157 | test "data/test_memory", Target.C 158 | 159 | task test_richstring, "Test richstring": 160 | test "test_richstring", Target.C 161 | 162 | task test_parsers, "Test richstring": 163 | test "test_parsers", Target.C 164 | 165 | task test_limits, "Test richstring": 166 | test "test_limits", Target.C 167 | 168 | task test_formatters, "Test formatters": 169 | test "test_formatters", Target.C 170 | 171 | task test_typeutils, "Test typeutils": 172 | test "test_typeutils", Target.C 173 | -------------------------------------------------------------------------------- /nimboost.nimble: -------------------------------------------------------------------------------- 1 | # [Package] 2 | version = "0.5.6" 3 | author = "Anatoly Galiulin " 4 | description = "Additions to the Nim's standard library, like boost for C++" 5 | license = "MIT" 6 | 7 | srcDir = "src" 8 | 9 | # [Deps] 10 | requires "nim >= 0.19.0", "patty >= 0.3.3" 11 | -------------------------------------------------------------------------------- /src/boost/data/impl/nodeiterator.nim: -------------------------------------------------------------------------------- 1 | # Immitate closure iterator (see https://github.com/nim-lang/Nim/issues/4695) 2 | type 3 | NodeIterator[K,V] = ref object 4 | stack: StackM[Node[K,V]] 5 | 6 | when compiles(RBTree): 7 | type Tree[K,V] = RBTree[K,V] 8 | else: 9 | type Tree[K,V] = RBTreeM[K,V] 10 | 11 | proc newNodeIterator[K,V](t: Tree[K,V]): NodeIterator[K,V] = 12 | new result 13 | result.stack = newStackM[Node[K,V]]() 14 | var root = t.root 15 | while root.isBranch: 16 | result.stack.push(root) 17 | root = root.l 18 | proc hasNext(n: NodeIterator): bool = not n.stack.isEmpty 19 | proc nextNode[K,V](n: var NodeIterator[K,V]): Node[K,V] = 20 | assert n.hasNext, "Empty iterator" 21 | result = n.stack.pop 22 | var root = result.r 23 | while root.isBranch: 24 | n.stack.push(root) 25 | root = root.l 26 | 27 | proc equalsImpl[K;V: NonVoid](l, r: Tree[K,V]): bool = 28 | var i1 = newNodeIterator(l) 29 | var i2 = newNodeIterator(r) 30 | result = true 31 | while true: 32 | let f1 = not i1.hasNext 33 | let f2 = not i2.hasNext 34 | if f1 and f2: 35 | break 36 | elif f1 != f2: 37 | return false 38 | let r1 = i1.nextNode 39 | let r2 = i2.nextNode 40 | if r1.k != r2.k or r1.v != r2.v: 41 | return false 42 | 43 | proc equalsImpl[K](l, r: Tree[K,void]): bool = 44 | var i1 = newNodeIterator(l) 45 | var i2 = newNodeIterator(r) 46 | result = true 47 | while true: 48 | let f1 = not i1.hasNext 49 | let f2 = not i2.hasNext 50 | if f1 and f2: 51 | break 52 | elif f1 != f2: 53 | return false 54 | let r1 = i1.nextNode 55 | let r2 = i2.nextNode 56 | if r1.k != r2.k: 57 | return false 58 | -------------------------------------------------------------------------------- /src/boost/data/memory.nim: -------------------------------------------------------------------------------- 1 | ## Low level memory primitives 2 | 3 | proc findInMem*(buf: pointer, size: int, what: pointer, wSize: int): tuple[pos: int, length: int] = 4 | ## Finds ``what`` of size ``wSize`` in ``buf`` of ``size`` and returns the ``position`` 5 | ## of match. If ``buf``'s tail contains only the part of ``what``, then ``length`` will 6 | ## contain the actual size of matched data. 7 | let buff = cast[cstring](buf) 8 | let sBuff = cast[cstring](what) 9 | var i = 0 10 | var si = 0 11 | while i < size and si < wSize: 12 | if buff[i] == sBuff[si]: 13 | inc si 14 | elif si != 0: 15 | i -= si - 1 16 | si = 0 17 | inc i 18 | if si == 0: 19 | result.pos = -1 20 | else: 21 | result.pos = i - si 22 | result.length = si 23 | 24 | -------------------------------------------------------------------------------- /src/boost/data/props.nim: -------------------------------------------------------------------------------- 1 | import strutils, strtabs 2 | 3 | ## The set of properties. Keys are case insensitive. 4 | 5 | type 6 | Prop = object 7 | name: string 8 | value: string 9 | Props* = distinct seq[Prop] 10 | ## Set of properties 11 | 12 | proc prop(n, v: string): Prop = Prop(name: n, value: v) 13 | 14 | iterator items(p: Props): Prop = 15 | for el in ((seq[Prop])p): 16 | yield el 17 | 18 | iterator pairs*(p: Props): (string, string) = 19 | ## Iterates key/value pairs 20 | for el in p: 21 | yield (el.name, el.value) 22 | 23 | proc len*(p: Props): int = 24 | ## Returns the number of properties 25 | ((seq[Prop])p).len 26 | 27 | proc getMVar(p: var Props, idx: int): ptr Prop = 28 | addr(((seq[Prop]p))[idx]) 29 | 30 | proc `[]`*(p: Props, name: string): string = 31 | ## Returns the property named ``name`` or empty string. 32 | ## If you need to know, if the property was set, use ``contains``. 33 | for n, v in p: 34 | if n.cmpIgnoreCase(name) == 0: 35 | return v 36 | return "" 37 | 38 | proc add*(p: var Props; n, v: string, overwrite = false, delimiter = ",") = 39 | ## Sets the property named ``n`` to the value ``v``. If the property was already set, 40 | ## then overwrite it's value if ``overwrite`` == `true` or append it using ``delimiter`` 41 | ## to the previous value. 42 | for idx in 0.. t.k: 115 | result = balance(t.l, t.k, t.value, ins(t.r, ok)) 116 | else: 117 | ok = true 118 | result = newNode(BLACK, t.l, k, v, t.r) 119 | else: 120 | if k < t.k: 121 | result = newNode(RED, ins(t.l, ok), t.k, t.value, t.r) 122 | elif k > t.k: 123 | result = newNode(RED, t.l, t.k, t.value, ins(t.r, ok)) 124 | else: 125 | ok = true 126 | result = newNode(RED, t.l, k, v, t.r) 127 | let res = ins(t, ok) 128 | if ok: 129 | result = (newNode(BLACK, res.l, res.k, res.value, res.r), ok) 130 | else: 131 | result = (t, ok) 132 | 133 | proc add*[K;V: NonVoid](t: RBTree[K,V], k: K, v: V): RBTree[K,V] = 134 | ## Returns the new tree from ``t`` where key ``k`` is set to value ``v``. 135 | let res = add(t.root, k, v) 136 | if not res[1]: 137 | result = t 138 | else: 139 | result = newRBTree(res[0], t.length + 1) 140 | 141 | proc add*[K](t: RBTree[K,void], k: K): RBTree[K,void] = 142 | ## Returns the new set from ``t`` where ``k`` is present. 143 | # RBTree[K,Unit] and RBTree[K,void] have the same 144 | # memory layout. So we use cast here 145 | cast[RBTree[K,void]](add(cast[RBTree[K,Unit]](t), k, ())) 146 | 147 | proc mkRBTree*[K;V: NonVoid](arr: openarray[(K,V)]): RBTree[K,V] = 148 | ## Creates new tree from the key/value pairs ``arr``. 149 | result = newRBTree[K,V]() 150 | for el in arr: 151 | result = result.add(el[0], el[1]) 152 | 153 | proc mkRBSet*[K](arr: openarray[K]): RBTree[K,void] = 154 | ## Creates new set from the values ``arr``. 155 | result = newRBSet[K]() 156 | for el in arr: 157 | result = result.add(el) 158 | 159 | proc findNode[K,V](t: Node[K,V], k: K): Node[K,V] = 160 | if t.isLeaf or t.k == k: 161 | t 162 | elif t.k < k: 163 | t.r.findNode(k) 164 | else: 165 | t.l.findNode(k) 166 | 167 | proc hasKey[K,V](t: Node[K,V], k: K): bool = 168 | not t.findNode(k).isLeaf 169 | 170 | proc hasKey*[K,V](t: RBTree[K,V], k: K): bool = 171 | ## Checks if the tree ``t`` has the key ``k``. 172 | t.root.hasKey(k) 173 | 174 | proc getOrDefault[K;V: NonVoid](t: Node[K,V], k: K): V = 175 | let res = t.findNode(k) 176 | if res.isBranch: 177 | result = res.value 178 | 179 | proc getOrDefault*[K;V: NonVoid](t: RBTree[K,V], k: K): V = 180 | ## Returns the value associated with the key ``k`` or `V()`. 181 | t.root.getOrDefault(k) 182 | 183 | proc maybeGet*[K;V: NonVoid](t: RBTree[K,V], k: K, v: var V): bool = 184 | ## Puts the value associated with the key ``k`` into ``v`` and returns 185 | ## `true`. If the key is absent, returns `false`. 186 | let res = t.root.findNode(k) 187 | result = res.isBranch 188 | if result: 189 | v = res.value 190 | 191 | proc blackToRed[K,V](t: Node[K,V]): Node[K,V] = 192 | assert(t.isBlack and t.isBranch, "Invariance violation") 193 | result = newNode(RED, t.l, t.k, t.value, t.r) 194 | 195 | proc balanceLeft[K,V](a: Node[K,V], k: K, v: V, b: Node[K,V]): Node[K,V] = 196 | if a.isRed: 197 | result = newNode(RED, newNode(BLACK, a.l, a.k, a.value, a.r), k, v, b) 198 | elif b.isBlack and b.isBranch: 199 | result = balance(a, k, v, b.blackToRed) 200 | elif b.isRed and b.l.isBlack and b.l.isBranch: 201 | result = newNode(RED, newNode(BLACK, a, k, v, b.l.l), b.l.k, b.l.value, balance(b.l.r, b.k, b.value, b.r.blackToRed)) 202 | else: 203 | doAssert false 204 | new result # Disable ProveInit warning 205 | 206 | proc balanceRight[K,V](a: Node[K,V], k: K, v: V, b: Node[K,V]): Node[K,V] = 207 | if b.isRed: 208 | result = newNode(RED, a, k, v, newNode(BLACK, b.l, b.k, b.value, b.r)) 209 | elif a.isBlack and a.isBranch: 210 | result = balance(a.blackToRed, k, v, b) 211 | elif a.isRed and a.r.isBlack and a.r.isBranch: 212 | result = newNode(RED, balance(a.l.blackToRed, a.k, a.value, a.r.l), a.r.k, a.r.value, newNode(BLACK, a.r.r, k, v, b)) 213 | else: 214 | doAssert false 215 | new result # Disable ProveInit warning 216 | 217 | proc app[K,V](a, b: Node[K,V]): Node[K,V] = 218 | if a.isEmpty: 219 | result = b 220 | elif b.isEmpty: 221 | result = a 222 | elif a.isRed and b.isRed: 223 | let ar = a.r.app(b.l) 224 | if ar.isRed: 225 | result = newNode(RED, newNode(RED, a.l, a.k, a.value, ar.l), ar.k, ar.value, newNode(RED, ar.r, b.k, b.value, b.r)) 226 | else: 227 | result = newNode(RED, a.l, a.k, a.value, newNode(RED, ar, b.k, b.value, b.r)) 228 | elif a.isBlack and b.isBlack: 229 | let ar = a.r.app(b.l) 230 | if ar.isRed: 231 | result = newNode(RED, newNode(BLACK, a.l, a.k, a.value, ar.l), ar.k, ar.value, newNode(BLACK, ar.r, b.k, b.value, b.r)) 232 | else: 233 | result = balanceLeft(a.l, a.k, a.value, newNode(BLACK, ar, b.k, b.value, b.r)) 234 | elif b.isRed: 235 | result = newNode(RED, app(a, b.l), b.k, b.value, b.r) 236 | elif a.isRed: 237 | result = newNode(RED, a.l, a.k, a.value, app(a.r, b)) 238 | else: 239 | doAssert false 240 | new result # Disable ProveInit warning 241 | 242 | proc del[K;V: NonVoid](t: Node[K,V], k: K): (Node[K,V], bool) = 243 | var ok = false 244 | proc del(t: Node[K,V], ok: var bool): Node[K,V] 245 | proc delformLeft(a: Node[K,V], k: K, v: V, b: Node[K,V]): Node[K,V] = 246 | if a.isBlack and a.isBranch: 247 | balanceLeft(del(a, ok), k, v, b) 248 | else: 249 | newNode(RED, del(a, ok), k, v, b) 250 | proc delformRight(a: Node[K,V], k: K, v: V, b: Node[K,V]): Node[K,V] = 251 | if b.isBlack and b.isBranch: 252 | balanceRight(a, k, v, del(b, ok)) 253 | else: 254 | newNode(RED, a, k, v, del(b, ok)) 255 | proc del(t: Node[K,V], ok: var bool): Node[K,V] = 256 | if t.isEmpty: 257 | result = t 258 | elif k < t.k: 259 | result = delformLeft(t.l, t.k, t.value, t.r) 260 | elif k > t.k: 261 | result = delformRight(t.l, t.k, t.value, t.r) 262 | else: 263 | ok = true 264 | result = t.l.app(t.r) 265 | 266 | let res = del(t, ok) 267 | result = ((if res.isLeaf: res else: newNode(BLACK, res.l, res.k, res.value, res.r)), ok) 268 | 269 | proc del*[K;V: NonVoid](t: RBTree[K,V], k: K): RBTree[K,V] = 270 | ## Returns the new tree from ``t`` where the key ``k`` is unset. 271 | let res = del(t.root, k) 272 | if res[1]: 273 | result = newRBtree(res[0], t.length - 1) 274 | else: 275 | result = t 276 | 277 | proc del*[K](t: RBTree[K,void], k: K): RBTree[K,void] = 278 | ## Returns the new tree from ``t`` where the key ``k`` is unset. 279 | # Same assumptions as in void version of `add` 280 | cast[RBTree[K,void]](del(cast[RBTree[K,Unit]](t), k)) 281 | 282 | template iterateNode(n: typed, next: untyped, op: untyped): untyped = 283 | var s = newStackM[type(n)]() 284 | var root = n 285 | while root.isBranch: 286 | push(s, root) 287 | root = root.l 288 | while not s.isEmpty: 289 | var next = s.pop 290 | root = next.r 291 | while root.isBranch: 292 | push(s, root) 293 | root = root.l 294 | `op` 295 | 296 | include impl/nodeiterator 297 | 298 | iterator items*[K,V](t: RBTree[K,V]): K = 299 | ## Iterates over the keys of the tree ``t``. 300 | iterateNode(t.root, next): 301 | yield next.k 302 | 303 | iterator keys*(t: RBTree): auto = 304 | ## Iterates over the keys of the tree ``t``. 305 | iterateNode(t.root, next): 306 | yield next.k 307 | 308 | iterator pairs*[K;V: NonVoid](t: RBTree[K,V]): (K,V) = 309 | ## Iterates over the key/value pairs of the tree ``t``. 310 | iterateNode(t.root, next): 311 | yield (next.k, next.v) 312 | 313 | iterator values*(t: RBTree): auto = 314 | ## Iterates over the values of the tree ``t``. 315 | iterateNode(t.root, next): 316 | yield next.value 317 | 318 | proc equals*[K;V: NonVoid](l, r: RBTree[K,V]): bool = 319 | ## Checks for the equality of ``l`` and ``r``. 320 | equalsImpl(l, r) 321 | 322 | proc `==`*[K;V: NonVoid](l, r: RBTree[K,V]): bool = 323 | ## Checks for the equality of ``l`` and ``r``. 324 | l.equals(r) 325 | 326 | proc equals*[K](l, r: RBTree[K,void]): bool = 327 | ## Checks for the equality of ``l`` and ``r``. 328 | equalsImpl(l, r) 329 | 330 | proc `==`*[K](l, r: RBTree[K,void]): bool = 331 | ## Checks for the equality of ``l`` and ``r``. 332 | l.equals(r) 333 | 334 | #[ 335 | # Pretty print 336 | ]# 337 | 338 | proc mkTab(tab, s: int): string = 339 | if tab == 0: 340 | result = "" 341 | else: 342 | result = newString(tab + s) 343 | for i in 0.. curr.k: 178 | curr = curr.r 179 | else: 180 | # Equal keys 181 | when compiles(pt.v): 182 | curr.v = pt.v 183 | pt = curr 184 | return (root, false) 185 | if pt.k < prev.k: 186 | prev.l = pt 187 | else: 188 | prev.r = pt 189 | pt.p = prev 190 | return (root, true) 191 | 192 | proc rotateLeft(root: var Node, pt: Node) {.inline.} = 193 | var r = pt.r 194 | pt.r = r.l 195 | if not pt.r.isNil: 196 | pt.r.p = pt 197 | r.p = pt.p 198 | if pt.p.isNil: 199 | root = r 200 | elif pt.isLeftChild: 201 | pt.p.l = r 202 | else: 203 | pt.p.r = r 204 | r.l = pt 205 | pt.p = r 206 | 207 | proc rotateRight(root: var Node, pt: Node) {.inline.} = 208 | var l = pt.l 209 | pt.l = l.r 210 | if not pt.l.isNil: 211 | pt.l.p = pt 212 | l.p = pt.p 213 | if pt.p.isNil: 214 | root = l 215 | elif pt.isLeftChild: 216 | pt.p.l = l 217 | else: 218 | pt.p.r = l 219 | l.r = pt 220 | pt.p = l 221 | 222 | proc fixInsert(root, pt: var Node) = 223 | var ppt: type(pt) 224 | var gppt: type(pt) 225 | 226 | while pt != root and pt.c != BLACK and pt.p.c == RED: 227 | ppt = pt.p 228 | gppt = ppt.p 229 | if ppt == gppt.l: 230 | var upt = gppt.r 231 | if not(upt.isNil) and upt.c == RED: 232 | gppt.c = RED 233 | ppt.c = BLACK 234 | upt.c = BLACK 235 | pt = gppt 236 | else: 237 | if pt == ppt.r: 238 | rotateLeft(root, ppt) 239 | pt = ppt 240 | ppt = pt.p 241 | rotateRight(root, gppt) 242 | swap(ppt.c, gppt.c) 243 | pt = ppt 244 | else: 245 | var upt = gppt.l 246 | if not(upt.isNil) and upt.c == RED: 247 | gppt.c = RED 248 | ppt.c = BLACK 249 | upt.c = BLACK 250 | pt = gppt 251 | else: 252 | if pt == ppt.l: 253 | rotateRight(root, ppt) 254 | pt = ppt 255 | ppt = pt.p 256 | rotateLeft(root, gppt) 257 | swap(ppt.c, gppt.c) 258 | pt = ppt 259 | root.c = BLACK 260 | 261 | proc add*[K;V: NonVoid](t: RBTreeM[K,V], k: K, v: V): RBTreeM[K,V] {.discardable.} = 262 | ## Sets the key ``k`` to the value ``v`` in the tree ``t``. Returns ``t``. 263 | var pt = newNode(k, v, RED) 264 | var (newRoot, ok) = bstInsert(t.root, pt) 265 | t.root = newRoot 266 | if ok: 267 | inc t.length 268 | fixInsert(t.root, pt) 269 | result = t 270 | 271 | proc add*[K](t: RBTreeM[K,void], k: K): RBTreeM[K,void] {.discardable.} = 272 | ## Sets the value ``k`` in the set ``t``. Returns ``t``. 273 | var pt = newNode(k, RED) 274 | var (newRoot, ok) = bstInsert(t.root, pt) 275 | t.root = newRoot 276 | if ok: 277 | inc t.length 278 | fixInsert(t.root, pt) 279 | result = t 280 | 281 | proc mkRBTreeM*[K;V: NonVoid](arr: openarray[(K,V)]): RBTreeM[K,V] = 282 | ## Creates new tree from the key/value pairs ``arr``. 283 | result = newRBTreeM[K,V]() 284 | for el in arr: 285 | result.add(el[0], el[1]) 286 | 287 | proc mkRBSetM*[K](arr: openarray[K]): RBTreeM[K,void] = 288 | ## Creates new set from the values ``arr``. 289 | result = newRBSetM[K]() 290 | for el in arr: 291 | result.add(el) 292 | 293 | proc findKey[K,V](n: Node[K,V], k: K): Node[K,V] {.inline.} = 294 | var curr = n 295 | while not(curr.isNil): 296 | if k == curr.k: 297 | return curr 298 | elif k < curr.k: 299 | curr = curr.l 300 | else: 301 | curr = curr.r 302 | return nil 303 | 304 | proc findMin[K,V](n: Node[K,V]): Node[K,V] {.inline.} = 305 | if n.isNil: 306 | return nil 307 | var curr = n 308 | while not(curr.l.isNil): 309 | curr = curr.l 310 | return curr 311 | 312 | proc findMax[K,V](n: Node[K,V]): Node[K,V] {.inline.} = 313 | if n.isNil: 314 | return nil 315 | var curr = n 316 | while not(curr.r.isNil): 317 | curr = curr.r 318 | return curr 319 | 320 | proc hasKey*[K,V](t: RBTreeM[K,V], k: K): bool = 321 | ## Checks if the tree ``t`` has the key ``k``. 322 | t.root.findKey(k) != nil 323 | 324 | proc getOrDefault*[K;V: NonVoid](t: RBTreeM[K,V], k: K): V = 325 | ## Returns the value associated with the key ``k`` or `V()`. 326 | let n = t.root.findKey(k) 327 | if not n.isNil: 328 | result = n.v 329 | 330 | proc maybeGet*[K;V: NonVoid](t: RBTreeM[K,V], k: K, v: var V): bool = 331 | ## Puts the value associated with the key ``k`` into ``v`` and returns 332 | ## `true`. If the key is absent, returns `false`. 333 | let res = t.root.findKey(k) 334 | result = not res.isNil 335 | if result: 336 | v = res.v 337 | 338 | proc min*[K,V](t: RBTreeM[K,V]): K = 339 | ## Finds the minimal key value in ``t``. 340 | let n = t.root.findMin() 341 | assert n != nil 342 | n.k 343 | 344 | proc max*[K,V](t: RBTreeM[K,V]): K = 345 | ## Finds the maximal key value in ``t``. 346 | let n = t.root.findMax() 347 | assert n != nil 348 | n.k 349 | 350 | include impl/nodeiterator 351 | 352 | proc equals*[K;V: NonVoid](l, r: RBTreeM[K,V]): bool = 353 | ## Checks for the equality of ``l`` and ``r``. 354 | equalsImpl(l, r) 355 | 356 | proc `==`*[K;V: NonVoid](l, r: RBTreeM[K,V]): bool = 357 | ## Checks for the equality of ``l`` and ``r``. 358 | l.equals(r) 359 | 360 | proc equals*[K](l, r: RBTreeM[K,void]): bool = 361 | ## Checks for the equality of ``l`` and ``r``. 362 | equalsImpl(l, r) 363 | 364 | proc `==`*[K](l, r: RBTreeM[K,void]): bool = 365 | ## Checks for the equality of ``l`` and ``r``. 366 | l.equals(r) 367 | 368 | proc bstDelete(root, pt: var Node): tuple[root: Node, deleted: Node] {.inline.} = 369 | assert(not pt.isNil) 370 | 371 | let haveLeft = not(pt.l.isNil) 372 | let haveRight = not(pt.r.isNil) 373 | let isRoot = pt == root 374 | 375 | if not(haveLeft) and not(haveRight): 376 | if isRoot: 377 | return (nil, nil) 378 | else: 379 | pt.replace(nil) 380 | return (root, pt) 381 | elif not(haveLeft) or not(haveRight): 382 | var n = if haveLeft: pt.l else: pt.r 383 | if isRoot: 384 | return (n.cleanParent, pt) 385 | else: 386 | pt.replace(n) 387 | return (root, pt) 388 | else: 389 | var n = pt.r.findMin 390 | pt.replace(n) 391 | if isRoot: 392 | return (n, pt) 393 | else: 394 | return (root, pt) 395 | 396 | proc succ(n: Node): Node = 397 | if n.isNil: 398 | return nil 399 | elif not n.right.isNil: 400 | var p = n.right 401 | while not p.left.isNil: 402 | p = p.left 403 | return p 404 | else: 405 | var p = n.parent 406 | var ch = n 407 | while not p.isNil and ch == p.right: 408 | ch = p 409 | p = p.parent 410 | return p 411 | 412 | proc fixDelete(root: var Node, n: Node) {.inline.} = 413 | assert(not root.isNil) 414 | assert(not n.isNil) 415 | 416 | var x = n 417 | 418 | while not x.isNil and x != root and x.color == BLACK: 419 | if x.isLeftChild: 420 | var sib = x.parent.right 421 | if sib.color == RED: 422 | setColor(sib, BLACK) 423 | setColor(x.parent, RED) 424 | rotateLeft(root, x.parent) 425 | sib = x.parent.right 426 | 427 | if sib.left.color == BLACK and sib.right.color == BLACK: 428 | setColor(sib, RED) 429 | x = x.parent 430 | else: 431 | if sib.right.color == BLACK: 432 | setColor(sib.left, BLACK) 433 | setColor(sib, RED) 434 | rotateRight(root, sib) 435 | sib = x.parent.right 436 | setColor(sib, x.parent.color) 437 | setColor(x.parent, BLACK) 438 | setColor(sib.right, BLACK) 439 | rotateLeft(root, x.parent) 440 | x = root 441 | else: 442 | var sib = x.parent.left 443 | if sib.color == RED: 444 | setColor(sib, BLACK) 445 | setColor(x.parent, RED) 446 | rotateRight(root, x.parent) 447 | sib = x.parent.left 448 | 449 | if sib.right.color == BLACK and sib.left.color == BLACK: 450 | setColor(sib, RED) 451 | x = x.parent 452 | else: 453 | if sib.left.color == BLACK: 454 | setColor(sib.right, BLACK) 455 | setColor(sib, RED) 456 | rotateLeft(root, sib) 457 | sib = x.parent.left 458 | setColor(sib, x.parent.color) 459 | setColor(x.parent, BLACK) 460 | setColor(sib.left, BLACK) 461 | rotateRight(root, x.parent) 462 | x = root 463 | 464 | setColor(x, BLACK) 465 | 466 | proc removeNode[K,V](root: var Node[K,V], n: Node[K,V]) = 467 | var p = n 468 | if not p.l.isNil and not p.r.isNil: 469 | var s = p.succ 470 | p.k = s.k 471 | when V isnot void: 472 | p.v = s.v 473 | p = s 474 | var repl = if not p.l.isNil: p.l else: p.r 475 | if not repl.isNil: 476 | repl.p = p.p 477 | if p.p.isNil: 478 | root = repl 479 | elif p == p.p.l: 480 | p.p.l = repl 481 | else: 482 | p.p.r = repl 483 | p.clean 484 | if p.c == BLACK: 485 | fixDelete(root, repl) 486 | elif p.p.isNil: 487 | root = nil 488 | else: 489 | if p.c == BLACK: 490 | fixDelete(root, p) 491 | if not p.p.isNil: 492 | if p.p.l == p: 493 | p.p.l = nil 494 | elif p.p.r == p: 495 | p.p.r = nil 496 | p.p = nil 497 | 498 | proc del*[K,V](t: RBTreeM[K,V], k: K): RBTreeM[K,V] {.discardable.} = 499 | ## Returns the new tree from ``t`` where the key ``k`` is unset. 500 | result = t 501 | var n = t.root.findKey(k) 502 | if n.isNil: 503 | return 504 | removeNode(t.root, n) 505 | dec t.length 506 | 507 | #[ 508 | # Pretty print 509 | ]# 510 | 511 | proc mkTab(tab, s: int): string = 512 | if tab == 0: 513 | result = "" 514 | else: 515 | result = newString(tab + s) 516 | for i in 0.. 0 38 | result = s.stack.v 39 | s.stack = s.stack.p 40 | dec s.length 41 | -------------------------------------------------------------------------------- /src/boost/formatters.nim: -------------------------------------------------------------------------------- 1 | ## Module contains `type` to string formatters for some `types`. 2 | 3 | import strutils 4 | 5 | proc mkDigit(v: int, lowerCase: bool): string {.inline.} = 6 | doAssert(v < 26) 7 | if v < 10: 8 | result = $chr(ord('0') + v) 9 | else: 10 | result = $chr(ord(if lowerCase: 'a' else: 'A') + v - 10) 11 | 12 | proc intToStr*(n: SomeNumber, radix = 10, len = 0, fill = ' ', lowerCase = false): string = 13 | ## Converts ``n`` to string. If ``n`` is `SomeReal`, it casts to `int64`. 14 | ## Conversion is done using ``radix``. If result's length is lesser then 15 | ## ``len``, it aligns result to the right with ``fill`` char. 16 | ## If ``len`` is negative, the result is aligned to the left. 17 | ## If `lowerCase` is true, formatted string will be in the lower case. 18 | when n is SomeUnsignedInt: 19 | var v = n.uint64 20 | let s = false 21 | else: 22 | var v = n.int64 23 | let s = v.int64 < 0 24 | if s: 25 | v = v * -1 26 | 27 | if v == 0: 28 | result = "0" 29 | else: 30 | result = "" 31 | while v > (type(v))0: 32 | let d = v mod (type(v))radix 33 | v = v div (type(v))radix 34 | result.add(mkDigit(d.int, lowerCase)) 35 | for idx in 0..<(result.len div 2): 36 | swap result[idx], result[result.len - idx - 1] 37 | var length = abs(len) 38 | if length == 0 or (s and (result.len >= length - 1)) or (not s and (result.len >= length)): 39 | if s: 40 | result = "-" & result 41 | elif len < 0: 42 | if s: 43 | result = "-" & result 44 | for i in result.len.. abs(len): 67 | result = s 68 | result.setLen(abs(len)) 69 | else: 70 | let fillLength = abs(len) - s.len 71 | if fillLength <= 0: 72 | result = s 73 | elif len > 0: 74 | result = repeat(fill, fillLength) & s 75 | else: 76 | result = s & repeat(fill, fillLength) 77 | 78 | proc floatToStr*(v: SomeNumber, len = 0, prec = 0, sep = '.', fill = ' ', scientific = false): string = 79 | ## Converts ``v`` to string with precision == ``prec``. If result's length 80 | ## is less then ``len``, it aligns result to the right with ``fill`` char. 81 | ## If ``len`` is negative, the result is aligned to the left. 82 | ## 83 | ## Precision and formatting are handled the following way (in terms of `strutils`): 84 | ## 1. If `scientific` is `true`, use `ffScientific` with precision `prec`. 85 | ## 2. If `scientific` is `false` and `prec` is zero, use `ffDefault` with 86 | ## precision `-1` (e.g. `1.0` is formatted as `"1"`). 87 | ## 3. If `scientific` is `false` and `prec` is not zero, use `ffDecimal` with 88 | ## precision `prec`. 89 | let f = if scientific: ffScientific else: (if prec == 0: ffDefault else: ffDecimal) 90 | 91 | # `formatBiggestFloat` changed its precision handling in 0.18.0, we have to 92 | # preserve our behavior. 93 | let prec0 = if f == ffDefault: -1 else: prec 94 | 95 | if len > 0 and v < 0 and fill == '0': 96 | result = "-" & alignStr(formatBiggestFloat(-v.BiggestFloat, f, prec0, sep), len-1, fill) 97 | else: 98 | result = alignStr(formatBiggestFloat(v.BiggestFloat, f, prec0, sep), len, fill) 99 | 100 | -------------------------------------------------------------------------------- /src/boost/http/asyncchunkedstream.nim: -------------------------------------------------------------------------------- 1 | import strutils, asyncdispatch 2 | import ../io/asyncstreams 3 | import ./httpcommon 4 | 5 | type 6 | # Possible states of the stream: 7 | # 1. Beginning of a chunk (bytesLeft = 0). 8 | # Underlying stream will next read the first byte of the next chunk size. 9 | # 2. Middle of a chunk (bytesLeft > 0). 10 | # Underlying stream will next read one of the bytes of chunk data. 11 | # 3. EOF. No more data to read 12 | # 4. Closed. 13 | State = enum sBeginning, sMiddle, sEOF, sFailed, sClosed 14 | AsyncChunkedStream* = ref AsyncChunkedStreamObj 15 | ## Asynchronous chunked stream. 16 | ## 17 | ## Decodes the underlying stream from `Transfer-Encoding: chunked`. 18 | ## 19 | AsyncChunkedStreamObj* = object of AsyncStreamObj 20 | ## Asynchronous chunked stream. 21 | src: AsyncStream 22 | state: State 23 | bytesLeft: Natural # until the end of the current chunk 24 | 25 | proc isFailed*(s: AsyncChunkedStream): bool = 26 | ## Returns `true` if parsing the message body has failed. 27 | s.state == sFailed 28 | 29 | proc cClose(s: AsyncStream) = 30 | if s.AsyncChunkedStream.state in {sBeginning, sMiddle, sEOF, sFailed}: 31 | s.AsyncChunkedStream.src.close 32 | 33 | # Make sure not to lose `sFailed` state 34 | s.AsyncChunkedStream.state = min(sClosed, s.AsyncChunkedStream.state) 35 | 36 | proc cAtEnd(s: AsyncStream): bool = 37 | s.AsyncChunkedStream.state in {sEOF, sClosed, sFailed} 38 | 39 | proc raiseError(s: AsyncChunkedStream, msg: string) {.noReturn.} = 40 | s.state = sFailed 41 | raise newException(MalformedHttpException, msg) 42 | 43 | proc readExactly(s: AsyncChunkedStream, size: Natural): Future[string] {.async.} = 44 | var res = newString(size) 45 | var pos = 0 46 | while pos < size: 47 | let bytesRead = await s.src.readBuffer(addr(res[pos]), size - pos) 48 | if bytesRead <= 0: raiseError(s, "unexpected EOF") 49 | pos.inc(bytesRead) 50 | return res 51 | 52 | proc readExpected( 53 | s: AsyncChunkedStream, 54 | expected: string, 55 | description: string = "" 56 | ): Future[void] {.async.} = 57 | let got = await s.readExactly(expected.len) 58 | if expected != got: 59 | let effectiveDesc = if description.len == 0: escape(expected) else: description 60 | raiseError(s, effectiveDesc & " expected, got " & escape(got)) 61 | 62 | proc readHttpLine(s: AsyncChunkedStream): Future[string] {.async.} = 63 | ## Like `src.readLine`, but the line must terminate in `"\c\l"`. The ending 64 | ## sequence itself is not a part of the resulting string. `'\0'` in the line 65 | ## is allowed. 66 | var res = "" 67 | while true: 68 | let c = await s.src.readChar 69 | if c == '\c': 70 | await s.readExpected("\L") 71 | # Found newline 72 | break 73 | elif c == '\L': 74 | raiseError(s, "unexpected \\L") 75 | elif c == '\0' and s.src.atEnd: 76 | raiseError(s, "unexpected EOF") 77 | else: 78 | res.add(c) 79 | 80 | return res 81 | 82 | proc readNewline(s: AsyncChunkedStream): Future[void] {.async.} = 83 | await s.readExpected("\c\l") 84 | 85 | proc readSizeAndExtensions(s: AsyncChunkedStream): Future[Natural] {.async.} = 86 | let line = await s.readHttpLine 87 | let sepPos = line.find(';') 88 | let sub = 89 | if sepPos == -1: line 90 | else: line[0.. bytesToRead: 128 | s.raiseError("read more bytes than requested") 129 | 130 | s.bytesLeft -= bytesRead 131 | 132 | if s.bytesLeft == 0: 133 | # We're at the end of a non-empty chunk - read up to the next one 134 | await readNewline(s) 135 | s.state = sBeginning 136 | else: 137 | # We're still in the middle 138 | s.state = sMiddle 139 | 140 | return bytesRead 141 | 142 | proc newAsyncChunkedStream*(src: AsyncStream): AsyncChunkedStream = 143 | ## Create a new chunked stream wrapper. 144 | ## 145 | ## Decodes `Transfer-Encoding: chunked` message body in the underlying stream. 146 | ## 147 | ## The underlying stream must be at the first byte of the first chunk's size. 148 | ## Unless an error is raised, after reading all the data from decoded stream 149 | ## the underlying stream is positioned at the first byte after the decoded 150 | ## message body. Closing the underlying stream is handled by caller to allow 151 | ## continuing HTTP connection. 152 | ## 153 | ## Decoded stream can throw a `MalformedHttpException` when 154 | ## reading or closing to signal unexpected data in the underlying stream. 155 | ## 156 | ## *Warning*: Discarding or closing the chunked stream before it reaches the 157 | ## end will leave the underlying connection in the middle of the message body, 158 | ## rendering it unusable. 159 | new result 160 | result.src = src 161 | result.state = sBeginning 162 | result.bytesLeft = 0 163 | # TODO: these casts are ugly. Does it even make sense to have effect tracking 164 | # for async procs, if they have `RootEffect` by default? 165 | result.closeImpl = cast[type(result.closeImpl)](cClose) 166 | result.atEndImpl = cast[type(result.atEndImpl)](cAtEnd) 167 | result.readImpl = cast[type(result.readImpl)](cRead) 168 | -------------------------------------------------------------------------------- /src/boost/http/asynchttpserver.nim: -------------------------------------------------------------------------------- 1 | # 2 | # 3 | # Nim's Runtime Library 4 | # (c) Copyright 2015 Dominik Picheta 5 | # 6 | # See the file "LICENSE", included in this 7 | # distribution, for details about the copyright. 8 | # 9 | 10 | ## This module implements a high performance asynchronous HTTP server. 11 | ## 12 | ## Examples 13 | ## -------- 14 | ## 15 | ## This example will create an HTTP server on port 8080. The server will 16 | ## respond to all requests with a ``200 OK`` response code and "Hello World" 17 | ## as the response body. 18 | ## 19 | ## .. code-block::nim 20 | ## import asynchttpserver, asyncdispatch 21 | ## 22 | ## var server = newAsyncHttpServer() 23 | ## proc cb(req: Request) {.async.} = 24 | ## await req.respond(Http200, "Hello World") 25 | ## 26 | ## waitFor server.serve(Port(8080), cb) 27 | 28 | import tables, asyncnet, asyncdispatch, parseutils, uri, strutils, options 29 | import ../io/asyncstreams, ./asyncchunkedstream 30 | from ./httpcommon import MalformedHttpException 31 | import httpcore 32 | import logging, ../richstring 33 | 34 | export httpcore except parseHeader 35 | 36 | # TODO: If it turns out that the decisions that asynchttpserver makes 37 | # explicitly, about whether to close the client sockets or upgrade them are 38 | # wrong, then add a return value which determines what to do for the callback. 39 | # Also, maybe move `client` out of `Request` object and into the args for 40 | # the proc. 41 | type 42 | TransferEncoding = enum teDefault, teChunked 43 | RequestBodyKind = enum rbkCached, rbkStreamed 44 | 45 | RequestBody* = ref RequestBodyObj 46 | RequestBodyObj = object 47 | ## The request body implemented as asynchronous stream 48 | case kind: RequestBodyKind 49 | of rbkCached: 50 | data: string 51 | of rbkStreamed: 52 | stream: AsyncStream 53 | contentLength: Option[int64] 54 | 55 | Request* = object 56 | client*: AsyncSocket # TODO: Separate this into a Response object? 57 | reqMethod*: string 58 | headers*: HttpHeaders 59 | protocol*: tuple[orig: string, major, minor: int] 60 | url*: Uri 61 | hostname*: string ## The hostname of the client that made the request. 62 | reqBody*: RequestBody 63 | 64 | AsyncHttpServer* = ref object 65 | socket: AsyncSocket 66 | reuseAddr: bool 67 | reusePort: bool 68 | 69 | {.deprecated: [TRequest: Request, PAsyncHttpServer: AsyncHttpServer, 70 | THttpCode: HttpCode, THttpVersion: HttpVersion].} 71 | 72 | proc newAsyncHttpServer*(reuseAddr = true, reusePort = false): AsyncHttpServer = 73 | ## Creates a new ``AsyncHttpServer`` instance. 74 | new result 75 | result.reuseAddr = reuseAddr 76 | result.reusePort = reusePort 77 | 78 | proc hasLen*(body: RequestBody): bool = 79 | ## Returns true if the body length is known (when using `Content-Length`, 80 | ## or when the body was previously cached). 81 | ## 82 | ## See `len`. 83 | case body.kind 84 | of rbkCached: return true 85 | of rbkStreamed: return body.contentLength.isSome 86 | 87 | proc len*(body: RequestBody): int64 = 88 | ## Returns the length of the body if available. 89 | ## 90 | ## If body length is unknown (e.g. `Transfer-Encoding: chunked`) raises a 91 | ## `ValueError`. To avoid this check `hasLen` beforehand. 92 | case body.kind 93 | of rbkCached: 94 | return body.data.len.int64 95 | of rbkStreamed: 96 | if body.contentLength.isSome: 97 | return body.contentLength.get 98 | else: 99 | raise newException(ValueError, "Body length is unknown") 100 | 101 | type 102 | # A stream wrapper that reads at most `length` bytes from the underlying stream 103 | RequestBodyStream = ref RequestBodyStreamObj 104 | RequestBodyStreamObj = object of AsyncStreamObj 105 | s: AsyncStream 106 | length: int64 107 | pos: int64 108 | 109 | proc rbClose(s: AsyncStream) = 110 | s.RequestBodyStream.s.close 111 | 112 | proc rbAtEnd(s: AsyncStream): bool = 113 | s.RequestBodyStream.pos >= s.RequestBodyStream.length 114 | proc rbGetPosition(s: AsyncStream): int64 = s.RequestBodyStream.pos 115 | proc rbReadData(s: AsyncStream, buff: pointer, buffLen: int): Future[int] {.async.} = 116 | var ss = s.RequestBodyStream 117 | let toRead = if ss.pos + buffLen > ss.length: ss.length - ss.pos else: buffLen 118 | # TODO: handle unexpected EOF somehow? 119 | result = await ss.s.readBuffer(buff, toRead.int) 120 | ss.pos += result 121 | 122 | proc newRequestBodyStream(s: AsyncStream, length: int64): RequestBodyStream = 123 | new result 124 | result.s = s 125 | result.length = length 126 | result.pos = 0 127 | result.closeImpl = rbClose 128 | result.atEndImpl = rbAtEnd 129 | result.getPositionImpl = rbGetPosition 130 | result.readImpl = cast[type(result.readImpl)](rbReadData) 131 | 132 | proc newSizedRequestBody(s: AsyncStream, contentLength: int64): RequestBody = 133 | RequestBody( 134 | kind: rbkStreamed, 135 | stream: newRequestBodyStream(s, contentLength), 136 | contentLength: contentLength.some 137 | ) 138 | 139 | proc newChunkedRequestBody(s: AsyncStream): RequestBody = 140 | RequestBody( 141 | kind: rbkStreamed, 142 | stream: newAsyncChunkedStream(s), 143 | contentLength: none(int64) 144 | ) 145 | 146 | proc newEmptyRequestBody(): RequestBody = 147 | RequestBody(kind: rbkCached, data: "") 148 | 149 | proc setBodyCache(body: RequestBody, data: string) = 150 | body[] = RequestBodyObj(kind: rbkCached, data: data) 151 | 152 | proc getStream*(body: RequestBody): AsyncStream = 153 | ## Returns the request's body as asynchronous stream 154 | ## 155 | ## The resulting stream can raise `boost.http.util.MalformedHttpException` 156 | ## in case of malformed request. 157 | case body.kind 158 | of rbkCached: 159 | return newAsyncStringStream(body.data) 160 | of rbkStreamed: 161 | return body.stream 162 | 163 | proc body*(request: Request): Future[string] {.async.} = 164 | ## Returns the body of the request as the string. 165 | ## 166 | ## The resulting future can raise `boost.http.util.MalformedHttpException` 167 | ## in case of malformed request. 168 | 169 | if request.reqBody.isNil: return "" 170 | else: 171 | case request.reqBody.kind 172 | of rbkStreamed: 173 | # this doesn't work if someone has already started reading the stream! 174 | let data = await request.reqBody.getStream.readAll 175 | request.reqBody.setBodyCache(data) 176 | return request.reqBody.data 177 | of rbkCached: 178 | return request.reqBody.data 179 | 180 | proc addHeaders(msg: var string, headers: HttpHeaders) = 181 | for k, v in headers: 182 | msg.add(k & ": " & v & "\c\L") 183 | 184 | proc formatHeadersForLog(headers: HttpHeaders, ignored: string): string = 185 | result = "{" 186 | var first = true 187 | for k, v in headers: 188 | if cmpIgnoreCase(k, ignored) == 0: continue 189 | if first: first = false else: result.add(", ") 190 | result.add(fmt"$k: $v") 191 | result.add("}") 192 | 193 | proc logRequest(req: Request) = 194 | let meth = req.reqMethod.toUpperAscii 195 | let headers = formatHeadersForLog(req.headers, "Authorization") 196 | debug(fmt"Request: ${req.protocol.orig} $meth ${req.url}, $headers") 197 | 198 | proc logResponse(req: Request, code: HttpCode, contentLen: int, headers: HttpHeaders) = 199 | let meth = req.reqMethod.toUpperAscii 200 | let headers = 201 | if headers.isNil: "{:}" 202 | else: formatHeadersForLog(headers, "WWW-Authenticate") 203 | 204 | debug(fmt"Response @ $meth ${req.url}: $code, $contentLen bytes, $headers") 205 | 206 | proc logMalformedHttpException(e: ref MalformedHttpException) = 207 | # These are less likely to signal a serious error ou our side, so we log them 208 | # as `debug` 209 | debug( 210 | "Failed to read request body: ", 211 | e.name, ": ", e.msg, "\L", 212 | e.getStackTrace 213 | ) 214 | 215 | proc logHandlerException(e: ref Exception) = 216 | warn( 217 | "Uncaught exception in HTTP handler: ", 218 | e.name, ": ", e.msg, "\L", 219 | e.getStackTrace 220 | ) 221 | 222 | proc sendHeaders*(req: Request, headers: HttpHeaders): Future[void] = 223 | ## Sends the specified headers to the requesting client. 224 | var msg = "" 225 | addHeaders(msg, headers) 226 | return req.client.send(msg) 227 | 228 | proc respond*(req: Request, code: HttpCode, content: string, 229 | headers: HttpHeaders = nil): Future[void] = 230 | ## Responds to the request with the specified ``HttpCode``, headers and 231 | ## content. 232 | ## 233 | ## This procedure will **not** close the client socket. 234 | logResponse(req, code, content.len, headers) 235 | 236 | var msg = "HTTP/1.1 " & $code & "\c\L" 237 | 238 | if headers != nil: 239 | msg.addHeaders(headers) 240 | msg.add("Content-Length: " & $content.len & "\c\L\c\L") 241 | msg.add(content) 242 | result = req.client.send(msg) 243 | 244 | proc parseProtocol(protocol: string): tuple[orig: string, major, minor: int] = 245 | var i = protocol.skipIgnoreCase("HTTP/") 246 | if i != 5: 247 | raise newException(ValueError, "Invalid request protocol. Got: " & 248 | protocol) 249 | result.orig = protocol 250 | i.inc protocol.parseInt(result.major, i) 251 | i.inc # Skip . 252 | i.inc protocol.parseInt(result.minor, i) 253 | 254 | proc sendStatus(client: AsyncSocket, status: string): Future[void] = 255 | client.send("HTTP/1.1 " & status & "\c\L") 256 | 257 | proc processOneRequest( 258 | client: AsyncSocket, address: string, 259 | callback: proc (request: Request): Future[void] {.closure, gcsafe.} 260 | ): Future[bool] {.async.} = 261 | ## Receives and handles one request. 262 | ## 263 | ## Returns `true` if the connection can be reused. 264 | ## 265 | ## Any raised exceptions mean that the connection was left in invalid state, 266 | ## and the calling code must not reuse it. 267 | 268 | var request: Request 269 | request.url = initUri() 270 | request.headers = newHttpHeaders() 271 | var lineFut = newFutureVar[string]("asynchttpserver.processClient") 272 | lineFut.mget() = newStringOfCap(80) 273 | 274 | # TODO: there a lot of places we `continue` in the middle of the request. 275 | # TODO: continuing circumvents closing the connection 276 | 277 | # GET /path HTTP/1.1 278 | # Header: val 279 | # \n 280 | request.headers.clear() 281 | request.reqBody = nil 282 | request.hostname.shallowCopy(address) 283 | assert client != nil 284 | request.client = client 285 | 286 | proc fail(reason = "Bad Request"): Future[void] {.async.} = 287 | await request.respond(Http400, reason) 288 | if true: raise newException(MalformedHttpException, reason) 289 | 290 | # We should skip empty lines before the request 291 | # https://tools.ietf.org/html/rfc7230#section-3.5 292 | while true: 293 | lineFut.mget().setLen(0) 294 | lineFut.clean() 295 | 296 | # It's possible to kill the server, and if we're not in the middle of 297 | # something (or even if we are?), that shouldn't be an error. 298 | # TODO: there should be a better place for this. 299 | if client.isClosed: return false 300 | 301 | await client.recvLineInto(lineFut) # TODO: Timeouts. 302 | 303 | if lineFut.mget.len == 0: 304 | # Can't read - connection is closed. 305 | return false 306 | 307 | if lineFut.mget != "\c\L": 308 | break 309 | 310 | # First line - GET /path HTTP/1.1 311 | var i = 0 312 | for linePart in lineFut.mget.split(' '): 313 | case i 314 | of 0: request.reqMethod.shallowCopy(linePart.normalize) 315 | of 1: parseUri(linePart, request.url) 316 | of 2: 317 | var failed = false 318 | try: 319 | request.protocol = parseProtocol(linePart) 320 | except ValueError: 321 | failed = true 322 | 323 | if failed: await fail("Invalid request protocol. Got: " & linePart) 324 | else: 325 | await fail("Invalid request. Got: " & lineFut.mget) 326 | inc i 327 | 328 | # Headers 329 | while true: 330 | i = 0 331 | lineFut.mget.setLen(0) 332 | lineFut.clean() 333 | await client.recvLineInto(lineFut) 334 | 335 | if lineFut.mget.len == 0: return false 336 | if lineFut.mget == "\c\L": break 337 | let (key, value) = parseHeader(lineFut.mget) 338 | request.headers[key] = value 339 | # Ensure the client isn't trying to DoS us. 340 | if request.headers.len > headerLimit: 341 | await fail("Too many headers in request") 342 | 343 | logRequest(request) 344 | 345 | if request.reqMethod == "post": 346 | # Check for Expect header 347 | if request.headers.hasKey("Expect"): 348 | if "100-continue" in request.headers["Expect"]: 349 | await client.sendStatus("100 Continue") 350 | else: 351 | await client.sendStatus("417 Expectation Failed") 352 | 353 | # Create body object (if needed) 354 | # - Parse relevant headers 355 | var contentLength = none(int64) 356 | if request.headers.hasKey("Content-Length"): 357 | var data: int64 = 0 358 | # TODO: multiple `Content-Length` fields 359 | if parseBiggestInt(request.headers["Content-Length"], data) == 0: 360 | # Can't determine the length - unrecoverable (RFC 7230 3.3.3) 361 | await fail("Invalid Content-Length") 362 | contentLength = data.some 363 | 364 | var transferEncoding = teDefault 365 | if request.headers.hasKey("Transfer-Encoding"): 366 | let value: string = request.headers["Transfer-Encoding"] 367 | # TODO: multiple encodings 368 | if value == "chunked": transferEncoding = teChunked 369 | else: 370 | # Can't determine the length - unrecoverable (RFC 7230 3.3.3). 371 | await fail("Unsupported Transfer-Encoding") 372 | 373 | # - Check header combinations, create the body 374 | # see RFC7230 3.3.3 375 | if transferEncoding == teChunked: 376 | # Content-Length is overriden. 377 | request.reqBody = newChunkedRequestBody( 378 | newAsyncSocketStream(client) 379 | ) 380 | elif contentLength.isSome: 381 | request.reqBody = newSizedRequestBody( 382 | newAsyncSocketStream(client), 383 | contentLength.get 384 | ) 385 | else: 386 | # No length or encoding - expecting empty body 387 | request.reqBody = newEmptyRequestBody() 388 | 389 | case request.reqMethod 390 | of "get", "post", "head", "put", "delete", "trace", "options", 391 | "connect", "patch": 392 | # `await` inside `try` is broken: https://github.com/nim-lang/Nim/issues/2528 393 | await callback(request) 394 | 395 | else: 396 | await request.respond(Http400, "Invalid request method. Got: " & 397 | request.reqMethod) 398 | # No failure here - we just skip this message 399 | 400 | # Make sure the body is fully read 401 | if not request.reqBody.isNil and request.reqBody.kind != rbkCached: 402 | # Malformed body exceptions here are not handled - we can't respond after the 403 | # user-provided callback has started. 404 | 405 | # TODO: `readAll` keeps the whole thing in memory. Replace it. 406 | # `await` inside `try` is broken: https://github.com/nim-lang/Nim/issues/2528 407 | discard(await(request.reqBody.getStream.readAll)) 408 | 409 | # Persistent connections 410 | # In HTTP 1.1 we assume that connection is persistent. Unless connection 411 | # header states otherwise. 412 | # In HTTP 1.0 we assume that the connection should not be persistent. 413 | # Unless the connection header states otherwise. 414 | let keepAlive = 415 | (request.protocol == HttpVer11 and 416 | request.headers.getOrDefault("connection").normalize != "close") or 417 | (request.protocol == HttpVer10 and 418 | request.headers.getOrDefault("connection").normalize == "keep-alive") 419 | 420 | return keepAlive 421 | 422 | proc processClient( 423 | client: AsyncSocket, address: string, 424 | callback: proc (request: Request): Future[void] {.closure, gcsafe.} 425 | ) {.async.} = 426 | ## Implements full HTTP connection lifecycle and logging. 427 | while true: 428 | let keepAliveFut = processOneRequest(client, address, callback) 429 | yield keepAliveFut 430 | 431 | var keepAlive = false 432 | try: 433 | keepAlive = keepAliveFut.read 434 | except MalformedHttpException: 435 | logMalformedHttpException((ref MalformedHttpException)getCurrentException()) 436 | except: 437 | logHandlerException(getCurrentException()) 438 | 439 | if not keepAlive: break 440 | 441 | client.close 442 | 443 | proc serve*(server: AsyncHttpServer, port: Port, 444 | callback: proc (request: Request): Future[void] {.closure,gcsafe.}, 445 | address = "") {.async.} = 446 | ## Starts the process of listening for incoming HTTP connections on the 447 | ## specified address and port. 448 | ## 449 | ## When a request is made by a client the specified callback will be called. 450 | server.socket = newAsyncSocket() 451 | if server.reuseAddr: 452 | server.socket.setSockOpt(OptReuseAddr, true) 453 | if server.reusePort: 454 | server.socket.setSockOpt(OptReusePort, true) 455 | server.socket.bindAddr(port, address) 456 | server.socket.listen() 457 | 458 | while true: 459 | # TODO: Causes compiler crash. 460 | #var (address, client) = await server.socket.acceptAddr() 461 | var fut = await server.socket.acceptAddr() 462 | asyncCheck processClient(fut.client, fut.address, callback) 463 | #echo(f.isNil) 464 | #echo(f.repr) 465 | 466 | proc close*(server: AsyncHttpServer) = 467 | ## Terminates the async http server instance. 468 | server.socket.close() 469 | 470 | when not defined(testing) and isMainModule: 471 | proc main = 472 | var server = newAsyncHttpServer() 473 | proc cb(req: Request) {.async.} = 474 | #echo(req.reqMethod, " ", req.url) 475 | #echo(req.headers) 476 | let headers = {"Date": "Tue, 29 Apr 2014 23:40:08 GMT", 477 | "Content-type": "text/plain; charset=utf-8"} 478 | await req.respond(Http200, "Hello World", headers.newHttpHeaders()) 479 | 480 | asyncCheck server.serve(Port(5555), cb) 481 | runForever() 482 | main() 483 | -------------------------------------------------------------------------------- /src/boost/http/httpcommon.nim: -------------------------------------------------------------------------------- 1 | import strutils, strtabs, ../data/props, parseutils, asyncdispatch, ../io/asyncstreams 2 | 3 | ## Module provides helper functions for HTTP protocol implementations 4 | 5 | proc urlEncode*(s: string): string = 6 | ## Encodes ``s`` to be `application/x-www-form-urlencoded` compilant 7 | result = newStringOfCap(s.len) 8 | for ch in s: 9 | case ch 10 | of 'a'..'z', 'A'..'Z', '0'..'9', '_': 11 | result.add(ch) 12 | of ' ': 13 | result.add('+') 14 | else: 15 | result.add('%') 16 | result.add(ord(ch).toHex(2)) 17 | 18 | proc urlDecode*(s: string): string = 19 | ## Decodes ``s`` from `application/x-www-form-urlencoded` compilant form 20 | result = newStringOfCap(s.len) 21 | var i = 0 22 | while i < s.len: 23 | let ch = s[i] 24 | case ch 25 | of '%': 26 | result.add(chr(parseHexInt(s[i+1..i+2]))) 27 | i += 2 28 | of '+': 29 | result.add(' ') 30 | else: 31 | result.add(ch) 32 | inc i 33 | 34 | proc formEncode*(form: Props): string = 35 | ## Encodes ``form`` to `application/x-www-form-urlencoded` format 36 | for k, v in form.pairs: 37 | let s = urlEncode(k) & "=" & urlEncode(v) 38 | if result.len == 0: 39 | result = s 40 | else: 41 | result &= "&" & s 42 | 43 | proc formDecode*(data: string, form: var Props) = 44 | ## Decodes ``data`` from `application/x-www-form-urlencoded` format into ``form`` 45 | if form.len == 0: 46 | form = newProps() 47 | else: 48 | form.clear 49 | for line in data.split('&'): 50 | let kv = line.split('=') 51 | if kv.len != 2: 52 | raise newException(ValueError, "Malformed form data") 53 | form[urlDecode(kv[0])] = urlDecode(kv[1]) 54 | 55 | proc formDecode*(data: string): Props = 56 | ## Decodes ``data`` from `application/x-www-form-urlencoded` format 57 | data.formDecode(result) 58 | 59 | proc parseHeader*(line: string): (string, string) = 60 | ## Parses one header from ``line`` into key/value tuple 61 | var idx = line.skipWhitespace 62 | idx += line.parseUntil(result[0], ':', idx) + 1 63 | idx += line.skipWhitespace(idx) 64 | result[1] = line[idx..^1] 65 | 66 | proc readHeaders*(s: AsyncStream): Future[Props] {.async.} = 67 | ## Reads http headers from the stream ``s`` 68 | result = newProps() 69 | var prevLine = "" 70 | while true: 71 | let line = await s.readLine 72 | if line.len == 0: 73 | break 74 | if line[0] == ' ' or line[0] == '\t': 75 | prevLine.add(line[line.skipWhitespace..^1]) 76 | else: 77 | if prevLine != "": 78 | let (k, v) = prevLine.parseHeader 79 | result.add(k, v) 80 | prevLine = line 81 | if prevLine != "": 82 | let (k, v) = prevLine.parseHeader 83 | result.add(k, v) 84 | 85 | proc parseCHeader(value: string): (string, seq[(string, string)]) = 86 | result[0] = "" 87 | result[1] = newSeq[(string, string)]() 88 | var idx = 0 89 | idx += value.skipWhitespace() 90 | idx += value.parseUntil(result[0], ';', idx) + 1 91 | while idx < value.len: 92 | idx += value.skipWhitespace(idx) 93 | var k = "" 94 | var v = "" 95 | idx += value.parseUntil(k, '=', idx) + 1 96 | idx += value.parseUntil(v, ';', idx) + 1 97 | result[1].add((k, v)) 98 | 99 | type 100 | ContentType* = object 101 | ## Structure describing `Content-Type` header 102 | mimeType*: string 103 | charset*: string 104 | boundary*: string 105 | 106 | MalformedHttpException* = object of Exception 107 | ## Raised in case of malformed HTTP request 108 | 109 | proc parseContentType*(value: string): ContentType = 110 | ## Parses the ``value`` of `Content-Type` header 111 | let (mt, rest) = value.parseCHeader 112 | result.mimeType = mt 113 | for h in rest: 114 | case h[0] 115 | of "charset": 116 | result.charset = h[1] 117 | of "boundary": 118 | result.boundary = h[1] 119 | 120 | proc `$`*(ct: ContentType): string = 121 | ## Forms the `Content-Type` value 122 | result = ct.mimeType 123 | if ct.charset.len > 0: 124 | result &= "; charset=" & ct.charset 125 | if ct.boundary.len > 0: 126 | result &= "; boundary=" & ct.boundary 127 | 128 | type 129 | ContentDisposition* = object 130 | ## Structure describing `Content-Disposition` header 131 | disposition*: string 132 | name*: string 133 | filename*: string 134 | size*: int64 135 | 136 | proc parseContentDisposition*(value: string): ContentDisposition = 137 | ## Parses the ``value`` of `Content-Disposition` header 138 | 139 | proc quotedString(s: string): string = 140 | if s.len < 2 or s[0] != '"' or s[^1] != '"': 141 | raise newException(ValueError, "Malformed Content-Disposition") 142 | s[1..^2] 143 | 144 | proc token(s: string): string = 145 | if s.len < 1: 146 | raise newException(ValueError, "Malformed Content-Disposition") 147 | s 148 | 149 | proc extValue(s: string): string = 150 | if s.len > 0 and s[0] == '"': 151 | quotedString(s) 152 | else: 153 | token(s) 154 | 155 | result.size = -1 156 | let (d, rest) = value.parseCHeader 157 | result.disposition = d 158 | for h in rest: 159 | case h[0] 160 | of "name": 161 | result.name = extValue(h[1]) 162 | of "filename": 163 | # filename MUST be quoted 164 | result.filename = quotedString(h[1]) 165 | of "size": 166 | result.size = extValue(h[1]).parseBiggestInt 167 | 168 | proc `$`*(cd: ContentDisposition): string = 169 | ## Forms the `Content-Disposition` value 170 | result = cd.disposition 171 | if cd.name.len > 0: 172 | result &= "; name=" & cd.name 173 | if cd.filename.len > 0: 174 | result &= "; filename=" & cd.filename 175 | if cd.size > 0: 176 | result &= "; size=" & $cd.size 177 | -------------------------------------------------------------------------------- /src/boost/http/impl/errorpages.nim: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2012 Dominik Picheta 2 | # MIT License - Look at LICENSE for details. 3 | import htmlgen 4 | proc error*(err, jesterVer: string): string = 5 | return html(head(title(err)), 6 | body(h1(err), 7 | "
", 8 | p("Jester " & jesterVer), 9 | style = "text-align: center;" 10 | ), 11 | xmlns="http://www.w3.org/1999/xhtml") 12 | 13 | proc routeException*(error: string, jesterVer: string): string = 14 | return html(head(title("Jester route exception")), 15 | body( 16 | h1("An error has occured in one of your routes."), 17 | p(b("Detail: "), error) 18 | ), 19 | xmlns="http://www.w3.org/1999/xhtml") 20 | -------------------------------------------------------------------------------- /src/boost/http/impl/patterns.nim: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2012-2018 Dominik Picheta 2 | # MIT License - Look at license.txt for details. 3 | import parseutils, strtabs 4 | type 5 | NodeType* = enum 6 | NodeText, NodeField 7 | Node* = object 8 | typ*: NodeType 9 | text*: string 10 | optional*: bool 11 | 12 | Pattern* = seq[Node] 13 | 14 | #/show/@id/? 15 | proc parsePattern*(pattern: string): Pattern = 16 | result = @[] 17 | template addNode(result: var Pattern, theT: NodeType, theText: string, 18 | isOptional: bool): typed = 19 | block: 20 | var newNode: Node 21 | newNode.typ = theT 22 | newNode.text = theText 23 | newNode.optional = isOptional 24 | result.add(newNode) 25 | 26 | template `{}`(s: string, i: int): char = 27 | if i >= len(s): 28 | '\0' 29 | else: 30 | s[i] 31 | 32 | var i = 0 33 | var text = "" 34 | while i < pattern.len(): 35 | case pattern[i] 36 | of '@': 37 | # Add the stored text. 38 | if text != "": 39 | result.addNode(NodeText, text, false) 40 | text = "" 41 | # Parse named parameter. 42 | inc(i) # Skip @ 43 | var nparam = "" 44 | i += pattern.parseUntil(nparam, {'/', '?'}, i) 45 | var optional = pattern{i} == '?' 46 | result.addNode(NodeField, nparam, optional) 47 | if pattern{i} == '?': inc(i) # Only skip ?. / should not be skipped. 48 | of '?': 49 | var optionalChar = text[^1] 50 | setLen(text, text.len-1) # Truncate ``text``. 51 | # Add the stored text. 52 | if text != "": 53 | result.addNode(NodeText, text, false) 54 | text = "" 55 | # Add optional char. 56 | inc(i) # Skip ? 57 | result.addNode(NodeText, $optionalChar, true) 58 | of '\\': 59 | inc i # Skip \ 60 | if pattern[i] notin {'?', '@', '\\'}: 61 | raise newException(ValueError, 62 | "This character does not require escaping: " & pattern[i]) 63 | text.add(pattern{i}) 64 | inc i # Skip ``pattern[i]`` 65 | else: 66 | text.add(pattern{i}) 67 | inc(i) 68 | 69 | if text != "": 70 | result.addNode(NodeText, text, false) 71 | 72 | proc findNextText(pattern: Pattern, i: int, toNode: var Node): bool = 73 | ## Finds the next NodeText in the pattern, starts looking from ``i``. 74 | result = false 75 | for n in i..pattern.len()-1: 76 | if pattern[n].typ == NodeText: 77 | toNode = pattern[n] 78 | return true 79 | 80 | proc check(n: Node, s: string, i: int): bool = 81 | let cutTo = (n.text.len-1)+i 82 | if cutTo > s.len-1: return false 83 | return s.substr(i, cutTo) == n.text 84 | 85 | proc match*(pattern: Pattern, s: string): 86 | tuple[matched: bool, params: StringTableRef] = 87 | var i = 0 # Location in ``s``. 88 | 89 | result.matched = true 90 | result.params = newStringTable(modeCaseSensitive) 91 | 92 | for ncount, node in pattern: 93 | case node.typ 94 | of NodeText: 95 | if node.optional: 96 | if check(node, s, i): 97 | inc(i, node.text.len) # Skip over this optional character. 98 | else: 99 | # If it's not there, we have nothing to do. It's optional after all. 100 | discard 101 | else: 102 | if check(node, s, i): 103 | inc(i, node.text.len) # Skip over this 104 | else: 105 | # No match. 106 | result.matched = false 107 | return 108 | of NodeField: 109 | var nextTxtNode: Node 110 | var stopChar = '/' 111 | if findNextText(pattern, ncount, nextTxtNode): 112 | stopChar = nextTxtNode.text[0] 113 | var matchNamed = "" 114 | i += s.parseUntil(matchNamed, stopChar, i) 115 | result.params[node.text] = matchNamed 116 | if matchNamed == "" and not node.optional: 117 | result.matched = false 118 | return 119 | 120 | if s.len != i: 121 | result.matched = false 122 | 123 | when isMainModule: 124 | let f = parsePattern("/show/@id/test/@show?/?") 125 | doAssert match(f, "/show/12/test/hallo/").matched 126 | doAssert match(f, "/show/2131726/test/jjjuuwąąss").matched 127 | doAssert(not match(f, "/").matched) 128 | doAssert(not match(f, "/show//test//").matched) 129 | doAssert(match(f, "/show/asd/test//").matched) 130 | doAssert(not match(f, "/show/asd/asd/test/jjj/").matched) 131 | doAssert(match(f, "/show/@łę¶ŧ←/test/asd/").params["id"] == "@łę¶ŧ←") 132 | 133 | let f2 = parsePattern("/test42/somefile.?@ext?/?") 134 | doAssert(match(f2, "/test42/somefile/").params["ext"] == "") 135 | doAssert(match(f2, "/test42/somefile.txt").params["ext"] == "txt") 136 | doAssert(match(f2, "/test42/somefile.txt/").params["ext"] == "txt") 137 | 138 | let f3 = parsePattern(r"/test32/\@\\\??") 139 | doAssert(match(f3, r"/test32/@\").matched) 140 | doAssert(not match(f3, r"/test32/@\\").matched) 141 | doAssert(match(f3, r"/test32/@\?").matched) 142 | -------------------------------------------------------------------------------- /src/boost/http/impl/utils.nim: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2012 Dominik Picheta 2 | # MIT License - Look at LICENSE for details. 3 | import parseutils, strtabs, strutils, tables 4 | from cgi import decodeUrl 5 | 6 | type 7 | MultiData* = OrderedTable[string, tuple[fields: StringTableRef, body: string]] 8 | 9 | #[ 10 | proc parseUrlQuery*(query: string, result: var StringTableRef) = 11 | var i = 0 12 | i = query.skip("?") 13 | while i < query.len()-1: 14 | var key = "" 15 | var val = "" 16 | i += query.parseUntil(key, '=', i) 17 | if query[i] != '=': 18 | raise newException(ValueError, "Expected '=' at " & $i & 19 | " but got: " & $query[i]) 20 | inc(i) # Skip = 21 | i += query.parseUntil(val, '&', i) 22 | inc(i) # Skip & 23 | result[decodeUrl(key)] = decodeUrl(val) 24 | 25 | ]# 26 | 27 | template parseContentDisposition(): untyped = 28 | var hCount = 0 29 | while hCount < hValue.len()-1: 30 | var key = "" 31 | hCount += hValue.parseUntil(key, {';', '='}, hCount) 32 | if hValue[hCount] == '=': 33 | var value = hvalue.captureBetween('"', start = hCount) 34 | hCount += value.len+2 35 | inc(hCount) # Skip ; 36 | hCount += hValue.skipWhitespace(hCount) 37 | if key == "name": name = value 38 | newPart[0][key] = value 39 | else: 40 | inc(hCount) 41 | hCount += hValue.skipWhitespace(hCount) 42 | 43 | proc parseMultiPart*(body: string, boundary: string): MultiData = 44 | result = initOrderedTable[string, tuple[fields: StringTableRef, body: string]]() 45 | var mboundary = "--" & boundary 46 | 47 | var i = 0 48 | var partsLeft = true 49 | while partsLeft: 50 | var firstBoundary = body.skip(mboundary, i) 51 | if firstBoundary == 0: 52 | raise newException(ValueError, "Expected boundary. Got: " & body.substr(i, i+25)) 53 | i += firstBoundary 54 | i += body.skipWhitespace(i) 55 | 56 | # Headers 57 | var newPart: tuple[fields: StringTableRef, body: string] = ({:}.newStringTable, "") 58 | var name = "" 59 | while true: 60 | if body[i] == '\c': 61 | inc(i, 2) # Skip \c\L 62 | break 63 | var hName = "" 64 | i += body.parseUntil(hName, ':', i) 65 | if body[i] != ':': 66 | raise newException(ValueError, "Expected : in headers.") 67 | inc(i) # Skip : 68 | i += body.skipWhitespace(i) 69 | var hValue = "" 70 | i += body.parseUntil(hValue, {'\c', '\L'}, i) 71 | if hName.toLowerAscii == "content-disposition": 72 | parseContentDisposition() 73 | newPart[0][hName] = hValue 74 | i += body.skip("\c\L", i) # Skip *one* \c\L 75 | 76 | # Parse body. 77 | while true: 78 | if body[i] == '\c' and body[i+1] == '\L' and 79 | body.skip(mboundary, i+2) != 0: 80 | if body.skip("--", i+2+mboundary.len) != 0: 81 | partsLeft = false 82 | break 83 | break 84 | else: 85 | newPart[1].add(body[i]) 86 | inc(i) 87 | i += body.skipWhitespace(i) 88 | 89 | result.add(name, newPart) 90 | 91 | 92 | proc parseMPFD*(contentType: string, body: string): MultiData = 93 | var boundaryEqIndex = contentType.find("boundary=")+9 94 | var boundary = contentType.substr(boundaryEqIndex, contentType.len()-1) 95 | return parseMultiPart(body, boundary) 96 | 97 | when not declared(tables.getOrDefault): 98 | template getOrDefault*(tab, key): untyped = tab[key] 99 | 100 | when isMainModule: 101 | var r = {:}.newStringTable 102 | parseUrlQuery("FirstName=Mickey", r) 103 | echo r 104 | -------------------------------------------------------------------------------- /src/boost/http/multipart.nim: -------------------------------------------------------------------------------- 1 | ## The module implements asynchronous handling of the multipart 2 | ## messages. 3 | ## 4 | 5 | import ./httpcommon, 6 | asyncdispatch, 7 | ../io/asyncstreams, 8 | strutils, 9 | ../data/props, 10 | ../data/memory, 11 | strtabs, 12 | tables 13 | 14 | type 15 | MultiPartMessage* = ref object 16 | ## The multipart message 17 | s: AsyncStream 18 | ct: ContentType 19 | preambleFinished: bool 20 | finished: bool 21 | boundary: string 22 | MessagePart* = ref object 23 | ## The body part of the multipart message 24 | msg: MultiPartMessage 25 | h: Props 26 | ct: ContentType 27 | cd: ContentDisposition 28 | enc: string 29 | cl: int 30 | 31 | proc open*(t: typedesc[MultiPartMessage], s: AsyncStream, contentType: ContentType): MultiPartMessage = 32 | ## Opens multipart message with ``contentType`` for reading from stream ``s``. 33 | if not contentType.mimeType.startsWith("multipart"): 34 | raise newException(ValueError, "MultiPartMessage can't handle this mime-type: " & contentType.mimeType) 35 | if contentType.boundary.len == 0: 36 | raise newException(ValueError, "ContentType boundary is absent") 37 | MultiPartMessage( 38 | # This breaks Keep-Alive! The underlying stream is moved 39 | # arbitrarily forward while parsing. 40 | # TODO: Find a way to make this work with Keep-Alive 41 | s: newAsyncBufferedStream(s), 42 | ct: contentType, 43 | boundary: "--" & contentType.boundary) 44 | 45 | proc atEnd*(m: MultiPartMessage): bool = 46 | ## Checks if there is no more body parts in the message ``m``. 47 | ## This can be detected only after the call of ``readNextPart`` that 48 | ## returned ``nil``. 49 | m.finished 50 | 51 | proc skipPreamble(m: MultipartMessage): Future[void] {.async.} = 52 | ## Skip preamble lines until an encapsulation line is found 53 | 54 | while true: 55 | if m.atEnd: 56 | break 57 | 58 | let line = await m.s.peekLine 59 | if line.startsWith(m.boundary): 60 | break 61 | 62 | discard await(m.s.readLine) 63 | 64 | proc readNextPart*(m: MultiPartMessage): Future[MessagePart] {.async.} = 65 | ## Returns the next part of the message ``m`` or nil if it's ended 66 | 67 | # We can have arbitrary preamble before the first message 68 | if not m.preambleFinished: 69 | await m.skipPreamble 70 | m.preambleFinished = true 71 | 72 | if m.atEnd: 73 | return 74 | let s = m.s 75 | var line = await s.readLine 76 | if line.len == 0: 77 | m.finished = true 78 | return 79 | if not line.startsWith(m.boundary): 80 | m.finished = true 81 | return 82 | if line.endsWith("--"): 83 | m.finished = true 84 | return 85 | let headers = await s.readHeaders 86 | result = MessagePart(msg: m, h: headers, enc: "", cl: -1) 87 | for k, v in headers: 88 | if k.cmpIgnoreCase("Content-Type") == 0: 89 | result.ct = v.parseContentType 90 | elif k.cmpIgnoreCase("Content-Disposition") == 0: 91 | result.cd = v.parseContentDisposition 92 | elif k.cmpIgnoreCase("Content-Transfer-Encoding") == 0: 93 | result.enc = v 94 | elif k.cmpIgnoreCase("Content-Length") == 0: 95 | result.cl = v.parseInt 96 | 97 | type 98 | EndOfPartStream = ref EndOfPartStreamObj 99 | EndOfPartStreamObj = object of AsyncStream 100 | p: MessagePart 101 | s: AsyncStream # wrapAsyncStream doesn't support nested fields 102 | eop: bool # end of part has beed riched 103 | 104 | proc eopRead(s: AsyncStream, buf: pointer, size: int): Future[int] {.async.} = 105 | let es = (EndOfPartStream)s 106 | if es.eop: 107 | return 0 108 | let bytes = await es.peekBuffer(buf, size) 109 | if bytes == 0: 110 | #TODO: Throw UnexpectedEndOfFile? 111 | es.eop = true 112 | return 0 113 | var boundary = "\c\L" & es.p.msg.boundary 114 | let (pos, _) = findInMem(buf, bytes, addr boundary[0], boundary.len) 115 | if pos == -1: 116 | # End of part was not found 117 | result = await es.s.readBuffer(buf, bytes) 118 | else: 119 | result = await es.s.readBuffer(buf, pos) 120 | let tb = await es.peekData(boundary.len + 2) 121 | if tb.startsWith(boundary): 122 | # two remaining characters 123 | let post = tb[^2..^1] 124 | if post == "\c\L": 125 | # Move cursor to the beginning of the boundary 126 | discard await es.s.readData(2) 127 | es.eop = true 128 | elif post == "--": 129 | # Trailing boundary found 130 | # The cursor is moved to the first character after the boundary 131 | # Everything after this point is outside the multipart, and does 132 | # not concern us 133 | discard await es.s.readData(tb.len) 134 | es.eop = true 135 | es.p.msg.finished = true 136 | else: 137 | # Unexpected characters after the boundary? This is 138 | # malformed HTTP. 139 | # TODO: throw an exception? 140 | discard await es.s.readData(2) 141 | es.eop = true 142 | es.p.msg.finished = true 143 | else: 144 | # We got a partial match on the boundary, but it's a false 145 | # positive. Cursor is now at its start. 146 | # If we return like this, the next read request will start at 147 | # the beginning of the partial match. If `size` is small 148 | # enough, this will lead to an infinite loop, so we have to make 149 | # sure we move forward. Fortunately, we have at least 1 byte 150 | # of space starting at `pos` (since we got a match). 151 | 152 | # pointer to byte at pos 153 | let posBuf = cast[pointer]( 154 | cast[uint](buf) + uint(pos) 155 | ) 156 | 157 | result.inc( 158 | await es.s.readBuffer(posBuf, 1) 159 | ) 160 | 161 | proc eopAtEnd(s: AsyncStream): bool = 162 | s.EndOfPartStream.eop 163 | 164 | proc newEndOfPartStream(p: MessagePart): EndOfPartStream = 165 | new result 166 | result.p = p 167 | # It's already buffered, see constructor of the MultiPartMessage 168 | result.s = p.msg.s 169 | 170 | wrapAsyncStream(EndOfPartStream, s) 171 | result.readImpl = cast[type(result.readImpl)](eopRead) 172 | result.atEndImpl = eopAtEnd 173 | 174 | proc getPartDataStream*(p: MessagePart): AsyncStream = 175 | ## Returns the body part as asynchronous stream. 176 | if p.isNil or p.msg.finished: 177 | raise newException(IOError, "Can't read multipart data, it's finished!") 178 | newEndOfPartStream(p) 179 | 180 | proc headers*(p: MessagePart): Props = 181 | ## Returns http headers of the multipart message part ``p`` 182 | p.h 183 | 184 | proc encoding*(p: MessagePart): string = 185 | ## Returns the encoding of the multipart message part ``p`` 186 | p.enc 187 | 188 | proc contentType*(p: MessagePart): ContentType = 189 | ## Returns the content type of the multipart message part ``p`` 190 | p.ct 191 | 192 | proc contentDisposition*(p: MessagePart): ContentDisposition = 193 | ## Returns the content disposition of the multipart message part ``p`` 194 | p.cd 195 | -------------------------------------------------------------------------------- /src/boost/io/asyncstreams.nim: -------------------------------------------------------------------------------- 1 | # 2 | # 3 | # Nim's Runtime Library 4 | # (c) Copyright 2016 Anatoly Galiulin 5 | # 6 | # See the file "copying.txt", included in this 7 | # distribution, for details about the copyright. 8 | # 9 | 10 | import asyncdispatch, asyncnet, asyncfile, macros 11 | 12 | ## This module provides the asynchronous stream interface and some of the implementations 13 | ## including ``AsyncStringStream``, ``AsyncFileStream`` and ``AsyncSocketStream``. 14 | ## 15 | ## If you want to implement your own asynchronous stream, you must provide the 16 | 17 | ## implementations of the streams operations as defined in ``AsyncStreamObj``. 18 | ## Also, you can use some helpers for the absent operations, like ``setPositionNotImplemented``, 19 | ## ``flushNop``, etc. 20 | ## 21 | ## Example: 22 | ## 23 | ## .. code-block:: Nim 24 | ## 25 | ## import asyncdispatch, asyncstreams, strutils 26 | ## 27 | ## proc main {.async.} = 28 | ## var s = newAsyncStringStream("""Hello 29 | ## world!""") 30 | ## var res = newSeq[string]() 31 | ## while true: 32 | ## let l = await s.readLine() 33 | ## if l.len == 0: 34 | ## break 35 | ## res.add(l) 36 | ## doAssert(res.join(", ") == "Hello, world!") 37 | ## waitFor main() 38 | 39 | type 40 | AsyncStream* = ref AsyncStreamObj 41 | ## Reference to the asynchronous stream. 42 | AsyncStreamObj* = object of RootObj 43 | ## Asychronous stream interface. Implementation details: 44 | ## 45 | ## * ``setPositionImpl`` can be nil, if the stream doesn't support ``setPosition``. 46 | ## * ``getPositionImpl`` can be nil, if the stream doesn't support ``getPosition``. 47 | ## * ``peekImpl`` can be nil, if the stream doesn't support ``peekBuffer``. In that case, 48 | ## this operation can be emulated via ``getPosition`` and ``setPosition`` by the module 49 | ## itself. 50 | ## * ``peekLineImpl`` is the optimized version for ``peekLine`` operation. If it's nil, 51 | ## then module tries to emulate ``peekLine`` if it's possible via: 52 | ## * ``getPosition``, ``setPosition`` and ``readLine`` 53 | ## * ``peekBuffer`` with fixed size buffer. 54 | ## * if ``flushImpl`` is nil, ``flush`` operation does nothing. 55 | closeImpl*: proc (s: AsyncStream) {.nimcall, tags:[], gcsafe.} 56 | atEndImpl*: proc (s: AsyncStream): bool {.nimcall, tags:[], gcsafe.} 57 | setPositionImpl*: proc (s: AsyncStream; pos: int64) {.nimcall, tags:[], gcsafe.} 58 | getPositionImpl*: proc (s: AsyncStream): int64 {.nimcall, tags:[], gcsafe.} 59 | readImpl*: proc (s: AsyncStream; buf: pointer, size: int): Future[int] {.nimcall, tags: [ReadIOEffect], gcsafe.} 60 | peekImpl*: proc (s: AsyncStream; buf: pointer, size: int): Future[int] {.nimcall, tags: [ReadIOEffect], gcsafe.} 61 | peekLineImpl*: proc (s: AsyncStream): Future[string] {.nimcall, tags: [ReadIOEffect], gcsafe.} 62 | writeImpl*: proc (s: AsyncStream; buf: pointer, size: int): Future[void] {.nimcall, tags: [WriteIOEffect], gcsafe.} 63 | flushImpl*: proc (s: AsyncStream): Future[void] {.nimcall, tags:[], gcsafe.} 64 | 65 | #[ 66 | # ``Not implemented`` stuff 67 | ]# 68 | 69 | template atEndNotImplemented = 70 | raise newException(IOError, "atEnd operation is not implemented") 71 | 72 | template setPositionNotImplemented = 73 | raise newException(IOError, "setPosition operation is not implemented") 74 | 75 | template getPositionNotImplemented = 76 | raise newException(IOError, "getPosition operation is not implemented") 77 | 78 | template readNotImplemented = 79 | raise newException(IOError, "read operation is not implemented") 80 | 81 | template peekNotImplemented = 82 | raise newException(IOError, "peek operation is not implemented") 83 | 84 | template writeNotImplemented = 85 | raise newException(IOError, "write operation is not implemented") 86 | 87 | proc flushNop(s: AsyncStream) {.async.} = 88 | discard 89 | 90 | #[ 91 | # AsyncStream 92 | ]# 93 | 94 | proc flush*(s: AsyncStream) {.async.} = 95 | ## Flushes the buffers of the stream ``s``. 96 | if s.flushImpl.isNil: 97 | await flushNop(s) 98 | await s.flushImpl(s) 99 | 100 | proc close*(s: AsyncStream) = 101 | ## Closes the stream ``s``. 102 | s.closeImpl(s) 103 | 104 | proc atEnd*(s: AsyncStream): bool = 105 | ## Checks if all data has been read from the stream ``s`` 106 | if s.atEndImpl.isNil: 107 | atEndNotImplemented 108 | s.atEndImpl(s) 109 | 110 | proc getPosition*(s: AsyncStream): int64 = 111 | ## Retrieves the current position in the stream ``s`` 112 | if s.getPositionImpl.isNil: 113 | getPositionNotImplemented 114 | s.getPositionImpl(s) 115 | 116 | proc setPosition*(s: AsyncStream, pos: int64) = 117 | ## Sets the current position in the stream ``s`` 118 | if s.setPositionImpl.isNil: 119 | setPositionNotImplemented 120 | s.setPositionImpl(s, pos) 121 | 122 | proc readBuffer*(s: AsyncStream, buffer: pointer, size: int): Future[int] {.async.} = 123 | ## Reads up to ``size`` bytes from the stream ``s`` into the ``buffer`` 124 | if s.readImpl.isNil: 125 | readNotImplemented 126 | result = await s.readImpl(s, buffer, size) 127 | 128 | proc peekBuffer*(s: AsyncStream, buffer: pointer, size: int): Future[int] {.async.} = 129 | ## Reads up to ``size`` bytes from the stream ``s`` into the ``buffer`` without moving 130 | ## stream position 131 | if s.peekImpl.isNil: 132 | if not s.getPositionImpl.isNil and not s.setPositionImpl.isNil: 133 | let pos = s.getPosition 134 | result = await s.readBuffer(buffer, size) 135 | s.setPosition(pos) 136 | else: 137 | peekNotImplemented 138 | result = await s.peekImpl(s, buffer, size) 139 | 140 | proc writeBuffer*(s: AsyncStream, buffer: pointer, size: int) {.async.} = 141 | ## Writes ``size`` bytes from the ``buffer`` into the stream ``s`` 142 | if s.writeImpl.isNil: 143 | writeNotImplemented 144 | await s.writeImpl(s, buffer, size) 145 | 146 | proc readData*(s: AsyncStream, size: int): Future[string] {.async.} = 147 | ## Reads up to the ``size`` bytes into the string from the stream ``s`` 148 | result = newString(size) 149 | let readed = await s.readBuffer(result.cstring, size) 150 | result.setLen(readed) 151 | 152 | proc peekData*(s: AsyncStream, size: int): Future[string] {.async.} = 153 | ## Peeks up to the ``size`` bytes into the string from the stream ``s`` 154 | result = newString(size) 155 | let readed = await s.peekBuffer(result.cstring, size) 156 | result.setLen(readed) 157 | 158 | proc writeData*(s: AsyncStream, data: string) {.async.} = 159 | ## Writes ``data`` to the stream ``s`` 160 | await s.writeBuffer(data.cstring, data.len) 161 | 162 | proc readChar*(s: AsyncStream): Future[char] {.async.} = 163 | ## Reads the char from the stream ``s`` 164 | let data = await s.readData(1) 165 | result = if data.len == 0: '\0' else: data[0] 166 | 167 | proc peekChar*(s: AsyncStream): Future[char] {.async.} = 168 | ## Peeks the char from the stream ``s`` 169 | let data = await s.readData(1) 170 | result = if data.len == 0: '\0' else: data[0] 171 | 172 | proc writeChar*(s: AsyncStream, c: char) {.async.} = 173 | ## Writes the char to the stream ``s`` 174 | await s.writeData($c) 175 | 176 | proc readLine*(s: AsyncStream): Future[string] {.async.} = 177 | ## Reads the line from the stream ``s`` until end of stream or the new line delimeter 178 | result = "" 179 | while true: 180 | let c = await s.readChar 181 | if c == '\c': 182 | discard await s.readChar 183 | break 184 | elif c == '\L' or c == '\0': 185 | break 186 | else: 187 | result.add(c) 188 | 189 | const PeekLineFallbackBuffLen = 4096 190 | 191 | proc peekLine*(s: AsyncStream): Future[string] {.async.} = 192 | ## Peeks the line from the stream ``s`` until end of stream or the 193 | ## new line delimeter. It works only if the stream supports 194 | ## `peekLine` itself, allows to get/set stream position, or 195 | ## supports `peek`. 196 | if not s.peekLineImpl.isNil: 197 | # Most optimized version 198 | result = await s.peekLineImpl(s) 199 | elif (not s.getPositionImpl.isNil) and (not s.setPositionImpl.isNil): 200 | # GetPos/SetPos version 201 | let pos = s.getPosition 202 | result = await s.readLine 203 | s.setPosition(pos) 204 | elif not s.peekImpl.isNil: 205 | # Fallback to read as much as possible 206 | # TODO: maximum length is arbitrary here. Make it a parameter? 207 | result = await s.peekData(PeekLineFallbackBuffLen) 208 | for i in 0.. str.data.len: str.data.len else: pos.int 511 | 512 | proc strGetPosition(s: AsyncStream): int64 = 513 | AsyncStringStream(s).pos 514 | 515 | proc strRead(s: AsyncStream, buf: pointer, size: int): Future[int] {.async.} = 516 | let str = AsyncStringStream(s) 517 | doAssert(not str.closed, "AsyncStringStream is closed") 518 | result = min(size, str.data.len - str.pos) 519 | if result == 0: 520 | str.eof = true 521 | else: 522 | copyMem(buf, addr str.data[str.pos], result) 523 | str.pos += result 524 | 525 | proc strWrite(s: AsyncStream, buf: pointer, size: int) {.async.} = 526 | let str = AsyncStringStream(s) 527 | doAssert(not str.closed, "AsyncStringStream is closed") 528 | if str.pos + size > str.data.len: 529 | str.data.setLen(str.pos + size) 530 | copyMem(addr str.data[str.pos], buf, size) 531 | str.pos += size 532 | 533 | proc `$`*(s: AsyncStringStream): string = 534 | ## Converts ``s`` to string 535 | s.data 536 | 537 | proc newAsyncStringStream*(data = ""): AsyncStringStream = 538 | ## Creates AsyncStringStream filled with ``data`` 539 | new result 540 | result.data = data 541 | 542 | result.closeImpl = strClose 543 | result.atEndImpl = strAtEnd 544 | result.setPositionImpl = strSetPosition 545 | result.getPositionImpl = strGetPosition 546 | result.readImpl = cast[type(result.readImpl)](strRead) 547 | result.writeImpl = cast[type(result.writeImpl)](strWrite) 548 | result.flushImpl = cast[type(result.flushImpl)](flushNop) 549 | 550 | #[ 551 | # AsyncSocketStream 552 | ]# 553 | 554 | type 555 | AsyncSocketStream* = ref AsyncSocketStreamObj 556 | ## Reference to the asynchronous socket stream. 557 | AsyncSocketStreamObj* = object of AsyncStreamObj 558 | ## Asynchronous socket stream. 559 | s: AsyncSocket 560 | closed: bool 561 | 562 | proc sockClose(s: AsyncStream) = 563 | AsyncSocketStream(s).s.close 564 | AsyncSocketStream(s).closed = true 565 | 566 | proc sockAtEnd(s: AsyncStream): bool = 567 | AsyncSocketStream(s).closed 568 | 569 | proc sockRead(s: AsyncStream, buf: pointer, size: int): Future[int] {.async.} = 570 | result = await AsyncSocketStream(s).s.recvInto(buf, size) 571 | if result == 0: 572 | AsyncSocketStream(s).closed = true 573 | 574 | proc sockWrite(s: AsyncStream; buf: pointer, size: int) {.async.} = 575 | await AsyncSocketStream(s).s.send(buf, size) 576 | 577 | proc initAsyncSocketStreamImpl(res: var AsyncSocketStreamObj, s: AsyncSocket) = 578 | res.s = s 579 | res.closed = false 580 | 581 | res.closeImpl = cast[type(res.closeImpl)](sockClose) 582 | res.atEndImpl = sockAtEnd 583 | res.readImpl = cast[type(res.readImpl)](sockRead) 584 | res.writeImpl = cast[type(res.writeImpl)](sockWrite) 585 | res.flushImpl = cast[type(res.flushImpl)](flushNop) 586 | 587 | proc newAsyncSocketStream*(s: AsyncSocket): AsyncSocketStream = 588 | ## Creates new AsyncSocketStream from the AsyncSocket ``s`` 589 | var res = new AsyncSocketStream 590 | initAsyncSocketStreamImpl(res[], s) 591 | result = res 592 | 593 | #[ 594 | # AsyncStreamWrapper 595 | ]# 596 | 597 | template wrapAsyncStream*(T: typedesc[AsyncStream], streamFieldName: untyped): untyped = 598 | ## Copies all the operations of stream ``result.streamFieldName`` to 599 | ## the stream of type ``T``. ``result`` of the calling context must 600 | ## point to the stream of type ``T``. 601 | ## 602 | ## The example of wrapping ``AsyncStringStream``: 603 | ## 604 | ## .. code-block:: Nim 605 | ## 606 | ## type 607 | ## WS = ref WSObj 608 | ## WSObj = object of AsyncStreamObj 609 | ## wrappedStream: AsyncStream 610 | ## 611 | ## proc newWS(s: AsyncStream): WS = 612 | ## new result 613 | ## result.wrappedStream = s 614 | ## wrapAsyncStream(WS, wrappedStream) 615 | 616 | proc ws(s: AsyncStream): AsyncStream {.inject, inline.} = 617 | result = ((T)s).streamFieldName 618 | 619 | if not ws(result).closeImpl.isNil: 620 | proc wsClose(s: AsyncStream) = 621 | ws(s).closeImpl(ws(s)) 622 | result.closeImpl = wsClose 623 | 624 | if not ws(result).atEndImpl.isNil: 625 | proc wsAtEnd(s: AsyncStream): bool = 626 | ws(s).atEndImpl(ws(s)) 627 | result.atEndImpl = wsAtEnd 628 | 629 | if not ws(result).setPositionImpl.isNil: 630 | proc wsSetPosition(s: AsyncStream, pos: int64) = 631 | ws(s).setPositionImpl(ws(s), pos) 632 | result.setPositionImpl = wsSetPosition 633 | 634 | if not ws(result).getPositionImpl.isNil: 635 | proc wsGetPosition(s: AsyncStream): int64 = 636 | ws(s).getPositionImpl(ws(s)) 637 | result.getPositionImpl = wsGetPosition 638 | 639 | if not ws(result).readImpl.isNil: 640 | proc wsRead(s: AsyncStream, buf: pointer, size: int): Future[int] {.async.} = 641 | result = await ws(s).readImpl(ws(s), buf, size) 642 | result.readImpl = cast[type(result.readImpl)](wsRead) 643 | 644 | if not ws(result).peekImpl.isNil: 645 | proc wsPeek(s: AsyncStream, buf: pointer, size: int): Future[int] {.async.} = 646 | result = await ws(s).peekImpl(ws(s), buf, size) 647 | result.peekImpl = cast[type(result.peekImpl)](wsPeek) 648 | 649 | if not ws(result).peekLineImpl.isNil: 650 | proc wsPeekLine(s: AsyncStream): Future[string] {.async.} = 651 | result = await ws(s).peekLineImpl(ws(s)) 652 | result.peekLineImpl = cast[type(result.peekLineImpl)](wsPeekLine) 653 | 654 | if not ws(result).writeImpl.isNil: 655 | proc wsWrite(s: AsyncStream, buf: pointer, size: int) {.async.} = 656 | await ws(s).writeImpl(ws(s), buf, size) 657 | result.writeImpl = cast[type(result.writeImpl)](wsWrite) 658 | 659 | if not ws(result).flushImpl.isNil: 660 | proc wsFlush(s: AsyncStream) {.async.} = 661 | await ws(s).flushImpl(ws(s)) 662 | result.flushImpl = cast[type(result.flushImpl)](wsFlush) 663 | 664 | #[ 665 | # AsyncBufferedStream 666 | ]# 667 | 668 | type 669 | AsyncBufferedStream* = ref AsyncBufferedStreamObj 670 | ## Reference to the asynchronous buffered stream. 671 | AsyncBufferedStreamObj* = object of AsyncStreamObj 672 | ## Asynchronous buffered stream. Adds ``peekBuffer`` operation to the streams 673 | ## that doesn't support it. For example: 674 | ## 675 | ## .. code-block:: Nim 676 | ## 677 | ## var s: AsyncSocketStream 678 | ## # This will throw the exception: 679 | ## var data = s.peekData(100) 680 | ## 681 | ## var bs = newAsyncBufferedStream(s) 682 | ## # And this won't 683 | ## data = bs.peekData(100) 684 | s: AsyncStream 685 | buff: seq[byte] 686 | length: int 687 | pos: int 688 | 689 | template bufS: untyped = AsyncBufferedStream(s) 690 | 691 | proc bsAnEnd(s: AsyncStream): bool = 692 | bufS.pos == bufS.length and bufS.s.atEnd 693 | 694 | proc bsGetPosition(s: AsyncStream): int64 = 695 | result = bufS.s.getPosition 696 | if bufS.length != 0: 697 | result -= bufS.length - bufS.pos 698 | if result < 0: 699 | result = 0 700 | 701 | proc bsSetPosition(s: AsyncStream, pos: int64) = 702 | bufS.s.setPosition(pos) 703 | bufS.length = 0 704 | bufS.pos = 0 705 | 706 | proc bsRead(s: AsyncStream, buf: pointer, size: int): Future[int] {.async.} = 707 | if bufS.length == bufS.pos: 708 | bufS.length = await bufS.s.readBuffer(addr bufS.buff[0], bufS.buff.len) 709 | bufS.pos = 0 710 | if bufS.length == 0: 711 | return 712 | # Here we have something to read 713 | result = min(size, bufS.length - bufS.pos) 714 | copyMem(buf, addr bufS.buff[bufS.pos], result) 715 | bufS.pos += result 716 | 717 | proc bsPeek(s: AsyncStream, buf: pointer, size: int): Future[int] {.async.} = 718 | if bufS.length == bufS.pos: 719 | bufS.length = await bufS.s.readBuffer(addr bufS.buff[0], bufS.buff.len) 720 | bufS.pos = 0 721 | if bufS.length == 0: 722 | return 723 | result = min(size, bufS.length - bufS.pos) 724 | if result < size and bufS.pos > 0: 725 | # We can move our data and read some more 726 | moveMem(addr bufS.buff[0], addr bufS.buff[bufS.pos], result) 727 | bufS.length = result 728 | bufS.pos = 0 729 | let readed = await bufS.s.readBuffer(addr bufS.buff[result], bufS.buff.len - result) 730 | bufS.length += readed 731 | result = min(size, bufS.length) 732 | copyMem(buf, addr bufS.buff[bufS.pos], result) 733 | 734 | proc newAsyncBufferedStream*(s: AsyncStream, buff: seq[byte]): AsyncBufferedStream = 735 | new result 736 | result.s = s 737 | result.buff = buff 738 | 739 | wrapAsyncStream(AsyncBufferedStream, s) 740 | 741 | result.atEndImpl = bsAnEnd 742 | 743 | if s.getPositionImpl != nil: 744 | result.getPositionImpl = bsGetPosition 745 | else: 746 | result.getPositionImpl = nil 747 | 748 | if s.setPositionImpl != nil: 749 | result.setPositionImpl = bsSetPosition 750 | else: 751 | result.setPositionImpl = nil 752 | 753 | result.readImpl = cast[type(result.readImpl)](bsRead) 754 | result.peekImpl = cast[type(result.peekImpl)](bsPeek) 755 | 756 | proc newAsyncBufferedStream*(s: AsyncStream, buffLen = 4096): AsyncBufferedStream = 757 | newAsyncBufferedStream(s, newSeq[byte](buffLen)) 758 | -------------------------------------------------------------------------------- /src/boost/jsonserialize.nim: -------------------------------------------------------------------------------- 1 | import json, strutils, algorithm, boost/parsers 2 | 3 | type 4 | FieldException* = object of Exception 5 | path: seq[string] 6 | 7 | proc newFieldException*(msg: string, path: seq[string] = @[]): ref FieldException = 8 | new result 9 | result.msg = msg 10 | result.path = path 11 | 12 | proc getPath*(e: ref FieldException): string = 13 | e.path.reversed.join(".") 14 | 15 | proc addPath*(e: ref FieldException, p: string) = 16 | e.path.add(p) 17 | 18 | proc fromJson*(_: typedesc[string], n: JsonNode): string = 19 | if n == nil: 20 | raise newFieldException("Can't get field of type string") 21 | else: 22 | return n.getStr 23 | 24 | proc toJson*(v: string): JsonNode = 25 | newJString(v) 26 | 27 | proc fromJson*(_: typedesc[int], n: JsonNode): int = 28 | if n == nil: 29 | raise newFieldException("Can't get field of type int") 30 | else: 31 | return n.getInt 32 | 33 | proc toJson*(v: int): JsonNode = 34 | newJInt(v) 35 | 36 | proc fromJson*[T: enum](_: typedesc[T], n: JsonNode): T = 37 | parsers.parseEnum[T](n.getStr) 38 | 39 | proc toJson*[T: enum](v: T): JsonNode = 40 | ($v).toJson 41 | 42 | proc fromJson*(_: typedesc[JsonNode], n: JsonNode): JsonNode = 43 | n 44 | 45 | proc toJson*(n: JsonNode): JsonNode = 46 | n 47 | -------------------------------------------------------------------------------- /src/boost/limits.nim: -------------------------------------------------------------------------------- 1 | ## This module introduces limits for some data types 2 | 3 | proc min*[T: Ordinal](t: typedesc[T]): T = T.low 4 | proc max*[T: Ordinal](t: typedesc[T]): T = T.high 5 | 6 | # uint and uint64 are not `Ordinal` types, see ``Pre-defined integer types`` 7 | # in the manual. 8 | proc min*(t: typedesc[uint64]): uint64 = 0'u64 9 | proc max*(t: typedesc[uint64]): uint64 = 0xFFFF_FFFF_FFFF_FFFF'u64 10 | 11 | proc min*(t: typedesc[uint]): uint = 0'u 12 | 13 | when (sizeof(uint) == 8): 14 | proc max*(t: typedesc[uint]): uint = 0xFFFF_FFFF_FFFF_FFFF'u 15 | elif (sizeof(uint) == 4): 16 | proc max*(t: typedesc[uint]): uint = 0xFFFF_FFFF'u 17 | else: 18 | {.fatal: "Limits: can't get minimal value for type uint when sizeof == " & $sizeof(uint).} 19 | -------------------------------------------------------------------------------- /src/boost/parsers.nim: -------------------------------------------------------------------------------- 1 | ## Miscellaneous parse tools 2 | 3 | import limits, strutils, typetraits 4 | 5 | {.push overflowChecks: off.} 6 | 7 | proc strToUInt64*(s: string, radix = 10): uint64 {.raises: ValueError.} = 8 | let d = s.strip 9 | result = 0 10 | var prev = result 11 | var i = 0 12 | var dl = d.len 13 | if dl > 0 and s[i] == '+': 14 | inc i 15 | for i in i..= radix: 24 | raise newException(ValueError, "Parse error: " & d & " bad format, radix = " & $radix) 25 | prev = result 26 | result = result * radix.uint64 + n.uint64 27 | if prev > result: 28 | raise newException(ValueError, "Overflow: " & d & " can't fit into uint64") 29 | elif d[i] == '_': 30 | continue 31 | else: 32 | raise newException(ValueError, "Parse error: " & d & " bad format") 33 | 34 | proc strToUInt*(s: string, radix = 10): uint {.raises: ValueError.} = 35 | let res = strToUInt64(s, radix) 36 | when sizeof(uint) != 8: 37 | if res > uint.max.uint64: 38 | raise newException(ValueError, "Overflow: " & s & " can't fit into uint") 39 | result = res.uint 40 | 41 | proc strToUInt32*(s: string, radix = 10): uint32 {.raises: ValueError.} = 42 | let res = strToUInt64(s, radix) 43 | if res > uint32.max.uint64: 44 | raise newException(ValueError, "Overflow: " & s & " can't fit into uint32") 45 | result = res.uint32 46 | 47 | proc strToUInt16*(s: string, radix = 10): uint16 {.raises: ValueError.} = 48 | let res = strToUInt64(s, radix) 49 | if res > uint16.max.uint64: 50 | raise newException(ValueError, "Overflow: " & s & " can't fit into uint16") 51 | result = res.uint16 52 | 53 | proc strToUInt8*(s: string, radix = 10): uint8 {.raises: ValueError.} = 54 | let res = strToUInt64(s, radix) 55 | if res > uint8.max.uint64: 56 | raise newException(ValueError, "Overflow: " & s & " can't fit into uint8") 57 | result = res.uint8 58 | 59 | proc strToInt64*(s: string, radix = 10): int64 {.raises: ValueError.} = 60 | let d = s.strip 61 | var negate = false 62 | if d[0] == '-': 63 | negate = true 64 | result = strToUInt64(if negate: d[1..^1] else: d, radix).int64 * (if negate: -1'i64 else: 1'i64) 65 | if negate and result > 0 or (not negate) and result < 0: 66 | raise newException(ValueError, "Overflow: " & s & " can't fit into int64") 67 | 68 | proc strToInt*(s: string, radix = 10): int {.raises: ValueError.} = 69 | let res = strToInt64(s, radix) 70 | when sizeof(int) != 8: 71 | if res < int.min.int64 or res > int.max.int64: 72 | raise newException(ValueError, "Overflow: " & s & " can't fit into int") 73 | result = res.int 74 | 75 | proc strToInt32*(s: string, radix = 10): int32 {.raises: ValueError.} = 76 | let res = strToInt64(s, radix) 77 | if res < int32.min.int64 or res > int32.max.int64: 78 | raise newException(ValueError, "Overflow: " & s & " can't fit into int32") 79 | result = res.int32 80 | 81 | proc strToInt16*(s: string, radix = 10): int16 {.raises: ValueError.} = 82 | let res = strToInt64(s, radix) 83 | if res < int16.min.int64 or res > int16.max.int64: 84 | raise newException(ValueError, "Overflow: " & s & " can't fit into int16") 85 | result = res.int16 86 | 87 | proc strToInt8*(s: string, radix = 10): int8 {.raises: ValueError.} = 88 | let res = strToInt64(s, radix) 89 | if res < int8.min.int64 or res > int8.max.int64: 90 | raise newException(ValueError, "Overflow: " & s & " can't fit into int8") 91 | result = res.int8 92 | 93 | {.pop.} 94 | 95 | proc parseEnum*[T: bool|enum](s: string): T = 96 | when T is bool: 97 | case s.strip.toLowerAscii 98 | of "true": 99 | result = true 100 | of "false": 101 | result = false 102 | else: 103 | raise newException(ValueError, "Unknown " & name(T) & " value " & s) 104 | else: 105 | for i in T: 106 | if i.`$`.toLowerAscii == s.strip.toLowerAscii: 107 | return i 108 | raise newException(ValueError, "Unknown " & name(T) & " value " & s) 109 | -------------------------------------------------------------------------------- /src/boost/richstring.nim: -------------------------------------------------------------------------------- 1 | ## Module provides string utilities 2 | 3 | import parseutils, sequtils, macros, options, strutils, ./formatters, ./parsers 4 | 5 | proc parseIntFmt(fmtp: string): tuple[maxLen: int, fillChar: char] = 6 | var maxLen = if fmtp.len == 0: 0 else: strToInt(fmtp) 7 | var minus = fmtp.len > 0 and fmtp[0] == '-' 8 | var fillChar = if ((minus and fmtp.len > 1) or fmtp.len > 0) and fmtp[if minus: 1 else: 0] == '0': '0' else: ' ' 9 | (maxLen, fillChar) 10 | 11 | proc parseFloatFmt(fmtp: string): tuple[maxLen: int, prec: Option[int], fillChar: char] = 12 | result.fillChar = ' ' 13 | 14 | if fmtp.len == 0: 15 | return 16 | var t = "" 17 | var minus = 1 18 | var idx = 0 19 | idx += fmtp.parseWhile(t, {'-'}, idx) 20 | if t == "-": 21 | minus = -1 22 | idx += fmtp.parseWhile(t, {'0'}, idx) 23 | if t == "0": 24 | result.fillChar = '0' 25 | idx += fmtp.parseWhile(t, {'0'..'9'}, idx) 26 | if t != "": 27 | result.maxLen = minus * strToInt(t) 28 | idx += fmtp.skipWhile({'.'}, idx) 29 | idx += fmtp.parseWhile(t, {'0'..'9'}, idx) 30 | if t != "": 31 | result.prec = some(strToInt(t)) 32 | else: 33 | result.prec = none(int) 34 | 35 | proc handleIntFormat(exp: string, fmtp: string, radix: int, lowerCase = false): NimNode {.compileTime.} = 36 | let (maxLen, fillChar) = parseIntFmt(fmtp) 37 | result = newCall(bindSym"intToStr", parseExpr(exp), newLit(radix), newLit(maxLen), newLit(fillChar), newLit(lowerCase)) 38 | 39 | proc handleDFormat(exp: string, fmtp: string): NimNode {.compileTime.} = 40 | result = handleIntFormat(exp, fmtp, 10) 41 | 42 | proc handleXFormat(exp: string, fmtp: string, lowerCase: bool): NimNode {.compileTime.} = 43 | result = handleIntFormat(exp, fmtp, 16, lowerCase) 44 | 45 | proc handleFFormat(exp: string, fmtp: string): NimNode {.compileTime.} = 46 | let (maxLen, prec0, fillChar) = parseFloatFmt(fmtp) 47 | let prec = prec0.get(0) 48 | result = newCall(bindSym"floatToStr", parseExpr(exp), newLit(maxLen), newLit(prec), newLit('.'), newLit(fillChar), newLit(false)) 49 | 50 | proc handleEFormat(exp: string, fmtp: string): NimNode {.compileTime.} = 51 | var (maxLen, prec0, fillChar) = parseFloatFmt(fmtp) 52 | # We use 6 digits by default, and `floatToStr` uses the minimum amount 53 | let prec = prec0.get(6) 54 | result = newCall(bindSym"floatToStr", parseExpr(exp), newLit(maxLen), newLit(prec), newLit('.'), newLit(fillChar), newLit(true)) 55 | 56 | proc handleSFormat(exp: string, fmtp: string): NimNode {.compileTime.} = 57 | var (maxLen, _) = parseIntFmt(fmtp) 58 | if maxLen == 0: 59 | result = parseExpr("$(" & exp & ")") 60 | else: 61 | result = newCall(bindSym"alignStr", parseExpr("$(" & exp & ")"), newLit(maxLen), newLit(' ')) 62 | 63 | proc handleFormat(exp: string, fmt: string, nodes: var seq[NimNode]) {.compileTime} = 64 | if fmt[1] == '%': 65 | nodes.add(parseExpr("$(" & exp & ")")) 66 | nodes.add(newLit(fmt[1..^1])) 67 | else: 68 | const formats = {'d', 'f', 's', 'x', 'X', 'e'} 69 | var idx = 1 70 | var fmtm = "" 71 | var fmtp = "" 72 | while idx < fmt.len: 73 | if fmt[idx].isAlphaAscii: 74 | if fmt[idx] in formats: 75 | fmtm = $fmt[idx] 76 | fmtp = fmt[1..idx-1] 77 | break 78 | inc idx 79 | if fmtm.len == 0: 80 | nodes.add(parseExpr("$(" & exp & ")")) 81 | nodes.add(newLit(fmt)) 82 | else: 83 | case fmtm 84 | of "d": 85 | nodes.add(handleDFormat(exp, fmtp)) 86 | of "x", "X": 87 | nodes.add(handleXFormat(exp, fmtp, lowerCase = fmtm == "x")) 88 | of "f": 89 | nodes.add(handleFFormat(exp, fmtp)) 90 | of "e": 91 | nodes.add(handleEFormat(exp, fmtp)) 92 | of "s": 93 | nodes.add(handleSFormat(exp, fmtp)) 94 | else: 95 | quit "Unknown format \"" & fmtm & "\"" 96 | nodes.add(newLit(fmt[idx+1..^1])) 97 | 98 | macro fmt*(fmt: static[string]): untyped = 99 | ## String interpolation macro with scala-like format specifiers. 100 | ## Knows about: 101 | ## * `d` - decimal number formatter 102 | ## * `x`, `X` - hex number formatter 103 | ## * `f` - float number formatter 104 | ## * `e` - float number formatter (scientific form) 105 | ## * `s` - string formatter 106 | ## 107 | ## Examples: 108 | ## 109 | ## .. code-block:: Nim 110 | ## 111 | ## import boost/richstring 112 | ## 113 | ## let s = "string" 114 | ## assert fmt"${s[0..2].toUpperAscii}" == "STR" 115 | ## assert fmt"${-10}%04d" == "-010" 116 | ## assert fmt"0x${10}%02X" == "0x0A" 117 | ## assert fmt"""${"test"}%-5s""" == "test " 118 | ## assert fmt"${1}%.3f" == "1.000" 119 | ## assert fmt"Hello, $s!" == "Hello, string!" 120 | 121 | proc esc(s: string): string {.inline.} = 122 | result = newStringOfCap(s.len) 123 | for ch in s: 124 | case ch 125 | of '\xD': 126 | result.add("\\x0D") 127 | of '\xA': 128 | result.add("\\x0A") 129 | of '\"': 130 | result.add("\\\"") 131 | else: 132 | result.add(ch) 133 | 134 | var nodes: seq[NimNode] = @[] 135 | var fragments = toSeq(fmt.interpolatedFragments) 136 | for idx in 0.. 1 and idx > 0 and fragments[idx-1][0] in {ikVar, ikExpr}: 144 | nodes.del(nodes.len-1) 145 | handleFormat(fragments[idx-1][1], v, nodes) 146 | else: 147 | nodes.add(parseExpr("\"" & v.esc & "\"")) 148 | else: 149 | nodes.add(parseExpr("$(" & v & ")")) 150 | result = newNimNode(nnkStmtList).add( 151 | foldr(nodes, a.infix("&", b))) 152 | -------------------------------------------------------------------------------- /src/boost/typeclasses.nim: -------------------------------------------------------------------------------- 1 | ## Common typeclasses definitions 2 | 3 | import types 4 | 5 | {.hint[XDeclaredButNotUsed]: off.} 6 | 7 | type 8 | NonVoid* = concept x 9 | ## Non-void type 10 | x isnot void 11 | Eq* = concept x, y 12 | ## Equality class 13 | (x == y) is bool 14 | # We can't use ``!=`` here because of issue https://github.com/nim-lang/Nim/issues/4020 15 | Ord* = concept x, y 16 | ## Ordered class 17 | x is Eq and y is Eq 18 | (x < y) is bool 19 | (x <= y) is bool 20 | # We can't use ``>`` and ``>=`` here because of issue https://github.com/nim-lang/Nim/issues/4020 21 | -------------------------------------------------------------------------------- /src/boost/types.nim: -------------------------------------------------------------------------------- 1 | ## This module provides some useful data types definitions. 2 | 3 | type Unit* = tuple[] 4 | ## The type that has only one instance: ``()``. 5 | ## It's an alternative to `void` that allows using it as a value. 6 | -------------------------------------------------------------------------------- /tests/boost/data/test_memory.nim: -------------------------------------------------------------------------------- 1 | import boost/data/memory, 2 | unittest 3 | 4 | suite "Memory": 5 | test "findInMem": 6 | var buf = "0123456789" 7 | var s1 = "123" 8 | var s2 = "8910" 9 | var s3 = "111" 10 | 11 | check: findInMem(addr buf[0], buf.len, addr s1[0], s1.len) == (1, 3) 12 | check: findInMem(addr buf[0], buf.len, addr s2[0], s2.len) == (8, 2) 13 | check: findInMem(addr buf[0], buf.len, addr s3[0], s3.len) == (-1, 0) 14 | -------------------------------------------------------------------------------- /tests/boost/data/test_props.nim: -------------------------------------------------------------------------------- 1 | import boost/data/props, unittest 2 | 3 | suite "Props": 4 | test "Props": 5 | var p = {"a": "a", "b": "b", "a": "c"}.newProps 6 | p["a"] = "A" 7 | p.add("b", "B") 8 | check: p["a"] == "A" 9 | check: p["b"] == "b,B" 10 | p["a"] = "a" 11 | p["b"] = "b" 12 | p["c"] = "c" 13 | for n, v in p: 14 | check: n == v 15 | check: p.toSeq == @{"a": "a", "b": "b", "c": "c"} 16 | 17 | p.add("a", "A", delimiter="|") 18 | check: p["a"] == "a|A" 19 | 20 | p.add("a", "A", overwrite = true) 21 | check: p["a"] == "A" 22 | p.clear 23 | check: p.len == 0 24 | -------------------------------------------------------------------------------- /tests/boost/data/test_rbtree.nim: -------------------------------------------------------------------------------- 1 | import unittest, boost/data/rbtree, sets, random, sequtils, algorithm, random 2 | 3 | suite "RBTree": 4 | test "Initialization": 5 | let t = newRBSet[int]() 6 | discard t 7 | 8 | test "Insert": 9 | check: newRBTree[int, string]().len == 0 10 | check: newRBTree[int, string]().add(1, "a").add(2, "b").add(3, "c").len == 3 11 | check: newRBSet[int]().len == 0 12 | check: newRBSet[int]().add(1).add(2).add(3).len == 3 13 | 14 | check: toSeq(mkRBSet([1, 2, 3]).items) == @[1, 2, 3] 15 | check: toSeq(mkRBSet([1, 2, 3]).keys) == @[1, 2, 3] 16 | let t = mkRBTree([(1, "a"), (2, "b"), (3, "c")]) 17 | check: toSeq(t.pairs) == @[(1, "a"), (2, "b"), (3, "c")] 18 | check: toSeq(t.values) == @["a", "b", "c"] 19 | check: t.add(1, "A").getOrDefault(1) == "A" 20 | 21 | test "Find": 22 | var t = newRBTree[int, string]() 23 | var v = "" 24 | for i in 0..100: 25 | t = t.add(i, $i) 26 | for i in 0..100: 27 | check: t.hasKey(i) 28 | check: t.getOrDefault(i) == $i 29 | check: t.maybeGet(i, v) 30 | check: v == $i 31 | check: not t.maybeGet(101, v) 32 | 33 | test "Equality": 34 | var t1 = mkRBTree([(1, "a"), (2, "b"), (3, "c")]) 35 | var t2 = mkRBTree([(3, "c"), (2, "b"), (1, "a")]) 36 | check: t1 == t2 37 | check: t1 != t2.add(4, "d") 38 | 39 | var t3 = mkRBSet([1, 2, 3]) 40 | var t4 = mkRBSet([3, 1, 2]) 41 | check: t3 == t4 42 | check: t3 != t4.add(4) 43 | 44 | test "Delete": 45 | var t = newRBSet[int]() 46 | for i in 1..5: 47 | t = t.add(i) 48 | for i in 1..5: 49 | t = t.del(i) 50 | 51 | test "Length": 52 | var t = mkRBTree([(1, "a"), (2, "b"), (3, "c")]) 53 | check: t.len == 3 54 | t = t.del(100) 55 | check: t.len == 3 56 | t = t.del(1) 57 | check: t.len == 2 58 | var s = mkRBSet([1,2,3,4]) 59 | check: s.len == 4 60 | s = s.del(100) 61 | check: s.len == 4 62 | s = s.del(1) 63 | check: s.len == 3 64 | 65 | test "Stress": 66 | randomize(1234) 67 | var t = newRBSet[int]() 68 | const SIZE = 10_000 69 | var indices = newSeq[int](SIZE) 70 | for i in 0..Not halted!" 27 | 28 | get "/guess/@who": 29 | if @"who" != "Frank": pass() 30 | resp "You've found me!" 31 | 32 | get "/guess/@_": 33 | resp "Haha. You will never find me!" 34 | 35 | get "/redirect/@url/?": 36 | redirect(uri(@"url")) 37 | 38 | get "/win": 39 | cond rand(4) < 3 40 | resp "You won!" 41 | 42 | get "/win": 43 | resp "Try your luck again, loser." 44 | 45 | get "/profile/@id/@value?/?": 46 | var html = "" 47 | html.add "Msg: " & @"id" & 48 | "
Name: " & @"value" 49 | html.add "
" 50 | html.add "Params: " & $request.params 51 | 52 | resp html 53 | 54 | get "/attachment": 55 | attachment "public/root/index.html" 56 | resp "blah" 57 | 58 | get "/error": 59 | proc blah = raise newException(Exception, "BLAH BLAH BLAH") 60 | blah() 61 | 62 | get "/live": 63 | await response.sendHeaders() 64 | for i in 0 .. 10: 65 | await response.send("The number is: " & $i & "
") 66 | await sleepAsync(1000) 67 | response.client.close() 68 | 69 | # curl -v -F file='blah' http://dom96.co.cc:5000 70 | # curl -X POST -d 'test=56' localhost:5000/post 71 | 72 | post "/post": 73 | body.add "Received:
" 74 | let fd = await request.formData.toMultiData 75 | body.add($fd) 76 | body.add "
\n" 77 | body.add($request.params) 78 | 79 | status = Http200 80 | 81 | get "/post": 82 | resp """ 83 |
84 | First name:
85 | Last name:
86 | 87 |
""" % [uri("/post", absolute = false)] 88 | 89 | get "/file": 90 | resp """ 91 |
93 | 94 | 95 |
96 | 97 |
""" 98 | 99 | get re"^\/([0-9]{2})\.html$": 100 | resp request.matches[0] 101 | 102 | patch "/patch": 103 | body.add "Received: " 104 | let b: string = await request.body.getStream.readAll 105 | body.add($b) 106 | status = Http200 107 | 108 | post "/simplePost": 109 | resp(await request.body.getStream.readAll) 110 | 111 | post "/multipart": 112 | let mp = request.formData 113 | var acc = newSeq[string]() 114 | while not mp.atEnd: 115 | let part = await mp.readNextPart() 116 | if part == nil: break 117 | 118 | acc.add(part.contentDisposition.name) 119 | 120 | let stream = part.getPartDataStream() 121 | if stream == nil: break 122 | 123 | let contentFut = readAll(stream) 124 | let content = await contentFut 125 | acc.add(content) 126 | 127 | resp(acc.join(",")) 128 | 129 | runForever() 130 | 131 | spawn run() 132 | 133 | runSocketServer() 134 | #TODO: Wait for server to start 135 | sleep(1000) 136 | 137 | proc url(path: string): auto = "http://" & HOST & ":" & $(PORT.int) & "/foo" & path 138 | -------------------------------------------------------------------------------- /tests/boost/http/test_asyncchunkedstream.nim: -------------------------------------------------------------------------------- 1 | import asyncdispatch, unittest, strutils, random, sequtils, sugar 2 | import boost/io/asyncstreams, boost/http/asyncchunkedstream 3 | import boost/http/httpcommon 4 | 5 | const allChars: seq[char] = toSeq('\0'..'\255') 6 | const allCharsExceptNewline = allChars.filter(t => t notIn {'\c', '\L'}) 7 | 8 | type 9 | # Restricts `readImpl` to read 1 to `maxBytesRead` bytes per call 10 | ThrottleStream = ref ThrottleStreamObj 11 | ThrottleStreamObj = object of AsyncStream 12 | src: AsyncStream 13 | maxBytesRead: Natural 14 | 15 | proc tRead(s: AsyncStream, buf: pointer, size0: int): Future[int] {.gcsafe.} = 16 | let size = rand(1..min(size0, s.ThrottleStream.maxBytesRead)) 17 | s.ThrottleStream.src.readBuffer(buf, size) 18 | 19 | proc newThrottleStream( 20 | input: AsyncStream, 21 | maxBytesRead: Natural = 4 22 | ): ThrottleStream = 23 | new result 24 | result.src = input 25 | wrapAsyncStream(ThrottleStream, src) 26 | 27 | result.maxBytesRead = maxBytesRead 28 | result.readImpl = cast[type(result.readImpl)](tRead) 29 | 30 | proc randomBool(p: float = 0.5): bool = rand(1.0) < p 31 | 32 | proc randomString(size: Natural, chars: openarray[char] = allChars): string = 33 | result = newString(size) 34 | for i in 0.. 0: 68 | let cnt = if left < buff.len: left else: buff.len 69 | discard s.send(addr buff[0], cnt) 70 | left -= cnt 71 | 72 | var line = "" 73 | discard s.recv(line, 1024) 74 | result = line.splitLines[^1] 75 | 76 | suite "asynchttpserver": 77 | 78 | let url = "http://" & HOST & ":" & $PORT.int 79 | 80 | spawn serverThread() 81 | #TODO: Wait for server to start 82 | sleep(1000) 83 | defer: discard newHttpClient().postContent(url & "/quit", "") 84 | 85 | test "GET": 86 | check: newHttpClient().getContent(url) == "Hello, world!" 87 | 88 | test "POST": 89 | check: url.postRequest(body = "Hi!", count = 1) == "Hi!" 90 | 91 | test "POST (count)": 92 | check: (url & "/count").postRequest(count = 2_000_000) == $2_000_000 93 | 94 | test "Ignoring POST body shouldn't break connection": 95 | let client = newHttpClient() 96 | let body = "foo\c\L\c\Lbar\c\Lbaz" 97 | check: client.postContent(url & "/discardbody", body = body) == "discarded" 98 | check: client.getContent(url) == "Hello, world!" 99 | 100 | test "Should support requests with chunked encoding": 101 | # Client can't send chunked requests, so we drop down to sockets 102 | let reqLine = "POST " & url & "/ HTTP/1.1\c\L" 103 | let headers = "Transfer-Encoding: chunked\c\L" 104 | let body = "3\c\Lfoo\c\L3\c\Lbar\c\L0\c\L\c\L" 105 | let request = reqLine & headers & "\c\L" & body 106 | var s = newSocket() 107 | s.connect(HOST, PORT) 108 | s.send(request) 109 | # Skip the headers and get to the body 110 | while s.recvLine(100) != "\c\L": 111 | discard 112 | 113 | let respBody = s.recv(6) 114 | check: respBody == "foobar" 115 | 116 | test "Should fail for request with invalid transfer encoding": 117 | let reqLine = "POST " & url & "/ HTTP/1.1\c\L" 118 | let headers = "Transfer-Encoding: invalid\c\L" 119 | let body = "invalid" 120 | 121 | let request = reqLine & headers & "\c\L" & body 122 | var s = newSocket() 123 | s.connect(HOST, PORT) 124 | s.send(request) 125 | 126 | # Don't read more than we need to - we'll lock if we get past the message 127 | let resp = s.recv(12, 1000) 128 | check: resp == "HTTP/1.1 400" 129 | 130 | test "Should fail for request with invalid content length": 131 | let reqLine = "POST " & url & "/ HTTP/1.1\c\L" 132 | let headers = "Content-Length: invalid\c\L" 133 | let body = "invalid" 134 | 135 | let request = reqLine & headers & "\c\L" & body 136 | var s = newSocket() 137 | s.connect(HOST, PORT) 138 | s.send(request) 139 | 140 | # Don't read more than we need to - we'll lock if we get past the message 141 | let resp = s.recv(12, 1000) 142 | check: resp == "HTTP/1.1 400" 143 | 144 | test "Malformed chunked messages in discarded body should not crash the server": 145 | # Client can't send chunked requests, so we drop down to sockets 146 | let reqLine = "POST " & url & "/discardbody HTTP/1.1\c\L" 147 | let headers = "Transfer-Encoding: chunked\c\L" 148 | let body = "invalid\c\Lmessage" 149 | let request = reqLine & headers & "\c\L" & body 150 | var s = newSocket() 151 | s.connect(HOST, PORT) 152 | s.send(request) 153 | 154 | # Is there a better way? 155 | sleep(100) 156 | 157 | # Check that the server is working 158 | check: newHttpClient().getContent(url) == "Hello, world!" 159 | 160 | test "Malformed chunked messages in callback should not crash the server": 161 | # Client can't send chunked requests, so we drop down to sockets 162 | let reqLine = "POST " & url & "/ HTTP/1.1\c\L" 163 | let headers = "Transfer-Encoding: chunked\c\L" 164 | let body = "invalid\c\Lmessage" 165 | let request = reqLine & headers & "\c\L" & body 166 | var s = newSocket() 167 | s.connect(HOST, PORT) 168 | s.send(request) 169 | 170 | sleep(100) 171 | 172 | # Check that the server is working 173 | check: newHttpClient().getContent(url) == "Hello, world!" 174 | -------------------------------------------------------------------------------- /tests/boost/http/test_asynchttpserver_internals.nim: -------------------------------------------------------------------------------- 1 | ## This is in a separate file since we need access to private functions, so 2 | ## we have to include rather than import. 3 | 4 | include boost/http/asynchttpserver 5 | 6 | import unittest, 7 | asyncdispatch, 8 | strutils, 9 | boost/io/asyncstreams 10 | 11 | suite "RequestBodyStream": 12 | test "should fulfill its contracts": 13 | # This is a catch-all test that checks a few invariants with the same setup. 14 | # Splitting it would lead to too much duplication/complexity. 15 | 16 | # We have no internal buffering, and we don't look at the data, so this 17 | # should be enough. 18 | let bodyStr = "0123456789abcdef" 19 | let followingData = "#####" 20 | # We check the case when we ask for more bytes than the body contains, too. 21 | for i in 0..bodyStr.len.succ: 22 | for j in max(0, bodyStr.len - i)..bodyStr.len.succ: 23 | # TODO: This produces a lot of output on failure 24 | # https://github.com/nim-lang/Nim/issues/6376 25 | checkpoint("i = $#, j = $#" % [$i, $j]) 26 | let underlying = newAsyncStringStream(bodyStr & followingData) 27 | let body = newRequestBodyStream(underlying, bodyStr.len) 28 | let s1 = waitFor(body.readData(i)) 29 | 30 | let expectedAtEnd = (i >= bodyStr.len) 31 | check: body.atEnd == expectedAtEnd 32 | 33 | let s2 = waitFor(body.readData(j)) 34 | let s3 = waitFor(body.readAll) 35 | 36 | let expectedS1 = bodyStr[0..Content of a.html. 25 | -----------------------------9051914041544843365972754266-- 26 | 27 | """.replace("\n", "\c\L") 28 | 29 | const MPWithPreamble = """This is the preamble. 30 | It should be ignored 31 | --12boundary34 32 | Content-Disposition: form-data; name="foo" 33 | Content-Type: text/plain 34 | 35 | First part. 36 | --12boundary34--""".replace("\n", "\c\L") 37 | 38 | # trigger partial detection branch for buffer size 10 39 | const TrickyMP = """--12345 40 | Content-Disposition: form-data; name="text" 41 | Convent-Transfer-Encoding: 8bit 42 | 43 | abc 44 | --1234(not a boundary yet)--12 45 | --12345-- 46 | """.replace("\n", "\c\L") 47 | 48 | suite "Multipart": 49 | test "read multipart message": 50 | let ct = "multipart/form-data; boundary=---------------------------9051914041544843365972754266".parseContentType 51 | let s = newAsyncStringStream(MP) 52 | 53 | let mp = MultiPartMessage.open(s, ct) 54 | check: not mp.atEnd 55 | 56 | var part = waitFor mp.readNextPart 57 | require: not part.isNil 58 | check: not mp.atEnd 59 | check: part.headers.toSeq == @{ 60 | "Content-Disposition": "form-data;name=\"text\"", 61 | "Content-Transfer-Encoding": "8bit" 62 | } 63 | check: part.encoding == "8bit" 64 | var ps = part.getPartDataStream() 65 | var data = waitFor ps.readAll 66 | check: data == "text default" 67 | 68 | part = waitFor mp.readNextPart 69 | require: not part.isNil 70 | check: not mp.atEnd 71 | check: part.headers.toSeq == @{ 72 | "Content-Disposition": "form-data; name=\"file1\"; filename=\"a.txt\"", 73 | "Content-Type": "text/plain" 74 | } 75 | ps = part.getPartDataStream() 76 | data = waitFor ps.readAll 77 | check: data == "Content of a.txt." 78 | 79 | part = waitFor mp.readNextPart 80 | require: not part.isNil 81 | check: not mp.atEnd 82 | check: part.headers.toSeq == @{ 83 | "Content-Disposition": "form-data; name=\"file2\"; filename=\"a.html\"", 84 | "Content-Type": "text/html" 85 | } 86 | ps = part.getPartDataStream() 87 | data = waitFor ps.readAll 88 | check: data == "Content of a.html." 89 | 90 | part = waitFor mp.readNextPart 91 | check: mp.atEnd 92 | check: part.isNil 93 | 94 | test "should skip preamble": 95 | let ct = "multipart/form-data; boundary=12boundary34".parseContentType 96 | let s = newAsyncStringStream(MPWithPreamble) 97 | 98 | let mp = MultipartMessage.open(s, ct) 99 | check: not mp.atEnd 100 | 101 | var part = waitFor mp.readNextPart 102 | require: not part.isNil 103 | check: not mp.atEnd 104 | check: part.headers.toSeq == @{ 105 | "Content-Disposition": "form-data; name=\"foo\"", 106 | "Content-Type": "text/plain" 107 | } 108 | 109 | var ps = part.getPartDataStream() 110 | var data = waitFor ps.readAll 111 | check: data == "First part." 112 | 113 | part = waitFor mp.readNextPart 114 | check: mp.atEnd 115 | check: part.isNil 116 | 117 | test "should properly handle part of boundary in content": 118 | 119 | # Small enough to trigger partial boundary detection, but large 120 | # enough to store the boundary 121 | const BufSize = 10 122 | 123 | let ct = "multipart/form-data; boundary=12345".parseContentType 124 | let s = newAsyncStringStream(TrickyMP) 125 | let mp = MultipartMessage.open(s, ct) 126 | check: not mp.atEnd 127 | 128 | var part = waitFor mp.readNextPart 129 | require: not part.isNil 130 | check: not mp.atEnd 131 | 132 | let ps = part.getPartDataStream() 133 | 134 | # Read the part by small chunks 135 | var acc = "" 136 | while not ps.atEnd: 137 | let data = waitFor ps.readData(BufSize) 138 | check: data.len <= BufSize 139 | acc.add(data) 140 | 141 | check: acc == "abc\c\L--1234(not a boundary yet)--12" 142 | 143 | test "should work even when the buffer doesn't fit the boundary": 144 | 145 | # shorter than the boundary 146 | const BufSize = 4 147 | 148 | let ct = "multipart/form-data; boundary=12345".parseContentType 149 | let s = newAsyncStringStream(TrickyMP) 150 | let mp = MultipartMessage.open(s, ct) 151 | check: not mp.atEnd 152 | 153 | var part = waitFor mp.readNextPart 154 | require: not part.isNil 155 | check: not mp.atEnd 156 | 157 | let ps = part.getPartDataStream() 158 | 159 | # Read the part by small chunks 160 | var acc = "" 161 | while not ps.atEnd: 162 | let data = waitFor ps.readData(BufSize) 163 | check: data.len <= BufSize 164 | acc.add(data) 165 | 166 | check: acc == "abc\c\L--1234(not a boundary yet)--12" 167 | -------------------------------------------------------------------------------- /tests/boost/io/test_asyncstreams.nim: -------------------------------------------------------------------------------- 1 | import boost/io/asyncstreams 2 | import unittest, asyncdispatch, asyncnet, threadpool, os, strutils 3 | 4 | const PORT = Port(9999) 5 | 6 | {.warning[SmallLshouldNotBeUsed]: off.} 7 | 8 | suite "asyncstreams": 9 | 10 | when not defined(noNet): 11 | test "AsyncSocketStream": 12 | proc runSocketServer = 13 | proc serve {.async.} = 14 | var s = newAsyncSocket() 15 | s.bindAddr(PORT) 16 | s.listen 17 | let c = newAsyncSocketStream(await s.accept) 18 | let ch = await c.readChar 19 | await c.writeChar(ch) 20 | let line = await c.readLine 21 | await c.writeLine("Hello, " & line) 22 | c.close 23 | s.close 24 | 25 | proc run {.gcsafe.} = 26 | waitFor serve() 27 | 28 | spawn run() 29 | 30 | runSocketServer() 31 | 32 | proc doTest {.async.} = 33 | let s = newAsyncSocket() 34 | await s.connect("localhost", PORT) 35 | let c = newAsyncSocketStream(s) 36 | 37 | await c.writeChar('A') 38 | let ch = await c.readChar 39 | check: ch == 'A' 40 | 41 | await c.writeLine("World!") 42 | let line = await c.readLine 43 | check: line == "Hello, World!" 44 | 45 | #TODO: Wait for server to start 46 | sleep(500) 47 | waitFor doTest() 48 | 49 | test "AsyncFileStream": 50 | proc doTest {.async.} = 51 | let fname = getTempDir() / "asyncstreamstest.nim" 52 | var s = newAsyncFileStream(fname, fmReadWrite) 53 | await s.writeLine("Hello, world!") 54 | s.setPosition(0) 55 | let line = await s.readLine 56 | check: line == "Hello, world!" 57 | check: not s.atEnd 58 | discard await s.readLine 59 | check: s.atEnd 60 | 61 | # File doesn't implement neither peekBuffer nor peekLine operations. 62 | # But it allows get/set position operation. So this must work 63 | s.setPosition(0) 64 | let l1 = await s.peekLine 65 | let l2 = await s.readLine 66 | check: l1 == l2 67 | 68 | s.close 69 | fname.removeFile 70 | waitFor doTest() 71 | 72 | proc doAsyncStringStreamTest(s: AsyncStream) {.async.} = 73 | await s.writeLine("Hello, world!") 74 | await s.flush 75 | s.setPosition(0) 76 | let pline = await s.peekLine 77 | let line = await s.readLine 78 | check: line == pline 79 | check: line == "Hello, world!" 80 | check: not s.atEnd 81 | discard await s.readLine 82 | check: s.atEnd 83 | 84 | test "AsyncStringStream": 85 | waitFor doAsyncStringStreamTest(newAsyncStringStream()) 86 | 87 | test "AsyncStreamWrapper": 88 | type 89 | WS = ref WSObj 90 | WSObj = object of AsyncStreamObj 91 | s: AsyncStream 92 | 93 | proc newWS(s: AsyncStream): WS = 94 | new result 95 | result.s = s 96 | wrapAsyncStream(WS, s) 97 | 98 | waitFor doAsyncStringStreamTest(newWS(newAsyncStringStream())) 99 | 100 | test "Operations": 101 | proc doTest {.async.} = 102 | let s = newAsyncStringStream() 103 | 104 | await s.writeChar('H') 105 | s.setPosition(0) 106 | let ch = await s.readChar 107 | check: ch == 'H' 108 | 109 | s.setPosition(0) 110 | await s.writeLine("Hello, world!") 111 | s.setPosition(0) 112 | let line = await s.readLine 113 | check: line == "Hello, world!" 114 | 115 | # String doesn't implement neither peekBuffer nor peekLine operations. 116 | # But it allows get/set position operation. So this must work 117 | s.setPosition(0) 118 | let l1 = await s.peekLine 119 | let l2 = await s.readLine 120 | check: l1 == l2 121 | 122 | s.setPosition(0) 123 | let all = await s.readAll 124 | check: all == "Hello, world!\n" 125 | 126 | s.setPosition(0) 127 | await s.writeByte(42) 128 | s.setPosition(0) 129 | let b = await s.readByte 130 | check: b == 42 131 | 132 | s.setPosition(0) 133 | await s.writeFloat(1.0) 134 | s.setPosition(0) 135 | let f = await s.readFloat 136 | check: f == 1.0 137 | 138 | s.setPosition(0) 139 | await s.writeBool(true) 140 | s.setPosition(0) 141 | let bo = await s.readBool 142 | check: bo 143 | 144 | s.setPosition(1000) 145 | try: 146 | discard await s.readBool 147 | except IOError: 148 | return 149 | check: false 150 | waitFor doTest() 151 | 152 | test "Example for the documentation": 153 | proc main {.async.} = 154 | var s = newAsyncStringStream("""Hello 155 | world!""") 156 | var res = newSeq[string]() 157 | while true: 158 | let l = await s.readLine() 159 | if l.len == 0: 160 | break 161 | res.add(l) 162 | doAssert(res.join(", ") == "Hello, world!") 163 | waitFor main() 164 | 165 | test "Buffered stream": 166 | proc doTest {.async.} = 167 | # 100 bytes of data 168 | const data = "01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789" 169 | let ss = newAsyncStringStream(data) 170 | # Read 10 bytes chunks 171 | let bs = newAsyncBufferedStream(ss, 10) 172 | 173 | var pd = await bs.peekData(5) 174 | var rd = await bs.readData(5) 175 | check: rd == pd 176 | check: rd == "01234" 177 | 178 | pd = await bs.peekData(5) 179 | rd = await bs.readData(5) 180 | check: rd == pd 181 | check: rd == "56789" 182 | 183 | pd = await bs.peekData(10) 184 | rd = await bs.readData(10) 185 | check: rd == pd 186 | check: rd == "0123456789" 187 | 188 | pd = await bs.peekData(100) 189 | rd = await bs.readData(100) 190 | check: rd == pd 191 | check: rd == "0123456789" 192 | 193 | # If we read the data, we get just buffered bytes 194 | discard await bs.readData(7) 195 | rd = await bs.readData(4) 196 | check: rd == "789" 197 | 198 | # But if we peek the data, we can get more 199 | discard await bs.readData(7) 200 | pd = await bs.peekData(4) 201 | rd = await bs.readData(4) 202 | check: rd == pd 203 | check: rd == "7890" 204 | 205 | bs.setPosition(0) 206 | pd = await bs.peekLine 207 | check: pd == data 208 | 209 | waitFor doTest() 210 | 211 | test "Buffered stream without position control": 212 | proc doTest {.async.} = 213 | # 20 bytes of data 214 | const data = "01234567890123456789\Labcde" 215 | let ss = newAsyncStringStream(data) 216 | # Mock stream without position control 217 | ss.getPositionImpl = nil 218 | ss.setPositionImpl = nil 219 | 220 | # Read 10 bytes chunks 221 | let bs = newAsyncBufferedStream(ss, 10) 222 | 223 | var pd = await bs.peekData(5) 224 | check: pd == "01234" 225 | pd = await bs.peekData(5) 226 | check: pd == "01234" 227 | 228 | var rd = await bs.readData(5) 229 | check: rd == "01234" 230 | 231 | pd = await bs.peekData(5) 232 | check: pd == "56789" 233 | 234 | rd = await bs.readData(5) 235 | check: rd == "56789" 236 | 237 | pd = await bs.peekLine 238 | check: pd == "0123456789" 239 | 240 | rd = await bs.readData(7) 241 | check: rd == "0123456" 242 | 243 | pd = await bs.peekLine 244 | check: pd == "789" 245 | 246 | rd = await bs.readLine 247 | check: rd == "789" 248 | 249 | pd = await bs.peekLine 250 | check: pd == "abcde" 251 | 252 | rd = await bs.readLine 253 | check: rd == "abcde" 254 | 255 | pd = await bs.peekLine 256 | check: pd.len == 0 257 | 258 | rd = await bs.readLine 259 | check: rd.len == 0 260 | 261 | waitFor doTest() 262 | -------------------------------------------------------------------------------- /tests/boost/test_all.nim: -------------------------------------------------------------------------------- 1 | when defined(js): 2 | # Core 3 | import test_typeclasses, 4 | test_limits, 5 | test_parsers, 6 | test_formatters, 7 | test_richstring, 8 | test_typeutils 9 | # Data 10 | import data/test_stackm, 11 | data/test_rbtreem, 12 | data/test_rbtree 13 | #TODO: https://github.com/vegansk/nimboost/issues/5 14 | else: 15 | # Core 16 | import test_limits, 17 | test_parsers, 18 | test_typeclasses, 19 | test_formatters, 20 | test_richstring, 21 | test_typeutils 22 | # Data 23 | import data/test_stackm, 24 | data/test_rbtreem, 25 | data/test_rbtree, 26 | data/test_props, 27 | data/test_memory 28 | 29 | # HTTP - pure parts 30 | # asyncstreams test slows down execution for some reason (dispatcher?). So we 31 | # run "heavy" tests before that. 32 | import http/test_httpcommon, 33 | http/test_multipart, 34 | http/test_asyncchunkedstream, 35 | http/test_asynchttpserver_internals 36 | 37 | # I/O 38 | import io/test_asyncstreams 39 | 40 | # HTTP server 41 | import http/test_asynchttpserver, 42 | http/test_jester 43 | -------------------------------------------------------------------------------- /tests/boost/test_formatters.nim: -------------------------------------------------------------------------------- 1 | import boost/formatters, 2 | unittest 3 | 4 | suite "formatters": 5 | test "intToStr": 6 | # Simple cases 7 | check: intToStr(0) == "0" 8 | when not defined(js): 9 | check: intToStr(1'u64) == "1" 10 | check: intToStr(-123456) == "-123456" 11 | check: intToStr(123.456'f64) == "123" 12 | 13 | # Max length and fill char 14 | check: intToStr(0, len = 3, fill = '0') == "000" 15 | check: intToStr(-100, len = 5) == " -100" 16 | check: intToStr(-100, len = 5, fill = '0') == "-0100" 17 | 18 | # Radix 19 | check: intToStr(10, radix = 16) == "A" 20 | check: intToStr(10, radix = 2, len = 8, fill = '0') == "00001010" 21 | 22 | # Negative justify 23 | check: intToStr(123, len = -5) == "123 " 24 | check: intToStr(-123, len = -5, fill = '0') == "-1230" 25 | 26 | test "alignStr": 27 | check: alignStr("abc", 5, '.') == "..abc" 28 | check: alignStr("abc", -5, '.') == "abc.." 29 | check: alignStr("abcd", 2, trunc = true) == "ab" 30 | check: alignStr("abcd", 2, trunc = false) == "abcd" 31 | check: alignStr("abcd", 5, trunc = false) == " abcd" 32 | check: alignStr("abcd", 5, trunc = true) == " abcd" 33 | 34 | test "floatToStr": 35 | check: floatToStr(1.0) == "1" 36 | check: floatToStr(10, prec = 3) == "10.000" 37 | check: floatToStr(-20, prec = 3, fill = '0', len = 8) == "-020.000" 38 | check: floatToStr(-20, prec = 3, fill = '0', len = -8) == "-20.0000" 39 | let fs = floatToStr(-20, scientific = true, prec = 3) 40 | when defined(js): 41 | check: fs == "-2.000e+1" 42 | else: 43 | # On Windows we get three digits in the exponent by default: 44 | # https://msdn.microsoft.com/en-us/library/0fatw238.aspx 45 | check: fs in ["-2.000e+01", "-2.000e+001"] 46 | -------------------------------------------------------------------------------- /tests/boost/test_limits.nim: -------------------------------------------------------------------------------- 1 | import unittest, boost/limits, typetraits 2 | 3 | # Workaround for https://github.com/nim-lang/Nim/issues/4714 4 | when defined(js): 5 | proc `$`(x: uint): string = 6 | x.int64.`$` 7 | 8 | template testLimit(t: typedesc): untyped = 9 | echo "Limits for ", t.name, " is [", t.min, "..", t.max, "]" 10 | 11 | suite "Limits": 12 | test "Output limits": 13 | int8.testLimit 14 | int16.testLimit 15 | int32.testLimit 16 | int64.testLimit 17 | int.testLimit 18 | 19 | uint8.testLimit 20 | uint16.testLimit 21 | uint32.testLimit 22 | when not defined(js): 23 | uint64.testLimit 24 | uint.testLimit 25 | -------------------------------------------------------------------------------- /tests/boost/test_parsers.nim: -------------------------------------------------------------------------------- 1 | import unittest, boost/parsers, boost/limits 2 | 3 | from logging import nil 4 | 5 | {.hint[XDeclaredButNotUsed]: off.} 6 | 7 | template checkInt(typ: untyped, fun: untyped): untyped = 8 | check: fun($typ.min) == typ.min 9 | check: fun($(typ.min + 13)) == typ.min + 13 10 | check: fun($typ.max) == typ.max 11 | check: fun($(typ.max - 77)) == typ.max - 77 12 | expect(ValueError): discard fun("ZERO") 13 | when sizeof(typ) != 8 and not defined(js): # Bug https://github.com/nim-lang/Nim/issues/4714 14 | expect(ValueError): discard fun($(typ.min.int64 - 1)) 15 | expect(ValueError): discard fun($(typ.max.int64 + 1)) 16 | 17 | template checkUInt(typ: untyped, fun: untyped): untyped = 18 | check: fun($typ.min) == typ.min 19 | check: fun($(typ.min + 13)) == typ.min + 13 20 | check: fun($typ.max) == typ.max 21 | check: fun($(typ.max - 77)) == typ.max - 77 22 | expect(ValueError): discard fun("ZERO") 23 | when sizeof(typ) != 8: 24 | expect(ValueError): discard fun($(typ.max.int64 + 1)) 25 | 26 | suite "Parsers": 27 | test "Ordinal types from string": 28 | when not defined(js): # Bug https://github.com/nim-lang/Nim/issues/4714 29 | int64.checkInt(strToInt64) 30 | int.checkInt(strToInt) 31 | int32.checkInt(strToInt32) 32 | int16.checkInt(strToInt16) 33 | int8.checkInt(strToInt8) 34 | 35 | when not defined(js): # Bug https://github.com/nim-lang/Nim/issues/4714 36 | uint64.checkUInt(strToUInt64) 37 | uint.checkUInt(strToUInt) 38 | uint32.checkUInt(strToUInt32) 39 | uint16.checkUInt(strToUInt16) 40 | uint8.checkUInt(strToUInt8) 41 | 42 | test "Radix": 43 | check: "10".strToInt(10) == 10 44 | check: "a".strToInt(16) == 10 45 | check: "A".strToInt(16) == 10 46 | check: "deadbeef".strToUInt(16) == 0xdeadbeef'u 47 | expect(ValueError): discard "deadbeef".strToInt32(16) 48 | 49 | test "Enums": 50 | check: parseEnum[logging.Level]("lvlAll") == logging.lvlAll 51 | expect(ValueError): discard parseEnum[logging.Level]("lvlAlll") 52 | check: parseEnum[bool]("true") == true 53 | check: parseEnum[bool]("false") == false 54 | expect(ValueError): discard parseEnum[bool]("unknown") 55 | -------------------------------------------------------------------------------- /tests/boost/test_richstring.nim: -------------------------------------------------------------------------------- 1 | import boost/richstring, 2 | strutils, 3 | boost/parsers, 4 | unittest 5 | 6 | {.warning[Deprecated]: off.} 7 | 8 | suite "richstring": 9 | test "string interpolation": 10 | # Simple cases 11 | check: fmt"Hello, world!" == "Hello, world!" 12 | let v = 1 13 | check: fmt"v = $v" == "v = 1" 14 | check: fmt"v = $$v" == "v = $v" 15 | let s = "string" 16 | check: not compiles(fmt(s)) 17 | check: fmt"${s[0..2].toUpperAscii}" == "STR" 18 | 19 | # Int formatters 20 | check: fmt"$v%" == "1%" 21 | check: fmt"$v%%" == "1%" 22 | check: fmt"$v%d" == "1" 23 | check: fmt"${-10}%04d" == "-010" 24 | check: fmt"${-10}%-04d" == "-100" 25 | check: fmt"${-10}%4d" == " -10" 26 | check: fmt"${-10}%-4d" == "-10 " 27 | check: fmt"${10}%x" == "a" 28 | check: fmt"0x${10}%02x" == "0x0a" 29 | check: fmt"${10}%X" == "A" 30 | check: fmt"0x${10}%02X" == "0x0A" 31 | 32 | # String formatters 33 | check: fmt"""${"test"}%s""" == "test" 34 | check: fmt"""${"test"}%5s""" == " test" 35 | check: fmt"""${"test"}%-5s""" == "test " 36 | 37 | # Float formatters 38 | check: fmt"${1}%f" == "1" 39 | check: fmt"${1}%.3f" == "1.000" 40 | check: fmt"${1}%3f" == " 1" 41 | check: fmt"${-1}%08.3f" == "-001.000" 42 | check: fmt"${-1}%-08.3f" == "-1.00000" 43 | check: fmt"${-1}%-8.3f" == "-1.000 " 44 | when defined(js): 45 | check: fmt"${1}%e" == "1.000000e+0" 46 | elif defined(windows): 47 | # This differs between versions! 48 | check: fmt"${1}%e" in ["1.000000e+00", "1.000000e+000"] 49 | else: 50 | check: fmt"${1}%e" == "1.000000e+00" 51 | 52 | # Escape characters 53 | check: fmt"\x0A" == "\L" 54 | check: fmt"\10" == "\L" 55 | check: fmt"\L" == "\L" 56 | check: fmt"$$\L" == "$\L" 57 | check: fmt"""${"\L"}""" == "\L" 58 | 59 | # Multiline 60 | check: fmt""" 61 | test: 62 | "test": 63 | ${1}%02d 64 | ${2}%02d 65 | """.strip == "test:\n\"test\":\n01\n02" 66 | 67 | check: fmt("a\r\n\r\lb") == "a\r\n\r\lb" 68 | check: fmt"a\r\n\r\lb" == "a\r\n\r\lb" 69 | check: fmt("a\n\r\l\rb") == "a\n\r\l\rb" 70 | check: fmt"a\n\r\l\rb" == "a\n\r\l\rb" 71 | 72 | check: fmt"""foo 73 | 74 | bar""" == """foo 75 | 76 | bar""" 77 | -------------------------------------------------------------------------------- /tests/boost/test_typeclasses.nim: -------------------------------------------------------------------------------- 1 | import unittest, boost/typeclasses, typetraits 2 | 3 | # Needed for the ``check`` macro pretty print 4 | proc `$`(t: typedesc): auto = t.name 5 | 6 | {.warning[SmallLshouldNotBeUsed]: off.} 7 | 8 | type 9 | TestObj = object 10 | i: int 11 | s: string 12 | 13 | suite "Typeclasses": 14 | test "NonVoid": 15 | check: void isnot NonVoid 16 | check: int is NonVoid 17 | 18 | test "Eq": 19 | check: int is Eq 20 | check: string is Eq 21 | check: float is Eq 22 | let a = (1, 'a') 23 | check: a is Eq 24 | let b = @[1,2,3] 25 | check: b is Eq 26 | check: TestObj is Eq # Objects is Eq by default 27 | 28 | test "Ord": 29 | check: int is Ord 30 | check: string is Ord 31 | check: float is Ord 32 | 33 | let a = (1, 'a') 34 | check: a is Ord 35 | 36 | let b = @[1,2,3] 37 | check: b isnot Ord # Sequences is not Ord by default 38 | proc `<`[T:Ord](s1, s2: seq[T]): bool = 39 | let l = min(s1.len, s2.len) 40 | for i in 0..= s2[i]: 42 | return false 43 | return s1.len < s2.len 44 | proc `<=`[T:Ord](s1, s2: seq[T]): bool = 45 | s1 < s2 or s1 == s2 46 | check: b is Ord # But we can make them Ord 47 | 48 | check: TestObj isnot Ord # Objects can be compared by theirs fields values 49 | -------------------------------------------------------------------------------- /tests/boost/test_typeutils.nim: -------------------------------------------------------------------------------- 1 | import boost/typeutils, 2 | boost/jsonserialize, 3 | unittest, 4 | macros, 5 | ./test_typeutils_int, 6 | patty, 7 | json 8 | 9 | suite "typeutils - constructor": 10 | test "simple object": 11 | type 12 | X1 = object 13 | a: int 14 | b: seq[string] 15 | check: compiles(X1.genConstructor) 16 | when compiles(X1.genConstructor): 17 | genConstructor X1 18 | check: declared(initX1) 19 | when declared(initX1): 20 | let x1 = initX1(1, @["2"]) 21 | check: x1.a == 1 22 | check: x1.b == @["2"] 23 | 24 | type 25 | X2 = ref object 26 | a: int 27 | b: seq[string] 28 | check: compiles(X2.genConstructor) 29 | when compiles(X2.genConstructor): 30 | genConstructor X2 31 | check: declared(newX2) 32 | when declared(newX2): 33 | let x2 = newX2(1, @["2"]) 34 | check: x2.a == 1 35 | check: x2.b == @["2"] 36 | 37 | test "exported object": 38 | check: declared(createGlobalX) 39 | when declared(createGlobalX): 40 | let x3 = createGlobalX(1, @["2"]) 41 | check: x3.a == 1 42 | check: x3.b == @["2"] 43 | 44 | test "generic fields": 45 | type A[T] = object 46 | x: T 47 | 48 | check: compiles(A.genConstructor) 49 | when compiles(A.genConstructor): 50 | genConstructor A 51 | check: declared(initA) 52 | when declared(initA): 53 | let a = initA(1) 54 | check: a.x == 1 55 | 56 | type B[T] = ref object 57 | x: T 58 | 59 | check: compiles(B.genConstructor) 60 | when compiles(B.genConstructor): 61 | genConstructor B 62 | check: declared(newB) 63 | when declared(newB): 64 | let b = newB(1) 65 | check: b.x == 1 66 | 67 | 68 | test "ref object": 69 | type XObj = object 70 | x: int 71 | type X = ref XObj 72 | check: compiles(X.genConstructor) 73 | when compiles(X.genConstructor): 74 | genConstructor X 75 | check: declared(newX) 76 | when declared(newX): 77 | let x = newX(1) 78 | check: x.x == 1 79 | 80 | type YObj[T,U] = object 81 | y1: T 82 | y2: U 83 | type Y[T] = ref YObj[T, int] 84 | check: compiles(Y.genConstructor) 85 | when compiles(Y.genConstructor): 86 | genConstructor Y 87 | check: declared(newY) 88 | when declared(newY): 89 | let y = newY(1, 2) 90 | check: y.y1 == 1 91 | check: y.y2 == 2 92 | 93 | test "complex fields": 94 | type A = object 95 | x: int 96 | type B = object 97 | a: A 98 | 99 | check: compiles(B.genConstructor) 100 | when compiles(B.genConstructor): 101 | genConstructor B 102 | check: declared(initB) 103 | when declared(initB): 104 | let b = initB(A(x: 1)) 105 | check: b.a.x == 1 106 | 107 | type C[D] = object 108 | x: D 109 | type E = object 110 | c: C[int] 111 | 112 | check: compiles(E.genConstructor) 113 | when compiles(E.genConstructor): 114 | genConstructor E 115 | check: declared(initE) 116 | when declared(initE): 117 | let e = initE(C[int](x: 1)) 118 | check: e.c.x == 1 119 | 120 | suite "typeutils - data keyword": 121 | data A: 122 | a: int 123 | let b = "a" 124 | var c: seq[int] = @[1,2,3] 125 | test "simple type": 126 | check compiles(initA) 127 | when compiles(initA): 128 | let a = initA(1, c = @[1,2]) 129 | check a.a == 1 and a.b == "a" and a.c == @[1,2] 130 | 131 | data B of A: 132 | d = "d" 133 | test "simple types inheritance": 134 | check compiles(initB) 135 | when compiles(initB): 136 | let b = initB(1) 137 | check b.a == 1 and b.b == "a" and b.c == @[1,2,3] and b.d == "d" 138 | 139 | data ARef ref object: 140 | a: int 141 | let b = "a" 142 | var c: seq[int] = @[1,2,3] 143 | test "reference type": 144 | check compiles(newARef) 145 | when compiles(newARef): 146 | let a = newARef(1, c = @[1,2]) 147 | check a.a == 1 and a.b == "a" and a.c == @[1,2] 148 | 149 | data BRef ref object of ARef: 150 | d: string 151 | test "reference types inheritance": 152 | check compiles(newBRef) 153 | when compiles(newBRef): 154 | let b = newBRef(1, d = "d") 155 | check b.a == 1 and b.b == "a" and b.c == @[1,2,3] and b.d == "d" 156 | 157 | test "export types": 158 | check compiles(GlobalData) 159 | when compiles(GlobalData): 160 | check initGlobalData(1).a == 1 161 | check compiles(GlobalDataRef) 162 | when compiles(GlobalDataRef): 163 | check newGlobalDataRef(1).a == 1 164 | 165 | test "full qualified parent type name": 166 | const isValid = compiles((block: 167 | data GlobalDataChild of test_typeutils_int.GlobalData: 168 | b: string 169 | )) 170 | check isValid 171 | when isValid: 172 | data GlobalDataChild of test_typeutils_int.GlobalData: 173 | b: string 174 | let d = initGlobalDataChild(1, "a") 175 | check d.a == 1 and d.b == "a" 176 | const isValidRef = compiles((block: 177 | data GlobalDataRefChild ref object of test_typeutils_int.GlobalDataRef: 178 | b: string 179 | )) 180 | check isValidRef 181 | when isValidRef: 182 | data GlobalDataRefChild ref object of test_typeutils_int.GlobalDataRef: 183 | b: string 184 | let dRef = newGlobalDataRefChild(1, "a") 185 | check dRef.a == 1 and dRef.b == "a" 186 | 187 | test "[im]mutability": 188 | data Obj: 189 | a: int # Immutable, same as let a: int 190 | let b: int # Immutable 191 | var c: int # Mutable 192 | var x = initObj(1,2,3) 193 | check x.a == 1 and x.b == 2 and x.c == 3 194 | check: not compiles((block: x.a = 10)) 195 | check: not compiles((block: x.b = 10)) 196 | check compiles((block: x.c = 10)) 197 | 198 | test "generics": 199 | const isValidSimple = compiles(( 200 | block: 201 | data Obj1[T,U]: 202 | x: T 203 | var y: U 204 | )) 205 | check isValidSimple 206 | when isValidSimple: 207 | data Obj1[T,U]: 208 | x: T 209 | var y: U 210 | let a = initObj1(1, "a") 211 | check a.x == 1 and a.y == "a" 212 | 213 | const isValidWithParent = compiles(( 214 | block: 215 | data Obj2[T,U,V] of Obj1[T,U]: 216 | z: V 217 | )) 218 | check isValidWithParent 219 | when isValidWithParent: 220 | data Obj2[T,U,V] of Obj1[T,U]: 221 | let z: V 222 | let b = initObj2(1, "a", 'a') 223 | check b.x == 1 and b.y == "a" and b.z == 'a' 224 | 225 | const isValidWithNonGenericParent = compiles(( 226 | block: 227 | data Obj3: 228 | x: int 229 | data Obj4[T] of Obj3: 230 | y: T 231 | )) 232 | check isValidWithNonGenericParent 233 | when isValidWithNonGenericParent: 234 | data Obj3: 235 | x: int 236 | data Obj4[T] of Obj3: 237 | y: T 238 | let c = initObj4(1, "a") 239 | check c.x == 1 and c.y == "a" 240 | 241 | const isValidRef = compiles(( 242 | block: 243 | data Obj5[T] ref object: 244 | x: T 245 | )) 246 | check: isValidRef 247 | when isValidRef: 248 | data Obj5[T] ref object: 249 | x: T 250 | let d = newObj5(1) 251 | check: d.x == 1 252 | const isValidRefWithParent = compiles(( 253 | block: 254 | data Obj5[T] ref object: 255 | x: T 256 | data Obj6[T,U] ref object of Obj5[T]: 257 | y: U 258 | )) 259 | check: isValidRefWithParent 260 | when isValidRefWithParent: 261 | data Obj6[T,U] ref object of Obj5[T]: 262 | y: U 263 | let e = newObj6(1, "a") 264 | check: e.x == 1 and e.y == "a" 265 | 266 | test "from example": 267 | data TypeA: 268 | a = 1 269 | var b = "a" 270 | 271 | var x = initTypeA(b = "b") 272 | assert x.a == 1 273 | assert x.b == "b" 274 | x.b = "a" 275 | check: not compiles((block: 276 | x.a = 2 277 | )) 278 | 279 | data TypeB[C,D] of TypeA: 280 | c: C 281 | var d: D 282 | discard initTypeB(100, "aaa", 'a', 1.0) 283 | 284 | data TypeC ref object of TypeA: 285 | c: char 286 | 287 | var z = newTypeC(c = 'a') 288 | z.b = "b" 289 | 290 | when not defined(js): 291 | # See https://github.com/nim-lang/Nim/issues/5517 292 | test "show typeclass": 293 | data TypeA1, show: 294 | a: int 295 | b: string 296 | let c: pointer = nil 297 | let x = initTypeA1(1, "a") 298 | check: compiles($x) 299 | when compiles($x): 300 | check: $x == "TypeA1(a: 1, b: a, c: ...)" 301 | 302 | test "copy macro": 303 | data Imm, copy: 304 | a: int 305 | b: string 306 | c = 'c' 307 | let x = initImm(1, "b") 308 | check: compiles(x.copyImm(a = 2)) 309 | when compiles(x.copyImm(a = 2)): 310 | let y = x.copyImm(a = 2) 311 | check: y.a == 2 and y.b == "b" and y.c == 'c' 312 | 313 | check: compiles(newGlobalDataRef(1).copyGlobalDataRef(a = 2)) 314 | when compiles(newGlobalDataRef(1).copyGlobalDataRef(a = 2)): 315 | check: newGlobalDataRef(1).copyGlobalDataRef(a = 2).a == 2 316 | 317 | test "Reserved words in type definition": 318 | data RWTest: 319 | `type`: string 320 | `for` = "b" 321 | let `let`: string = "let" 322 | var `var`: string 323 | var `yield`: string = "yield" 324 | let x = initRWTest(`type` = "type", `var` = "var", `for` = "for", `let` = "let", `yield` = "yield") 325 | check: x.`type` == "type" 326 | check: x.`var` == "var" 327 | check: x.`for` == "for" 328 | check: x.`let` == "let" 329 | check: x.`yield` == "yield" 330 | 331 | test "Modifiers": 332 | check: declared(TestModifiers) 333 | check: not declared(testModifiers) 334 | check: not compiles(( 335 | block: 336 | var tmv: TestModifiers 337 | discard tmv.a 338 | )) 339 | check: not compiles(( 340 | block: 341 | var tmv: TestModifiers 342 | discard tmv.copyTestModifiers(a = 1) 343 | )) 344 | #TODO: It works because of standard `$` implementation 345 | # check: not compiles(( 346 | # block: 347 | # var tmv: TestModifiers 348 | # discard tmv.`$` 349 | # )) 350 | 351 | test "Sequences support": 352 | data TypeWithSeq, show: 353 | a: seq[int] 354 | b = @["a"] 355 | 356 | let x = initTypeWithSeq(@[1]) 357 | check: x.a == @[1] and x.b == @["a"] 358 | 359 | data RefTypeWithSeq ref object, show: 360 | let a: seq[int] = @[] 361 | let b: seq[string] = @[] 362 | 363 | let y = newRefTypeWithSeq(@[1]) 364 | check: y.a == @[1] and y.b == @[] 365 | 366 | test "ADT": 367 | data ADT1, show, copy: 368 | Branch1: 369 | a: int 370 | Branch2: 371 | b: string 372 | c = 123 373 | let x1 = initBranch1(100).copyBranch1(a = 10) 374 | check: x1.a == 10 375 | let y1 = initBranch2("test").copyBranch2(c = 1000) 376 | check: y1.b == "test" and y1.c == 1000 377 | 378 | data ADT2 ref object, show, copy: 379 | a: int 380 | Branch3: 381 | b = "b" 382 | Branch4: 383 | c = 'c' 384 | let x2 = newBranch3(10).copyBranch3(b = "bbb") 385 | check: x2.a == 10 and x2.b == "bbb" 386 | let y2 = newBranch4(10).copyBranch4(a = 100) 387 | check: y2.a == 100 and y2.c == 'c' 388 | 389 | test "Patty compatibility": 390 | data Shape, show, copy: 391 | Circle: 392 | r: float 393 | Rectangle: 394 | w: float 395 | h: float 396 | 397 | match initCircle(10.0): 398 | Circle(r): 399 | check: r == 10.0 400 | Rectangle: 401 | check: false 402 | 403 | test "Eq typeclass": 404 | data Test[T], eq: 405 | a: T 406 | b = "b" 407 | check: initTest(1) == initTest(1, "b") 408 | check: initTest(1) != initTest(1, "a") 409 | 410 | data TestAdt, eq: 411 | Br1: 412 | a: int 413 | Br2: 414 | b: string 415 | check: initBr2("a") == initBr2("a") 416 | check: initBr2("a") != initBr1(1) 417 | 418 | test "Json typeclass": 419 | data Test[T], json: 420 | a: T 421 | b: int 422 | 423 | let x1 = fromJson(Test[string], parseJson"""{ "a": "a", "b": 1 }""") 424 | check: x1.a == "a" and x1.b == 1 425 | expect(FieldException): 426 | discard fromJson(Test[string], parseJson"""{ "A": "a", "B": 1 }""") 427 | 428 | let j1 = x1.toJson 429 | check: j1["a"].getStr == "a" 430 | check: j1["b"].getInt == 1 431 | 432 | data TestAdt, json: 433 | Br1: 434 | a: int 435 | Br2: 436 | b: string 437 | 438 | let x2 = TestAdt.fromJson(parseJson("""{"kind": "Br1", "a": 1}""")) 439 | check: x2.a == 1 440 | expect(FieldException): 441 | discard fromJson(TestAdt, parseJson"""{ "A": "a", "B": 1 }""") 442 | let j2 = x2.toJson 443 | check: j2["kind"].getStr == "Br1" 444 | check: j2["a"].getInt == 1 445 | 446 | test "ADT with empty branch": 447 | data ADT, copy, eq, show, json: 448 | WithData: 449 | a: int 450 | WithoutData: 451 | discard 452 | 453 | check: ADT.fromJson(initWithoutData().copyWithoutData().toJson()).kind == WithoutData 454 | check: initWithoutData() == initWithoutData() 455 | check: initWithoutData().`$` == "WithoutData()" 456 | -------------------------------------------------------------------------------- /tests/boost/test_typeutils_int.nim: -------------------------------------------------------------------------------- 1 | import boost/typeutils 2 | 3 | type GlobalX* = object 4 | a*: int 5 | b*: seq[string] 6 | 7 | when compiles(GlobalX.genConstructor): 8 | GlobalX.genConstructor createGlobalX, exported 9 | 10 | data GlobalData, exported: 11 | a: int 12 | 13 | data GlobalDataRef ref object, exported, copy: 14 | a: int 15 | 16 | data TestModifiers, exported(noconstructor,nocopy,noshow,nofields), constructor(testModifiers), copy, show: 17 | a: int 18 | -------------------------------------------------------------------------------- /utils/index_html.nim: -------------------------------------------------------------------------------- 1 | #? stdtmpl 2 | # 3 | #import strutils, ospaths 4 | # 5 | #proc index_html*(files: seq[(string, seq[string])]): string = 6 | # result = "" 7 | 8 | 9 | 10 | 11 | nimboost library documentation 12 | 13 | 14 | 15 | 16 | 23 | 24 | 25 |
26 |
27 |
28 |
29 |
30 | The nimboost library documentation 31 |
32 | 37 |
38 |
39 |
40 | # for v in files: 41 |
42 |
Version ${v[0]}
43 | 48 |
49 |
50 | # end for 51 |
52 |
53 |
54 | 55 | 56 | --------------------------------------------------------------------------------