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": [],
|
||||
"dependencies": {
|
||||
"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-lang/core": "5.1.1 <= v < 6.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": [],
|
||||
"dependencies": {
|
||||
"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",
|
||||
"elm-community/elm-test": "4.0.0 <= v < 5.0.0",
|
||||
"elm-community/maybe-extra": "4.0.0 <= v < 5.0.0",
|
||||
|
|
Loading…
Reference in a new issue