Initial http_protocol utility
This commit is contained in:
commit
ff6f7553e6
|
@ -0,0 +1 @@
|
||||||
|
http_translator
|
|
@ -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.
|
|
@ -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> .
|
|
@ -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>
|
||||||
|
]
|
||||||
|
]
|
|
@ -0,0 +1,2 @@
|
||||||
|
include_rules
|
||||||
|
: foreach ../*.prs |> !preserves_schema_nim |> %B.nim
|
|
@ -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))
|
|
@ -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()
|
|
@ -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"
|
Loading…
Reference in New Issue