Skip to content

Commit dd31f8d

Browse files
Add minimal API for generating requests and parsing responses to/from byte streams
1 parent 5f369b7 commit dd31f8d

File tree

1 file changed

+174
-0
lines changed

1 file changed

+174
-0
lines changed

libraries/common/io/requests.effekt

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import io/error
44
import stringbuffer
55
import bytearray
66
import io/uri
7+
import scanner
8+
import char
79

810
// Async iterables
911
// ---------------
@@ -297,6 +299,135 @@ namespace jsWeb {
297299
}
298300
}
299301

302+
namespace plain {
303+
def constructRequest{ body: => Unit / RequestBuilder }: Unit / emit[Byte] = {
304+
def crlf() = {
305+
do emit('\n'.toInt.toByte)
306+
do emit('\r'.toInt.toByte)
307+
}
308+
309+
with encodeUTF8
310+
var method = GET()
311+
var port = 80
312+
var path = "/"
313+
var headers = Nil[(String, String)]()
314+
var reqBody: ByteArray = allocate(0)
315+
try body() with RequestBuilder {
316+
def protocol(p) = p match {
317+
case HTTP() => ()
318+
case _ => panic("Only HTTP is supported by constructRequest")
319+
}
320+
def method(m) = resume(method = m)
321+
def hostname(n) = <>
322+
def auth(a) = <>
323+
def port(p) = resume(port = p)
324+
def path(p) = resume(path = p)
325+
def header(k, v) = resume(headers = Cons((k, v), headers))
326+
def body() = resume{ {b} => reqBody = collectBytes{ b() } }
327+
}
328+
// start line
329+
method.show.each
330+
do emit(' ')
331+
path.each
332+
do emit(' ')
333+
"HTTP/1.1".each
334+
crlf()
335+
// field-line's
336+
headers.reverse.foreach{ case (k,v) =>
337+
k.each
338+
do emit(':')
339+
do emit(' ')
340+
v.each
341+
crlf()
342+
}
343+
crlf()
344+
// body
345+
reqBody.each
346+
}
347+
348+
def parseHTTPVersion(): (Int, Int) / { Scan[Char], Exception[WrongFormat] } = {
349+
returning::expect("HTTP version"){
350+
readIf('H'); readIf('T'); readIf('T'); readIf('P')
351+
readIf('/')
352+
val maj = tryRead{ c => digitValue(c) }
353+
readIf('.')
354+
val min = tryRead{ c => digitValue(c) }
355+
(maj, min)
356+
}
357+
}
358+
359+
def isHTTPTokenChar(c: Char): Bool = c match {
360+
case '!' => true
361+
case '#' => true
362+
case '$' => true
363+
case '%' => true
364+
case '&' => true
365+
case '\'' => true
366+
case '*' => true
367+
case '+' => true
368+
case '-' => true
369+
case '.' => true
370+
case '^' => true
371+
case '_' => true
372+
case '`' => true
373+
case '|' => true
374+
case '~' => true
375+
case d and d.isAlphanumeric => true
376+
case _ => false
377+
}
378+
379+
def parseHeaderLine(): (String, String) / { Scan[Char], Exception[WrongFormat] } = {
380+
with returning::expect("HTTP Header line")
381+
val k = collectString { readWhile { c => c.isHTTPTokenChar() } }
382+
readIf(':')
383+
skipWhile { c => c == ' ' || c == '\t' }
384+
val v = collectString { readWhile { c => c != '\n' } }
385+
skipWhile { c => c == ' ' || c == '\t' }
386+
(k, v)
387+
}
388+
389+
def parseResponse[R]{ body: {ResponseReader} => R }: R / { read[Byte], Exception[WrongFormat] } = {
390+
with decodeUTF8
391+
with returning::scanner[Char, R]
392+
393+
var statusCode = -1
394+
var headers: List[(String, String)] = Nil()
395+
expect("HTTP response"){
396+
val version = parseHTTPVersion()
397+
readIf(' ')
398+
statusCode = readInteger()
399+
if (statusCode > 599 || statusCode < 100) { do raise(WrongFormat(), "Invalid status code") }
400+
readIf(' ')
401+
val reason = collectString{ readWhile{ c => c != '\n' } }
402+
readIf('\n')
403+
readIf('\r')
404+
headers = collectList {
405+
while(do peek[Char]() != '\n') {
406+
do emit(parseHeaderLine())
407+
readIf('\n')
408+
readIf('\r')
409+
}
410+
}
411+
readIf('\n')
412+
readIf('\r')
413+
}
414+
415+
try body{res} with res: ResponseReader {
416+
def status() = resume(statusCode)
417+
def getHeader(key) = { // TODO should be case-insensitive
418+
var r = None()
419+
headers.foreach {
420+
case (k, v) and k == key => r = Some(v)
421+
case _ => ()
422+
}
423+
resume(r)
424+
}
425+
def body() = resume { exhaustively{ do emit(do read[Byte]()) } }
426+
}
427+
}
428+
}
429+
430+
300431
/// Sets the values in the `RequestBuilder` from a given URI string.
301432
def uri(uri: String): Unit / { RequestBuilder, Exception[WrongFormat] } = {
302433
stringBuffer{
@@ -463,10 +594,53 @@ namespace example {
463594
with httpClient
464595
println(get("https://effekt-lang.org"))
465596
}
597+
def plainApi(): Unit = {
598+
with on[WrongFormat].panic
599+
println(collectString{
600+
with source[Byte]{
601+
plain::constructRequest{
602+
do method(GET())
603+
uri("http://effekt-lang.org")
604+
defaultHeaders()
605+
}
606+
}
607+
with decodeUTF8
608+
exhaustively{ do emit(do read[Char]()) }
609+
})
610+
with source[Byte]{
611+
def crlf() = {
612+
do emit('\n'.toInt.toByte)
613+
do emit('\r'.toInt.toByte)
614+
}
615+
with encodeUTF8;
616+
"HTTP/1.1 200 OK".each;
617+
crlf()
618+
"content-type: text/plain".each;
619+
crlf()
620+
crlf()
621+
"Hello!".each
622+
}
623+
with def res = plain::parseResponse
624+
println(res.status())
625+
println(res.getHeader("content-type").show{ x => x })
626+
with source[Byte]{ res.body() }
627+
with decodeUTF8
628+
with stringBuffer
629+
exhaustively{
630+
do read[Char]() match {
631+
case '\n' => println(do flush())
632+
case c =>
633+
do write(c.show)
634+
}
635+
}
636+
println(do flush())
637+
}
466638
def main() = {
467639
println("Low-level: ")
468640
lowLevelApi()
469641
println("Simple: ")
470642
simpleApi()
643+
println("Plain: ")
644+
plainApi()
471645
}
472646
}

0 commit comments

Comments
 (0)