├── .gitignore ├── LICENSE ├── README.md ├── elm-package.json └── src ├── Blob.elm ├── Http.elm └── Native └── Http.js /.gitignore: -------------------------------------------------------------------------------- 1 | elm-stuff 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Evan Czaplicki 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of Evan Czaplicki nor the names of other 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATED 2 | 3 | You want [`elm-lang/http`](https://github.com/elm-lang/http) instead. 4 | -------------------------------------------------------------------------------- /elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "3.0.1", 3 | "summary": "Basic foundation for HTTP communication", 4 | "repository": "http://github.com/evancz/elm-http.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | "src" 8 | ], 9 | "exposed-modules": [ 10 | "Http" 11 | ], 12 | "native-modules": true, 13 | "dependencies": { 14 | "elm-lang/core": "4.0.0 <= v < 5.0.0" 15 | }, 16 | "elm-version": "0.17.0 <= v < 0.18.0" 17 | } -------------------------------------------------------------------------------- /src/Blob.elm: -------------------------------------------------------------------------------- 1 | module Blob where 2 | {-| https://developer.mozilla.org/en-US/docs/Web/API/Blob 3 | -} 4 | 5 | type Data 6 | = ArrayData (List Data) 7 | | ArrayBufferData 8 | | StringData String 9 | | BlobData Blob 10 | 11 | 12 | blob : Data -> Blob 13 | 14 | 15 | {-| Get the size of a blob in bytes. 16 | -} 17 | size : Blob -> Int 18 | 19 | 20 | {-| Get the MIME type of the data stored in the `Blob` if that information 21 | is available. 22 | -} 23 | mimeType : Blob -> Maybe String 24 | 25 | 26 | slice : Int -> Int -> Maybe String -> Blob -> Blob -------------------------------------------------------------------------------- /src/Http.elm: -------------------------------------------------------------------------------- 1 | module Http exposing 2 | ( getString, get, post, send 3 | , url, uriEncode, uriDecode 4 | , Request 5 | , Body, empty, string, multipart 6 | , Data, stringData 7 | , Settings, defaultSettings 8 | , Response, Value(..) 9 | , Error(..), RawError(..) 10 | , fromJson 11 | ) 12 | 13 | {-| 14 | 15 | # Encoding and Decoding 16 | @docs url, uriEncode, uriDecode 17 | 18 | # Fetch Strings and JSON 19 | @docs getString, get, post, Error 20 | 21 | # Body Values 22 | @docs Body, empty, string, multipart, Data, stringData 23 | 24 | # Arbitrary Requests 25 | @docs send, Request, Settings, defaultSettings 26 | 27 | # Responses 28 | @docs Response, Value, fromJson, RawError 29 | -} 30 | 31 | import Dict exposing (Dict) 32 | import Json.Decode as Json 33 | import Native.Http 34 | import Task exposing (Task, andThen, mapError, succeed, fail) 35 | import String 36 | import Time exposing (Time) 37 | 38 | 39 | type Blob = TODO_implement_blob_in_another_library 40 | type File = TODO_implement_file_in_another_library 41 | 42 | 43 | -- REQUESTS 44 | 45 | {-| Create a properly encoded URL with a [query string][qs]. The first argument is 46 | the portion of the URL before the query string, which is assumed to be 47 | properly encoded already. The second argument is a list of all the 48 | key/value pairs needed for the query string. Both the keys and values 49 | will be appropriately encoded, so they can contain spaces, ampersands, etc. 50 | 51 | [qs]: http://en.wikipedia.org/wiki/Query_string 52 | 53 | url "http://example.com/users" [ ("name", "john doe"), ("age", "30") ] 54 | -- http://example.com/users?name=john+doe&age=30 55 | -} 56 | url : String -> List (String,String) -> String 57 | url baseUrl args = 58 | case args of 59 | [] -> 60 | baseUrl 61 | 62 | _ -> 63 | baseUrl ++ "?" ++ String.join "&" (List.map queryPair args) 64 | 65 | 66 | queryPair : (String,String) -> String 67 | queryPair (key,value) = 68 | queryEscape key ++ "=" ++ queryEscape value 69 | 70 | 71 | queryEscape : String -> String 72 | queryEscape string = 73 | String.join "+" (String.split "%20" (uriEncode string)) 74 | 75 | 76 | {-| Encode a string to be placed in any part of a URI. Same behavior as 77 | JavaScript's `encodeURIComponent` function. 78 | -} 79 | uriEncode : String -> String 80 | uriEncode = 81 | Native.Http.uriEncode 82 | 83 | 84 | {-| Decode a URI string. Same behavior as JavaScript's `decodeURIComponent` 85 | function. 86 | -} 87 | uriDecode : String -> String 88 | uriDecode = 89 | Native.Http.uriDecode 90 | 91 | 92 | {-| Fully specify the request you want to send. For example, if you want to 93 | send a request between domains (CORS request) you will need to specify some 94 | headers manually. 95 | 96 | corsPost : Request 97 | corsPost = 98 | { verb = "POST" 99 | , headers = 100 | [ ("Origin", "http://elm-lang.org") 101 | , ("Access-Control-Request-Method", "POST") 102 | , ("Access-Control-Request-Headers", "X-Custom-Header") 103 | ] 104 | , url = "http://example.com/hats" 105 | , body = empty 106 | } 107 | -} 108 | type alias Request = 109 | { verb : String 110 | , headers : List (String, String) 111 | , url : String 112 | , body : Body 113 | } 114 | 115 | 116 | {-| An opaque type representing the body of your HTTP message. With GET 117 | requests this is empty, but in other cases it may be a string or blob. 118 | -} 119 | type Body 120 | = Empty 121 | | BodyString String 122 | | ArrayBuffer 123 | | BodyFormData 124 | | BodyBlob Blob 125 | 126 | 127 | {-| An empty request body, no value will be sent along. 128 | -} 129 | empty : Body 130 | empty = 131 | Empty 132 | 133 | 134 | {-| Provide a string as the body of the request. Useful if you need to send 135 | JSON data to a server that does not belong in the URL. 136 | 137 | import Json.Decode as JS 138 | 139 | coolestHats : Task Error (List String) 140 | coolestHats = 141 | post 142 | (JS.list JS.string) 143 | "http://example.com/hats" 144 | (string """{ "sortBy": "coolness", "take": 10 }""") 145 | -} 146 | string : String -> Body 147 | string = 148 | BodyString 149 | 150 | 151 | {-- 152 | arrayBuffer : ArrayBuffer -> Body 153 | 154 | 155 | blob : Blob -> Body 156 | blob _ = 157 | BodyBlob 158 | --} 159 | 160 | {-| Represents data that can be put in a multi-part body. Right now it only 161 | supports strings, but we will support blobs and files when we get an API for 162 | them in Elm. 163 | -} 164 | type Data 165 | = StringData String String 166 | | BlobData String (Maybe String) Blob 167 | | FileData String (Maybe String) File 168 | 169 | 170 | {-| Create multi-part request bodies, allowing you to send many chunks of data 171 | all in one request. All chunks of data must be given a name. 172 | 173 | Currently, you can only construct `stringData`, but we will support `blobData` 174 | and `fileData` once we have proper APIs for those types of data in Elm. 175 | -} 176 | multipart : List Data -> Body 177 | multipart = 178 | Native.Http.multipart 179 | 180 | 181 | {-| A named chunk of string data. 182 | 183 | import Json.Encode as JS 184 | 185 | body = 186 | multipart 187 | [ stringData "user" (JS.encode user) 188 | , stringData "payload" (JS.encode payload) 189 | ] 190 | -} 191 | stringData : String -> String -> Data 192 | stringData = 193 | StringData 194 | 195 | 196 | {-| A named chunk of blob data. You provide a name for this piece of data, 197 | an optional file name for where the data came from, and the blob itself. If 198 | no file name is given, it will default to `"blob"`. 199 | 200 | Currently the only way to obtain a `Blob` is in a `Response` but support will 201 | expand once we have an API for blobs in Elm. 202 | -} 203 | blobData : String -> Maybe String -> Blob -> Data 204 | blobData = 205 | BlobData 206 | 207 | 208 | {-- 209 | fileData : String -> Maybe String -> File -> Data 210 | fileData = 211 | FileData 212 | --} 213 | 214 | 215 | -- SETTINGS 216 | 217 | 218 | {-| Configure your request if you need specific behavior. 219 | 220 | * `timeout` lets you specify how long you are willing to wait for a response 221 | before giving up. By default it is 0 which means “never give 222 | up!” 223 | 224 | * `onStart` and `onProgress` allow you to monitor progress. This is useful 225 | if you want to show a progress bar when uploading a large amount of data. 226 | 227 | * `desiredResponseType` lets you override the MIME type of the response, so 228 | you can influence what kind of `Value` you get in the `Response`. 229 | -} 230 | type alias Settings = 231 | { timeout : Time 232 | , onStart : Maybe (Task () ()) 233 | , onProgress : Maybe (Maybe { loaded : Int, total : Int } -> Task () ()) 234 | , desiredResponseType : Maybe String 235 | , withCredentials : Bool 236 | } 237 | 238 | 239 | {-| The default settings used by `get` and `post`. 240 | 241 | { timeout = 0 242 | , onStart = Nothing 243 | , onProgress = Nothing 244 | , desiredResponseType = Nothing 245 | , withCredentials = False 246 | } 247 | -} 248 | defaultSettings : Settings 249 | defaultSettings = 250 | { timeout = 0 251 | , onStart = Nothing 252 | , onProgress = Nothing 253 | , desiredResponseType = Nothing 254 | , withCredentials = False 255 | } 256 | 257 | 258 | -- RESPONSE HANDLER 259 | 260 | {-| All the details of the response. There are many weird facts about 261 | responses which include: 262 | 263 | * The `status` may be 0 in the case that you load something from `file://` 264 | * You cannot handle redirects yourself, they will all be followed 265 | automatically. If you want to know if you have gone through one or more 266 | redirect, the `url` field will let you know who sent you the response, so 267 | you will know if it does not match the URL you requested. 268 | * You are allowed to have duplicate headers, and their values will be 269 | combined into a single comma-separated string. 270 | 271 | We have left these underlying facts about `XMLHttpRequest` as is because one 272 | goal of this library is to give a low-level enough API that others can build 273 | whatever helpful behavior they want on top of it. 274 | -} 275 | type alias Response = 276 | { status : Int 277 | , statusText : String 278 | , headers : Dict String String 279 | , url : String 280 | , value : Value 281 | } 282 | 283 | 284 | {-| The information given in the response. Currently there is no way to handle 285 | `Blob` types since we do not have an Elm API for that yet. This type will 286 | expand as more values become available in Elm itself. 287 | -} 288 | type Value 289 | = Text String 290 | -- | ArrayBuffer ArrayBuffer 291 | | Blob Blob 292 | -- | Document Document 293 | 294 | 295 | -- Errors 296 | 297 | {-| The things that count as errors at the lowest level. Technically, getting 298 | a response back with status 404 is a “successful” response in that 299 | you actually got all the information you asked for. 300 | 301 | The `fromJson` function and `Error` type provide higher-level errors, but the 302 | point of `RawError` is to allow you to define higher-level errors however you 303 | want. 304 | -} 305 | type RawError 306 | = RawTimeout 307 | | RawNetworkError 308 | 309 | 310 | {-| The kinds of errors you typically want in practice. When you get a 311 | response but its status is not in the 200 range, it will trigger a 312 | `BadResponse`. When you try to decode JSON but something goes wrong, 313 | you will get an `UnexpectedPayload`. 314 | -} 315 | type Error 316 | = Timeout 317 | | NetworkError 318 | | UnexpectedPayload String 319 | | BadResponse Int String 320 | 321 | 322 | -- ACTUALLY SEND REQUESTS 323 | 324 | {-| Send a request exactly how you want it. The `Settings` argument lets you 325 | configure things like timeouts and progress monitoring. The `Request` argument 326 | defines all the information that will actually be sent along to a server. 327 | 328 | crossOriginGet : String -> String -> Task RawError Response 329 | crossOriginGet origin url = 330 | send defaultSettings 331 | { verb = "GET" 332 | , headers = [("Origin", origin)] 333 | , url = url 334 | , body = empty 335 | } 336 | -} 337 | send : Settings -> Request -> Task RawError Response 338 | send = 339 | Native.Http.send 340 | 341 | 342 | -- HIGH-LEVEL REQUESTS 343 | 344 | {-| Send a GET request to the given URL. You will get the entire response as a 345 | string. 346 | 347 | hats : Task Error String 348 | hats = 349 | getString "http://example.com/hat-categories.markdown" 350 | 351 | -} 352 | getString : String -> Task Error String 353 | getString url = 354 | let request = 355 | { verb = "GET" 356 | , headers = [] 357 | , url = url 358 | , body = empty 359 | } 360 | in 361 | mapError promoteError (send defaultSettings request) 362 | `andThen` handleResponse succeed 363 | 364 | 365 | {-| Send a GET request to the given URL. You also specify how to decode the 366 | response. 367 | 368 | import Json.Decode exposing (list, string) 369 | 370 | hats : Task Error (List String) 371 | hats = 372 | get (list string) "http://example.com/hat-categories.json" 373 | 374 | -} 375 | get : Json.Decoder value -> String -> Task Error value 376 | get decoder url = 377 | let request = 378 | { verb = "GET" 379 | , headers = [] 380 | , url = url 381 | , body = empty 382 | } 383 | in 384 | fromJson decoder (send defaultSettings request) 385 | 386 | 387 | {-| Send a POST request to the given URL, carrying the given body. You also 388 | specify how to decode the response with [a JSON decoder][json]. 389 | 390 | [json]: http://package.elm-lang.org/packages/elm-lang/core/latest/Json-Decode#Decoder 391 | 392 | import Json.Decode exposing (list, string) 393 | 394 | hats : Task Error (List String) 395 | hats = 396 | post (list string) "http://example.com/hat-categories.json" empty 397 | 398 | -} 399 | post : Json.Decoder value -> String -> Body -> Task Error value 400 | post decoder url body = 401 | let request = 402 | { verb = "POST" 403 | , headers = [] 404 | , url = url 405 | , body = body 406 | } 407 | in 408 | fromJson decoder (send defaultSettings request) 409 | 410 | 411 | {-| Turn a `Response` into an Elm value that is easier to deal with. Helpful 412 | if you are making customized HTTP requests with `send`, as is the case with 413 | `get` and `post`. 414 | 415 | Given a `Response` this function will: 416 | 417 | * Check that the status code is in the 200 range. 418 | * Make sure the response `Value` is a string. 419 | * Convert the string to Elm with the given `Decoder`. 420 | 421 | Assuming all these steps succeed, you will get an Elm value as the result! 422 | -} 423 | fromJson : Json.Decoder a -> Task RawError Response -> Task Error a 424 | fromJson decoder response = 425 | let decode str = 426 | case Json.decodeString decoder str of 427 | Ok v -> succeed v 428 | Err msg -> fail (UnexpectedPayload msg) 429 | in 430 | mapError promoteError response 431 | `andThen` handleResponse decode 432 | 433 | 434 | handleResponse : (String -> Task Error a) -> Response -> Task Error a 435 | handleResponse handle response = 436 | if 200 <= response.status && response.status < 300 then 437 | 438 | case response.value of 439 | Text str -> 440 | handle str 441 | 442 | _ -> 443 | fail (UnexpectedPayload "Response body is a blob, expecting a string.") 444 | 445 | else 446 | 447 | fail (BadResponse response.status response.statusText) 448 | 449 | 450 | 451 | promoteError : RawError -> Error 452 | promoteError rawError = 453 | case rawError of 454 | RawTimeout -> Timeout 455 | RawNetworkError -> NetworkError 456 | -------------------------------------------------------------------------------- /src/Native/Http.js: -------------------------------------------------------------------------------- 1 | //import Dict, List, Maybe, Native.Scheduler // 2 | 3 | var _evancz$elm_http$Native_Http = function() { 4 | 5 | function send(settings, request) 6 | { 7 | return _elm_lang$core$Native_Scheduler.nativeBinding(function(callback) { 8 | var req = new XMLHttpRequest(); 9 | 10 | // start 11 | if (settings.onStart.ctor === 'Just') 12 | { 13 | req.addEventListener('loadStart', function() { 14 | var task = settings.onStart._0; 15 | _elm_lang$core$Native_Scheduler.rawSpawn(task); 16 | }); 17 | } 18 | 19 | // progress 20 | if (settings.onProgress.ctor === 'Just') 21 | { 22 | req.addEventListener('progress', function(event) { 23 | var progress = !event.lengthComputable 24 | ? _elm_lang$core$Maybe$Nothing 25 | : _elm_lang$core$Maybe$Just({ 26 | loaded: event.loaded, 27 | total: event.total 28 | }); 29 | var task = settings.onProgress._0(progress); 30 | _elm_lang$core$Native_Scheduler.rawSpawn(task); 31 | }); 32 | } 33 | 34 | // end 35 | req.addEventListener('error', function() { 36 | return callback(_elm_lang$core$Native_Scheduler.fail({ ctor: 'RawNetworkError' })); 37 | }); 38 | 39 | req.addEventListener('timeout', function() { 40 | return callback(_elm_lang$core$Native_Scheduler.fail({ ctor: 'RawTimeout' })); 41 | }); 42 | 43 | req.addEventListener('load', function() { 44 | return callback(_elm_lang$core$Native_Scheduler.succeed(toResponse(req))); 45 | }); 46 | 47 | req.open(request.verb, request.url, true); 48 | 49 | // set all the headers 50 | function setHeader(pair) { 51 | req.setRequestHeader(pair._0, pair._1); 52 | } 53 | A2(_elm_lang$core$List$map, setHeader, request.headers); 54 | 55 | // set the timeout 56 | req.timeout = settings.timeout; 57 | 58 | // enable this withCredentials thing 59 | req.withCredentials = settings.withCredentials; 60 | 61 | // ask for a specific MIME type for the response 62 | if (settings.desiredResponseType.ctor === 'Just') 63 | { 64 | req.overrideMimeType(settings.desiredResponseType._0); 65 | } 66 | 67 | // actually send the request 68 | if(request.body.ctor === "BodyFormData") 69 | { 70 | req.send(request.body.formData) 71 | } 72 | else 73 | { 74 | req.send(request.body._0); 75 | } 76 | 77 | return function() { 78 | req.abort(); 79 | }; 80 | }); 81 | } 82 | 83 | 84 | // deal with responses 85 | 86 | function toResponse(req) 87 | { 88 | var tag = req.responseType === 'blob' ? 'Blob' : 'Text' 89 | var response = tag === 'Blob' ? req.response : req.responseText; 90 | return { 91 | status: req.status, 92 | statusText: req.statusText, 93 | headers: parseHeaders(req.getAllResponseHeaders()), 94 | url: req.responseURL, 95 | value: { ctor: tag, _0: response } 96 | }; 97 | } 98 | 99 | 100 | function parseHeaders(rawHeaders) 101 | { 102 | var headers = _elm_lang$core$Dict$empty; 103 | 104 | if (!rawHeaders) 105 | { 106 | return headers; 107 | } 108 | 109 | var headerPairs = rawHeaders.split('\u000d\u000a'); 110 | for (var i = headerPairs.length; i--; ) 111 | { 112 | var headerPair = headerPairs[i]; 113 | var index = headerPair.indexOf('\u003a\u0020'); 114 | if (index > 0) 115 | { 116 | var key = headerPair.substring(0, index); 117 | var value = headerPair.substring(index + 2); 118 | 119 | headers = A3(_elm_lang$core$Dict$update, key, function(oldValue) { 120 | if (oldValue.ctor === 'Just') 121 | { 122 | return _elm_lang$core$Maybe$Just(value + ', ' + oldValue._0); 123 | } 124 | return _elm_lang$core$Maybe$Just(value); 125 | }, headers); 126 | } 127 | } 128 | 129 | return headers; 130 | } 131 | 132 | 133 | function multipart(dataList) 134 | { 135 | var formData = new FormData(); 136 | 137 | while (dataList.ctor !== '[]') 138 | { 139 | var data = dataList._0; 140 | if (data.ctor === 'StringData') 141 | { 142 | formData.append(data._0, data._1); 143 | } 144 | else 145 | { 146 | var fileName = data._1.ctor === 'Nothing' 147 | ? undefined 148 | : data._1._0; 149 | formData.append(data._0, data._2, fileName); 150 | } 151 | dataList = dataList._1; 152 | } 153 | 154 | return { ctor: 'BodyFormData', formData: formData }; 155 | } 156 | 157 | 158 | function uriEncode(string) 159 | { 160 | return encodeURIComponent(string); 161 | } 162 | 163 | function uriDecode(string) 164 | { 165 | return decodeURIComponent(string); 166 | } 167 | 168 | return { 169 | send: F2(send), 170 | multipart: multipart, 171 | uriEncode: uriEncode, 172 | uriDecode: uriDecode 173 | }; 174 | 175 | }(); 176 | --------------------------------------------------------------------------------