@@ -4,6 +4,8 @@ import io/error
4
4
import stringbuffer
5
5
import bytearray
6
6
import io/uri
7
+ import scanner
8
+ import char
7
9
8
10
// Async iterables
9
11
// ---------------
@@ -297,6 +299,135 @@ namespace jsWeb {
297
299
}
298
300
}
299
301
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
+
300
431
/// Sets the values in the `RequestBuilder` from a given URI string.
301
432
def uri(uri: String): Unit / { RequestBuilder, Exception[WrongFormat] } = {
302
433
stringBuffer{
@@ -463,10 +594,53 @@ namespace example {
463
594
with httpClient
464
595
println(get("https://effekt-lang.org"))
465
596
}
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
+ }
466
638
def main() = {
467
639
println("Low-level: ")
468
640
lowLevelApi()
469
641
println("Simple: ")
470
642
simpleApi()
643
+ println("Plain: ")
644
+ plainApi()
471
645
}
472
646
}
0 commit comments