all repos — litestore @ dc7d7fcb5e7b09550420aab71ac3f787c78725b2

A minimalist nosql document store.

Refactoring + implemented alternative syntax for ordering data.
h3rald h3rald@h3rald.com
Sat, 31 Jan 2015 12:48:47 +0100
commit

dc7d7fcb5e7b09550420aab71ac3f787c78725b2

parent

03a48496e6fb3fb0b1a1660a4018bba58255f6a3

6 files changed, 173 insertions(+), 150 deletions(-)

jump to
D api.nim

@@ -1,38 +0,0 @@

-import asynchttpserver, asyncdispatch, strutils -from strtabs import StringTableRef, newStringTable -import types, core - -proc resError*(code: HttpCode, message: string): Response = - result.code = code - result.content = """{"code": $1, "message":"$2"}""" % [($code)[0..2], message] - result.headers = CT_JSON.newStringTable - -proc resDocumentNotFound*(id): Response = - resError(Http404, "Document '$1' not found." % id) - -proc getRawDocument*(LS: LiteStore, id: string, options = newQueryOptions()): Response = - let doc = LS.store.retrieveRawDocument(id, options) - result.headers = CT_JSON.newStringTable - if doc == "": - result = resDocumentNotFound(id) - else: - result.content = doc - result.code = Http200 - -proc getDocument*(LS: LiteStore, id: string, options = newQueryOptions()): Response = - let doc = LS.store.retrieveDocument(id, options) - if doc.data == "": - result = resDocumentNotFound(id) - else: - result.headers = doc.contenttype.ctHeader - result.content = doc.data - result.code = Http200 - -proc getRawDocuments*(LS: LiteStore, options = newQueryOptions()): Response = - let docs = LS.store.retrieveRawDocuments(options) # TODO Implement query options - if docs.len == 0: - result = resError(Http404, "No documents found.") - else: - result.headers = ctJsonHeader() - result.content = docs - result.code = Http200
A api_v1.nim

@@ -0,0 +1,124 @@

+import asynchttpserver, asyncdispatch, strutils, cgi, strtabs, pegs +import types, core, utils + +# Helper procs + +proc orderByClause(clause: string): string = + var matches = @["", ""] + if clause.find(peg"{[-+ ]} {(id / created / modified)}", matches) != -1: + if matches[0] == "-": + return "$1 DESC" % matches[1] + else: + return "$1 ASC" % matches[1] + else: + return "" + +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) + try: + pair[1] = pair[1].decodeURL + except: + raise newException(EInvalidRequest, "Unable to decode query string fragment '$1'" % fragment) + case pair[0]: + of "search": + options.search = pair[1] + of "tags": + options.tags = pair[1] + of "limit": + try: + options.limit = pair[1].parseInt + except: + raise newException(EInvalidRequest, "Invalid limit value: $1" % getCurrentExceptionMsg()) + of "sort": + let orderby = pair[1].orderByClause() + if orderby != "": + options.orderby = orderby + else: + raise newException(EInvalidRequest, "Invalid sort value: $1" % pair[1]) + else: + return + +proc parseQueryOptions(querystring: string, options: var QueryOptions) = + var fragments = querystring.split('&') + for f in fragments: + f.parseQueryOption(options) + +# Low level procs + +proc getRawDocument(LS: LiteStore, id: string, options = newQueryOptions()): Response = + let doc = LS.store.retrieveRawDocument(id, options) + result.headers = ctJsonHeader() + if doc == "": + result = resDocumentNotFound(id) + else: + result.content = doc + result.code = Http200 + +proc getDocument(LS: LiteStore, id: string, options = newQueryOptions()): Response = + let doc = LS.store.retrieveDocument(id, options) + if doc.data == "": + result = resDocumentNotFound(id) + else: + result.headers = doc.contenttype.ctHeader + result.content = doc.data + result.code = Http200 + +proc getRawDocuments(LS: LiteStore, options = newQueryOptions()): Response = + let docs = LS.store.retrieveRawDocuments(options) + if docs == "[]": + result = resError(Http404, "No documents found.") + else: + result.headers = ctJsonHeader() + result.content = docs + result.code = Http200 + +# Main routing + +proc options(req: Request, LS: LiteStore, id = ""): Response = + if id != "": + result.code = Http202 + result.content = "" + result.headers = {"Allow": "HEAD,GET,PUT,PATCH,DELETE"}.newStringTable + else: + result.code = Http202 + result.content = "" + result.headers = {"Allow": "HEAD,GET,POST,DELETE"}.newStringTable + +proc head(req: Request, LS: LiteStore, id = ""): Response = + var options = newQueryOptions() + options.select = "id, content_type, binary, searchable, created, modified" + try: + parseQueryOptions(req.url.query, options); + if id != "": + return LS.getRawDocument(id, options) + else: + return LS.getRawDocuments(options) + except: + return resError(Http400, "Bad request - $1" % getCurrentExceptionMsg()) + +proc get(req: Request, LS: LiteStore, id = ""): Response = + var options = newQueryOptions() + try: + parseQueryOptions(req.url.query, options); + if id != "": + if req.url.query.contains("raw=true") or req.headers["Content-Type"] == "application/json": + return LS.getRawDocument(id, options) + else: + return LS.getDocument(id, options) + else: + return LS.getRawDocuments(options) + except: + return resError(Http400, "Bad request - $1" % getCurrentExceptionMsg()) + +proc route*(req: Request, LS: LiteStore, id = ""): Response = + case req.reqMethod: + of "HEAD": + return req.head(LS, id) + of "OPTIONS": + return req.options(LS, id) + of "GET": + return req.get(LS, id) + else: + return resError(Http405, "Method not allowed: $1" % req.reqMethod)
M core.nimcore.nim

@@ -154,10 +154,10 @@ if d_ct.isBinary:

d_binary = 1 d_searchable = 0 discard store.createDocument(d_id, d_contents, d_ct, d_binary, d_searchable) - store.db.exec(SQL_INSERT_TAG, "$dir:"&dir, d_id) + store.db.exec(SQL_INSERT_TAG, "$collection:"&dir, d_id) proc unpackDir*(store: Datastore, dir: string) = - let docs = store.db.getAllRows(SQL_SELECT_DOCUMENTS_BY_TAG, "$dir:"&dir) + let docs = store.db.getAllRows(SQL_SELECT_DOCUMENTS_BY_TAG, "$collection:"&dir) for doc in docs: let file = doc[0] var data: string
M server.nimserver.nim

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

import asynchttpserver, asyncdispatch, times, strutils, pegs, strtabs, cgi -import types, api +import types, utils, api_v1 proc getReqInfo(req: Request): string = return $getLocalTime(getTime()) & " - " & req.hostname & " " & req.reqMethod & " " & req.url.path

@@ -8,111 +8,30 @@ proc handleCtrlC() {.noconv.} =

echo "\nExiting..." quit() -proc validOrderBy(clause: string):bool = - return clause == "id ASC" or - clause == "id DESC" or - clause == "created ASC" or - clause == "created DESC" or - clause == "modified ASC" or - clause == "modified DESC" +proc parseApiUrl(req: Request): ResourceInfo = + var matches = @["", "", ""] + if req.url.path.find(PEG_URL, matches) != -1: + result.version = matches[0] + result.resource = matches[1] + result.id = matches[2] + else: + raise newException(EInvalidRequest, req.url.path&"?"&req.url.query) -proc parseQueryOption(fragment: string, options: var QueryOptions) = - var pair = fragment.split('=') - if pair.len < 2: - return +proc route(req: Request, LS: LiteStore): Response = try: - pair[1] = pair[1].decodeURL - except: - raise newException(EInvalidRequest, "Unable to decode query string fragment '$1'" % fragment) - case pair[0]: - of "search": - options.search = pair[1] - of "tags": - options.tags = pair[1] - of "limit": - try: - options.limit = pair[1].parseInt - except: - raise newException(EInvalidRequest, "LIMIT - $1" % getCurrentExceptionMsg()) - of "orderby": - if pair[1].validOrderBy(): - options.orderby = pair[1] - else: - raise newException(EInvalidRequest, "ORDERBY - Invalid clause '$1'" % pair[1]) + var info = req.parseApiUrl + if info.version == "v1" and info.resource == "docs": + return api_v1.route(req, LS, info.id) else: - return - -proc parseQueryOptions(querystring: string, options: var QueryOptions) = - var fragments = querystring.split('&') - for f in fragments: - f.parseQueryOption(options) - - -proc rDocs(path: string, matches: var seq[string]): bool = - return path.find(peg"""^\/? {(.*)}""", matches) != -1 - -proc optionsRoutes(req: Request, LS: LiteStore): Response = - var matches = @[""] - if req.url.path.rDocs(matches): - if matches[0] != "": - result.code = Http200 - result.content = "" - result.headers = {"Allow": "HEAD,GET,PUT,PATCH,DELETE"}.newStringTable - else: - result.code = Http200 - result.content = "" - result.headers = {"Allow": "HEAD,GET,POST,DELETE"}.newStringTable - else: - return resError(Http400, "Bad request: $1" % req.url.path) - -proc headRoutes(req: Request, LS: LiteStore): Response = - var matches = @[""] - if req.url.path.rDocs(matches): - var options = newQueryOptions() - options.select = "id, content_type, binary, searchable, created, modified" - try: - parseQueryOptions(req.url.query, options); - if matches[0] != "": - # Retrieve a single document - return LS.getRawDocument(matches[0], options) + if info.version != "v1": + return resError(Http400, "Bad request - Invalid API version: $1" % info.version) else: - # Retrieve a multiple documents - return LS.getRawDocuments(options) - except: - return resError(Http400, "Bad request: $1" % getCurrentExceptionMsg()) - else: - return resError(Http400, "Bad request: $1" % req.url.path) - -proc getRoutes(req: Request, LS: LiteStore): Response = - var matches = @[""] - if req.url.path.rDocs(matches): - var options = newQueryOptions() - try: - parseQueryOptions(req.url.query, options); - if matches[0] != "": - # Retrieve a single document - if req.url.query.contains("raw=true") or req.headers["Content-Type"] == "application/json": - return LS.getRawDocument(matches[0], options) + if info.resource.decodeURL.strip == "": + return resError(Http400, "Bad request - No resource specified" % info.resource) else: - return LS.getDocument(matches[0], options) - else: - # Retrieve a multiple documents - return LS.getRawDocuments(options) - except: - return resError(Http400, "Bad request: $1" % getCurrentExceptionMsg()) - else: - return resError(Http400, "Bad request: $1" % req.url.path) - -proc route(req: Request, LS: LiteStore): Response = - case req.reqMethod: - of "HEAD": - return req.headRoutes(LS) - of "OPTIONS": - return req.optionsRoutes(LS) - of "GET": - return req.getRoutes(LS) - else: - return resError(Http501, "Method $1 not implemented" % req.reqMethod) + return resError(Http400, "Bad request - Invalid resource: $1" % info.resource) + except: + return resError(Http400, "Bad request: $1" % getCurrentExceptionMsg()) setControlCHook(handleCtrlC)
M types.nimtypes.nim

@@ -1,4 +1,4 @@

-import db_sqlite +import db_sqlite, pegs from asynchttpserver import HttpCode from strtabs import StringTableRef, newStringTable

@@ -38,6 +38,23 @@ Response* = tuple[

code: HttpCode, content: string, headers: StringTableRef] + ResourceInfo* = tuple[ + resource: string, + id: string, + version: string + ] + +let PEG_TAG* = peg""" +^\$? [a-zA-Z0-9_\-?~:.@#^!]+$ +""" + +let PEG_USER_TAG* = peg""" +^[a-zA-Z0-9_\-?~:.@#^!]+$ +""" + +let PEG_URL* = peg""" + ^\/{(v\d+)} \/ {([^\/]+)} \/ {(.*)} +""" const CT_JSON* = {"Content-Type": "application/json"}
M utils.nimutils.nim

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

-import json, db_sqlite, strutils, pegs +import json, db_sqlite, strutils, pegs, asyncdispatch, asynchttpserver import types, queries, contenttypes -let PEG_TAG* = peg""" -^\$? [a-zA-Z0-9_\-?~:.@#^!]+$ -""" - -let PEG_USER_TAG* = peg""" -^[a-zA-Z0-9_\-?~:.@#^!]+$ -""" proc dbQuote*(s: string): string = result = "'" for c in items(s):

@@ -96,3 +89,11 @@ proc error*(code, msg) =

stderr.writeln(msg) quit(code) +proc resError*(code: HttpCode, message: string): Response = + result.code = code + result.content = """{"error":"$1"}""" % message + result.headers = ctJsonHeader() + +proc resDocumentNotFound*(id): Response = + resError(Http404, "Document '$1' not found." % id) +