diff --git a/.gitignore b/.gitignore index 56c4a5d..ca894d7 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,8 @@ _release *.byte *.native *.install + +# BuckleScript + +lib/ +*.bs.js diff --git a/bsconfig.json b/bsconfig.json new file mode 100644 index 0000000..8804255 --- /dev/null +++ b/bsconfig.json @@ -0,0 +1,23 @@ +{ + "name": "Fetch", + "namespace": true, + "sources": [ + { + "dir": "src/fetch-js", + "subdirs": true + }, + { + "dir": "src/fetch-core", + "subdirs": true + } + ], + "package-specs": { + "module": "commonjs", + "in-source": true + }, + "refmt": 3, + "suffix": ".bs.js", + "generate-merlin": true, + "bs-dev-dependencies": ["bs-fetch", "reason-promise"], + "bs-dependencies": ["bs-fetch", "reason-promise"] +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..e738398 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,70 @@ +{ + "name": "fetch-js", + "version": "0.1.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "bs-fetch": { + "version": "github:lessp/bs-fetch#758b76406ce20f2885a81de89c55ce22a370ceba", + "from": "github:lessp/bs-fetch#758b764" + }, + "bs-platform": { + "version": "github:jaredly/bucklescript#32a426d4ef57097fdab11437773f4c8decaaae60", + "from": "github:jaredly/bucklescript#letop" + }, + "encoding": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", + "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", + "requires": { + "iconv-lite": "~0.4.13" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + }, + "isomorphic-fetch": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", + "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=", + "requires": { + "node-fetch": "^1.0.1", + "whatwg-fetch": ">=0.10.0" + } + }, + "node-fetch": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", + "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", + "requires": { + "encoding": "^0.1.11", + "is-stream": "^1.0.1" + } + }, + "reason-promise": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/reason-promise/-/reason-promise-1.0.2.tgz", + "integrity": "sha512-j8DWV+71wNEKQmyW6zBOowIZq1Qec5CDWyLI37BvgOmAZgRFGFQ1MaJnbhqDe3JsYmaGyqdNMmpCJLl9EDbDAg==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "whatwg-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz", + "integrity": "sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..508376a --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "fetch-js", + "version": "0.1.0", + "license": "MIT", + "scripts": { + "clean": "bsb -clean-world", + "build": "bsb -make-world", + "watch": "bsb -make-world -w", + "example": "bsb -make-world && node src/fetch-js/examples/WithLetOperators.bs.js" + }, + "dependencies": { + "bs-fetch": "lessp/bs-fetch#758b764", + "reason-promise": "^1.0.2", + "bs-platform": "jaredly/bucklescript#letop", + "isomorphic-fetch": "^2.2.1" + }, + "peerDependencies": { + "bs-platform": ">= 5.0.0" + }, + "files": [ + "src/fetch-js/Fetch.rei", + "src/fetch-js/Fetch.re", + "bsconfig.json" + ] +} diff --git a/src/fetch-js/.npmignore b/src/fetch-js/.npmignore new file mode 100644 index 0000000..a069fd3 --- /dev/null +++ b/src/fetch-js/.npmignore @@ -0,0 +1,5 @@ +.DS_Store +*.install +*.merlin +*.lock +_esy diff --git a/src/fetch-js/README.md b/src/fetch-js/README.md new file mode 100644 index 0000000..d9e7628 --- /dev/null +++ b/src/fetch-js/README.md @@ -0,0 +1,23 @@ +# Fetch JS + +Fetch client for BuckleScript. + +## Examples + +```reason +Fetch_Js.( + post( + "https://httpbin.org/post", + ~headers=[("Authorization", "Bearer xyz")], + ~body="Hello, World!", + ) + ->Promise.flatMap( + fun + | Ok({Response.body, _}) => Body.toString(body)->Promise.resolved + | Error(errorMsg) => errorMsg->Promise.resolved, + ) + ->Promise.map(Js.log) +); +``` + +See [the examples](./examples/). diff --git a/src/fetch-js/examples/GetRequest.re b/src/fetch-js/examples/GetRequest.re new file mode 100644 index 0000000..d93bb24 --- /dev/null +++ b/src/fetch-js/examples/GetRequest.re @@ -0,0 +1,9 @@ +Fetch_Js.( + get("https://httpbin.org/get") + ->Promise.map( + fun + | Ok({Response.status, _}) when Status.isSuccessful(status) => "Success!" + | _ => "That's anything but successful. :-(", + ) + ->Promise.get(Js.log) +); diff --git a/src/fetch-js/examples/JsonPostRequest.re b/src/fetch-js/examples/JsonPostRequest.re new file mode 100644 index 0000000..076b583 --- /dev/null +++ b/src/fetch-js/examples/JsonPostRequest.re @@ -0,0 +1,25 @@ +open LetOperators; + +let jsonBody = Js.Json.parseExn({| +{ + "foo": "bar" +} +|}); + +Fetch_Js.( + { + let.flatMapOk {Response.body, _} = + post( + "https://httpbin.org/post", + ~headers=[ + ("Authorization", "Bearer xyz"), + ("content-type", "application/json"), + ], + ~body=Js.Json.stringify(jsonBody), + ); + + Js.log2("Parse JSON: ", Body.toString(body)->Js.Json.parseExn); + + Promise.resolved(Ok()); + } +); diff --git a/src/fetch-js/examples/LetOperators.re b/src/fetch-js/examples/LetOperators.re new file mode 100644 index 0000000..06d63b2 --- /dev/null +++ b/src/fetch-js/examples/LetOperators.re @@ -0,0 +1,11 @@ +let (let.flatMapOk) = (promise, fn) => + Promise.flatMap( + promise, + fun + | Ok(response) => fn(response) + | Error(e) => Promise.resolved(Belt.Result.Error(e)), + ); + +let (let.flatMap) = (promise, fn) => Promise.flatMap(promise, fn); +let (let.map) = Promise.map; +let (let.mapOk) = Promise.mapOk; diff --git a/src/fetch-js/examples/Simple.re b/src/fetch-js/examples/Simple.re new file mode 100644 index 0000000..08982f5 --- /dev/null +++ b/src/fetch-js/examples/Simple.re @@ -0,0 +1,10 @@ +Fetch_Js.( + { + get("https://httpbin.org/get") + ->Promise.map( + fun + | Ok({Response.body, _}) => Js.log2("Body:", Body.toString(body)) + | Error(err) => Js.log2("Error: ", err), + ); + } +); diff --git a/src/fetch-js/examples/WithLetOperators.re b/src/fetch-js/examples/WithLetOperators.re new file mode 100644 index 0000000..e4a9174 --- /dev/null +++ b/src/fetch-js/examples/WithLetOperators.re @@ -0,0 +1,11 @@ +open LetOperators; + +Fetch_Js.( + { + let.flatMapOk {Response.body, _} = get("https://httpbin.org/get"); + + Js.log(Body.toString(body)); + + Promise.resolved(Ok()); + } +); diff --git a/src/fetch-js/src/Fetch_Js.re b/src/fetch-js/src/Fetch_Js.re new file mode 100644 index 0000000..50be56c --- /dev/null +++ b/src/fetch-js/src/Fetch_Js.re @@ -0,0 +1,106 @@ +[%bs.raw {| require("isomorphic-fetch") |}]; + +let decodeRequestMethod = meth => { + let methStringified = Method.toString(meth); + + switch (methStringified) { + | "GET" => BsFetch.Bs_Fetch.Get + | "HEAD" => Head + | "POST" => Post + | "PUT" => Put + | "DELETE" => Delete + | "CONNECT" => Connect + | "OPTIONS" => Options + | "TRACE" => Trace + | "PATCH" => Patch + | otherMethod => Other(otherMethod) + }; +}; + +module FetchImplementation = { + module Headers = Fetch_Core.Headers; + module Method = Fetch_Core.Method; + module Status = Fetch_Core.Status; + + type promise('a) = Promise.t('a); + type result('a, 'error) = Belt.Result.t('a, 'error); + + module Body = { + type t = string; + + let make = body => body; + + let toString = body => body; + + let ofString = body => make(body); + }; + + module Response = { + module Body = Body; + module Status = Status; + + type t = { + body: Body.t, + headers: list(Headers.t), + status: Status.t, + url: string, + }; + + let make = (~body, ~headers, ~status, ~url) => { + body, + headers, + status, + url, + }; + }; + + let fetch = (~body=?, ~headers=[], ~meth=`GET, url) => { + let {Fetch_Core.Request.headers, body, meth, url} = + Fetch_Core.Request.create(~body, ~headers, ~meth, ~url); + + let (promise, resolve) = Promise.pending(); + + BsFetch.Bs_Fetch.fetchWithInit( + url, + BsFetch.Bs_Fetch.RequestInit.make( + ~method_=decodeRequestMethod(meth), + ~body= + BsFetch.Bs_Fetch.BodyInit.make( + Belt.Option.getWithDefault(body, ""), + ), + ~headers= + BsFetch.Bs_Fetch.HeadersInit.makeWithDict( + Js.Dict.fromList(headers), + ), + (), + ), + ) + |> Js.Promise.then_(response => { + BsFetch.Bs_Fetch.Response.text(response) + |> Js.Promise.then_(body => { + resolve( + Ok( + Response.make( + ~status= + Status.make( + BsFetch.Bs_Fetch.Response.status(response), + ), + ~body=Body.make(body), + ~headers, + ~url, + ), + ), + ); + Js.Promise.resolve(); + }) + }) + |> Js.Promise.catch(error => + Js.Promise.resolve(resolve(Error(Js.String.make(error)))) + ) + |> ignore; + + promise; + }; +}; + +include Fetch_Core.Fetchify.CreateFetchImplementation(FetchImplementation); diff --git a/src/fetch-js/src/Fetch_Js.rei b/src/fetch-js/src/Fetch_Js.rei new file mode 100644 index 0000000..8d5e3ce --- /dev/null +++ b/src/fetch-js/src/Fetch_Js.rei @@ -0,0 +1,4 @@ +include + Fetch_Core.Fetchify.FETCHIFIED with + type promise('a) := Promise.t('a) and + type result('a, 'error) := Belt.Result.t('a, 'error); diff --git a/test/fetch-core/__snapshots__/placeholder b/test/fetch-core/__snapshots__/placeholder deleted file mode 100644 index e69de29..0000000