about summary refs log tree commit diff stats
path: root/src/loader/response.nim
blob: 6fa6a974ad69d5289288789bdb1b12c9ce930f02 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
import std/streams
import std/unicode

import bindings/quickjs
import io/promise
import js/error
import js/javascript
import loader/headers
import loader/request
import types/blob
import types/url

import chakasu/charset
import chakasu/decoderstream
import chakasu/encoderstream

type
  ResponseType* = enum
    TYPE_DEFAULT = "default"
    TYPE_BASIC = "basic"
    TYPE_CORS = "cors"
    TYPE_ERROR = "error"
    TYPE_OPAQUE = "opaque"
    TYPE_OPAQUEREDIRECT = "opaqueredirect"

  #TODO fully implement headers guards
  HeadersGuard* = enum
    GUARD_IMMUTABLE = "immutable"
    GUARD_REQUEST = "request"
    GUARD_REQUEST_NO_CORS = "request-no-cors"
    GUARD_RESPONSE = "response"
    GUARD_NONE = "none"

  Response* = ref object
    responseType* {.jsget: "type".}: ResponseType
    res*: int
    fd*: int
    body*: Stream
    bodyUsed* {.jsget.}: bool
    contentType*: string
    status* {.jsget.}: uint16
    headers* {.jsget.}: Headers
    headersGuard: HeadersGuard
    redirect*: Request
    url*: URL #TODO should be urllist?
    unregisterFun*: proc()
    bodyRead*: Promise[string]
    charset*: Charset
    internalMessage*: string # should NOT be exposed to JS!

jsDestructor(Response)

proc newResponse*(res: int, request: Request, fd = -1, stream: Stream = nil):
    Response =
  return Response(
    res: res,
    url: request.url,
    body: stream,
    bodyRead: Promise[string](),
    fd: fd
  )

func makeNetworkError*(): Response =
  #TODO use "create" function
  #TODO headers immutable
  return Response(
    res: 0,
    responseType: TYPE_ERROR,
    status: 0,
    headers: newHeaders(),
    headersGuard: GUARD_IMMUTABLE
  )

proc error(): Response {.jsstfunc: "Response".} =
  return makeNetworkError()

func sok(response: Response): bool {.jsfget: "ok".} =
  return response.status in 200u16 .. 299u16

func surl(response: Response): string {.jsfget: "url".} =
  if response.responseType == TYPE_ERROR:
    return ""
  return $response.url

#TODO: this should be a property of body
proc close*(response: Response) {.jsfunc.} =
  response.bodyUsed = true
  if response.unregisterFun != nil:
    response.unregisterFun()
  if response.body != nil:
    response.body.close()

proc text*(response: Response): Promise[JSResult[string]] {.jsfunc.} =
  if response.body == nil:
    let p = newPromise[JSResult[string]]()
    p.resolve(JSResult[string].ok(""))
    return p
  if response.bodyUsed:
    let p = newPromise[JSResult[string]]()
    let err = JSResult[string]
      .err(newTypeError("Body has already been consumed"))
    p.resolve(err)
    return p
  let bodyRead = response.bodyRead
  response.bodyRead = nil
  return bodyRead.then(proc(s: string): JSResult[string] =
    let cs = if response.charset == CHARSET_UNKNOWN:
      CHARSET_UTF_8
    else:
      response.charset
    if cs == CHARSET_UTF_8 and s.validateUtf8() == -1:
      ok(s)
    else:
      let ss = newStringStream(s)
      let ds = newDecoderStream(ss, cs)
      let es = newEncoderStream(ds, CHARSET_UTF_8)
      return ok(es.readAll())
  )

proc blob*(response: Response): Promise[JSResult[Blob]] {.jsfunc.} =
  if response.bodyRead == nil:
    let p = newPromise[JSResult[Blob]]()
    let err = JSResult[Blob]
      .err(newTypeError("Body has already been consumed"))
    p.resolve(err)
    return p
  let bodyRead = response.bodyRead
  response.bodyRead = nil
  return bodyRead.then(proc(s: string): JSResult[Blob] =
    if s.len == 0:
      return ok(newBlob(nil, 0, response.contentType, nil))
    GC_ref(s)
    let deallocFun = proc() =
      GC_unref(s)
    let blob = newBlob(unsafeAddr s[0], s.len, response.contentType, deallocFun)
    ok(blob))

proc json(ctx: JSContext, this: Response): Promise[JSResult[JSValue]]
    {.jsfunc.} =
  return this.text().then(proc(s: JSResult[string]): JSResult[JSValue] =
    let s = ?s
    return ok(JS_ParseJSON(ctx, cstring(s), cast[csize_t](s.len),
      cstring"<input>")))

proc addResponseModule*(ctx: JSContext) =
  ctx.registerType(Response)