diff --git a/src/App.elm b/src/App.elm index 8407a35..d54c2fe 100644 --- a/src/App.elm +++ b/src/App.elm @@ -51,31 +51,35 @@ update msg model = case msg of GotObjects data -> let - updatePaginated new = - let - updated = - Paginated.update - (RemoteData.toMaybe model.objects) - new - in - case updated of - Paginated.Partial options items -> - ( updated - , Paginated.request options - |> Paginated.httpRequest - |> RemoteData.sendRequest - |> Cmd.map GotObjects - ) + next p = + case p of + Paginated.Complete _ -> + Cmd.none - Paginated.Complete items -> - ( updated, Cmd.none ) + Paginated.Partial request _ -> + Paginated.httpRequest request + |> RemoteData.sendRequest + |> Cmd.map GotObjects ( objects, cmd ) = 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 in - ( { model | objects = objects }, cmd ) + ( { model | objects = objects } + , cmd + ) _ -> ( model, Cmd.none ) diff --git a/src/Gitlab.elm b/src/Gitlab.elm index 4e84e51..491558c 100644 --- a/src/Gitlab.elm +++ b/src/Gitlab.elm @@ -102,7 +102,7 @@ getObjects repo client = [ "projects" , Http.encodeUri (repo.owner ++ "/" ++ repo.name) , "repository" - , "tree?recursive=true" + , "tree?recursive=true&per_page=100" ] decoder client diff --git a/src/Http/Util.elm b/src/Http/Util.elm new file mode 100644 index 0000000..8669c48 --- /dev/null +++ b/src/Http/Util.elm @@ -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 diff --git a/src/Paginated.elm b/src/Paginated.elm index 489f776..7cbf860 100644 --- a/src/Paginated.elm +++ b/src/Paginated.elm @@ -1,6 +1,7 @@ module Paginated exposing ( Request + , RequestOptions , Response(..) , request , get @@ -8,17 +9,92 @@ module Paginated , send , update , 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 Http +import Http.Util import Json.Decode exposing (Decoder) import Maybe.Extra -import Regex import Time +{-| Describes an API request. +-} type alias RequestOptions a = { method : String , 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 = 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 (RequestOptions a) (List 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 @@ -57,6 +158,8 @@ get url decoder = } +{-| Build a POST request. +-} post : String -> Http.Body -> Decoder a -> Request a post url body decoder = request @@ -70,6 +173,8 @@ post url body decoder = } +{-| Send a `Request`. +-} send : (Result Http.Error (Response a) -> msg) -> Request a @@ -79,22 +184,24 @@ send resultToMessage 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 = case ( old, new ) of - ( Nothing, _ ) -> - new - - ( Just (Complete items), _ ) -> + ( Complete items, _ ) -> Complete items - ( Just (Partial _ oldItems), Complete newItems ) -> + ( Partial _ oldItems, Complete newItems ) -> Complete (oldItems ++ newItems) - ( Just (Partial _ oldItems), Partial request 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 @@ -127,12 +234,8 @@ fromResponse options response = nextPage = Dict.get "Link" response.headers - |> Maybe.map links + |> Maybe.map Http.Util.links |> Maybe.andThen (Dict.get "next") - - newOptions : Result String (RequestOptions a) - newOptions = - Err "Next request not implemented" in case nextPage of Nothing -> @@ -140,26 +243,6 @@ fromResponse options response = Just url -> Result.map - (Partial { options | url = url }) + (Partial (request { options | url = url })) 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