Add part5

This commit is contained in:
Richard Feldman
2018-04-28 16:55:52 -04:00
parent d4718c64b4
commit b0f8f8ee42
576 changed files with 97325 additions and 0 deletions

View File

@@ -0,0 +1 @@
elm-stuff

View File

@@ -0,0 +1,30 @@
Copyright (c) 2016-present, 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.

View File

@@ -0,0 +1,57 @@
# HTTP in Elm
Make HTTP requests in Elm.
```elm
import Http
import Json.Decode as Decode
-- GET A STRING
getWarAndPeace : Http.Request String
getWarAndPeace =
Http.getString "https://example.com/books/war-and-peace"
-- GET JSON
getMetadata : Http.Request Metadata
getMetadata =
Http.get "https://example.com/books/war-and-peace/metadata" decodeMetadata
type alias Metadata =
{ author : String
, pages : Int
}
decodeMetadata : Decode.Decoder Metadata
decodeMetadata =
Decode.map2 Metadata
(Decode.field "author" Decode.string)
(Decode.field "pages" Decode.int)
-- SEND REQUESTS
type Msg
= LoadMetadata (Result Http.Error Metadata)
send : Cmd Msg
send =
Http.send LoadMetadata getMetadata
```
## Examples
- GET requests - [demo and code](http://elm-lang.org/examples/http)
- Download progress - [demo](https://hirafuji.com.br/elm/http-progress-example/) and [code](https://gist.github.com/pablohirafuji/fa373d07c42016756d5bca28962008c4)
## Learn More
To understand how HTTP works in Elm, check out:
- [The HTTP example in the guide](https://guide.elm-lang.org/architecture/effects/http.html) to see a simple usage with some explanation.
- [The Elm Architecture](https://guide.elm-lang.org/architecture/) to understand how HTTP fits into Elm in a more complete way. This will explain concepts like `Cmd` and `Sub` that appear in this package.

View File

@@ -0,0 +1,18 @@
{
"version": "1.0.0",
"summary": "Make HTTP requests (download progress, rate-limit, debounce, throttle)",
"repository": "https://github.com/elm-lang/http.git",
"license": "BSD3",
"source-directories": [
"src"
],
"exposed-modules": [
"Http",
"Http.Progress"
],
"native-modules": true,
"dependencies": {
"elm-lang/core": "5.0.0 <= v < 6.0.0"
},
"elm-version": "0.18.0 <= v < 0.19.0"
}

View File

@@ -0,0 +1,40 @@
## Custom Rate-Limiting Strategies
This package has `Http.RateLimit` which helps you rate-limit the HTTP requests you make. Instead of sending one request per keystroke, you filter it down because not all requests are important.
The `Http.RateLimit` module comes with a `debounce` strategy that covers the common case, but you may want to define a custom strategy with other characteristics. Maybe you want to send the first request. Maybe you want to send when the previous request is done instead of using timers. Etc.
If so, you can define a custom strategy with `Http.RateLimit.customStrategy`. For example, you would define `throttle` like this:
```elm
import Http.RateLimit as Limit
throttle : Time -> Limit.Strategy
throttle ms =
Limit.customStrategy <| \timeNow event state ->
case event of
Limit.New _ ->
-- wait after a new request
[ Limit.WakeUpIn ms ]
Limit.Done _ ->
-- we do not care when requests finish
[]
Limit.WakeUp ->
case state.next of
Nothing ->
-- do nothing if there is no pending request
[]
Just req ->
-- send if enough time has passed since the previous request
case state.prev of
Nothing ->
[ Limit.Send req.id ]
Just prev ->
if timeNow - prev.time >= ms then [ Limit.Send req.id ] else []
```
It would be nice to have some useful strategies defined in a separate package so folks can experiment and find names and implementations that work well for specific scenarios.

View File

@@ -0,0 +1,411 @@
module Http exposing
( Request, send, Error(..)
, getString, get
, post
, request
, Header, header
, Body, emptyBody, jsonBody, stringBody, multipartBody, Part, stringPart
, Expect, expectString, expectJson, expectStringResponse, Response
, encodeUri, decodeUri, toTask
)
{-| Create and send HTTP requests.
# Send Requests
@docs Request, send, Error
# GET
@docs getString, get
# POST
@docs post
# Custom Requests
@docs request
## Headers
@docs Header, header
## Request Bodies
@docs Body, emptyBody, jsonBody, stringBody, multipartBody, Part, stringPart
## Responses
@docs Expect, expectString, expectJson, expectStringResponse, Response
# Low-Level
@docs encodeUri, decodeUri, toTask
-}
import Dict exposing (Dict)
import Http.Internal
import Json.Decode as Decode
import Json.Encode as Encode
import Maybe exposing (Maybe(..))
import Native.Http
import Platform.Cmd as Cmd exposing (Cmd)
import Result exposing (Result(..))
import Task exposing (Task)
import Time exposing (Time)
-- REQUESTS
{-| Describes an HTTP request.
-}
type alias Request a =
Http.Internal.Request a
{-| Send a `Request`. We could get the text of War and Peace like this:
import Http
type Msg = Click | NewBook (Result Http.Error String)
update : Msg -> Model -> Model
update msg model =
case msg of
Click ->
( model, getWarAndPeace )
NewBook (Ok book) ->
...
NewBook (Err _) ->
...
getWarAndPeace : Cmd Msg
getWarAndPeace =
Http.send NewBook <|
Http.getString "https://example.com/books/war-and-peace.md"
-}
send : (Result Error a -> msg) -> Request a -> Cmd msg
send resultToMessage request =
Task.attempt resultToMessage (toTask request)
{-| Convert a `Request` into a `Task`. This is only really useful if you want
to chain together a bunch of requests (or any other tasks) in a single command.
-}
toTask : Request a -> Task Error a
toTask (Http.Internal.Request request) =
Native.Http.toTask request Nothing
{-| A `Request` can fail in a couple ways:
- `BadUrl` means you did not provide a valid URL.
- `Timeout` means it took too long to get a response.
- `NetworkError` means the user turned off their wifi, went in a cave, etc.
- `BadStatus` means you got a response back, but the [status code][sc]
indicates failure.
- `BadPayload` means you got a response back with a nice status code, but
the body of the response was something unexpected. The `String` in this
case is a debugging message that explains what went wrong with your JSON
decoder or whatever.
[sc]: https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
-}
type Error
= BadUrl String
| Timeout
| NetworkError
| BadStatus (Response String)
| BadPayload String (Response String)
-- GET
{-| Create a `GET` request and interpret the response body as a `String`.
import Http
getWarAndPeace : Http.Request String
getWarAndPeace =
Http.getString "https://example.com/books/war-and-peace"
-}
getString : String -> Request String
getString url =
request
{ method = "GET"
, headers = []
, url = url
, body = emptyBody
, expect = expectString
, timeout = Nothing
, withCredentials = False
}
{-| Create a `GET` request and try to decode the response body from JSON to
some Elm value.
import Http
import Json.Decode exposing (list, string)
getBooks : Http.Request (List String)
getBooks =
Http.get "https://example.com/books" (list string)
You can learn more about how JSON decoders work [here][] in the guide.
[here]: https://guide.elm-lang.org/interop/json.html
-}
get : String -> Decode.Decoder a -> Request a
get url decoder =
request
{ method = "GET"
, headers = []
, url = url
, body = emptyBody
, expect = expectJson decoder
, timeout = Nothing
, withCredentials = False
}
-- POST
{-| Create a `POST` request and try to decode the response body from JSON to
an Elm value. For example, if we want to send a POST without any data in the
request body, it would be like this:
import Http
import Json.Decode exposing (list, string)
postBooks : Http.Request (List String)
postBooks =
Http.post "https://example.com/books" Http.emptyBody (list string)
See [`jsonBody`](#jsonBody) to learn how to have a more interesting request
body. And check out [this section][here] of the guide to learn more about
JSON decoders.
[here]: https://guide.elm-lang.org/interop/json.html
-}
post : String -> Body -> Decode.Decoder a -> Request a
post url body decoder =
request
{ method = "POST"
, headers = []
, url = url
, body = body
, expect = expectJson decoder
, timeout = Nothing
, withCredentials = False
}
-- CUSTOM REQUESTS
{-| Create a custom request. For example, a custom PUT request would look like
this:
put : String -> Body -> Request ()
put url body =
request
{ method = "PUT"
, headers = []
, url = url
, body = body
, expect = expectStringResponse (\_ -> Ok ())
, timeout = Nothing
, withCredentials = False
}
-}
request
: { method : String
, headers : List Header
, url : String
, body : Body
, expect : Expect a
, timeout : Maybe Time
, withCredentials : Bool
}
-> Request a
request =
Http.Internal.Request
-- HEADERS
{-| An HTTP header for configuring requests. See a bunch of common headers
[here][].
[here]: https://en.wikipedia.org/wiki/List_of_HTTP_header_fields
-}
type alias Header = Http.Internal.Header
{-| Create a `Header`.
header "If-Modified-Since" "Sat 29 Oct 1994 19:43:31 GMT"
header "Max-Forwards" "10"
header "X-Requested-With" "XMLHttpRequest"
**Note:** In the future, we may split this out into an `Http.Headers` module
and provide helpers for cases that are common on the client-side. If this
sounds nice to you, open an issue [here][] describing the helper you want and
why you need it.
[here]: https://github.com/elm-lang/http/issues
-}
header : String -> String -> Header
header =
Http.Internal.Header
-- BODY
{-| Represents the body of a `Request`.
-}
type alias Body = Http.Internal.Body
{-| Create an empty body for your `Request`. This is useful for GET requests
and POST requests where you are not sending any data.
-}
emptyBody : Body
emptyBody =
Http.Internal.EmptyBody
{-| Put some JSON value in the body of your `Request`. This will automatically
add the `Content-Type: application/json` header.
-}
jsonBody : Encode.Value -> Body
jsonBody value =
Http.Internal.StringBody "application/json" (Encode.encode 0 value)
{-| Put some string in the body of your `Request`. Defining `jsonBody` looks
like this:
import Json.Encode as Encode
jsonBody : Encode.Value -> Body
jsonBody value =
stringBody "application/json" (Encode.encode 0 value)
Notice that the first argument is a [MIME type][mime] so we know to add
`Content-Type: application/json` to our request headers. Make sure your
MIME type matches your data. Some servers are strict about this!
[mime]: https://en.wikipedia.org/wiki/Media_type
-}
stringBody : String -> String -> Body
stringBody =
Http.Internal.StringBody
{-| Create multi-part bodies for your `Request`, automatically adding the
`Content-Type: multipart/form-data` header.
-}
multipartBody : List Part -> Body
multipartBody =
Native.Http.multipart
{-| Contents of a multi-part body. Right now it only supports strings, but we
will support blobs and files when we get an API for them in Elm.
-}
type Part
= StringPart String String
{-| A named chunk of string data.
body =
multipartBody
[ stringPart "user" "tom"
, stringPart "payload" "42"
]
-}
stringPart : String -> String -> Part
stringPart =
StringPart
-- RESPONSES
{-| Logic for interpreting a response body.
-}
type alias Expect a =
Http.Internal.Expect a
{-| Expect the response body to be a `String`.
-}
expectString : Expect String
expectString =
expectStringResponse (\response -> Ok response.body)
{-| Expect the response body to be JSON. You provide a `Decoder` to turn that
JSON into an Elm value. If the body cannot be parsed as JSON or if the JSON
does not match the decoder, the request will resolve to a `BadPayload` error.
-}
expectJson : Decode.Decoder a -> Expect a
expectJson decoder =
expectStringResponse (\response -> Decode.decodeString decoder response.body)
{-| Maybe you want the whole `Response`: status code, headers, body, etc. This
lets you get all of that information. From there you can use functions like
`Json.Decode.decodeString` to interpret it as JSON or whatever else you want.
-}
expectStringResponse : (Response String -> Result String a) -> Expect a
expectStringResponse =
Native.Http.expectStringResponse
{-| The response from a `Request`.
-}
type alias Response body =
{ url : String
, status : { code : Int, message : String }
, headers : Dict String String
, body : body
}
-- LOW-LEVEL
{-| Use this to escape query parameters. Converts characters like `/` to `%2F`
so that it does not clash with normal URL
It work just like `encodeURIComponent` in JavaScript.
-}
encodeUri : String -> String
encodeUri =
Native.Http.encodeUri
{-| Use this to unescape query parameters. It converts things like `%2F` to
`/`. It can fail in some cases. For example, there is no way to unescape `%`
because it could never appear alone in a properly escaped string.
It works just like `decodeURIComponent` in JavaScript.
-}
decodeUri : String -> Maybe String
decodeUri =
Native.Http.decodeUri

View File

@@ -0,0 +1,45 @@
module Http.Internal exposing
( Request(..)
, RawRequest
, Expect
, Body(..)
, Header(..)
, map
)
import Native.Http
import Time exposing (Time)
type Request a = Request (RawRequest a)
type alias RawRequest a =
{ method : String
, headers : List Header
, url : String
, body : Body
, expect : Expect a
, timeout : Maybe Time
, withCredentials : Bool
}
type Expect a = Expect
type Body
= EmptyBody
| StringBody String String
| FormDataBody
type Header = Header String String
map : (a -> b) -> RawRequest a -> RawRequest b
map func request =
{ request | expect = Native.Http.mapExpect func request.expect }

View File

@@ -0,0 +1,200 @@
effect module Http.Progress where { subscription = MySub } exposing
( Progress(..)
, track
)
{-| Track the progress of an HTTP request. This can be useful if you are
requesting a large amount of data and want to show the user a progress bar
or something.
Here is an example usage: [demo][] and [code][].
[demo]: https://hirafuji.com.br/elm/http-progress-example/
[code]: https://gist.github.com/pablohirafuji/fa373d07c42016756d5bca28962008c4
**Note:** If you stop tracking progress, you cancel the request.
# Progress
@docs Progress, track
-}
import Dict
import Http
import Http.Internal exposing ( Request(Request) )
import Task exposing (Task)
import Platform exposing (Router)
import Process
-- PROGRESS
{-| The progress of an HTTP request.
You start with `None`. As data starts to come in, you will see `Some`. The
`bytesExpected` field will match the `Content-Length` header, indicating how
long the response body is in bytes (8-bits). The `bytes` field indicates how
many bytes have been loaded so far, so if you want progress as a percentage,
you would say:
Some { bytes, bytesExpected } ->
toFloat bytes / toFloat bytesExpected
You will end up with `Fail` or `Done` depending on the success of the request.
-}
type Progress data
= None
| Some { bytes : Int, bytesExpected : Int}
| Fail Http.Error
| Done data
-- TRACK
{-| Create a subscription that tracks the progress of an HTTP request.
See it in action in this example: [demo][] and [code][].
[demo]: https://hirafuji.com.br/elm/http-progress-example/
[code]: https://gist.github.com/pablohirafuji/fa373d07c42016756d5bca28962008c4
-}
track : String -> (Progress data -> msg) -> Http.Request data -> Sub msg
track id toMessage (Request request) =
subscription <| Track id <|
{ request = Http.Internal.map (Done >> toMessage) request
, toProgress = Some >> toMessage
, toError = Fail >> toMessage
}
type alias TrackedRequest msg =
{ request : Http.Internal.RawRequest msg
, toProgress : { bytes : Int, bytesExpected : Int } -> msg
, toError : Http.Error -> msg
}
map : (a -> b) -> TrackedRequest a -> TrackedRequest b
map func { request, toProgress, toError } =
{ request = Http.Internal.map func request
, toProgress = toProgress >> func
, toError = toError >> func
}
-- SUBSCRIPTIONS
type MySub msg =
Track String (TrackedRequest msg)
subMap : (a -> b) -> MySub a -> MySub b
subMap func (Track id trackedRequest) =
Track id (map func trackedRequest)
-- EFFECT MANAGER
type alias State =
Dict.Dict String Process.Id
init : Task Never State
init =
Task.succeed Dict.empty
-- APP MESSAGES
onEffects : Platform.Router msg Never -> List (MySub msg) -> State -> Task Never State
onEffects router subs state =
let
subDict =
collectSubs subs
leftStep id process (dead, ongoing, new) =
( Process.kill process :: dead
, ongoing
, new
)
bothStep id process _ (dead, ongoing, new) =
( dead
, Dict.insert id process ongoing
, new
)
rightStep id trackedRequest (dead, ongoing, new) =
( dead
, ongoing
, (id, trackedRequest) :: new
)
(dead, ongoing, new) =
Dict.merge leftStep bothStep rightStep state subDict ([], Dict.empty, [])
in
Task.sequence dead
|> Task.andThen (\_ -> spawnRequests router new ongoing)
spawnRequests : Router msg Never -> List (String, TrackedRequest msg) -> State -> Task Never State
spawnRequests router trackedRequests state =
case trackedRequests of
[] ->
Task.succeed state
(id, trackedRequest) :: others ->
Process.spawn (toTask router trackedRequest)
|> Task.andThen (\process -> spawnRequests router others (Dict.insert id process state))
toTask : Router msg Never -> TrackedRequest msg -> Task Never ()
toTask router { request, toProgress, toError } =
Native.Http.toTask request (Just (Platform.sendToApp router << toProgress))
|> Task.andThen (Platform.sendToApp router)
|> Task.onError (Platform.sendToApp router << toError)
-- COLLECT SUBS AS DICT
type alias SubDict msg =
Dict.Dict String (TrackedRequest msg)
collectSubs : List (MySub msg) -> SubDict msg
collectSubs subs =
List.foldl addSub Dict.empty subs
addSub : MySub msg -> SubDict msg -> SubDict msg
addSub (Track id trackedRequest) subDict =
let
request =
trackedRequest.request
uid =
id ++ request.method ++ request.url
in
Dict.insert uid trackedRequest subDict
-- SELF MESSAGES
onSelfMsg : Platform.Router msg Never -> Never -> State -> Task Never State
onSelfMsg router _ state =
Task.succeed state

View File

@@ -0,0 +1,238 @@
var _elm_lang$http$Native_Http = function() {
// ENCODING AND DECODING
function encodeUri(string)
{
return encodeURIComponent(string);
}
function decodeUri(string)
{
try
{
return _elm_lang$core$Maybe$Just(decodeURIComponent(string));
}
catch(e)
{
return _elm_lang$core$Maybe$Nothing;
}
}
// SEND REQUEST
function toTask(request, maybeProgress)
{
return _elm_lang$core$Native_Scheduler.nativeBinding(function(callback)
{
var xhr = new XMLHttpRequest();
configureProgress(xhr, maybeProgress);
xhr.addEventListener('error', function() {
callback(_elm_lang$core$Native_Scheduler.fail({ ctor: 'NetworkError' }));
});
xhr.addEventListener('timeout', function() {
callback(_elm_lang$core$Native_Scheduler.fail({ ctor: 'Timeout' }));
});
xhr.addEventListener('load', function() {
callback(handleResponse(xhr, request.expect.responseToResult));
});
try
{
xhr.open(request.method, request.url, true);
}
catch (e)
{
return callback(_elm_lang$core$Native_Scheduler.fail({ ctor: 'BadUrl', _0: request.url }));
}
configureRequest(xhr, request);
send(xhr, request.body);
return function() { xhr.abort(); };
});
}
function configureProgress(xhr, maybeProgress)
{
if (maybeProgress.ctor === 'Nothing')
{
return;
}
xhr.addEventListener('progress', function(event) {
if (!event.lengthComputable)
{
return;
}
_elm_lang$core$Native_Scheduler.rawSpawn(maybeProgress._0({
bytes: event.loaded,
bytesExpected: event.total
}));
});
}
function configureRequest(xhr, request)
{
function setHeader(pair)
{
xhr.setRequestHeader(pair._0, pair._1);
}
A2(_elm_lang$core$List$map, setHeader, request.headers);
xhr.responseType = request.expect.responseType;
xhr.withCredentials = request.withCredentials;
if (request.timeout.ctor === 'Just')
{
xhr.timeout = request.timeout._0;
}
}
function send(xhr, body)
{
switch (body.ctor)
{
case 'EmptyBody':
xhr.send();
return;
case 'StringBody':
xhr.setRequestHeader('Content-Type', body._0);
xhr.send(body._1);
return;
case 'FormDataBody':
xhr.send(body._0);
return;
}
}
// RESPONSES
function handleResponse(xhr, responseToResult)
{
var response = toResponse(xhr);
if (xhr.status < 200 || 300 <= xhr.status)
{
response.body = xhr.responseText;
return _elm_lang$core$Native_Scheduler.fail({
ctor: 'BadStatus',
_0: response
});
}
var result = responseToResult(response);
if (result.ctor === 'Ok')
{
return _elm_lang$core$Native_Scheduler.succeed(result._0);
}
else
{
response.body = xhr.responseText;
return _elm_lang$core$Native_Scheduler.fail({
ctor: 'BadPayload',
_0: result._0,
_1: response
});
}
}
function toResponse(xhr)
{
return {
status: { code: xhr.status, message: xhr.statusText },
headers: parseHeaders(xhr.getAllResponseHeaders()),
url: xhr.responseURL,
body: xhr.response
};
}
function parseHeaders(rawHeaders)
{
var headers = _elm_lang$core$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(_elm_lang$core$Dict$update, key, function(oldValue) {
if (oldValue.ctor === 'Just')
{
return _elm_lang$core$Maybe$Just(value + ', ' + oldValue._0);
}
return _elm_lang$core$Maybe$Just(value);
}, headers);
}
}
return headers;
}
// EXPECTORS
function expectStringResponse(responseToResult)
{
return {
responseType: 'text',
responseToResult: responseToResult
};
}
function mapExpect(func, expect)
{
return {
responseType: expect.responseType,
responseToResult: function(response) {
var convertedResponse = expect.responseToResult(response);
return A2(_elm_lang$core$Result$map, func, convertedResponse);
}
};
}
// BODY
function multipart(parts)
{
var formData = new FormData();
while (parts.ctor !== '[]')
{
var part = parts._0;
formData.append(part._0, part._1);
parts = parts._1;
}
return { ctor: 'FormDataBody', _0: formData };
}
return {
toTask: F2(toTask),
expectStringResponse: expectStringResponse,
mapExpect: F2(mapExpect),
multipart: multipart,
encodeUri: encodeUri,
decodeUri: decodeUri
};
}();