Check in initial draft of elm-http
This commit is contained in:
commit
c9d7a8a4f7
5 changed files with 596 additions and 0 deletions
30
LICENSE
Normal file
30
LICENSE
Normal file
|
@ -0,0 +1,30 @@
|
|||
Copyright (c) 2015, Evan Czaplicki
|
||||
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following
|
||||
disclaimer in the documentation and/or other materials provided
|
||||
with the distribution.
|
||||
|
||||
* Neither the name of Evan Czaplicki nor the names of other
|
||||
contributors may be used to endorse or promote products derived
|
||||
from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
16
elm-package.json
Normal file
16
elm-package.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"version": "1.0.0",
|
||||
"summary": "Basic foundation for HTTP communication",
|
||||
"repository": "http://github.com/evancz/elm-http.git",
|
||||
"license": "BSD3",
|
||||
"source-directories": [
|
||||
"src"
|
||||
],
|
||||
"exposed-modules": [
|
||||
"Http"
|
||||
],
|
||||
"native-modules": true,
|
||||
"dependencies": {
|
||||
"elm-lang/core": "2.0.0 <= v < 3.0.0"
|
||||
}
|
||||
}
|
26
src/Blob.elm
Normal file
26
src/Blob.elm
Normal file
|
@ -0,0 +1,26 @@
|
|||
module Blob where
|
||||
{-| https://developer.mozilla.org/en-US/docs/Web/API/Blob
|
||||
-}
|
||||
|
||||
type Data
|
||||
= ArrayData (List Data)
|
||||
| ArrayBufferData
|
||||
| StringData String
|
||||
| BlobData Blob
|
||||
|
||||
|
||||
blob : Data -> Blob
|
||||
|
||||
|
||||
{-| Get the size of a blob in bytes.
|
||||
-}
|
||||
size : Blob -> Int
|
||||
|
||||
|
||||
{-| Get the MIME type of the data stored in the `Blob` if that information
|
||||
is available.
|
||||
-}
|
||||
mimeType : Blob -> Maybe String
|
||||
|
||||
|
||||
slice : Int -> Int -> Maybe String -> Blob -> Blob
|
363
src/Http.elm
Normal file
363
src/Http.elm
Normal file
|
@ -0,0 +1,363 @@
|
|||
module Http
|
||||
( get, post, send
|
||||
, Request
|
||||
, Body, empty, string, multipart
|
||||
, Data, stringData, blobData
|
||||
, Settings, defaultSettings
|
||||
, Response, Value(..)
|
||||
, Error
|
||||
) where
|
||||
{-|
|
||||
|
||||
# Fetching JSON
|
||||
@docs get, post, Error
|
||||
|
||||
# Body Values
|
||||
@docs empty, string, multipart, stringData
|
||||
|
||||
# Arbitrariy Requests
|
||||
@docs send, Request, Settings, defaultSettigs
|
||||
|
||||
# Responses
|
||||
@docs Response, Value, fromJson, RawError
|
||||
-}
|
||||
|
||||
import Basics exposing ((&&), (<=), (<))
|
||||
import Dict exposing (Dict)
|
||||
import JavaScript.Decode as JavaScript
|
||||
import Native.Http
|
||||
import Promise exposing (Promise, andThen, mapError, succeed, fail)
|
||||
|
||||
|
||||
type Blob = TODO_impliment_blob_in_another_library
|
||||
type File = TODO_impliment_file_in_another_library
|
||||
|
||||
|
||||
-- REQUESTS
|
||||
|
||||
{-| Fully specify the request you want to send. For example, if you want to
|
||||
send a request between domains (CORS request) you will need to specify some
|
||||
headers manually.
|
||||
|
||||
corsPost : Request
|
||||
corsPost =
|
||||
{ verb = "POST"
|
||||
, headers =
|
||||
[ ("Origin", "http://elm-lang.org")
|
||||
, ("Access-Control-Request-Method", "POST")
|
||||
, ("Access-Control-Request-Headers", "X-Custom-Header")
|
||||
]
|
||||
, url = "http://www.example.com/hats"
|
||||
, body = empty
|
||||
}
|
||||
-}
|
||||
type alias Request =
|
||||
{ verb : String
|
||||
, headers : List (String, String)
|
||||
, url : String
|
||||
, body : Body
|
||||
}
|
||||
|
||||
|
||||
type Body
|
||||
= Empty
|
||||
| BodyString String
|
||||
| ArrayBuffer
|
||||
| BodyFormData
|
||||
| BodyBlob Blob
|
||||
|
||||
|
||||
{-| An empty request body, no value will be sent along.
|
||||
-}
|
||||
empty : Body
|
||||
empty =
|
||||
Empty
|
||||
|
||||
|
||||
{-| Provide a string as the body of the request. Useful if you need to send
|
||||
JSON data to a server that does not belong in the URL.
|
||||
|
||||
import JavaScript.Decode as JS
|
||||
|
||||
coolestHats : Promise Error (List String)
|
||||
coolestHats =
|
||||
post
|
||||
(JS.list JS.string)
|
||||
"http://www.example.com/hats"
|
||||
(string """{ "sortBy": "coolness", "take": 10 }""")
|
||||
-}
|
||||
string : String -> Body
|
||||
string =
|
||||
BodyString
|
||||
|
||||
|
||||
{--
|
||||
arrayBuffer : ArrayBuffer -> Body
|
||||
|
||||
|
||||
blob : Blob -> Body
|
||||
blob _ =
|
||||
BodyBlob
|
||||
--}
|
||||
|
||||
type Data
|
||||
= StringData String String
|
||||
| BlobData String (Maybe String) Blob
|
||||
| FileData String (Maybe String) File
|
||||
|
||||
|
||||
{-| Create multi-part request bodies, allowing you to send many chunks of data
|
||||
all in one request. All chunks of data must be given a name.
|
||||
|
||||
Currently, you can only construct `stringData`, but we will support `blobData`
|
||||
and `fileData` once we have proper APIs for those types of data in Elm.
|
||||
-}
|
||||
multipart : List Data -> Body
|
||||
multipart =
|
||||
Native.Http.multipart
|
||||
|
||||
|
||||
{-| A named chunk of string data.
|
||||
|
||||
import JavaScript.Encode as JS
|
||||
|
||||
body =
|
||||
multipart
|
||||
[ stringData "user" (JS.encode user)
|
||||
, stringData "payload" (JS.encode payload)
|
||||
]
|
||||
-}
|
||||
stringData : String -> String -> Data
|
||||
stringData =
|
||||
StringData
|
||||
|
||||
|
||||
{-| A named chunk of blob data. You provide a name for this piece of data,
|
||||
an optional file name for where the data came from, and the blob itself. If
|
||||
no file name is given, it will default to `"blob"`.
|
||||
|
||||
Currently the only way to obtain a `Blob` is in a `Response` but support will
|
||||
expand once we have an API for blobs in Elm.
|
||||
-}
|
||||
blobData : String -> Maybe String -> Blob -> Data
|
||||
blobData =
|
||||
BlobData
|
||||
|
||||
|
||||
{--
|
||||
fileData : String -> Maybe String -> File -> Data
|
||||
fileData =
|
||||
FileData
|
||||
--}
|
||||
|
||||
|
||||
-- SETTINGS
|
||||
|
||||
|
||||
{-| Configure your request if you need specific behavior.
|
||||
|
||||
* `timeout` lets you specify how long you are willing to wait for a response
|
||||
before giving up. By default it is 0 which means “never give
|
||||
up!”
|
||||
|
||||
* `onStart` and `onProgress` allow you to monitor progress. This is useful
|
||||
if you want to show a progress bar when uploading a large amount of data.
|
||||
|
||||
* `desiredResponseType` lets you override the MIME type of the response, so
|
||||
you can influence what kind of `Value` you get in the `Response`.
|
||||
-}
|
||||
type alias Settings =
|
||||
{ timeout : Time
|
||||
, onStart : Maybe (Promise () ())
|
||||
, onProgress : Maybe (Maybe { loaded : Int, total : Int } -> Promise () ())
|
||||
, desiredResponseType : Maybe String
|
||||
}
|
||||
|
||||
|
||||
{-| The default settings used by `get` and `post`.
|
||||
|
||||
{ timeout = 0
|
||||
, onStart = Nothing
|
||||
, onProgress = Nothing
|
||||
, desiredResponseType = Nothing
|
||||
}
|
||||
-}
|
||||
defaultSettings : Settings
|
||||
defaultSettings =
|
||||
{ timeout = 0
|
||||
, onStart = Nothing
|
||||
, onProgress = Nothing
|
||||
, desiredResponseType = Nothing
|
||||
}
|
||||
|
||||
|
||||
-- RESPONSE HANDLER
|
||||
|
||||
{-| All the details of the response. There are many weird facts about
|
||||
responses which include:
|
||||
|
||||
* The `status` may be 0 in the case that you load something from `file://`
|
||||
* You cannot handle redirects yourself, they will all be followed
|
||||
automatically. If you want to know if you have gone through one or more
|
||||
redirect, the `url` field will let you know who sent you the response, so
|
||||
you will know if it does not match the URL you requested.
|
||||
* You are allowed to have duplicate headers, and their values will be
|
||||
combined into a single comma-separated string.
|
||||
|
||||
We have left these underlying facts about `XMLHttpRequest` as is because one
|
||||
goal of this library is to give a low-level enough API that others can build
|
||||
whatever helpful behavior they want on top of it.
|
||||
-}
|
||||
type alias Response =
|
||||
{ status : Int
|
||||
, statusText : String
|
||||
, headers : Dict String String
|
||||
, url : String
|
||||
, value : Value
|
||||
}
|
||||
|
||||
|
||||
{-| The information given in the response. Currently there is no way to handle
|
||||
`Blob` types since we do not have an Elm API for that yet. This type will
|
||||
expand as more values become available in Elm itself.
|
||||
-}
|
||||
type Value
|
||||
= Text String
|
||||
-- | ArrayBuffer ArrayBuffer
|
||||
| Blob Blob
|
||||
-- | Document Document
|
||||
|
||||
|
||||
-- Errors
|
||||
|
||||
{-| The things that count as errors at the lowest level. Technically, getting
|
||||
a response back with status 404 is a “successful” response in that
|
||||
you actually got all the information you asked for.
|
||||
|
||||
The `fromJson` function and `Error` type provide higher-level errors, but the
|
||||
point of `RawError` is to allow you to define higher-level errors however you
|
||||
want.
|
||||
-}
|
||||
type RawError
|
||||
= RawTimeout
|
||||
| RawNetworkError
|
||||
|
||||
|
||||
{-| The kinds of errors you typically want in practice. When you get a
|
||||
response but its status is not in the 200 range, it will trigger a
|
||||
`BadResponse`. When you try to decode JSON but something goes wrong,
|
||||
you will get an `UnexpectedPayload`.
|
||||
-}
|
||||
type Error
|
||||
= Timeout
|
||||
| NetworkError
|
||||
| UnexpectedPayload String
|
||||
| BadResponse Int String
|
||||
|
||||
|
||||
-- ACTUALLY SEND REQUESTS
|
||||
|
||||
{-| Send a request exactly how you want it. The `Settings` argument lets you
|
||||
configure things like timeouts and progress monitoring. The `Request` argument
|
||||
defines all the information that will actually be sent along to a server.
|
||||
|
||||
crossOriginGet : String -> String -> Promise Error Response
|
||||
crossOriginGet origin url =
|
||||
send defaultSettings
|
||||
{ verb = "GET"
|
||||
, headers = [("Origin", origin)]
|
||||
, url = url
|
||||
, body = empty
|
||||
}
|
||||
-}
|
||||
send : Settings -> Request -> Promise RawError Response
|
||||
send =
|
||||
Native.Http.send
|
||||
|
||||
|
||||
-- HIGH-LEVEL REQUESTS
|
||||
|
||||
{-| Send a GET request to the given url. You also specify how to decode the
|
||||
response.
|
||||
|
||||
import JavaScript.Decode (list, string)
|
||||
|
||||
hats : Promise Error (List String)
|
||||
hats =
|
||||
get (list string) "http://example.com/hat-categories.json"
|
||||
|
||||
-}
|
||||
get : JavaScript.Decoder value -> String -> Promise Error value
|
||||
get decoder url =
|
||||
let request =
|
||||
{ verb = "GET"
|
||||
, headers = []
|
||||
, url = url
|
||||
, body = empty
|
||||
}
|
||||
in
|
||||
fromJson decoder (send defaultSettings request)
|
||||
|
||||
|
||||
{-| Send a POST send to the given url, carrying the given string as the body.
|
||||
You also specify how to decode the response.
|
||||
|
||||
import JavaScript.Decode (list, string)
|
||||
|
||||
hats : Promise Error (List String)
|
||||
hats =
|
||||
post (list string) "http://example.com/hat-categories.json" empty
|
||||
|
||||
-}
|
||||
post : JavaScript.Decoder value -> String -> Body -> Promise Error value
|
||||
post decoder url body =
|
||||
let request =
|
||||
{ verb = "POST"
|
||||
, headers = []
|
||||
, url = url
|
||||
, body = body
|
||||
}
|
||||
in
|
||||
fromJson decoder (send defaultSettings request)
|
||||
|
||||
|
||||
{-| Turn a `Response` into an Elm value that is easier to deal with. Helpful
|
||||
if you are making customized HTTP requests with `send`, as is the case with
|
||||
`get` and `post`.
|
||||
|
||||
Given a `Response` this function will:
|
||||
|
||||
* Check that the status code is in the 200 range.
|
||||
* Make sure the response `Value` is a string.
|
||||
* Convert the string to Elm with the given `Decoder`.
|
||||
|
||||
Assuming all these steps succeed, you will get an Elm value as the result!
|
||||
-}
|
||||
fromJson : JavaScript.Decoder a -> Promise RawError Response -> Promise Error a
|
||||
fromJson decoder response =
|
||||
mapError promoteError response
|
||||
`andThen` handleResponse decoder
|
||||
|
||||
|
||||
handleResponse : JavaScript.Decoder a -> Response -> Promise Error a
|
||||
handleResponse decoder response =
|
||||
case 200 <= response.status && response.status < 300 of
|
||||
False ->
|
||||
fail (BadResponse response.status response.statusText)
|
||||
|
||||
True ->
|
||||
case response.value of
|
||||
Text rawJson ->
|
||||
case JavaScript.decodeString decoder rawJson of
|
||||
Ok v -> succeed v
|
||||
Err msg -> fail (UnexpectedPayload msg)
|
||||
|
||||
_ ->
|
||||
fail (UnexpectedPayload "Response body is a blob, expecting a string.")
|
||||
|
||||
|
||||
promoteError : RawError -> Error
|
||||
promoteError rawError =
|
||||
case rawError of
|
||||
RawTimeout -> Timeout
|
||||
RawNetworkError -> NetworkError
|
161
src/Native/Http.js
Normal file
161
src/Native/Http.js
Normal file
|
@ -0,0 +1,161 @@
|
|||
Elm.Native.Http = {};
|
||||
Elm.Native.Http.make = function(localRuntime) {
|
||||
|
||||
localRuntime.Native = localRuntime.Native || {};
|
||||
localRuntime.Native.Http = localRuntime.Native.Http || {};
|
||||
if (localRuntime.Native.Http.values)
|
||||
{
|
||||
return localRuntime.Native.Http.values;
|
||||
}
|
||||
|
||||
var Dict = Elm.Dict.make(localRuntime);
|
||||
var List = Elm.List.make(localRuntime);
|
||||
var Maybe = Elm.Maybe.make(localRuntime);
|
||||
var Promise = Elm.Native.Promise.make(localRuntime);
|
||||
|
||||
|
||||
function send(settings, request)
|
||||
{
|
||||
return Promise.asyncFunction(function(callback) {
|
||||
var req = new XMLHttpRequest();
|
||||
|
||||
// start
|
||||
if (settings.onStart.ctor === 'Just')
|
||||
{
|
||||
req.addEventListener('loadStart', function() {
|
||||
var promise = settings.onStart._0;
|
||||
Promise.spawn(promise);
|
||||
});
|
||||
}
|
||||
|
||||
// progress
|
||||
if (settings.onProgress.ctor === 'Just')
|
||||
{
|
||||
req.addEventListener('progress', function(event) {
|
||||
var progress = !event.lengthComputable
|
||||
? Maybe.Nothing
|
||||
: Maybe.Just({
|
||||
_: {},
|
||||
loaded: event.loaded,
|
||||
total: event.total
|
||||
});
|
||||
};
|
||||
var promise = settings.onProgress._0(progress);
|
||||
Promise.spawn(promise);
|
||||
});
|
||||
}
|
||||
|
||||
// end
|
||||
req.addEventListener('error', function() {
|
||||
return callback(Promise.fail({ ctor: 'RawNetworkError' }));
|
||||
});
|
||||
|
||||
req.addEventListener('timeout', function() {
|
||||
return callback(Promise.fail({ ctor: 'RawTimeout' }));
|
||||
});
|
||||
|
||||
req.addEventListener('load', function() {
|
||||
return callback(Promise.succeed(toResponse(req)));
|
||||
});
|
||||
|
||||
req.open(request.verb, request.url, true);
|
||||
|
||||
// set all the headers
|
||||
function setHeader(pair) {
|
||||
req.setRequestHeader(pair._0, pair._1);
|
||||
}
|
||||
A2(List.map, setHeader, request.headers);
|
||||
|
||||
// set the timeout
|
||||
req.timeout = settings.timeout;
|
||||
|
||||
// ask for a specific MIME type for the response
|
||||
if (settings.desiredResponseType.ctor === 'Just')
|
||||
{
|
||||
req.overrideMimeType(settings.desiredResponseType._0);
|
||||
}
|
||||
|
||||
req.send(request.body._0);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// deal with responses
|
||||
|
||||
function toResponse(req)
|
||||
{
|
||||
var tag = typeof req.response === 'string' ? 'Text' : 'Blob';
|
||||
return {
|
||||
_: {},
|
||||
status: req.status,
|
||||
statusText: req.statusText,
|
||||
headers: parseHeaders(req.getAllResponseHeaders()),
|
||||
url: req.responseURL,
|
||||
value: { ctor: tag, _0: req.response }
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function parseHeaders(rawHeaders)
|
||||
{
|
||||
var headers = Dict.empty;
|
||||
|
||||
if (!rawHeaders)
|
||||
{
|
||||
return headers;
|
||||
}
|
||||
|
||||
var headerPairs = rawHeaders.split('\u000d\u000a');
|
||||
for (var i = headerPairs.length; i--; )
|
||||
{
|
||||
var headerPair = headerPairs[i];
|
||||
var index = headerPair.indexOf('\u003a\u0020');
|
||||
if (index > 0)
|
||||
{
|
||||
var key = headerPair.substring(0, index);
|
||||
var value = headerPair.substring(index + 2);
|
||||
|
||||
headers = A3(Dict.update, key, function(oldValue) {
|
||||
if (oldValue.ctor === 'Just')
|
||||
{
|
||||
return Maybe.Just(value + ', ' + oldValue._0);
|
||||
}
|
||||
return Maybe.Just(value);
|
||||
}, headers);
|
||||
}
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
|
||||
function multipart(dataList)
|
||||
{
|
||||
var formData = new FormData();
|
||||
|
||||
while (dataList.ctor !== '[]')
|
||||
{
|
||||
var data = dataList._0;
|
||||
if (type === 'StringData')
|
||||
{
|
||||
formData.append(data._0, data._1);
|
||||
}
|
||||
else
|
||||
{
|
||||
var fileName = data._1.ctor === 'Nothing'
|
||||
? undefined
|
||||
: data._1._0;
|
||||
formData.append(data._0, data._2, fileName);
|
||||
}
|
||||
dataList = dataList._1;
|
||||
}
|
||||
|
||||
return { ctor: 'FormData', formData: formData };
|
||||
}
|
||||
|
||||
|
||||
return localRuntime.Native.Http.values = {
|
||||
send: F2(send),
|
||||
multipart: multipart
|
||||
};
|
||||
};
|
Loading…
Reference in a new issue