Extract the Paginated module into its own library
This commit is contained in:
parent
28cf4c1d70
commit
9368410d4b
5 changed files with 2 additions and 315 deletions
|
@ -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",
|
||||||
|
|
|
@ -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
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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)
|
|
||||||
]
|
|
|
@ -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",
|
||||||
|
|
Loading…
Reference in a new issue