Tidy up and document the Paginated module
This commit is contained in:
parent
b73272864b
commit
28cf4c1d70
4 changed files with 178 additions and 56 deletions
42
src/App.elm
42
src/App.elm
|
@ -51,31 +51,35 @@ update msg model =
|
||||||
case msg of
|
case msg of
|
||||||
GotObjects data ->
|
GotObjects data ->
|
||||||
let
|
let
|
||||||
updatePaginated new =
|
next p =
|
||||||
let
|
case p of
|
||||||
updated =
|
Paginated.Complete _ ->
|
||||||
Paginated.update
|
Cmd.none
|
||||||
(RemoteData.toMaybe model.objects)
|
|
||||||
new
|
|
||||||
in
|
|
||||||
case updated of
|
|
||||||
Paginated.Partial options items ->
|
|
||||||
( updated
|
|
||||||
, Paginated.request options
|
|
||||||
|> Paginated.httpRequest
|
|
||||||
|> RemoteData.sendRequest
|
|
||||||
|> Cmd.map GotObjects
|
|
||||||
)
|
|
||||||
|
|
||||||
Paginated.Complete items ->
|
Paginated.Partial request _ ->
|
||||||
( updated, Cmd.none )
|
Paginated.httpRequest request
|
||||||
|
|> RemoteData.sendRequest
|
||||||
|
|> Cmd.map GotObjects
|
||||||
|
|
||||||
( objects, cmd ) =
|
( objects, cmd ) =
|
||||||
RemoteData.update
|
RemoteData.update
|
||||||
updatePaginated
|
(\b ->
|
||||||
|
case model.objects of
|
||||||
|
RemoteData.Success a ->
|
||||||
|
let
|
||||||
|
updated =
|
||||||
|
Paginated.update a b
|
||||||
|
in
|
||||||
|
( updated, next updated )
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
( b, next b )
|
||||||
|
)
|
||||||
data
|
data
|
||||||
in
|
in
|
||||||
( { model | objects = objects }, cmd )
|
( { model | objects = objects }
|
||||||
|
, cmd
|
||||||
|
)
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
( model, Cmd.none )
|
( model, Cmd.none )
|
||||||
|
|
|
@ -102,7 +102,7 @@ getObjects repo client =
|
||||||
[ "projects"
|
[ "projects"
|
||||||
, Http.encodeUri (repo.owner ++ "/" ++ repo.name)
|
, Http.encodeUri (repo.owner ++ "/" ++ repo.name)
|
||||||
, "repository"
|
, "repository"
|
||||||
, "tree?recursive=true"
|
, "tree?recursive=true&per_page=100"
|
||||||
]
|
]
|
||||||
decoder
|
decoder
|
||||||
client
|
client
|
||||||
|
|
35
src/Http/Util.elm
Normal file
35
src/Http/Util.elm
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
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
|
|
@ -1,6 +1,7 @@
|
||||||
module Paginated
|
module Paginated
|
||||||
exposing
|
exposing
|
||||||
( Request
|
( Request
|
||||||
|
, RequestOptions
|
||||||
, Response(..)
|
, Response(..)
|
||||||
, request
|
, request
|
||||||
, get
|
, get
|
||||||
|
@ -8,17 +9,92 @@ module Paginated
|
||||||
, send
|
, send
|
||||||
, update
|
, update
|
||||||
, httpRequest
|
, httpRequest
|
||||||
, links
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
{-| 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 Dict exposing (Dict)
|
||||||
import Http
|
import Http
|
||||||
|
import Http.Util
|
||||||
import Json.Decode exposing (Decoder)
|
import Json.Decode exposing (Decoder)
|
||||||
import Maybe.Extra
|
import Maybe.Extra
|
||||||
import Regex
|
|
||||||
import Time
|
import Time
|
||||||
|
|
||||||
|
|
||||||
|
{-| Describes an API request.
|
||||||
|
-}
|
||||||
type alias RequestOptions a =
|
type alias RequestOptions a =
|
||||||
{ method : String
|
{ method : String
|
||||||
, headers : List Http.Header
|
, headers : List Http.Header
|
||||||
|
@ -30,20 +106,45 @@ type alias RequestOptions a =
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| Encapsulates an API request for a list of items of type `a`.
|
||||||
|
-}
|
||||||
type Request a
|
type Request a
|
||||||
= Request (RequestOptions 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
|
type Response a
|
||||||
= Partial (RequestOptions a) (List a)
|
= Partial (Request a) (List a)
|
||||||
| Complete (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 : RequestOptions a -> Request a
|
||||||
request =
|
request =
|
||||||
Request
|
Request
|
||||||
|
|
||||||
|
|
||||||
|
{-| Build a GET request.
|
||||||
|
-}
|
||||||
get : String -> Decoder a -> Request a
|
get : String -> Decoder a -> Request a
|
||||||
get url decoder =
|
get url decoder =
|
||||||
request
|
request
|
||||||
|
@ -57,6 +158,8 @@ get url decoder =
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| Build a POST request.
|
||||||
|
-}
|
||||||
post : String -> Http.Body -> Decoder a -> Request a
|
post : String -> Http.Body -> Decoder a -> Request a
|
||||||
post url body decoder =
|
post url body decoder =
|
||||||
request
|
request
|
||||||
|
@ -70,6 +173,8 @@ post url body decoder =
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| Send a `Request`.
|
||||||
|
-}
|
||||||
send :
|
send :
|
||||||
(Result Http.Error (Response a) -> msg)
|
(Result Http.Error (Response a) -> msg)
|
||||||
-> Request a
|
-> Request a
|
||||||
|
@ -79,22 +184,24 @@ send resultToMessage request =
|
||||||
httpRequest request
|
httpRequest request
|
||||||
|
|
||||||
|
|
||||||
update : Maybe (Response a) -> Response a -> Response a
|
{-| Append two paginated responses, collecting the results within.
|
||||||
|
-}
|
||||||
|
update : Response a -> Response a -> Response a
|
||||||
update old new =
|
update old new =
|
||||||
case ( old, new ) of
|
case ( old, new ) of
|
||||||
( Nothing, _ ) ->
|
( Complete items, _ ) ->
|
||||||
new
|
|
||||||
|
|
||||||
( Just (Complete items), _ ) ->
|
|
||||||
Complete items
|
Complete items
|
||||||
|
|
||||||
( Just (Partial _ oldItems), Complete newItems ) ->
|
( Partial _ oldItems, Complete newItems ) ->
|
||||||
Complete (oldItems ++ newItems)
|
Complete (oldItems ++ newItems)
|
||||||
|
|
||||||
( Just (Partial _ oldItems), Partial request newItems ) ->
|
( Partial _ oldItems, Partial request newItems ) ->
|
||||||
Partial request (oldItems ++ 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 a -> Http.Request (Response a)
|
||||||
httpRequest (Request options) =
|
httpRequest (Request options) =
|
||||||
Http.request
|
Http.request
|
||||||
|
@ -127,12 +234,8 @@ fromResponse options response =
|
||||||
|
|
||||||
nextPage =
|
nextPage =
|
||||||
Dict.get "Link" response.headers
|
Dict.get "Link" response.headers
|
||||||
|> Maybe.map links
|
|> Maybe.map Http.Util.links
|
||||||
|> Maybe.andThen (Dict.get "next")
|
|> Maybe.andThen (Dict.get "next")
|
||||||
|
|
||||||
newOptions : Result String (RequestOptions a)
|
|
||||||
newOptions =
|
|
||||||
Err "Next request not implemented"
|
|
||||||
in
|
in
|
||||||
case nextPage of
|
case nextPage of
|
||||||
Nothing ->
|
Nothing ->
|
||||||
|
@ -140,26 +243,6 @@ fromResponse options response =
|
||||||
|
|
||||||
Just url ->
|
Just url ->
|
||||||
Result.map
|
Result.map
|
||||||
(Partial { options | url = url })
|
(Partial (request { options | url = url }))
|
||||||
items
|
items
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
Loading…
Reference in a new issue