all repos — litestore @ e4f1854c8feff76ddf5e1c241b5867848a498bfa

A minimalist nosql document store.

Implemented support for library usage & Jester integration example.
h3rald h3rald@h3rald.com
Sat, 07 Oct 2017 12:28:25 +0200
commit

e4f1854c8feff76ddf5e1c241b5867848a498bfa

parent

050bb4cabb9c45af93ea71e821120c2b79ea42db

M .gitignore.gitignore

@@ -5,3 +5,4 @@ *.db-journal

litestore*.zip litestore nakefile +jester_integration
A examples/jester_integration.nim

@@ -0,0 +1,53 @@

+import jester, ../litestore, asyncdispatch, uri, strutils, sequtils, httpcore + +proc lsReq(req: jester.Request): LSRequest = + var params = newSeq[string](0) + for key, value in pairs(req.params): + params.add("$1=$2" % @[key, value]) + let query = params.join("&") + var protocol = "http" + if req.secure: + protocol = "https" + result.reqMethod = req.reqMeth + result.url = parseUri("$1://$2:$3/$4?$5" % @[protocol, req.host, $req.port, req.path, query]) + result.hostname = req.host + result.body = req.body + +LS.init() + +routes: + + # Just a simple, unrelated Jester route + get "/": + resp "Hello, World!" + + # Remapping LiteStore routes on Jester + get "/litestore/@resource/@id?": + let r = get(request.lsReq, LS, @"resource", @"id") + resp(r.code, r.content, r.headers["content-type"]) + + post "/litestore/@resource/@id?": + let r = post(request.lsReq, LS, @"resource", @"id") + resp(r.code, r.content, r.headers["content-type"]) + + put "/litestore/@resource/@id?": + let r = put(request.lsReq, LS, @"resource", @"id") + resp(r.code, r.content, r.headers["content-type"]) + + patch "/litestore/@resource/@id?": + let r = patch(request.lsReq, LS, @"resource", @"id") + resp(r.code, r.content, r.headers["content-type"]) + + delete "/litestore/@resource/@id?": + let r = delete(request.lsReq, LS, @"resource", @"id") + resp(r.code, r.content, r.headers["content-type"]) + + head "/litestore/@resource/@id?": + let r = head(request.lsReq, LS, @"resource", @"id") + resp(r.code, r.content, r.headers["content-type"]) + + options "/litestore/@resource/@id?": + let r = options(request.lsReq, LS, @"resource", @"id") + resp(r.code, r.content, r.headers["content-type"]) + +runForever()
M lib/api_v1.nimlib/api_v1.nim

@@ -1,5 +1,5 @@

import - x_asynchttpserver, + asynchttpserver, asyncdispatch, strutils, cgi,

@@ -108,7 +108,7 @@ return true

# Low level procs -proc getRawDocument(LS: LiteStore, id: string, options = newQueryOptions()): Response = +proc getRawDocument(LS: LiteStore, id: string, options = newQueryOptions()): LSResponse = let doc = LS.store.retrieveRawDocument(id, options) result.headers = ctJsonHeader() if doc == "":

@@ -117,7 +117,7 @@ else:

result.content = doc result.code = Http200 -proc getDocument(LS: LiteStore, id: string, options = newQueryOptions()): Response = +proc getDocument(LS: LiteStore, id: string, options = newQueryOptions()): LSResponse = let doc = LS.store.retrieveDocument(id, options) if doc.data == "": result = resDocumentNotFound(id)

@@ -126,7 +126,7 @@ result.headers = doc.contenttype.ctHeader

result.content = doc.data result.code = Http200 -proc deleteDocument(LS: LiteStore, id: string): Response = +proc deleteDocument(LS: LiteStore, id: string): LSResponse = let doc = LS.store.retrieveDocument(id) if doc.data == "": result = resDocumentNotFound(id)

@@ -136,14 +136,14 @@ let res = LS.store.destroyDocument(id)

if res == 0: result = resError(Http500, "Unable to delete document '$1'" % id) else: - result.headers = TAB_HEADERS.newStringTable + result.headers = newHttpHeaders(TAB_HEADERS) result.headers["Content-Length"] = "0" result.content = "" result.code = Http204 except: result = resError(Http500, "Unable to delete document '$1'" % id) -proc getRawDocuments(LS: LiteStore, options: QueryOptions = newQueryOptions()): Response = +proc getRawDocuments(LS: LiteStore, options: QueryOptions = newQueryOptions()): LSResponse = var options = options let t0 = cpuTime() let docs = LS.store.retrieveRawDocuments(options)

@@ -176,7 +176,7 @@ result.headers = ctJsonHeader()

result.content = content.pretty result.code = Http200 -proc getInfo(LS: LiteStore): Response = +proc getInfo(LS: LiteStore): LSResponse = let info = LS.store.retrieveInfo() let version = info[0] let total_documents = info[1]

@@ -200,7 +200,7 @@ result.headers = ctJsonHeader()

result.content = content.pretty result.code = Http200 -proc postDocument(LS: LiteStore, body: string, ct: string): Response = +proc postDocument(LS: LiteStore, body: string, ct: string): LSResponse = try: var doc = LS.store.createDocument("", body, ct) if doc != "":

@@ -212,7 +212,7 @@ result = resError(Http500, "Unable to create document.")

except: result = resError(Http500, "Unable to create document.") -proc putDocument(LS: LiteStore, id: string, body: string, ct: string): Response = +proc putDocument(LS: LiteStore, id: string, body: string, ct: string): LSResponse = let doc = LS.store.retrieveDocument(id) if doc.data == "": # Create a new document

@@ -236,7 +236,7 @@ result = resError(Http500, "Unable to update document '$1'." % id)

except: result = resError(Http500, "Unable to update document '$1'." % id) -proc patchDocument(LS: LiteStore, id: string, body: string): Response = +proc patchDocument(LS: LiteStore, id: string, body: string): LSResponse = var apply = true let jbody = body.parseJson if jbody.kind != JArray:

@@ -277,7 +277,7 @@ return LS.getRawDocument(id)

# Main routing -proc options(req: Request, LS: LiteStore, resource: string, id = ""): Response = +proc options(req: LSRequest, LS: LiteStore, resource: string, id = ""): LSResponse = case resource: of "info": if id != "":

@@ -285,13 +285,13 @@ return resError(Http404, "Info '$1' not found." % id)

else: result.code = Http200 result.content = "" - result.headers = TAB_HEADERS.newStringTable + result.headers = newHttpHeaders(TAB_HEADERS) result.headers["Allow"] = "GET,OPTIONS" result.headers["Access-Control-Allow-Methods"] = "GET,OPTIONS" of "dir": result.code = Http200 result.content = "" - result.headers = TAB_HEADERS.newStringTable + result.headers = newHttpHeaders(TAB_HEADERS) result.headers["Allow"] = "GET,OPTIONS" result.headers["Access-Control-Allow-Methods"] = "GET,OPTIONS" of "docs":

@@ -299,11 +299,11 @@ if id != "":

result.code = Http200 result.content = "" if LS.readonly: - result.headers = TAB_HEADERS.newStringTable + result.headers = newHttpHeaders(TAB_HEADERS) result.headers["Allow"] = "HEAD,GET,OPTIONS" result.headers["Access-Control-Allow-Methods"] = "HEAD,GET,OPTIONS" else: - result.headers = TAB_HEADERS.newStringTable + result.headers = newHttpHeaders(TAB_HEADERS) result.headers["Allow"] = "HEAD,GET,OPTIONS,PUT,PATCH,DELETE" result.headers["Allow-Patch"] = "application/json-patch+json" result.headers["Access-Control-Allow-Methods"] = "HEAD,GET,OPTIONS,PUT,PATCH,DELETE"

@@ -311,17 +311,17 @@ else:

result.code = Http200 result.content = "" if LS.readonly: - result.headers = TAB_HEADERS.newStringTable + result.headers = newHttpHeaders(TAB_HEADERS) result.headers["Allow"] = "HEAD,GET,OPTIONS" result.headers["Access-Control-Allow-Methods"] = "HEAD,GET,OPTIONS" else: - result.headers = TAB_HEADERS.newStringTable + result.headers = newHttpHeaders(TAB_HEADERS) result.headers["Allow"] = "HEAD,GET,OPTIONS,POST" result.headers["Access-Control-Allow-Methods"] = "HEAD,GET,OPTIONS,POST" else: discard # never happens really. -proc head(req: Request, LS: LiteStore, resource: string, id = ""): Response = +proc head(req: LSRequest, LS: LiteStore, resource: string, id = ""): LSResponse = var options = newQueryOptions() options.select = @["documents.id AS id", "created", "modified"] try:

@@ -335,7 +335,7 @@ result.content = ""

except: return resError(Http400, "Bad request - $1" % getCurrentExceptionMsg()) -proc get(req: Request, LS: LiteStore, resource: string, id = ""): Response = +proc get(req: LSRequest, LS: LiteStore, resource: string, id = ""): LSResponse = let id = id.decodeURL case resource: of "docs":

@@ -361,7 +361,7 @@ else:

discard # never happens really. -proc post(req: Request, LS: LiteStore, resource: string, id = ""): Response = +proc post(req: LSRequest, LS: LiteStore, resource: string, id = ""): LSResponse = if id == "": var ct = "text/plain" if req.headers.hasKey("Content-Type"):

@@ -370,7 +370,7 @@ return LS.postDocument(req.body.strip, ct)

else: return resError(Http400, "Bad request: document ID cannot be specified in POST requests.") -proc put(req: Request, LS: LiteStore, resource: string, id = ""): Response = +proc put(req: LSRequest, LS: LiteStore, resource: string, id = ""): LSResponse = if id != "": var ct = "text/plain" if req.headers.hasKey("Content-Type"):

@@ -379,21 +379,21 @@ return LS.putDocument(id, req.body.strip, ct)

else: return resError(Http400, "Bad request: document ID must be specified in PUT requests.") -proc delete(req: Request, LS: LiteStore, resource: string, id = ""): Response = +proc delete(req: LSRequest, LS: LiteStore, resource: string, id = ""): LSResponse = if id != "": return LS.deleteDocument(id) else: return resError(Http400, "Bad request: document ID must be specified in DELETE requests.") -proc patch(req: Request, LS: LiteStore, resource: string, id = ""): Response = +proc patch(req: LSRequest, LS: LiteStore, resource: string, id = ""): LSResponse = if id != "": return LS.patchDocument(id, req.body) else: return resError(Http400, "Bad request: document ID must be specified in PATCH requests.") -proc serveFile*(req: Request, LS: LiteStore, id: string): Response = +proc serveFile*(req: LSRequest, LS: LiteStore, id: string): LSResponse = let path = LS.directory / id - var reqMethod = req.reqMethod + var reqMethod = $req.reqMethod if req.headers.hasKey("X-HTTP-Method-Override"): reqMethod = req.headers["X-HTTP-Method-Override"] case reqMethod.toUpperAscii:

@@ -415,24 +415,24 @@ return resError(Http500, "Unable to read file '$1'." % path)

else: return resError(Http404, "File '$1' not found." % path) else: - return resError(Http405, "Method not allowed: $1" % req.reqMethod) + return resError(Http405, "Method not allowed: $1" % $req.reqMethod) -proc route*(req: Request, LS: LiteStore, resource = "docs", id = ""): Response = - var reqMethod = req.reqMethod +proc route*(req: LSRequest, LS: LiteStore, resource = "docs", id = ""): LSResponse = + var reqMethod = $req.reqMethod if req.headers.hasKey("X-HTTP-Method-Override"): reqMethod = req.headers["X-HTTP-Method-Override"] case reqMethod.toUpperAscii: of "POST": if LS.readonly: - return resError(Http405, "Method not allowed: $1" % req.reqMethod) + return resError(Http405, "Method not allowed: $1" % $req.reqMethod) return validate(req, LS, resource, id, post) of "PUT": if LS.readonly: - return resError(Http405, "Method not allowed: $1" % req.reqMethod) + return resError(Http405, "Method not allowed: $1" % $req.reqMethod) return validate(req, LS, resource, id, put) of "DELETE": if LS.readonly: - return resError(Http405, "Method not allowed: $1" % req.reqMethod) + return resError(Http405, "Method not allowed: $1" % $req.reqMethod) return validate(req, LS, resource, id, delete) of "HEAD": return validate(req, LS, resource, id, head)

@@ -442,7 +442,7 @@ of "GET":

return validate(req, LS, resource, id, get) of "PATCH": if LS.readonly: - return resError(Http405, "Method not allowed: $1" % req.reqMethod) + return resError(Http405, "Method not allowed: $1" % $req.reqMethod) return validate(req, LS, resource, id, patch) else: - return resError(Http405, "Method not allowed: $1" % req.reqMethod) + return resError(Http405, "Method not allowed: $1" % $req.reqMethod)
M lib/api_v2.nimlib/api_v2.nim

@@ -1,5 +1,5 @@

import - x_asynchttpserver, + asynchttpserver, asyncdispatch, strutils, cgi,

@@ -14,11 +14,10 @@ contenttypes,

core, utils, logger - # Helper procs -proc orderByClauses(str: string): string = +proc orderByClauses*(str: string): string = var clauses = newSeq[string]() var fragments = str.split(",") for f in fragments:

@@ -30,7 +29,7 @@ else:

clauses.add("$1 ASC" % matches[1]) return clauses.join(", ") -proc parseQueryOption(fragment: string, options: var QueryOptions) = +proc parseQueryOption*(fragment: string, options: var QueryOptions) = var pair = fragment.split('=') if pair.len < 2 or pair[1] == "": raise newException(EInvalidRequest, "Invalid query string fragment '$1'" % fragment)

@@ -62,13 +61,13 @@ raise newException(EInvalidRequest, "Invalid sort value: $1" % pair[1])

else: return -proc parseQueryOptions(querystring: string, options: var QueryOptions) = +proc parseQueryOptions*(querystring: string, options: var QueryOptions) = var fragments = querystring.split('&') for f in fragments: f.parseQueryOption(options) -proc validate(req: Request, LS: LiteStore, resource: string, id: string, cb: proc(req: Request, LS: LiteStore, resource: string, id: string):Response): Response = - if req.reqMethod == "POST" or req.reqMethod == "PUT" or req.reqMethod == "PATCH": +proc validate*(req: LSRequest, LS: LiteStore, resource: string, id: string, cb: proc(req: LSRequest, LS: LiteStore, resource: string, id: string):LSResponse): LSResponse = + if req.reqMethod == HttpPost or req.reqMethod == HttpPut or req.reqMethod == HttpPatch: var ct = "" let body = req.body.strip if body == "":

@@ -85,7 +84,7 @@ else:

discard return cb(req, LS, resource, id) -proc applyPatchOperation(tags: var seq[string], op: string, path: string, value: string): bool = +proc applyPatchOperation*(tags: var seq[string], op: string, path: string, value: string): bool = var matches = @[""] if path.find(peg"^\/tags\/{\d+}$", matches) == -1: raise newException(EInvalidRequest, "cannot patch path '$1'" % path)

@@ -126,7 +125,7 @@ return true

# Low level procs -proc getRawDocument(LS: LiteStore, id: string, options = newQueryOptions()): Response = +proc getRawDocument*(LS: LiteStore, id: string, options = newQueryOptions()): LSResponse = let doc = LS.store.retrieveRawDocument(id, options) result.headers = ctJsonHeader() if doc == "":

@@ -135,7 +134,7 @@ else:

result.content = doc result.code = Http200 -proc getDocument(LS: LiteStore, id: string, options = newQueryOptions()): Response = +proc getDocument*(LS: LiteStore, id: string, options = newQueryOptions()): LSResponse = let doc = LS.store.retrieveDocument(id, options) if doc.data == "": result = resDocumentNotFound(id)

@@ -144,7 +143,7 @@ result.headers = doc.contenttype.ctHeader

result.content = doc.data result.code = Http200 -proc deleteDocument(LS: LiteStore, id: string): Response = +proc deleteDocument*(LS: LiteStore, id: string): LSResponse = let doc = LS.store.retrieveDocument(id) if doc.data == "": result = resDocumentNotFound(id)

@@ -154,14 +153,14 @@ let res = LS.store.destroyDocument(id)

if res == 0: result = resError(Http500, "Unable to delete document '$1'" % id) else: - result.headers = TAB_HEADERS.newStringTable + result.headers = newHttpHeaders(TAB_HEADERS) result.headers["Content-Length"] = "0" result.content = "" result.code = Http204 except: result = resError(Http500, "Unable to delete document '$1'" % id) -proc getRawDocuments(LS: LiteStore, options: QueryOptions = newQueryOptions()): Response = +proc getRawDocuments*(LS: LiteStore, options: QueryOptions = newQueryOptions()): LSResponse = var options = options let t0 = cpuTime() let docs = LS.store.retrieveRawDocuments(options)

@@ -196,7 +195,7 @@ result.headers = ctJsonHeader()

result.content = content.pretty result.code = Http200 -proc getInfo(LS: LiteStore): Response = +proc getInfo*(LS: LiteStore): LSResponse = let info = LS.store.retrieveInfo() let version = info[0] let total_documents = info[1]

@@ -220,7 +219,7 @@ result.headers = ctJsonHeader()

result.content = content.pretty result.code = Http200 -proc postDocument(LS: LiteStore, body: string, ct: string, folder=""): Response = +proc postDocument*(LS: LiteStore, body: string, ct: string, folder=""): LSResponse = if not folder.isFolder: return resError(Http400, "Invalid folder specified when creating document: $folder" % folder) try:

@@ -234,7 +233,7 @@ result = resError(Http500, "Unable to create document.")

except: result = resError(Http500, "Unable to create document.") -proc putDocument(LS: LiteStore, id: string, body: string, ct: string): Response = +proc putDocument*(LS: LiteStore, id: string, body: string, ct: string): LSResponse = if id.isFolder: return resError(Http400, "Invalid ID '$1' (Document IDs cannot end with '/')." % id) let doc = LS.store.retrieveDocument(id)

@@ -260,7 +259,7 @@ result = resError(Http500, "Unable to update document '$1'." % id)

except: result = resError(Http500, "Unable to update document '$1'." % id) -proc patchDocument(LS: LiteStore, id: string, body: string): Response = +proc patchDocument*(LS: LiteStore, id: string, body: string): LSResponse = var apply = true let jbody = body.parseJson if jbody.kind != JArray:

@@ -301,7 +300,7 @@ return LS.getRawDocument(id)

# Main routing -proc options(req: Request, LS: LiteStore, resource: string, id = ""): Response = +proc options*(req: LSRequest, LS: LiteStore, resource: string, id = ""): LSResponse = case resource: of "info": if id != "":

@@ -309,13 +308,13 @@ return resError(Http404, "Info '$1' not found." % id)

else: result.code = Http200 result.content = "" - result.headers = TAB_HEADERS.newStringTable + result.headers = newHttpHeaders(TAB_HEADERS) result.headers["Allow"] = "GET,OPTIONS" result.headers["Access-Control-Allow-Methods"] = "GET,OPTIONS" of "dir": result.code = Http200 result.content = "" - result.headers = TAB_HEADERS.newStringTable + result.headers = newHttpHeaders(TAB_HEADERS) result.headers["Allow"] = "GET,OPTIONS" result.headers["Access-Control-Allow-Methods"] = "GET,OPTIONS" of "docs":

@@ -326,22 +325,22 @@ if not folder.isNil:

result.code = Http200 result.content = "" if LS.readonly: - result.headers = TAB_HEADERS.newStringTable + result.headers = newHttpHeaders(TAB_HEADERS) result.headers["Allow"] = "HEAD,GET,OPTIONS" result.headers["Access-Control-Allow-Methods"] = "HEAD,GET,OPTIONS" else: - result.headers = TAB_HEADERS.newStringTable + result.headers = newHttpHeaders(TAB_HEADERS) result.headers["Allow"] = "HEAD,GET,OPTIONS,POST,PUT" result.headers["Access-Control-Allow-Methods"] = "HEAD,GET,OPTIONS,POST,PUT" elif id != "": result.code = Http200 result.content = "" if LS.readonly: - result.headers = TAB_HEADERS.newStringTable + result.headers = newHttpHeaders(TAB_HEADERS) result.headers["Allow"] = "HEAD,GET,OPTIONS" result.headers["Access-Control-Allow-Methods"] = "HEAD,GET,OPTIONS" else: - result.headers = TAB_HEADERS.newStringTable + result.headers = newHttpHeaders(TAB_HEADERS) result.headers["Allow"] = "HEAD,GET,OPTIONS,PUT,PATCH,DELETE" result.headers["Allow-Patch"] = "application/json-patch+json" result.headers["Access-Control-Allow-Methods"] = "HEAD,GET,OPTIONS,PUT,PATCH,DELETE"

@@ -349,17 +348,17 @@ else:

result.code = Http200 result.content = "" if LS.readonly: - result.headers = TAB_HEADERS.newStringTable + result.headers = newHttpHeaders(TAB_HEADERS) result.headers["Allow"] = "HEAD,GET,OPTIONS" result.headers["Access-Control-Allow-Methods"] = "HEAD,GET,OPTIONS" else: - result.headers = TAB_HEADERS.newStringTable + result.headers = newHttpHeaders(TAB_HEADERS) result.headers["Allow"] = "HEAD,GET,OPTIONS,POST" result.headers["Access-Control-Allow-Methods"] = "HEAD,GET,OPTIONS,POST" else: discard # never happens really. -proc head(req: Request, LS: LiteStore, resource: string, id = ""): Response = +proc head*(req: LSRequest, LS: LiteStore, resource: string, id = ""): LSResponse = var options = newQueryOptions() options.select = @["documents.id AS id", "created", "modified"] if id.isFolder:

@@ -375,7 +374,7 @@ result.content = ""

except: return resError(Http400, "Bad request - $1" % getCurrentExceptionMsg()) -proc get(req: Request, LS: LiteStore, resource: string, id = ""): Response = +proc get*(req: LSRequest, LS: LiteStore, resource: string, id = ""): LSResponse = let id = id.decodeURL case resource: of "docs":

@@ -403,13 +402,13 @@ else:

discard # never happens really. -proc post(req: Request, LS: LiteStore, resource: string, id = ""): Response = +proc post*(req: LSRequest, LS: LiteStore, resource: string, id = ""): LSResponse = var ct = "text/plain" if req.headers.hasKey("Content-Type"): ct = req.headers["Content-Type"] return LS.postDocument(req.body.strip, ct, id) -proc put(req: Request, LS: LiteStore, resource: string, id = ""): Response = +proc put*(req: LSRequest, LS: LiteStore, resource: string, id = ""): LSResponse = if id != "": var ct = "text/plain" if req.headers.hasKey("Content-Type"):

@@ -418,21 +417,21 @@ return LS.putDocument(id, req.body.strip, ct)

else: return resError(Http400, "Bad request: document ID must be specified in PUT requests.") -proc delete(req: Request, LS: LiteStore, resource: string, id = ""): Response = +proc delete*(req: LSRequest, LS: LiteStore, resource: string, id = ""): LSResponse = if id != "": return LS.deleteDocument(id) else: return resError(Http400, "Bad request: document ID must be specified in DELETE requests.") -proc patch(req: Request, LS: LiteStore, resource: string, id = ""): Response = +proc patch*(req: LSRequest, LS: LiteStore, resource: string, id = ""): LSResponse = if id != "": return LS.patchDocument(id, req.body) else: return resError(Http400, "Bad request: document ID must be specified in PATCH requests.") -proc serveFile*(req: Request, LS: LiteStore, id: string): Response = +proc serveFile*(req: LSRequest, LS: LiteStore, id: string): LSResponse = let path = LS.directory / id - var reqMethod = req.reqMethod + var reqMethod = $req.reqMethod if req.headers.hasKey("X-HTTP-Method-Override"): reqMethod = req.headers["X-HTTP-Method-Override"] case reqMethod.toUpperAscii:

@@ -454,24 +453,24 @@ return resError(Http500, "Unable to read file '$1'." % path)

else: return resError(Http404, "File '$1' not found." % path) else: - return resError(Http405, "Method not allowed: $1" % req.reqMethod) + return resError(Http405, "Method not allowed: $1" % $req.reqMethod) -proc route*(req: Request, LS: LiteStore, resource = "docs", id = ""): Response = - var reqMethod = req.reqMethod +proc route*(req: LSRequest, LS: LiteStore, resource = "docs", id = ""): LSResponse = + var reqMethod = $req.reqMethod if req.headers.hasKey("X-HTTP-Method-Override"): reqMethod = req.headers["X-HTTP-Method-Override"] case reqMethod.toUpperAscii: of "POST": if LS.readonly: - return resError(Http405, "Method not allowed: $1" % req.reqMethod) + return resError(Http405, "Method not allowed: $1" % $req.reqMethod) return validate(req, LS, resource, id, post) of "PUT": if LS.readonly: - return resError(Http405, "Method not allowed: $1" % req.reqMethod) + return resError(Http405, "Method not allowed: $1" % $req.reqMethod) return validate(req, LS, resource, id, put) of "DELETE": if LS.readonly: - return resError(Http405, "Method not allowed: $1" % req.reqMethod) + return resError(Http405, "Method not allowed: $1" % $req.reqMethod) return validate(req, LS, resource, id, delete) of "HEAD": return validate(req, LS, resource, id, head)

@@ -481,7 +480,7 @@ of "GET":

return validate(req, LS, resource, id, get) of "PATCH": if LS.readonly: - return resError(Http405, "Method not allowed: $1" % req.reqMethod) + return resError(Http405, "Method not allowed: $1" % $req.reqMethod) return validate(req, LS, resource, id, patch) else: - return resError(Http405, "Method not allowed: $1" % req.reqMethod) + return resError(Http405, "Method not allowed: $1" % $req.reqMethod)
M lib/server.nimlib/server.nim

@@ -1,5 +1,5 @@

import - x_asynchttpserver, + asynchttpserver, asyncdispatch, times, strutils,

@@ -14,20 +14,23 @@ utils,

api_v1, api_v2 -proc getReqInfo(req: Request): string = +export + api_v2 + +proc getReqInfo(req: LSRequest): string = var url = req.url.path if req.url.anchor != "": url = url & "#" & req.url.anchor if req.url.query != "": url = url & "?" & req.url.query - return req.hostname & " " & req.reqMethod & " " & url + return req.hostname & " " & $req.reqMethod & " " & url proc handleCtrlC() {.noconv.} = echo "" LOG.info("Exiting...") quit() -proc processApiUrl(req: Request, LS: LiteStore, info: ResourceInfo): Response = +proc processApiUrl(req: LSRequest, LS: LiteStore, info: ResourceInfo): LSResponse = if info.version == "v2": if info.resource.match(peg"^docs / info$"): return api_v2.route(req, LS, info.resource, info.id)

@@ -57,9 +60,9 @@ return resError(Http400, "Bad request - No resource specified." % info.resource)

else: return resError(Http400, "Bad request - Invalid resource: $1" % info.resource) -proc process(req: Request, LS: LiteStore): Response {.gcsafe.}= +proc process(req: LSRequest, LS: LiteStore): LSResponse {.gcsafe.}= var matches = @["", "", ""] - template route(req: Request, peg: Peg, op: untyped): untyped = + template route(req: LSRequest, peg: Peg, op: untyped): untyped = if req.url.path.find(peg, matches) != -1: op try:

@@ -87,7 +90,7 @@ raise newException(EInvalidRequest, req.getReqInfo())

except EInvalidRequest: let e = (ref EInvalidRequest)(getCurrentException()) let trace = e.getStackTrace() - return resError(Http400, "Bad Request: $1" % getCurrentExceptionMsg(), trace) + return resError(Http400, "Bad LSRequest: $1" % getCurrentExceptionMsg(), trace) except: let e = getCurrentException() let trace = e.getStackTrace()

@@ -97,10 +100,11 @@ setControlCHook(handleCtrlC)

proc serve*(LS: LiteStore) = var server = newAsyncHttpServer() - proc handleHttpRequest(req: Request): Future[void] {.async, gcsafe, closure.} = + proc handleHttpRequest(req: LSRequest): Future[void] {.async, gcsafe, closure.} = LOG.info(getReqInfo(req).replace("$", "$$")) let res = req.process(LS) - await req.respond(res.code, res.content, res.headers) + let areq = asynchttpserver.Request(req) + await areq.respond(res.code, res.content, res.headers) echo(LS.appname & " v" & LS.appversion & " started on " & LS.address & ":" & $LS.port & ".") if LS.mount: echo("Mirroring datastore changes to: " & LS.directory)
M lib/types.nimlib/types.nim

@@ -1,6 +1,6 @@

import x_db_sqlite, - x_asynchttpserver, + asynchttpserver, pegs, strtabs import

@@ -62,10 +62,11 @@ appname*: string

appversion*: string favicon*:string loglevel*:string - Response* = tuple[ + LSRequest* = asynchttpserver.Request + LSResponse* = tuple[ code: HttpCode, content: string, - headers: StringTableRef] + headers: HttpHeaders] ResourceInfo* = tuple[ resource: string, id: string,
M lib/utils.nimlib/utils.nim

@@ -1,7 +1,7 @@

import x_sqlite3, x_db_sqlite, - x_asynchttpserver, + asynchttpserver, json, strutils, pegs,

@@ -166,15 +166,15 @@ proc fail*(code: int, msg: string) =

LOG.error(msg) quit(code) -proc ctHeader*(ct: string): StringTableRef = - var h = TAB_HEADERS.newStringTable +proc ctHeader*(ct: string): HttpHeaders = + var h = newHttpHeaders(TAB_HEADERS) h["Content-Type"] = ct return h -proc ctJsonHeader*(): StringTableRef = +proc ctJsonHeader*(): HttpHeaders = return ctHeader("application/json") -proc resError*(code: HttpCode, message: string, trace = ""): Response = +proc resError*(code: HttpCode, message: string, trace = ""): LSResponse = LOG.warn(message.replace("$", "$$")) if trace.len > 0: LOG.debug(trace.replace("$", "$$"))

@@ -182,7 +182,7 @@ result.code = code

result.content = """{"error":"$1"}""" % message result.headers = ctJsonHeader() -proc resDocumentNotFound*(id: string): Response = +proc resDocumentNotFound*(id: string): LSResponse = resError(Http404, "Document '$1' not found." % id) proc eWarn*() =

@@ -190,8 +190,8 @@ var e = getCurrentException()

LOG.warn(e.msg) LOG.debug(getStackTrace(e)) -proc validate*(req: Request, LS: LiteStore, resource: string, id: string, cb: proc(req: Request, LS: LiteStore, resource: string, id: string):Response): Response = - if req.reqMethod == "POST" or req.reqMethod == "PUT" or req.reqMethod == "PATCH": +proc validate*(req: Request, LS: LiteStore, resource: string, id: string, cb: proc(req: Request, LS: LiteStore, resource: string, id: string):LSResponse): LSResponse = + if req.reqMethod == HttpPost or req.reqMethod == HttpPut or req.reqMethod == HttpPatch: var ct = "" let body = req.body.strip if body == "":
D lib/x_asynchttpserver.nim

@@ -1,275 +0,0 @@

-# -# Nim's Runtime Library -# (c) Copyright 2014 Dominik Picheta -# -# See the file "copying.txt", included in this -# distribution, for details about the copyright. -# -# Modified by Fabio Cevasco to allow simple processing of PUT and PATCH methods -# - -## This module implements a high performance asynchronous HTTP server. -## -## Examples -## -------- -## -## This example will create an HTTP server on port 8080. The server will -## respond to all requests with a ``200 OK`` response code and "Hello World" -## as the response body. -## -## .. code-block::nim -## var server = newAsyncHttpServer() -## proc cb(req: Request) {.async.} = -## await req.respond(Http200, "Hello World") -## -## asyncCheck server.serve(Port(8080), cb) -## runForever() - -import strtabs, asyncnet, asyncdispatch, parseutils, uri, strutils -type - Request* = object - client*: AsyncSocket # TODO: Separate this into a Response object? - reqMethod*: string - headers*: StringTableRef - protocol*: tuple[orig: string, major, minor: int] - url*: Uri - hostname*: string ## The hostname of the client that made the request. - body*: string - - AsyncHttpServer* = ref object - socket: AsyncSocket - reuseAddr: bool - - HttpCode* = enum - Http100 = "100 Continue", - Http101 = "101 Switching Protocols", - Http200 = "200 OK", - Http201 = "201 Created", - Http202 = "202 Accepted", - Http204 = "204 No Content", - Http205 = "205 Reset Content", - Http206 = "206 Partial Content", - Http300 = "300 Multiple Choices", - Http301 = "301 Moved Permanently", - Http302 = "302 Found", - Http303 = "303 See Other", - Http304 = "304 Not Modified", - Http305 = "305 Use Proxy", - Http307 = "307 Temporary Redirect", - Http400 = "400 Bad Request", - Http401 = "401 Unauthorized", - Http403 = "403 Forbidden", - Http404 = "404 Not Found", - Http405 = "405 Method Not Allowed", - Http406 = "406 Not Acceptable", - Http407 = "407 Proxy Authentication Required", - Http408 = "408 Request Timeout", - Http409 = "409 Conflict", - Http410 = "410 Gone", - Http411 = "411 Length Required", - Http418 = "418 I'm a teapot", - Http500 = "500 Internal Server Error", - Http501 = "501 Not Implemented", - Http502 = "502 Bad Gateway", - Http503 = "503 Service Unavailable", - Http504 = "504 Gateway Timeout", - Http505 = "505 HTTP Version Not Supported" - - HttpVersion* = enum - HttpVer11, - HttpVer10 - -{.deprecated: [TRequest: Request, PAsyncHttpServer: AsyncHttpServer, - THttpCode: HttpCode, THttpVersion: HttpVersion].} - -proc `==`*(protocol: tuple[orig: string, major, minor: int], - ver: HttpVersion): bool = - let major = - case ver - of HttpVer11, HttpVer10: 1 - let minor = - case ver - of HttpVer11: 1 - of HttpVer10: 0 - result = protocol.major == major and protocol.minor == minor - -proc newAsyncHttpServer*(reuseAddr = true): AsyncHttpServer = - ## Creates a new ``AsyncHttpServer`` instance. - new result - result.reuseAddr = reuseAddr - -proc addHeaders(msg: var string, headers: StringTableRef) = - for k, v in headers: - msg.add(k & ": " & v & "\c\L") - -proc sendHeaders*(req: Request, headers: StringTableRef): Future[void] = - ## Sends the specified headers to the requesting client. - var msg = "" - addHeaders(msg, headers) - return req.client.send(msg) - -proc respond*(req: Request, code: HttpCode, - content: string, headers = newStringTable()) {.async.} = - ## Responds to the request with the specified ``HttpCode``, headers and - ## content. - ## - ## This procedure will **not** close the client socket. - var customHeaders = headers - customHeaders["Content-Length"] = $content.len - var msg = "HTTP/1.1 " & $code & "\c\L" - msg.addHeaders(customHeaders) - await req.client.send(msg & "\c\L" & content) - -proc newRequest(): Request = - result.headers = newStringTable(modeCaseInsensitive) - result.hostname = "" - result.body = "" - -proc parseHeader(line: string): tuple[key, value: string] = - var i = 0 - i = line.parseUntil(result.key, ':') - inc(i) # skip : - i += line.skipWhiteSpace(i) - i += line.parseUntil(result.value, {'\c', '\L'}, i) - -proc parseProtocol(protocol: string): tuple[orig: string, major, minor: int] = - var i = protocol.skipIgnoreCase("HTTP/") - if i != 5: - raise newException(ValueError, "Invalid request protocol. Got: " & - protocol) - result.orig = protocol - i.inc protocol.parseInt(result.major, i) - i.inc # Skip . - i.inc protocol.parseInt(result.minor, i) - -proc sendStatus(client: AsyncSocket, status: string): Future[void] = - client.send("HTTP/1.1 " & status & "\c\L") - -proc processClient(client: AsyncSocket, address: string, - callback: proc (request: Request): - Future[void] {.closure, gcsafe.}) {.async.} = - while not client.isClosed: - # GET /path HTTP/1.1 - # Header: val - # \n - var request = newRequest() - request.hostname = address - assert client != nil - request.client = client - - # First line - GET /path HTTP/1.1 - let line = await client.recvLine() # TODO: Timeouts. - if line == "": - client.close() - return - let lineParts = line.split(' ') - if lineParts.len != 3: - await request.respond(Http400, "Invalid request. Got: " & line) - continue - - let reqMethod = lineParts[0] - let path = lineParts[1] - let protocol = lineParts[2] - - # Headers - var i = 0 - while true: - i = 0 - let headerLine = await client.recvLine() - if headerLine == "": - client.close(); return - if headerLine == "\c\L": break - # TODO: Compiler crash - #let (key, value) = parseHeader(headerLine) - let kv = parseHeader(headerLine) - request.headers[kv.key] = kv.value - - request.reqMethod = reqMethod - request.url = parseUri(path) - try: - request.protocol = protocol.parseProtocol() - except ValueError: - asyncCheck request.respond(Http400, "Invalid request protocol. Got: " & - protocol) - continue - - var nMethod = reqMethod.normalize - if nMethod == "post" or nMethod == "put" or nMethod == "patch": - # Check for Expect header - if request.headers.hasKey("Expect"): - if request.headers["Expect"].toLowerAscii == "100-continue": - await client.sendStatus("100 Continue") - else: - await client.sendStatus("417 Expectation Failed") - - # Read the body - # - Check for Content-length header - if request.headers.hasKey("Content-Length"): - var contentLength = 0 - if parseInt(request.headers["Content-Length"], contentLength) == 0: - await request.respond(Http400, "Bad Request. Invalid Content-Length.") - else: - request.body = await client.recv(contentLength) - assert request.body.len == contentLength - else: - await request.respond(Http400, "Bad Request. No Content-Length.") - continue - - case reqMethod.normalize - of "get", "post", "head", "put", "delete", "trace", "options", "connect", "patch": - await callback(request) - else: - await request.respond(Http400, "Invalid request method. Got: " & reqMethod) - - # Persistent connections - if (request.protocol == HttpVer11 and - request.headers["connection"].normalize != "close") or - (request.protocol == HttpVer10 and - request.headers["connection"].normalize == "keep-alive"): - # In HTTP 1.1 we assume that connection is persistent. Unless connection - # header states otherwise. - # In HTTP 1.0 we assume that the connection should not be persistent. - # Unless the connection header states otherwise. - discard - else: - request.client.close() - break - -proc serve*(server: AsyncHttpServer, port: Port, - callback: proc (request: Request): Future[void] {.closure,gcsafe.}, - address = "") {.async.} = - ## Starts the process of listening for incoming HTTP connections on the - ## specified address and port. - ## - ## When a request is made by a client the specified callback will be called. - server.socket = newAsyncSocket() - if server.reuseAddr: - server.socket.setSockOpt(OptReuseAddr, true) - server.socket.bindAddr(port, address) - server.socket.listen() - - while true: - # TODO: Causes compiler crash. - #var (address, client) = await server.socket.acceptAddr() - var fut = await server.socket.acceptAddr() - asyncCheck processClient(fut.client, fut.address, callback) - #echo(f.isNil) - #echo(f.repr) - -proc close*(server: AsyncHttpServer) = - ## Terminates the async http server instance. - server.socket.close() - -when isMainModule: - proc main = - var server = newAsyncHttpServer() - proc cb(req: Request) {.async.} = - #echo(req.reqMethod, " ", req.url) - #echo(req.headers) - let headers = {"Date": "Tue, 29 Apr 2014 23:40:08 GMT", - "Content-type": "text/plain; charset=utf-8"} - await req.respond(Http200, "Hello World", headers.newStringTable()) - - asyncCheck server.serve(Port(5555), cb) - runForever() - main()
M litestore.nimlitestore.nim

@@ -17,14 +17,18 @@ lib/core,

lib/cli, lib/server +export + server, + types, + core, + server + from asyncdispatch import runForever {.compile: "vendor/sqlite/libsqlite3.c".} {.passC: "-DSQLITE_ENABLE_FTS3=1 -DSQLITE_ENABLE_FTS3_PARENTHESIS -DSQLITE_ENABLE_JSON1".} -when isMainModule: - - # Initialize Datastore +proc init*(LS: var LiteStore, open = true) = if not LS.file.fileExists: try: LOG.debug("Creating datastore: ", LS.file)

@@ -32,22 +36,31 @@ LS.file.createDatastore()

except: eWarn() fail(200, "Unable to create datastore '$1'" % [LS.file]) + if (open): + try: + LS.store = LS.file.openDatastore() + if LS.mount: + try: + LS.store.mountDir(LS.directory) + except: + eWarn() + fail(202, "Unable to mount directory '$1'" % [LS.directory]) + except: + fail(201, "Unable to open datastore '$1'" % [LS.file]) + +when isMainModule: + + # Initialize Datastore + LS.init() # Manage vacuum operation separately if LS.operation == opVacuum: + LS.init(false) vacuum LS.file + else: + # Open Datastore + LS.init(true) - # Open Datastore and execute operation - try: - LS.store = LS.file.openDatastore() - if LS.mount: - try: - LS.store.mountDir(LS.directory) - except: - eWarn() - fail(202, "Unable to mount directory '$1'" % [LS.directory]) - except: - fail(201, "Unable to open datastore '$1'" % [LS.file]) case LS.operation: of opRun: LS.serve
M litestore.nimblelitestore.nimble

@@ -1,6 +1,6 @@

[Package] name = "litestore" -version = "1.1.1" +version = "1.2.0" author = "Fabio Cevasco" description = "Self-contained, lightweight, RESTful document store." license = "MIT"