Retrieve all query strings from a url

I’m building a search interface where the user can type in a search query, but also choose from a fixed set of filters (category, color, brand, price etc).

The filters are completely dynamic and, possibly, unique for each category so there is no way for me to specify all the filter alternatives in Elm. What I’d like to do is to fetch all the selected options in a query string like this:

/search?q=Gore-Tex&color=green,blue&brand=Nike&sortBy=price&sortOrder=ASC

and parse it into a Dict String (List String), the above example should result in:

Dict.fromList
    [ ( "q", ["Gore-Tex"] )
    , ( "color", [ "green", "blue"] )
    , ( "brand", ["Nike"] )
    , ( "sortBy", ["price"] )
    , ( "sortOrder", ["ASC"] )
    ]

I’m not too comfortable with the Url Parser library but it seems that every function has to take a string key. But the QueryParser actually seems to represent the query exactly as the type of Dict that I’m looking for, without any way to retrieve the entire Dict for further processing. https://github.com/elm/url/blob/1.0.0/src/Url/Parser/Internal.elm#L9

I imagine something like this should be sufficient:

import Url.Parser exposing (Parser, (</>), (<?>), int, map, oneOf, s, string)
import Url.Parser.Query as Query

type Route
    | Search (Dict String (List String))

routeParser : Parser (Route -> a) a
routeParser =
oneOf
    [ 
    , map Search (s "search" <?> Query.all)
    ]

with the all function defined like this:

all : (Dict String (List String)) -> Parser a
all func =
  Q.Parser <| \dict -> func dict

Any help on how to achieve the above would be appreciated.

1 Like

I don’t think that you can do this with Url.Parser.Query. Also I don’t think that the parser would split your color=green,blue parameter anyway.

However percentDecode has the following note:

Use Url.Parser instead! It will decode query parameters appropriately already! percentDecode is only available so that extremely custom cases are possible, if needed.

So because the Url type is not opaque, you can do your own custom function.
Something like:

import Dict exposing (Dict)
import Url exposing (Url)

queryParameters : Url -> Dict String (List String)
queryParameters url =
    let
        toTuples : String -> List ( String, String )
        toTuples str =
            case String.split "=" str of
                key :: value -> [ ( key, String.join "=" value ) ]
                [] -> []

        toDict : List ( String, String ) -> Dict String (List String)
        toDict parameters =
            List.foldl
                (\( k, v ) dict -> Dict.update k (addParam v) dict)
                Dict.empty
                parameters

        addParam : String -> Maybe (List String) -> Maybe (List String)
        addParam value maybeValues =
            case maybeValues of
                Just values -> Just (value :: values)
                Nothing -> Just [ value ]
    in
    url.query
        |> Maybe.andThen Url.percentDecode
        |> Maybe.map (String.split "&" >> List.concatMap toTuples >> toDict)
        |> Maybe.withDefault Dict.empty

Let’s test it in elm repl:

> import Url

> url = Url.fromString "https://test.com/search?q=Gore-Tex&color=green&color=blue&brand=Nike&sortBy=price&sortOrder=ASC"
Just { fragment = Nothing, host = "test.com", path = "/search", port_ = Nothing, protocol = Https, query = Just "q=Gore-Tex&color=green&color=blue&brand=Nike&sortBy=price&sortOrder=ASC" }
    : Maybe Url.Url

> url |> Maybe.map queryParameters
Just (Dict.fromList [("brand",["Nike"]),("color",["blue","green"]),("q",["Gore-Tex"]),("sortBy",["price"]),("sortOrder",["ASC"])])
    : Maybe (Dict.Dict String (List String))

Note that I changed color=green,blue to color=green&color=blue to be more generic but you could use your custom interpretation of query parameters by modifying the toTuples function implementation to:

toTuples : String -> List ( String, String )
toTuples str =
    case String.split "=" str of
        key :: value ->
            String.split "," (String.join "=" value)
                |> List.map (Tuple.pair key)

        [] ->
            []
1 Like

I’ve also had success using https://package.elm-lang.org/packages/sporto/qs/latest/ in the past, which makes this straight forward:

QS.parse
    QS.config
    "?a=1&b=x"

== Dict.fromList
    [ ( "a", One <| Number 1 )
    , ( "b", One <| Str "x" ) 
    ]
1 Like

Thank you! I do realize that the “green,blue” case needs to be handle separately, which is ok.

Just to be clear, the idea here is to not use the Parser at all in this case?

You could parse the url using Url.fromString but then parse the query string using your own or QS parser.

This also does not prevent you to parse some paths using Url.Parser, then add some parameters based on the query string you would parse yourself.

1 Like

Great! Again, thank you very much!

@jackfranklin: Thank you for the suggestion!

1 Like

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.