Extract the Paginated module into its own library

This commit is contained in:
Correl Roush 2018-01-18 14:37:03 -05:00
parent 28cf4c1d70
commit 9368410d4b
5 changed files with 2 additions and 315 deletions

View file

@ -9,6 +9,7 @@
"exposed-modules": [], "exposed-modules": [],
"dependencies": { "dependencies": {
"NoRedInk/elm-decode-pipeline": "3.0.0 <= v < 4.0.0", "NoRedInk/elm-decode-pipeline": "3.0.0 <= v < 4.0.0",
"correl/elm-paginated": "1.0.0 <= v < 2.0.0",
"elm-community/maybe-extra": "4.0.0 <= v < 5.0.0", "elm-community/maybe-extra": "4.0.0 <= v < 5.0.0",
"elm-lang/core": "5.1.1 <= v < 6.0.0", "elm-lang/core": "5.1.1 <= v < 6.0.0",
"elm-lang/html": "2.0.0 <= v < 3.0.0", "elm-lang/html": "2.0.0 <= v < 3.0.0",

View file

@ -1,35 +0,0 @@
module Http.Util exposing (links)
import Dict exposing (Dict)
import Maybe.Extra
import Regex
{-| Parse an HTTP Link header into a dictionary. For example, to look
for a link to additional results in an API response, you could do the
following:
Dict.get "Link" response.headers
|> Maybe.map links
|> Maybe.andThen (Dict.get "next")
-}
links : String -> Dict String String
links s =
let
toTuples xs =
case xs of
[ Just a, Just b ] ->
Just ( b, a )
_ ->
Nothing
in
Regex.find
Regex.All
(Regex.regex "<(.*?)>; rel=\"(.*?)\"")
s
|> List.map .submatches
|> List.map toTuples
|> Maybe.Extra.values
|> Dict.fromList

View file

@ -1,248 +0,0 @@
module Paginated
exposing
( Request
, RequestOptions
, Response(..)
, request
, get
, post
, send
, update
, httpRequest
)
{-| Facilitates fetching data from a paginated JSON API.
import Http
import Json.Decode exposing (string)
import Paginated exposing (Response(..))
type alias Model =
{ results : Maybe (Paginated.Response String) }
type Msg
= Search
| Results (Result Http.Error (Paginated.Response String))
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Search ->
( model, doSearch )
Results (Ok response) ->
case response of
Partial request results ->
( { model
| results =
Maybe.map (\x -> Paginated.update x response)
model.results
}
, Paginated.send Results request
)
Complete results ->
( { model
| results =
Maybe.map (\x -> Paginated.update x response)
model.results
}
, Cmd.none
)
Results (Err _) ->
( model, Cmd.none )
doSearch : Cmd Msg
doSearch =
Paginated.send Results <|
Paginated.get "http://example.com/items" string
# Requests and Responses
@docs Request, Response, get, post
## Custom requests
@docs RequestOptions, request
## Converting
@docs httpRequest
# Sending requests
@docs send
# Handling responses
@docs Response, update
-}
import Dict exposing (Dict)
import Http
import Http.Util
import Json.Decode exposing (Decoder)
import Maybe.Extra
import Time
{-| Describes an API request.
-}
type alias RequestOptions a =
{ method : String
, headers : List Http.Header
, url : String
, body : Http.Body
, decoder : Decoder a
, timeout : Maybe Time.Time
, withCredentials : Bool
}
{-| Encapsulates an API request for a list of items of type `a`.
-}
type Request a
= Request (RequestOptions a)
{-| Describes an API response.
A response may either be Partial (there are more pages of results yet
to be fetched), or Complete (all records have been fetched). The
response includes all of the items fetched in order.
-}
type Response a
= Partial (Request a) (List a)
| Complete (List a)
{-| Create a custom request, allowing the specification of HTTP
headers and other options. For example:
Paginated.request
{ method = "GET"
, headers = [Http.header "Private-Token" "XXXXXXXXXXXXXXXX"]
, url = url
, body = Http.emptyBody
, decoder = decoder
, timeout = Nothing
, withCredentials = False
}
-}
request : RequestOptions a -> Request a
request =
Request
{-| Build a GET request.
-}
get : String -> Decoder a -> Request a
get url decoder =
request
{ method = "GET"
, headers = []
, url = url
, body = Http.emptyBody
, decoder = decoder
, timeout = Nothing
, withCredentials = False
}
{-| Build a POST request.
-}
post : String -> Http.Body -> Decoder a -> Request a
post url body decoder =
request
{ method = "POST"
, headers = []
, url = url
, body = body
, decoder = decoder
, timeout = Nothing
, withCredentials = False
}
{-| Send a `Request`.
-}
send :
(Result Http.Error (Response a) -> msg)
-> Request a
-> Cmd msg
send resultToMessage request =
Http.send resultToMessage <|
httpRequest request
{-| Append two paginated responses, collecting the results within.
-}
update : Response a -> Response a -> Response a
update old new =
case ( old, new ) of
( Complete items, _ ) ->
Complete items
( Partial _ oldItems, Complete newItems ) ->
Complete (oldItems ++ newItems)
( Partial _ oldItems, Partial request newItems ) ->
Partial request (oldItems ++ newItems)
{-| Convert a `Request` to a `Http.Request` that can then be sent via
`Http.send`.
-}
httpRequest : Request a -> Http.Request (Response a)
httpRequest (Request options) =
Http.request
{ method = options.method
, headers = options.headers
, url = options.url
, body = options.body
, expect = expect options
, timeout = options.timeout
, withCredentials = options.withCredentials
}
expect : RequestOptions a -> Http.Expect (Response a)
expect options =
Http.expectStringResponse (fromResponse options)
fromResponse :
RequestOptions a
-> Http.Response String
-> Result String (Response a)
fromResponse options response =
let
items : Result String (List a)
items =
Json.Decode.decodeString
(Json.Decode.list options.decoder)
response.body
nextPage =
Dict.get "Link" response.headers
|> Maybe.map Http.Util.links
|> Maybe.andThen (Dict.get "next")
in
case nextPage of
Nothing ->
Result.map Complete items
Just url ->
Result.map
(Partial (request { options | url = url }))
items

View file

@ -1,32 +0,0 @@
module PaginatedTests exposing (..)
import Dict
import Expect
import Paginated
import Test exposing (..)
suite : Test
suite =
describe "Paginated"
[ test "Parse links" <|
\() ->
let
header =
String.join ", "
[ "<https://gitlab.example.com/api/v4/projects/8/issues/8/notes?page=1&per_page=3>; rel=\"prev\""
, "<https://gitlab.example.com/api/v4/projects/8/issues/8/notes?page=3&per_page=3>; rel=\"next\""
, "<https://gitlab.example.com/api/v4/projects/8/issues/8/notes?page=1&per_page=3>; rel=\"first\""
, "<https://gitlab.example.com/api/v4/projects/8/issues/8/notes?page=3&per_page=3>; rel=\"last\""
]
expected =
Dict.fromList
[ ( "prev", "https://gitlab.example.com/api/v4/projects/8/issues/8/notes?page=1&per_page=3" )
, ( "next", "https://gitlab.example.com/api/v4/projects/8/issues/8/notes?page=3&per_page=3" )
, ( "first", "https://gitlab.example.com/api/v4/projects/8/issues/8/notes?page=1&per_page=3" )
, ( "last", "https://gitlab.example.com/api/v4/projects/8/issues/8/notes?page=3&per_page=3" )
]
in
Expect.equalDicts expected (Paginated.links header)
]

View file

@ -10,6 +10,7 @@
"exposed-modules": [], "exposed-modules": [],
"dependencies": { "dependencies": {
"NoRedInk/elm-decode-pipeline": "3.0.0 <= v < 4.0.0", "NoRedInk/elm-decode-pipeline": "3.0.0 <= v < 4.0.0",
"correl/elm-paginated": "1.0.0 <= v < 2.0.0",
"eeue56/elm-html-test": "5.1.2 <= v < 6.0.0", "eeue56/elm-html-test": "5.1.2 <= v < 6.0.0",
"elm-community/elm-test": "4.0.0 <= v < 5.0.0", "elm-community/elm-test": "4.0.0 <= v < 5.0.0",
"elm-community/maybe-extra": "4.0.0 <= v < 5.0.0", "elm-community/maybe-extra": "4.0.0 <= v < 5.0.0",