summary refs log tree commit diff stats
path: root/lib/std/jsfetch.nim
blob: 21959461973d494ca68991c2b9f8cb48e865b455 (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
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
## - Fetch for the JavaScript target: https://developer.mozilla.org/docs/Web/API/Fetch_API
when not defined(js):
  {.fatal: "Module jsfetch is designed to be used with the JavaScript backend.".}

import std/[asyncjs, jsformdata, jsheaders]
export jsformdata, jsheaders
from std/httpcore import HttpMethod
from std/jsffi import JsObject

type
  FetchOptions* = ref object of JsRoot  ## Options for Fetch API.
    keepalive*: bool
    metod* {.importjs: "method".}: cstring
    body*, integrity*, referrer*, mode*, credentials*, cache*, redirect*, referrerPolicy*: cstring
    headers*: Headers

  FetchModes* = enum  ## Mode options.
    fmCors = "cors"
    fmNoCors = "no-cors"
    fmSameOrigin = "same-origin"

  FetchCredentials* = enum  ## Credential options. See https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials
    fcInclude = "include"
    fcSameOrigin = "same-origin"
    fcOmit = "omit"

  FetchCaches* = enum  ## https://developer.mozilla.org/docs/Web/API/Request/cache
    fchDefault = "default"
    fchNoStore = "no-store"
    fchReload = "reload"
    fchNoCache = "no-cache"
    fchForceCache = "force-cache"

  FetchRedirects* = enum  ## Redirects options.
    frFollow = "follow"
    frError = "error"
    frManual = "manual"

  FetchReferrerPolicies* = enum  ## Referrer Policy options.
    frpNoReferrer = "no-referrer"
    frpNoReferrerWhenDowngrade = "no-referrer-when-downgrade"
    frpOrigin = "origin"
    frpOriginWhenCrossOrigin = "origin-when-cross-origin"
    frpUnsafeUrl = "unsafe-url"

  Response* = ref object of JsRoot  ## https://developer.mozilla.org/en-US/docs/Web/API/Response
    bodyUsed*, ok*, redirected*: bool
    typ* {.importjs: "type".}: cstring
    url*, statusText*: cstring
    status*: cint
    headers*: Headers
    body*: cstring

  Request* = ref object of JsRoot  ## https://developer.mozilla.org/en-US/docs/Web/API/Request
    bodyUsed*, ok*, redirected*: bool
    typ* {.importjs: "type".}: cstring
    url*, statusText*: cstring
    status*: cint
    headers*: Headers
    body*: cstring

func newResponse*(body: cstring | FormData): Response {.importjs: "(new Response(#))".}
  ## Constructor for `Response`. This does *not* call `fetch()`. Same as `new Response()`.

func newRequest*(url: cstring): Request {.importjs: "(new Request(#))".}
  ## Constructor for `Request`. This does *not* call `fetch()`. Same as `new Request()`.

func newRequest*(url: cstring; fetchOptions: FetchOptions): Request {.importjs: "(new Request(#, #))".}
  ## Constructor for `Request` with `fetchOptions`. Same as `fetch(url, fetchOptions)`.

func clone*(self: Response | Request): Response {.importjs: "#.$1()".}
  ## https://developer.mozilla.org/en-US/docs/Web/API/Response/clone

proc text*(self: Response): Future[cstring] {.importjs: "#.$1()".}
  ## https://developer.mozilla.org/en-US/docs/Web/API/Response/text

proc json*(self: Response): Future[JsObject] {.importjs: "#.$1()".}
  ## https://developer.mozilla.org/en-US/docs/Web/API/Response/json

proc formData*(self: Response): Future[FormData] {.importjs: "#.$1()".}
  ## https://developer.mozilla.org/en-US/docs/Web/API/Response/formData

proc unsafeNewFetchOptions*(metod, body, mode, credentials, cache, referrerPolicy: cstring;
    keepalive: bool; redirect = "follow".cstring; referrer = "client".cstring; integrity = "".cstring; headers: Headers = newHeaders()): FetchOptions {.importjs:
    "{method: #, body: #, mode: #, credentials: #, cache: #, referrerPolicy: #, keepalive: #, redirect: #, referrer: #, integrity: #, headers: #}".}
  ## .. warning:: Unsafe `newfetchOptions`.

func newfetchOptions*(metod = HttpGet; body: cstring = nil;
    mode = fmCors; credentials = fcSameOrigin; cache = fchDefault; referrerPolicy = frpNoReferrerWhenDowngrade;
    keepalive = false; redirect = frFollow; referrer = "client".cstring; integrity = "".cstring,
    headers: Headers = newHeaders()): FetchOptions =
  ## Constructor for `FetchOptions`.
  result = FetchOptions(
    body: if metod notin {HttpHead, HttpGet}: body else: nil, 
    mode: cstring($mode), credentials: cstring($credentials), cache: cstring($cache), referrerPolicy: cstring($referrerPolicy),
    keepalive: keepalive, redirect: cstring($redirect), referrer: referrer, integrity: integrity, headers: headers,
    metod: (case metod
      of HttpHead:   "HEAD".cstring
      of HttpGet:    "GET".cstring
      of HttpPost:   "POST".cstring
      of HttpPut:    "PUT".cstring
      of HttpDelete: "DELETE".cstring
      of HttpPatch:  "PATCH".cstring
      else:          "GET".cstring
    )
  )

proc fetch*(url: cstring | Request): Future[Response] {.importjs: "$1(#)".}
  ## `fetch()` API, simple `GET` only, returns a `Future[Response]`.

proc fetch*(url: cstring | Request; options: FetchOptions): Future[Response] {.importjs: "$1(#, #)".}
  ## `fetch()` API that takes a `FetchOptions`, returns a `Future[Response]`.

func toCstring*(self: Request | Response | FetchOptions): cstring {.importjs: "JSON.stringify(#)".}

func `$`*(self: Request | Response | FetchOptions): string = $toCstring(self)


runnableExamples("-r:off"):
  import std/[asyncjs, jsconsole, jsformdata, jsheaders]
  from std/httpcore import HttpMethod
  from std/jsffi import JsObject
  from std/sugar import `=>`

  block:
    let options0: FetchOptions = unsafeNewFetchOptions(
      metod = "POST".cstring,
      body = """{"key": "value"}""".cstring,
      mode = "no-cors".cstring,
      credentials = "omit".cstring,
      cache = "no-cache".cstring,
      referrerPolicy = "no-referrer".cstring,
      keepalive = false,
      redirect = "follow".cstring,
      referrer = "client".cstring,
      integrity = "".cstring,
      headers = newHeaders()
    )
    assert options0.keepalive == false
    assert options0.metod == "POST".cstring
    assert options0.body == """{"key": "value"}""".cstring
    assert options0.mode == "no-cors".cstring
    assert options0.credentials == "omit".cstring
    assert options0.cache == "no-cache".cstring
    assert options0.referrerPolicy == "no-referrer".cstring
    assert options0.redirect == "follow".cstring
    assert options0.referrer == "client".cstring
    assert options0.integrity == "".cstring
    assert options0.headers.len == 0

  block:
    let options1: FetchOptions = newFetchOptions(
      metod =  HttpPost,
      body = """{"key": "value"}""".cstring,
      mode = fmNoCors,
      credentials = fcOmit,
      cache = fchNoCache,
      referrerPolicy = frpNoReferrer,
      keepalive = false,
      redirect = frFollow,
      referrer = "client".cstring,
      integrity = "".cstring,
      headers = newHeaders()
    )
    assert options1.keepalive == false
    assert options1.metod == $HttpPost
    assert options1.body == """{"key": "value"}""".cstring
    assert options1.mode == $fmNoCors
    assert options1.credentials == $fcOmit
    assert options1.cache == $fchNoCache
    assert options1.referrerPolicy == $frpNoReferrer
    assert options1.redirect == $frFollow
    assert options1.referrer == "client".cstring
    assert options1.integrity == "".cstring
    assert options1.headers.len == 0

  block:
    let response: Response = newResponse(body = "-. .. --".cstring)
    let request: Request = newRequest(url = "http://nim-lang.org".cstring)

  if not defined(nodejs):
    block:
      proc doFetch(): Future[Response] {.async.} =
        fetch "https://httpbin.org/get".cstring

      proc example() {.async.} =
        let response: Response = await doFetch()
        assert response.ok
        assert response.status == 200.cint
        assert response.headers is Headers
        assert response.body is cstring

      discard example()

    block:
      proc example2 {.async.} =
        await fetch("https://api.github.com/users/torvalds".cstring)
          .then((response: Response) => response.json())
          .then((json: JsObject) => console.log(json))
          .catch((err: Error) => console.log("Request Failed", err))

      discard example2()
typ != nil and tfReturnsNew in n.sons[0].typ.flags: result = newCall else: result = newNone proc deps(w: var W; dest, src: PNode; destInfo: set[RootInfo]) = # let x = (localA, localB) # compute 'returnsNew' property: let retNew = if src.isNil: newNone else: returnsNewExpr(src) if dest.kind == nkSym and dest.sym.kind == skResult: if retNew != newNone: if w.returnsNew != asgnOther: w.returnsNew = asgnNew else: w.returnsNew = asgnOther # mark the dependency, but # rule out obviously innocent assignments like 'somebool = true' if dest.kind == nkSym and retNew == newLit: discard else: let L = w.assignments.len w.assignments.setLen(L+1) addAsgn(w.assignments[L], dest, src, destInfo) proc depsArgs(w: var W; n: PNode) = if n.sons[0].typ.isNil: return var typ = skipTypes(n.sons[0].typ, abstractInst) if typ.kind != tyProc: return # echo n.info, " ", n, " ", w.owner.name.s, " ", typeToString(typ) assert(len(typ) == len(typ.n)) for i in 1 ..< n.len: let it = n.sons[i] if i < len(typ): assert(typ.n.sons[i].kind == nkSym) let paramType = typ.n.sons[i] if paramType.typ.isCompileTimeOnly: continue var destInfo: set[RootInfo] = {} if sfWrittenTo in paramType.sym.flags or paramType.typ.kind == tyVar: # p(f(x, y), X, g(h, z)) destInfo.incl markAsWrittenTo if sfEscapes in paramType.sym.flags: destInfo.incl markAsEscaping if destInfo != {}: deps(w, it, nil, destInfo) proc deps(w: var W; n: PNode) = case n.kind of nkLetSection, nkVarSection: for child in n: let last = lastSon(child) if last.kind == nkEmpty: continue if child.kind == nkVarTuple and last.kind in {nkPar, nkTupleConstr}: if child.len-2 != last.len: return for i in 0 .. child.len-3: deps(w, child.sons[i], last.sons[i], {}) else: for i in 0 .. child.len-3: deps(w, child.sons[i], last, {}) of nkAsgn, nkFastAsgn: deps(w, n.sons[0], n.sons[1], {}) else: for i in 0 ..< n.safeLen: deps(w, n.sons[i]) if n.kind in nkCallKinds: if getMagic(n) in {mNew, mNewFinalize, mNewSeq}: # may not look like an assignment, but it is: deps(w, n.sons[1], newNodeIT(nkObjConstr, n.info, n.sons[1].typ), {}) else: depsArgs(w, n) proc possibleAliases(w: var W; result: var seq[ptr TSym]) = # this is an expensive fixpoint iteration. We could speed up this analysis # by a smarter data-structure but we wait until profiling shows us it's # expensive. Usually 'w.assignments' is small enough. var alreadySeen = initIntSet() template addNoDup(x) = if not alreadySeen.containsOrIncl(x.id): result.add x for x in result: alreadySeen.incl x.id var todo = 0 while todo < result.len: let x = result[todo] inc todo for i in 0..<len(w.assignments): let a = addr(w.assignments[i]) #if a.srcHasSym(x): # # y = f(..., x, ...) # for i in 0 ..< a.destNoTc: addNoDup a.dest[i] if a.destNoTc > 0 and a.dest[0] == x and rootIsSym in a.destInfo: # x = f(..., y, ....) for i in 0 ..< a.srcNoTc: addNoDup a.src[i] proc markWriteOrEscape(w: var W; conf: ConfigRef) = ## Both 'writes' and 'escapes' effects ultimately only care ## about *parameters*. ## However, due to aliasing, even locals that might not look as parameters ## have to count as parameters if they can alias a parameter: ## ## .. code-block:: nim ## proc modifies(n: Node) {.writes: [n].} = ## let x = n ## x.data = "abc" ## ## We call a symbol *parameter-like* if it is a parameter or can alias a ## parameter. ## Let ``p``, ``q`` be *parameter-like* and ``x``, ``y`` be general ## expressions. ## ## A write then looks like ``p[] = x``. ## An escape looks like ``p[] = q`` or more generally ## like ``p[] = f(q)`` where ``f`` can forward ``q``. for i in 0..<len(w.assignments): let a = addr(w.assignments[i]) if a.destInfo != {}: possibleAliases(w, a.dest) if {rootIsHeapAccess, markAsWrittenTo} * a.destInfo != {}: for p in a.dest: if p.kind == skParam and p.owner == w.owner: incl(p.flags, sfWrittenTo) if w.owner.kind == skFunc and p.typ.kind != tyVar: localError(conf, a.info, "write access to non-var parameter: " & p.name.s) if {rootIsResultOrParam, rootIsHeapAccess, markAsEscaping}*a.destInfo != {}: var destIsParam = false for p in a.dest: if p.kind in {skResult, skParam} and p.owner == w.owner: destIsParam = true break if destIsParam: possibleAliases(w, a.src) for p in a.src: if p.kind == skParam and p.owner == w.owner: incl(p.flags, sfEscapes) proc trackWrites*(owner: PSym; body: PNode; conf: ConfigRef) = var w: W w.owner = owner w.assignments = @[] # Phase 1: Collect and preprocess any assignments in the proc body: deps(w, body) # Phase 2: Compute the 'writes' and 'escapes' effects: markWriteOrEscape(w, conf) if w.returnsNew != asgnOther and not isEmptyType(owner.typ.sons[0]) and containsGarbageCollectedRef(owner.typ.sons[0]): incl(owner.typ.flags, tfReturnsNew)