Skip to content

Commit 25abc38

Browse files
committed
Options approach
1 parent 627c90f commit 25abc38

16 files changed

+629
-0
lines changed

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/.*
2+
!/.gitignore
3+
/bower_components/
4+
/node_modules/
5+
/output/
6+
/tmp/

bower.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"name": "purescript-affjax",
3+
"homepage": "https://github.com/slamdata/purescript-affjax",
4+
"description": "An asynchronous AJAX library built using Aff.",
5+
"keywords": [
6+
"purescript",
7+
"ajax"
8+
],
9+
"license": "MIT",
10+
"ignore": [
11+
"**/.*",
12+
"bower_components",
13+
"node_modules",
14+
"output",
15+
"test",
16+
"tmp",
17+
"bower.json",
18+
"gulpfile.js",
19+
"package.json"
20+
],
21+
"dependencies": {
22+
"purescript-aff": "~0.7.0",
23+
"purescript-arraybuffer-types": "~0.1.1",
24+
"purescript-dom": "~0.1.2",
25+
"purescript-foreign": "~0.4.1",
26+
"purescript-integers": "~0.0.1",
27+
"purescript-options": "~0.2.1"
28+
}
29+
}

gulpfile.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"use strict";
2+
3+
var gulp = require("gulp");
4+
var plumber = require("gulp-plumber");
5+
var purescript = require("gulp-purescript");
6+
var jsvalidate = require("gulp-jsvalidate");
7+
8+
gulp.task("make", function() {
9+
return gulp.src(["src/**/*.purs", "bower_components/purescript-*/src/**/*.purs"])
10+
.pipe(plumber())
11+
.pipe(purescript.pscMake());
12+
});
13+
14+
gulp.task("make-test", function() {
15+
return gulp.src(["src/**/*.purs", "test/**/*.purs", "bower_components/purescript-*/src/**/*.purs"])
16+
.pipe(plumber())
17+
.pipe(purescript.psc({ main: "Test.Main", output: "test.js" }))
18+
.pipe(gulp.dest("tmp/"));
19+
});
20+
21+
gulp.task("jsvalidate", ["make"], function () {
22+
return gulp.src("output/**/*.js")
23+
.pipe(plumber())
24+
.pipe(jsvalidate());
25+
});
26+
27+
gulp.task("docs", function () {
28+
return gulp.src("src/**/*.purs")
29+
.pipe(plumber())
30+
.pipe(purescript.pscDocs())
31+
.pipe(gulp.dest("README.md"));
32+
});
33+
34+
// gulp.task("default", ["jsvalidate", "docs"]);
35+
gulp.task("default", ["make-test"]);

package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"private": true,
3+
"devDependencies": {
4+
"gulp": "^3.8.11",
5+
"gulp-jsvalidate": "^1.0.1",
6+
"gulp-plumber": "^1.0.0",
7+
"gulp-purescript": "^0.1.2"
8+
}
9+
}

src/Data/Proxy.purs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module Data.Proxy where
2+
3+
data Proxy a = Proxy

src/Network/HTTP/Affjax.purs

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
module Network.HTTP.Affjax
2+
( Ajax()
3+
, AffjaxOptions()
4+
, AffjaxResponse()
5+
, url, method, content, headers, username, password
6+
, affjax
7+
, affjax'
8+
) where
9+
10+
import Control.Monad.Aff (Aff(), makeAff)
11+
import Control.Monad.Eff (Eff())
12+
import Control.Monad.Eff.Exception (Error())
13+
import Data.Foreign (Foreign(..))
14+
import Data.Function (Fn4(), runFn4)
15+
import Data.Options (Option(), Options(), IsOption, options, (:=), opt)
16+
import Data.Proxy (Proxy(..))
17+
import Network.HTTP.Affjax.Request
18+
import Network.HTTP.Affjax.Response
19+
import Network.HTTP.Affjax.ResponseType
20+
import Network.HTTP.Method (Method())
21+
import Network.HTTP.RequestHeader (RequestHeader())
22+
import Network.HTTP.ResponseHeader (ResponseHeader(), responseHeader)
23+
import Network.HTTP.StatusCode (StatusCode())
24+
25+
-- | The effect type for AJAX requests made with Affjax.
26+
foreign import data Ajax :: !
27+
28+
-- | Options type for Affjax requests.
29+
foreign import data AffjaxOptions :: * -> *
30+
31+
-- | The type of records that will be received as an Affjax response.
32+
type AffjaxResponse a =
33+
{ status :: StatusCode
34+
, headers :: [ResponseHeader]
35+
, response :: a
36+
}
37+
38+
-- | Sets the URL for a request.
39+
url :: forall a. Option (AffjaxOptions a) String
40+
url = opt "url"
41+
42+
-- | Sets the HTTP method for a request.
43+
method :: forall a. Option (AffjaxOptions a) Method
44+
method = opt "method"
45+
46+
-- | Sets the content to send in a request.
47+
content :: forall a. (Requestable a, IsOption a) => Option (AffjaxOptions a) a
48+
content = opt "content"
49+
50+
-- | Sets the headers to send with a request.
51+
headers :: forall a. Option (AffjaxOptions a) [RequestHeader]
52+
headers = opt "headers"
53+
54+
-- | Sets the HTTP auth username to send with a request.
55+
username :: forall a. Option (AffjaxOptions a) String
56+
username = opt "username"
57+
58+
-- | Sets the HTTP auth password to send with a request.
59+
password :: forall a. Option (AffjaxOptions a) String
60+
password = opt "password"
61+
62+
-- | Sets the expected response type for a request. This is not exposed outside
63+
-- | of the module as the `ResponseType` is set based on the `Responsable`
64+
-- | instance for the expected result content type.
65+
responseType = opt "responseType" :: forall a. Option (AffjaxOptions a) ResponseType
66+
67+
-- | Runs a request.
68+
affjax :: forall e a b. (Requestable a, Responsable b) =>
69+
Options (AffjaxOptions a) ->
70+
Aff (ajax :: Ajax | e) (AffjaxResponse b)
71+
affjax = makeAff <<< affjax'
72+
73+
-- | Runs a request directly in Eff.
74+
affjax' :: forall e a b. (Requestable a, Responsable b) =>
75+
Options (AffjaxOptions a) ->
76+
(Error -> Eff (ajax :: Ajax | e) Unit) ->
77+
(AffjaxResponse b -> Eff (ajax :: Ajax | e) Unit) ->
78+
Eff (ajax :: Ajax | e) Unit
79+
affjax' opts eb cb =
80+
let opts' = opts <> responseType := toResponseType (Proxy :: Proxy b)
81+
in runFn4 unsafeAjax responseHeader (options opts') eb cb
82+
83+
foreign import unsafeAjax
84+
"""
85+
function unsafeAjax (mkHeader, options, errback, callback) {
86+
return function () {
87+
var xhr = new XMLHttpRequest();
88+
xhr.open(options.method || "GET", options.url || "/", true, options.username, options.password);
89+
if (options.headers) {
90+
for (var i = 0, header; header = options.headers[i]; i++) {
91+
xhr.setRequestHeader(header.field, header.value);
92+
}
93+
}
94+
xhr.onerror = function (err) {
95+
errback(err)();
96+
};
97+
xhr.onload = function () {
98+
callback({
99+
status: xhr.status,
100+
headers: xhr.getAllResponseHeaders().split("\n")
101+
.filter(function (header) {
102+
return header.length > 0;
103+
})
104+
.map(function (header) {
105+
var i = header.indexOf(":");
106+
return mkHeader(header.substring(0, i))(header.substring(i + 2));
107+
}),
108+
response: xhr.response
109+
})();
110+
};
111+
if (options.responseType) xhr.responseType = options.responseType;
112+
xhr.send(options.content);
113+
};
114+
}
115+
""" :: forall e a b. Fn4 (String -> String -> ResponseHeader)
116+
Foreign
117+
(Error -> Eff (ajax :: Ajax | e) Unit)
118+
(AffjaxResponse b -> Eff (ajax :: Ajax | e) Unit)
119+
(Eff (ajax :: Ajax | e) Unit)

src/Network/HTTP/Affjax/Request.purs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
module Network.HTTP.Affjax.Request
2+
( RequestContent()
3+
, Requestable, toContent
4+
) where
5+
6+
import Data.Options (Option(), Options(), IsOption, optionFn, (:=))
7+
import DOM (Document())
8+
import DOM.File (Blob())
9+
import DOM.XHR (FormData())
10+
import Network.HTTP.MimeType (MimeType())
11+
import qualified Data.ArrayBuffer.Types as A
12+
13+
-- | Type representing all content types that be sent via XHR (ArrayBufferView,
14+
-- | Blob, Document, String, FormData).
15+
foreign import data RequestContent :: *
16+
17+
instance isOptionRequestContent :: IsOption RequestContent where
18+
(:=) = unsafeIsOption
19+
20+
-- | A class for types that can be converted to values that can be sent with
21+
-- | XHR requests.
22+
class Requestable a where
23+
toContent :: a -> RequestContent
24+
25+
instance requestableAjaxRequestContent :: Requestable RequestContent where
26+
toContent = id
27+
28+
instance requestableInt8Array :: Requestable (A.ArrayView A.Int8) where
29+
toContent = unsafeConversion
30+
31+
instance requestableInt16Array :: Requestable (A.ArrayView A.Int16) where
32+
toContent = unsafeConversion
33+
34+
instance requestableInt32Array :: Requestable (A.ArrayView A.Int32) where
35+
toContent = unsafeConversion
36+
37+
instance requestableUint8Array :: Requestable (A.ArrayView A.Uint8) where
38+
toContent = unsafeConversion
39+
40+
instance requestableUint16Array :: Requestable (A.ArrayView A.Uint16) where
41+
toContent = unsafeConversion
42+
43+
instance requestableUint32Array :: Requestable (A.ArrayView A.Uint32) where
44+
toContent = unsafeConversion
45+
46+
instance requestableUint8ClampedArray :: Requestable (A.ArrayView A.Uint8Clamped) where
47+
toContent = unsafeConversion
48+
49+
instance requestableFloat32Array :: Requestable (A.ArrayView A.Float32) where
50+
toContent = unsafeConversion
51+
52+
instance requestableFloat64Array :: Requestable (A.ArrayView A.Float64) where
53+
toContent = unsafeConversion
54+
55+
instance requestableBlob :: Requestable Blob where
56+
toContent = unsafeConversion
57+
58+
instance requestableDocument :: Requestable Document where
59+
toContent = unsafeConversion
60+
61+
instance requestableString :: Requestable String where
62+
toContent = unsafeConversion
63+
64+
instance requestableFormData :: Requestable FormData where
65+
toContent = unsafeConversion
66+
67+
instance requestableUnit :: Requestable Unit where
68+
toContent = unsafeConversion
69+
70+
foreign import unsafeIsOption
71+
"""
72+
function unsafeIsOption(k) {
73+
return function (v) {
74+
return [[k, v]];
75+
};
76+
}
77+
""" :: forall b a. (Option b a) -> a -> (Options b)
78+
79+
foreign import unsafeConversion
80+
"""
81+
function unsafeConversion (x) {
82+
return x;
83+
}
84+
""" :: forall a b. a -> b

src/Network/HTTP/Affjax/Response.purs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
module Network.HTTP.Affjax.Response
2+
( ResponseContent()
3+
, Responsable, toResponseType, fromContent
4+
) where
5+
6+
import Data.Either (Either(..))
7+
import Data.Foreign (Foreign(), ForeignError())
8+
import Data.Options (IsOption, optionFn, (:=))
9+
import Data.Proxy (Proxy())
10+
import DOM (Document())
11+
import DOM.File (Blob())
12+
import DOM.XHR (FormData())
13+
import Network.HTTP.Affjax.ResponseType
14+
import qualified Data.ArrayBuffer.Types as A
15+
16+
-- | Type representing content types that be received from an XHR request
17+
-- | (ArrayBuffer, Blob, Document, JSON, String).
18+
type ResponseContent = Foreign
19+
20+
-- | Class for types that converted from values returned from an XHR request.
21+
class Responsable a where
22+
toResponseType :: Proxy a -> ResponseType
23+
fromContent :: ResponseContent -> Either ForeignError a
24+
25+
instance responsableUnit :: Responsable Unit where
26+
toResponseType _ = StringResponse
27+
fromContent _ = Right unit
28+
29+
instance responsableInt8Array :: Responsable (A.ArrayView A.Int8) where
30+
toResponseType _ = ArrayBufferResponse
31+
fromContent = arrayBufferConversion
32+
33+
instance responsableInt16Array :: Responsable (A.ArrayView A.Int16) where
34+
toResponseType _ = ArrayBufferResponse
35+
fromContent = arrayBufferConversion
36+
37+
instance responsableInt32Array :: Responsable (A.ArrayView A.Int32) where
38+
toResponseType _ = ArrayBufferResponse
39+
fromContent = arrayBufferConversion
40+
41+
instance responsableUint8Array :: Responsable (A.ArrayView A.Uint8) where
42+
toResponseType _ = ArrayBufferResponse
43+
fromContent = arrayBufferConversion
44+
45+
instance responsableUint16Array :: Responsable (A.ArrayView A.Uint16) where
46+
toResponseType _ = ArrayBufferResponse
47+
fromContent = arrayBufferConversion
48+
49+
instance responsableUint32Array :: Responsable (A.ArrayView A.Uint32) where
50+
toResponseType _ = ArrayBufferResponse
51+
fromContent = arrayBufferConversion
52+
53+
instance responsableUint8ClampedArray :: Responsable (A.ArrayView A.Uint8Clamped) where
54+
toResponseType _ = ArrayBufferResponse
55+
fromContent = arrayBufferConversion
56+
57+
instance responsableFloat32Array :: Responsable (A.ArrayView A.Float32) where
58+
toResponseType _ = ArrayBufferResponse
59+
fromContent = arrayBufferConversion
60+
61+
instance responsableFloat64Array :: Responsable (A.ArrayView A.Float64) where
62+
toResponseType _ = ArrayBufferResponse
63+
fromContent = arrayBufferConversion
64+
65+
instance responsableBlob :: Responsable Blob where
66+
toResponseType _ = BlobResponse
67+
fromContent = unsafeConversion
68+
69+
instance responsableDocument :: Responsable Document where
70+
toResponseType _ = DocumentResponse
71+
fromContent = unsafeConversion
72+
73+
instance responsableString :: Responsable String where
74+
toResponseType _ = StringResponse
75+
fromContent = Right <<< unsafeConversion
76+
77+
-- TODO: this, properly
78+
foreign import arrayBufferConversion
79+
"""
80+
function arrayBufferConversion (x) {
81+
return x;
82+
}
83+
""" :: forall a b. a -> b
84+
85+
-- TODO: not this either, at least use foreign to check the tag of returned values to ensure they are not null, etc.
86+
foreign import unsafeConversion
87+
"""
88+
function unsafeConversion (x) {
89+
return x;
90+
}
91+
""" :: forall a b. a -> b

0 commit comments

Comments
 (0)