Initial http_protocol utility

This commit is contained in:
Emery Hemingway 2022-06-08 20:25:45 -05:00
commit ff6f7553e6
8 changed files with 193 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
http_translator

7
README.md Normal file
View File

@ -0,0 +1,7 @@
# Syndicate utils
## http_translator
Dispatches HTTP requests to registered handlers.
See [http_translator.config-example.pr](./http_translator.config-example.pr) for an example configuration.

20
http_protocol.prs Normal file
View File

@ -0,0 +1,20 @@
version 1 .
Method = =GET / =HEAD / =POST / =PUT / =DELETE / =CONNECT / =OPTIONS / =TRACE / =PATCH .
Methods = #{Method} .
; A URL path split into elements
Path = [string ...] .
; Register an entity that will handle requests at path prefix.
; TODO: assert the public base URL of the handler to the entity.
Handler = <handler @methods Methods @path Path @entity #!any> .
Headers = {string: [string ...] ...:...} .
; A request awaiting a response at handle.
; TODO: query parameters
Request = <http @handle int @method Method @headers Headers @path Path @body string> .
; A response to handle.
Response = <http @handle int @code int @headers Headers @body string> .

View File

@ -0,0 +1,21 @@
<require-service <daemon http_translator>>
<daemon http_translator {
argv: "/home/repo/syndicate/syndicate_utils/src/http_translator"
protocol: text/syndicate
}>
let ?other = dataspace
$other [
? <http ?handle GET ?headers ?path ?body> [
<http $handle 200 {} "get handler invoked">
]
]
? <service-object <daemon http_translator> ?cap> [
$cap [
; publish GET requests with prefix "/foo/bar" to other dataspace
<handler #{GET} ["foo" "bar" ] $other>
]
]

2
src/Tupfile Normal file
View File

@ -0,0 +1,2 @@
include_rules
: foreach ../*.prs |> !preserves_schema_nim |> %B.nim

40
src/http_protocol.nim Normal file
View File

@ -0,0 +1,40 @@
import
std/typetraits, preserves, std/tables, std/sets
type
Path* = seq[string]
Headers* = TableRef[string, seq[string]]
Response* {.preservesRecord: "http".} = object
`handle`*: int
`code`*: int
`headers`*: Headers
`body`*: string
Handler*[E] {.preservesRecord: "handler".} = ref object
`methods`*: Methods
`path`*: Path
`entity`*: Preserve[E]
`Method`* {.preservesOr, pure.} = enum
`GET`, `HEAD`, `POST`, `PUT`, `DELETE`, `CONNECT`, `OPTIONS`, `TRACE`,
`PATCH`
Request* {.preservesRecord: "http".} = object
`handle`*: int
`method`*: Method
`headers`*: Headers
`path`*: Path
`body`*: string
Methods* = HashSet[Method]
proc `$`*[E](x: Handler[E]): string =
`$`(toPreserve(x, E))
proc encode*[E](x: Handler[E]): seq[byte] =
encode(toPreserve(x, E))
proc `$`*(x: Path | Headers | Response | Request | Methods): string =
`$`(toPreserve(x))
proc encode*(x: Path | Headers | Response | Request | Methods): seq[byte] =
encode(toPreserve(x))

89
src/http_translator.nim Normal file
View File

@ -0,0 +1,89 @@
# SPDX-FileCopyrightText: ☭ 2022 Emery Hemingway
# SPDX-License-Identifier: Unlicense
import std/[asyncdispatch, asynchttpserver, sets, strutils, tables, uri]
import preserves
import syndicate, syndicate/actors
import ./http_protocol.nim
func toHttpCore(methods: Methods): set[HttpMethod] =
# Convert the schema type to the type in the httpcore module.
for m in methods:
result.incl(
case m
of Method.GET: HttpGET
of Method.HEAD: HttpHEAD
of Method.POST: HttpPOST
of Method.PUT: HttpPUT
of Method.DELETE: HttpDELETE
of Method.CONNECT: HttpCONNECT
of Method.OPTIONS: HttpOPTIONS
of Method.TRACE: HttpTRACE
of Method.PATCH: HttpPATCH)
proc splitPath(u: Uri): Path =
u.path.strip(chars = {'/'}).split("/")
proc hitch(a, b: Future[void]) =
a.addCallback do (f: Future[void]):
if f.failed: fail(b, f.error)
else: complete(b)
bootDataspace("main") do (ds: Ref; turn: var Turn):
connectStdio(ds, turn)
var
handlers: Table[seq[string], (Ref, set[HttpMethod])]
pathPrefixMaxLen = 0
requestIdSource = 0
during(turn, ds, ?Handler[Ref]) do (methods: Methods; path: seq[string]; entity: Ref):
handlers[path] = (entity, methods.toHttpCore)
pathPrefixMaxLen = max(pathPrefixMaxLen, path.len)
do:
handlers.del(path)
pathPrefixMaxLen = 0
for path in handlers.keys:
pathPrefixMaxLen = max(pathPrefixMaxLen, path.len)
var parentFacet = turn.facet
proc handleRequest(req: asynchttpserver.Request): Future[void] =
# TODO: use pattern matching
var
entity: Ref
methods: set[HttpMethod]
path = req.url.splitPath()
block:
var prefix = path[0..min(pathPrefixMaxLen.succ, path.high)]
while entity.isNil:
(entity, methods) = handlers.getOrDefault(prefix)
if prefix.len == 0: break
else: discard prefix.pop()
if entity.isNil:
result = req.respond(Http503, "no handler registered for this path")
else:
var parentFut = newFuture[void]("handleRequest")
result = parentFut
if req.reqMethod notin methods:
result = req.respond(Http405, "method not valid for this handler")
else:
run(parentFacet) do (turn: var Turn):
inc requestIdSource
let
rId = requestIdSource
let rHandle = publish(turn, entity, http_protocol.Request(
handle: rId,
`method`: Method.GET,
headers: req.headers.table,
path: path,
body: req.body))
onPublish(turn, entity, Response ? { 0: ?rId, 1: ?int, 3: ?string }) do (code: HttpCode, body: string):
req.respond(code, body).addCallback do (fut: Future[void]):
run(parentFacet) do (turn: var Turn): retract(turn, rHandle)
hitch(fut, parentFut)
var http = newAsyncHttpServer()
asyncCheck serve(http, Port 8888, handleRequest)
runForever()

13
syndicate_utils.nimble Normal file
View File

@ -0,0 +1,13 @@
# Package
version = "0.1.0"
author = "Emery Hemingway"
description = "Utilites for Syndicated Actors and Synit"
license = "unlicense"
srcDir = "src"
bin = @["http_translator"]
# Dependencies
requires "nim >= 1.6.6", "syndicate >= 0.3.1"