Typed, bidirectional query parameter handling.
Query parameters are defined as codecs that can both parse query strings and build them back. They compose independently from path codecs.
type alias SearchParams =
{ query : Maybe String
, page : Int
, tags : List String
}
searchQuery : QueryCodec SearchParams
searchQuery =
Query.succeed SearchParams
|> Query.with .query (Query.string "q")
|> Query.with .page (Query.int "page" |> Query.withDefault 1)
|> Query.with .tags (Query.list "tag")
|> Query.build
Query.parseQuery searchQuery "q=elm&page=2&tag=fp&tag=web"
--> { query = Just "elm", page = 2, tags = [ "fp", "web" ] }
Query.buildQuery searchQuery
{ query = Just "elm", page = 2, tags = [ "fp", "web" ] }
--> "q=elm&page=2&tag=fp&tag=web"All primitives that may be absent return Maybe a. Lift to a non-Maybe
type with a default via withDefault.
An optional string query parameter.
Query.string "q"
-- parses "?q=hello" -> Just "hello"
-- parses "?q=" -> Nothing (empty values are treated as absent)
-- parses "?" -> Nothing
-- builds Just "hello" -> "q=hello"
-- builds Nothing -> ""
-- builds (Just "") -> ""For the older "missing means empty string" behavior, use:
Query.string "q" |> Query.withDefault ""An optional Int query parameter.
Query.int "page"
-- parses "?page=3" -> Just 3
-- parses "?page=abc" -> Nothing
-- parses "?" -> NothingAn optional Float query parameter. Mirrors Route.float on the path
side and pairs with Query.int for non-integer numeric values.
Query.float "ratio"
-- parses "?ratio=0.5" -> Just 0.5
-- parses "?ratio=abc" -> Nothing
-- parses "?" -> NothingAn optional boolean query parameter. Parses "true"/"1" as True, "false"/"0" as False.
Query.bool "active"
-- parses "?active=true" -> Just True
-- parses "?active=0" -> Just False
-- parses "?" -> NothingA flag query parameter. Present = True, absent = False. The value is ignored; only presence matters.
Query.flag "verbose"
-- parses "?verbose" -> True
-- parses "?verbose=anything" -> True
-- parses "?" -> FalseAn enum query parameter that maps between strings and custom types.
type SortOrder
= DateAsc
| DateDesc
| Title
sortCodec : QueryCodec (Maybe SortOrder)
sortCodec =
Query.enum "sort"
[ ( "date-asc", DateAsc )
, ( "date-desc", DateDesc )
, ( "title", Title )
]A list-valued query parameter. Repeated keys are collected into a list.
Query.list "tag"
-- parses "?tag=elm&tag=fp" -> [ "elm", "fp" ]
-- parses "?" -> []A comma-separated list-valued query parameter. Parses
?tag=fp,web,clojure into [ "fp", "web", "clojure" ].
Query.csv "tag"
-- parses "?tag=fp,web" -> [ "fp", "web" ]
-- parses "?tag=" -> []
-- parses "?" -> []Distinct from Query.list, which collects repeated keys
(?tag=fp&tag=web). Both styles are common in the wild; choose the
one your URLs already use.
The values must not contain commas. If they might, use Query.list
instead, since this codec uses , as the delimiter and would treat
an embedded comma as a separator. Empty values produced by adjacent
or trailing commas are filtered out, so Query.csv and Query.list
agree on whether a key contributes any values.
Provide a default value for a Maybe-shaped codec, lifting it to a
non-Maybe type. The default is omitted from the built query string, so
round-tripping still works.
Query.int "page" |> Query.withDefault 1
-- parses "?" -> 1
-- parses "?page=2" -> 2
-- builds 1 -> ""
-- builds 2 -> "page=2"The value type must support structural equality (no functions inside).
Mark a query parameter as required. The key is added to the
codec's required-key list, which is consulted only by
parseQueryStrict. Lenient parseQuery ignores the marker, so this
modifier never changes a value's parsed type.
Query.string "q" |> Query.required "q"
-- still QueryCodec (Maybe String)
-- but parseQueryStrict fails when "q" is absentThe key is repeated explicitly so that required composes with any
codec, including ones built from map or withDefault whose own
key is not externally inspectable. If the key is already marked
required, this is a no-op.
Build composite query codecs in a record-friendly pipeline. This is the recommended way to compose more than one or two parameters.
An intermediate builder for a pipelined query codec. Construct one
with succeed, add fields with with, finalize with build.
The two type parameters are:
state — the parser state, which evolves from the constructor type
(e.g., String -> Int -> SearchParams) to the final result type as
fields are added.result — the final record type. Stays constant across the pipeline
so each with step can extract the field value from a record value.Begin a pipelined query codec from a record constructor.
Query.succeed SearchParams
|> Query.with .q (Query.string "q")
|> Query.with .page (Query.int "page" |> Query.withDefault 1)
|> Query.buildAdd a field to a pipelined query codec. The first argument is a record-field accessor used when building. The second is the codec for that field.
Query.succeed SearchParams
|> Query.with .q (Query.string "q")Finalize a pipeline into a QueryCodec.
The state and result types must be equal — i.e., every field has
been provided to satisfy the constructor passed to succeed.
The original mapN combinators. They remain exported for backwards
compatibility, but new code should prefer the pipeline.
Map over a query codec's parsed value.
Combine two query codecs using a constructor and a decomposer.
For three or more fields prefer the succeed/with/build pipeline.
map2 SearchQuery
(\s -> ( s.query, s.page ))
(Query.string "q")
(Query.int "page")Combine three query codecs.
For three or more fields prefer the succeed/with/build pipeline.
Combine four query codecs.
For three or more fields prefer the succeed/with/build pipeline.
Parse a query string into a value using a codec. The query string should not include the leading "?".
parseQuery codec "page=3&sort=date"Strict variant of parseQuery. Returns Nothing if any key
flagged with Query.required is absent from the input. Otherwise
returns Just of the same value parseQuery would have produced.
paramsCodec =
Query.succeed identity
|> Query.with identity (Query.string "q" |> Query.required "q")
|> Query.build
Query.parseQueryStrict paramsCodec "q=elm" --> Just (Just "elm")
Query.parseQueryStrict paramsCodec "" --> NothingUseful when a route's query parameter is not actually optional and a missing value should fail the route match rather than silently falling back to a default.
Result-typed variant of parseQueryStrict. Returns
Err missingKeys listing every required key absent from the input,
or Ok of the parsed value when all required keys are present.
paramsCodec =
Query.succeed Pair
|> Query.with .q (Query.string "q" |> Query.required "q")
|> Query.with .page (Query.int "page" |> Query.required "page")
|> Query.build
Query.parseQueryResult paramsCodec "q=elm&page=2"
--> Ok (Pair (Just "elm") (Just 2))
Query.parseQueryResult paramsCodec "q=elm"
--> Err [ "page" ]
Query.parseQueryResult paramsCodec ""
--> Err [ "q", "page" ]Useful when missing keys should produce a meaningful error message (form validation feedback, structured logs, an HTTP 400 response).
Build a query string from a value using a codec. Returns the query string without a leading "?". Returns empty string if no parameters are produced.
buildQuery codec { page = Just 3, sort = Just DateAsc }
--> "page=3&sort=date-asc"Convenience wrappers over the parseQuery* family that take a Url
directly and read its query field, removing
url.query |> Maybe.withDefault "" boilerplate at every call site.
Convenience wrapper over parseQuery that takes a Url directly
and reads its query field. Saves the
url.query |> Maybe.withDefault "" |> Query.parseQuery codec
boilerplate at every call site, common in Browser.application page
init functions.
Query.parseUrl searchQuery url
--> { query = Just "elm", page = 1, tags = [] }An absent query (url.query == Nothing) is treated as empty.
Strict variant of parseUrl. Returns Nothing if any key flagged
with Query.required is absent from the URL's query. Otherwise returns
Just of the same value parseUrl would have produced.
Query.parseUrlStrict requiredQuery url
--> Just { q = "elm" } -- when q is present
--> Nothing -- when q is absentResult-typed variant of parseUrl. Returns Err missingKeys
listing every required key absent from the URL's query, or Ok of
the parsed value when all required keys are present.
Query.parseUrlResult requiredQuery url
--> Ok { q = "elm", page = 2 } -- both present
--> Err [ "page" ] -- only q present
--> Err [ "q", "page" ] -- both missingFor the common "change one key, keep all the others" pattern in pagination and filter UIs, work directly on the raw query string.
Set or remove a single key in a raw query string, leaving every other key untouched.
Pass Just value to set the key (replacing all existing occurrences and
appending if absent), or Nothing to remove it (equivalent to drop).
A leading ? on the input is accepted and stripped. The result is
returned without a leading ?.
Query.update "page" (Just "5") "q=elm&page=2&tag=fp"
--> "q=elm&tag=fp&page=5"
Query.update "page" Nothing "q=elm&page=2"
--> "q=elm"
Query.update "sort" (Just "date") ""
--> "sort=date"This is intended for "navigate to the same page with page= changed"
patterns where rebuilding the full typed record would be needlessly
verbose.
Remove every occurrence of a key from a raw query string. Equivalent
to update key Nothing.
Query.drop "tag" "q=elm&tag=fp&tag=web"
--> "q=elm"Escape hatches that expose the parsed List ( String, String )
representation, plus targeted lookup and merging. Use these when
you need a query operation that does not fit the typed pipeline.
Parse a raw query string into a list of key-value pairs.
Tolerates a leading ?. Preserves the original order and any
duplicate keys. Keys and values are percent-decoded, so the round
trip with Query.fromList is clean even for keys or values that
contain URL-significant characters.
Query.toList "?q=elm&tag=fp&tag=web"
--> [ ( "q", "elm" ), ( "tag", "fp" ), ( "tag", "web" ) ]
Query.toList ""
--> []Build a raw query string from a list of key-value pairs.
Keys and values are percent-encoded. The order of pairs is preserved.
A pair with an empty value is rendered as a bare key (flag rather
than flag=), matching the behavior of Query.flag.
Query.fromList [ ( "q", "elm" ), ( "tag", "fp" ), ( "tag", "web" ) ]
--> "q=elm&tag=fp&tag=web"
Query.fromList []
--> ""Look up the first value for a key in a raw query string.
Tolerates a leading ?. Returns Nothing if the key is absent.
For multi-valued keys, only the first value is returned, matching
how the typed primitives like Query.string behave.
Query.lookup "q" "q=elm&page=2"
--> Just "elm"
Query.lookup "missing" "q=elm"
--> Nothing
Query.lookup "tag" "tag=fp&tag=web"
--> Just "fp"Combine two raw query strings.
On key conflict every entry from the first argument is kept and
every entry from the second argument with that key is dropped.
Keys present only in the second argument are appended in their
original order. Tolerates a leading ? on either side.
Query.merge "q=elm" "utm_source=twitter"
--> "q=elm&utm_source=twitter"
Query.merge "q=elm" "q=haskell&utm_source=twitter"
--> "q=elm&utm_source=twitter"
Query.merge "" "utm_source=twitter"
--> "utm_source=twitter"Useful for the "overlay tracking parameters on a route's natural query" pattern, where the route owns its own keys but other layers (analytics, A/B testing, deep links) need to add their own without clobbering them.
Keep only the listed keys from a raw query string, dropping
everything else. Order of the surviving entries is preserved.
Tolerates a leading ?.
Query.preserve [ "q", "page" ] "q=elm&page=2&utm_source=tw"
--> "q=elm&page=2"
Query.preserve [ "utm_source", "utm_campaign" ]
"q=elm&utm_source=tw&utm_campaign=launch"
--> "utm_source=tw&utm_campaign=launch"
Query.preserve [] "q=elm&page=2"
--> ""Useful for the "carry these tracking parameters across navigation" pattern, or for stripping a query string down to a known-safe allowlist before logging or caching.
Drop the listed keys from a raw query string, keeping everything
else. The list-based complement to Query.drop. Tolerates a leading
?.
Query.except [ "utm_source", "utm_campaign" ]
"q=elm&page=2&utm_source=tw&utm_campaign=launch"
--> "q=elm&page=2"
Query.except [ "page" ] "q=elm&page=2"
--> "q=elm"
Query.except [] "q=elm&page=2"
--> "q=elm&page=2"Useful when you have a denylist of keys to strip (sensitive params before sharing a URL, navigation-only keys before persisting state) without having to list out every key you do want to keep.
Normalize a raw query string to a canonical form by sorting its
entries lexicographically (by key, then by value). Percent-encoding
is also normalized as a side effect of round-tripping through
toList / fromList.
Useful as a stable cache key, deduplication discriminator, or analytics input where two semantically-equivalent URLs should hash to the same string.
Query.canonicalString "page=2&q=elm"
--> "page=2&q=elm"
Query.canonicalString "q=elm&page=2"
--> "page=2&q=elm"
Query.canonicalString "tag=web&q=elm&tag=fp"
--> "q=elm&tag=fp&tag=web"Tolerates a leading ?; the result never includes one.
A bidirectional codec for a single query parameter or a composed set of query parameters.
The internal
requiredKeysfield lists keys flagged withQuery.required. It is consulted only byparseQueryStrict; lenientparseQueryignores it.