Skip to content

Commit

Permalink
Add module jsfetch (nim-lang#12531)
Browse files Browse the repository at this point in the history
* Add module jsfetch for fetch support for JavaScript target https://developer.mozilla.org/docs/Web/API/Fetch_API

* Update lib/std/jsheaders.nim

* Update lib/std/jsformdata.nim

* Update lib/std/jsfetch.nim

Co-authored-by: Timothee Cour <timothee.cour2@gmail.com>
Co-authored-by: flywind <xzsflywind@gmail.com>
  • Loading branch information
3 people committed Mar 22, 2021
1 parent e00f49c commit c1ca6c3
Show file tree
Hide file tree
Showing 5 changed files with 351 additions and 1 deletion.
4 changes: 4 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,10 @@ provided by the operating system.

- `std/options` changed `$some(3)` to `"some(3)"` instead of `"Some(3)"`
and `$none(int)` to `"none(int)"` instead of `"None[int]"`.
- Added `std/jsfetch` module [Fetch](https://developer.mozilla.org/docs/Web/API/Fetch_API) wrapper for JavaScript target.
- Added `std/jsheaders` module [Headers](https://developer.mozilla.org/en-US/docs/Web/API/Headers) wrapper for JavaScript target.
- Added `std/jsformdata` module [FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData) wrapper for JavaScript target.


- `system.addEscapedChar` now renders `\r` as `\r` instead of `\c`, to be compatible
with most other languages.
Expand Down
198 changes: 198 additions & 0 deletions lib/std/jsfetch.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
## - Fetch for the JavaScript target: https://developer.mozilla.org/docs/Web/API/Fetch_API
## .. Note:: jsfetch is Experimental. jsfetch module requires `-d:nimExperimentalJsfetch`
when not defined(js):
{.fatal: "Module jsfetch is designed to be used with the JavaScript backend.".}

when defined(nimExperimentalJsfetch) or defined(nimdoc):
import std/[asyncjs, jsheaders, jsformdata]
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

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"

Body* = ref object of JsRoot ## https://developer.mozilla.org/en-US/docs/Web/API/Body
bodyUsed*: bool

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*: Body

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*: Body


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 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/Body/text

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

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

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

func newfetchOptions*(metod: HttpMethod; body: cstring;
mode: FetchModes; credentials: FetchCredentials; cache: FetchCaches; referrerPolicy: FetchReferrerPolicies;
keepalive: bool; redirect = frFollow; referrer = "client".cstring; integrity = "".cstring): FetchOptions =
## Constructor for `FetchOptions`.
result = FetchOptions(
body: body, mode: $mode, credentials: $credentials, cache: $cache, referrerPolicy: $referrerPolicy,
keepalive: keepalive, redirect: $redirect, referrer: referrer, integrity: integrity,
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 | Body | FetchOptions): cstring {.importjs: "JSON.stringify(#)".}

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


runnableExamples("-d:nimExperimentalJsfetch -r:off"):
import std/[asyncjs, jsconsole, jsheaders, jsformdata]
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
)
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

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
)
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

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 Body

discard example()

when defined(nimExperimentalAsyncjsThen):
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()
65 changes: 65 additions & 0 deletions lib/std/jsformdata.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
## - `FormData` for the JavaScript target: https://developer.mozilla.org/en-US/docs/Web/API/FormData
when not defined(js):
{.fatal: "Module jsformdata is designed to be used with the JavaScript backend.".}

type FormData* = ref object of JsRoot ## FormData API.

func newFormData*(): FormData {.importjs: "new FormData()".}

func add*(self: FormData; name: cstring; value: SomeNumber | bool | cstring) {.importjs: "#.append(#, #)".}
## https://developer.mozilla.org/en-US/docs/Web/API/FormData/append
## Duplicate keys are allowed and order is preserved.

func add*(self: FormData; name: cstring; value: SomeNumber | bool | cstring, filename: cstring) {.importjs: "#.append(#, #, #)".}
## https://developer.mozilla.org/en-US/docs/Web/API/FormData/append
## Duplicate keys are allowed and order is preserved.

func delete*(self: FormData; name: cstring) {.importjs: "#.$1(#)".}
## https://developer.mozilla.org/en-US/docs/Web/API/FormData/delete
##
## .. Warning:: Deletes *all items* with the same key name.

func getAll*(self: FormData; name: cstring): seq[cstring] {.importjs: "#.$1(#)".}
## https://developer.mozilla.org/en-US/docs/Web/API/FormData/getAll

func hasKey*(self: FormData; name: cstring): bool {.importjs: "#.has(#)".}
## https://developer.mozilla.org/en-US/docs/Web/API/FormData/has

func keys*(self: FormData): seq[cstring] {.importjs: "Array.from(#.$1())".}
## https://developer.mozilla.org/en-US/docs/Web/API/FormData/keys

func values*(self: FormData): seq[cstring] {.importjs: "Array.from(#.$1())".}
## https://developer.mozilla.org/en-US/docs/Web/API/FormData/values

func pairs*(self: FormData): seq[tuple[key, val: cstring]] {.importjs: "Array.from(#.entries())".}
## https://developer.mozilla.org/en-US/docs/Web/API/FormData/entries

func put*(self: FormData; name, value, filename: cstring) {.importjs: "#.set(#, #, #)".}
## https://developer.mozilla.org/en-US/docs/Web/API/FormData/set

func `[]=`*(self: FormData; name, value: cstring) {.importjs: "#.set(#, #)".}
## https://developer.mozilla.org/en-US/docs/Web/API/FormData/set

func `[]`*(self: FormData; name: cstring): cstring {.importjs: "#.get(#)".}
## https://developer.mozilla.org/en-US/docs/Web/API/FormData/get

func clear*(self: FormData) {.importjs:
"(() => { const frmdt = #; Array.from(frmdt.keys()).forEach((key) => frmdt.delete(key)) })()".}
## Convenience func to delete all items from `FormData`.

func toCstring*(self: FormData): cstring {.importjs: "JSON.stringify(#)".}

func `$`*(self: FormData): string = $toCstring(self)

func len*(self: FormData): int {.importjs: "Array.from(#.entries()).length".}


runnableExamples("-r:off"):
let data: FormData = newFormData()
data["key0"] = "value0".cstring
data.add("key1".cstring, "value1".cstring)
data.delete("key1")
assert data.hasKey("key0")
assert data["key0"] == "value0".cstring
data.clear()
assert data.len == 0
83 changes: 83 additions & 0 deletions lib/std/jsheaders.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
## - HTTP Headers for the JavaScript target: https://developer.mozilla.org/en-US/docs/Web/API/Headers
when not defined(js):
{.fatal: "Module jsheaders is designed to be used with the JavaScript backend.".}

type Headers* = ref object of JsRoot ## HTTP Headers API.

func newHeaders*(): Headers {.importjs: "new Headers()".}
## https://developer.mozilla.org/en-US/docs/Web/API/Headers

func add*(self: Headers; key: cstring; value: cstring) {.importjs: "#.append(#, #)".}
## Allows duplicated keys.
## https://developer.mozilla.org/en-US/docs/Web/API/Headers/append

func delete*(self: Headers; key: cstring) {.importjs: "#.$1(#)".}
## https://developer.mozilla.org/en-US/docs/Web/API/Headers/delete
##
## .. Warning:: Delete *all* items with `key` from the headers, including duplicated keys.

func hasKey*(self: Headers; key: cstring): bool {.importjs: "#.has(#)".}
## https://developer.mozilla.org/en-US/docs/Web/API/Headers/has

func keys*(self: Headers): seq[cstring] {.importjs: "Array.from(#.$1())".}
## https://developer.mozilla.org/en-US/docs/Web/API/Headers/keys

func values*(self: Headers): seq[cstring] {.importjs: "Array.from(#.$1())".}
## https://developer.mozilla.org/en-US/docs/Web/API/Headers/values

func entries*(self: Headers): seq[tuple[key, value: cstring]] {.importjs: "Array.from(#.$1())".}
## https://developer.mozilla.org/en-US/docs/Web/API/Headers/entries

func `[]`*(self: Headers; key: cstring): cstring {.importjs: "#.get(#)".}
## Get *all* items with `key` from the headers, including duplicated values.
## https://developer.mozilla.org/en-US/docs/Web/API/Headers/get

func `[]=`*(self: Headers; key: cstring; value: cstring) {.importjs: "#.set(#, #)".}
## Do *not* allow duplicated keys, overwrites duplicated keys.
## https://developer.mozilla.org/en-US/docs/Web/API/Headers/set

func clear*(self: Headers) {.importjs:
"(() => { const header = #; Array.from(header.keys()).forEach((key) => header.delete(key)) })()".}
## Convenience func to delete all items from `Headers`.

func toCstring*(self: Headers): cstring {.importjs: "JSON.stringify(Array.from(#.entries()))".}
## Returns a `cstring` representation of `Headers`.

func `$`*(self: Headers): string = $toCstring(self)

func len*(self: Headers): int {.importjs: "Array.from(#.entries()).length".}


runnableExamples("-r:off"):

block:
let header: Headers = newHeaders()
header.add("key", "value")
assert header.hasKey("key")
assert header.keys() == @["key".cstring]
assert header.values() == @["value".cstring]
assert header["key"] == "value".cstring
header["other"] = "another".cstring
assert header["other"] == "another".cstring
assert header.entries() == @[("key".cstring, "value".cstring), ("other".cstring, "another".cstring)]
assert header.toCstring() == """[["key","value"],["other","another"]]""".cstring
header.delete("other")
assert header.entries() == @[("key".cstring, "value".cstring)]
header.clear()
assert header.entries() == @[]
assert header.len == 0

block:
let header: Headers = newHeaders()
header.add("key", "a")
header.add("key", "b") ## Duplicated.
header.add("key", "c") ## Duplicated.
assert header["key"] == "a, b, c".cstring
header["key"] = "value".cstring
assert header["key"] == "value".cstring

block:
let header: Headers = newHeaders()
header["key"] = "a"
header["key"] = "b" ## Overwrites.
assert header["key"] == "b".cstring
2 changes: 1 addition & 1 deletion tools/kochdocs.nim
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const
webUploadOutput = "web/upload"

var nimExe*: string
const allowList = ["jsbigints.nim"]
const allowList = ["jsbigints.nim", "jsheaders.nim", "jsformdata.nim", "jsfetch.nim"]

template isJsOnly(file: string): bool =
file.isRelativeTo("lib/js") or
Expand Down

0 comments on commit c1ca6c3

Please sign in to comment.