An alternative to Url.Parser
and Url.Builder
modules from the elm/url
package.
Allows you to define both the URL parser and the URL builder at the same time.
Note that if you only need an URL parser, the Url.SimpleParser
module will be nicer to use (require less boilerplate) while providing the same
functionality.
CodecInProgress is an unfinished codec that needs some more steps to be able to fully parse and build URLs.
Whenever you see CodecInProgress with two unequal types, you're likely missing
an argument to your final value somewhere.
You'll typically start with something like
type Route
= CommentPage String Int
| PostPage String
isComment : Route -> Bool
isComment route =
case route of
CommentPage _ _ ->
True
_ ->
False
myCodec =
Url.Codec.succeed CommentPage isCommentAt this point, the codec is of type:
myCodec : CodecInProgress Route (String -> Int -> Route)Your goal here is to provide both arguments to CommentPage: the String and
the Int. Do that with the various combinators (int,
string, fragment and all the *query* functions).
You can also use the s (as in "segment") function to provide hardcoded
segments in the URL path:
myCodec =
Url.Codec.succeed CommentPage isComment
|> Url.Codec.s "post"
|> Url.Codec.string getCommentPageSlug
|> Url.Codec.s "page"
|> Url.Codec.int getCommentPageNumberNow that we've used both string and int, the second part
of the type signature has changed from String -> Int -> Route to Route,
meaning both parts are the same (CodecInProgress Route Route, also expressible
as Codec Route) and the codec is ready to use! It will now be able to both
parse and build URLs:
Url.Codec.parsePath [myCodec] "/post/hello-world/page/1"
--> Ok (CommentPage "hello-world" 1)
Url.Codec.toString [myCodec] (CommentPage "you-too" 222)
--> Just "post/you-too/page/222"Note that the functions that provide you with data will need you to provide
a getter, used by the toString function to get the data from your Route type.
Here is a typical implementation of one:
getCommentPageSlug : Route -> Maybe String
getCommentPageSlug route =
case route of
CommentPage slug _ ->
Just slug
_ ->
NothingSidenote: In principle it should be possible to make an elm-review rule that
would generate these getters for you. The more noise you'll make (ping
@janiczek on the Elm Slack or on Twitter), the better chance it will come into
existence :)
All the ways the parsing can fail.
Parse the URL path string, trying out multiple codecs if necessary.
Will stop at the first success.
Will prefer to report error from the parser that had most success parsing.
allCodecs =
[ helloCodec, homeCodec ]
Url.Codec.parsePath allCodecs "hello/123"
--> Ok (HelloPage 123)
Url.Codec.parsePath allCodecs "/hello/123?comments=1"
--> Ok (HelloPage 123)
Url.Codec.parsePath allCodecs "hello/123whoops"
--> Err (WasNotInt "123whoops")
Url.Codec.parsePath allCodecs ""
--> Ok HomePage
Url.Codec.parsePath [] ""
--> Err NoCodecsConvert the given value into an URL string, trying out multiple codecs if necessary.
Will stop at the first success.
Can fail (eg. if you use a codec for one route with a string belonging to a
different route, such that the predicate given to success will return False
or the getters return Nothing).
allCodecs =
[ helloCodec, postCodec ]
Url.Codec.toString allCodecs (HelloPage 123)
--> Just "hello/123"
Url.Codec.toString allCodecs (PostPage "goto-bad")
--> Just "post/goto-bad"
Url.Codec.toString [nonHelloPageCodec] (HelloPage 123)
--> NothingA way to start your Codec definition.
unfinishedCodec : CodecInProgress Route (String -> Route)
unfinishedCodec =
-- needs a string provided via a combinator like `Url.Codec.string`
Url.Codec.succeed UserRoute isUserRoute
isUserRoute : Route -> Bool
isUserRoute route =
case route of
UserRoute _ ->
True
_ ->
FalseYou'll then need to continue with some other combinators to provide the data to your route constructor.
Can also work standalone for URLs without path segments:
-- same as CodecInProgress Route Route
codec : Codec Route
codec =
Url.Codec.succeed HomeRoute isHomeRoute
isHomeRoute : Route -> Bool
isHomeRoute route =
route == HomeRoute
Url.Codec.parsePath [codec] ""
--> Ok HomeRoute
Url.Codec.toString [codec] HomeRoute
--> Just ""A hardcoded path segment.
type Route
= HomeRoute
codec : Codec Route
codec =
Url.Codec.succeed HomeRoute isHomeRoute
|> Url.Codec.s "home"
isHomeRoute : Route -> Bool
isHomeRoute route =
route == HomeRoute
Url.Codec.parsePath [codec] "home"
--> Ok HomeRoute
Url.Codec.toString [codec] HomeRoute
--> Just "home"An integer path segment.
type Route
= UserRoute Int
| ...
codec : Codec Route
codec =
Url.Codec.succeed UserRoute isUserRoute
|> Url.Codec.s "user"
|> Url.Codec.int getUserRouteId
isUserRoute : Route -> Bool
isUserRoute route =
case route of
UserRoute _ ->
True
_ ->
False
getUserRouteId : Route -> Maybe Int
getUserRouteId route =
case route of
UserRoute id ->
Just id
_ ->
Nothing
Url.Codec.parsePath [codec] "user/123"
--> Ok (UserRoute 123)
Url.Codec.parsePath [codec] "user"
--> Err SegmentNotAvailable
Url.Codec.toString [codec] (UserRoute 999)
--> Just "user/999"A string path segment.
type Route
= PostRoute String
| ...
codec : Codec Route
codec =
Url.Codec.succeed PostRoute isPostRoute
|> Url.Codec.s "post"
|> Url.Codec.string getPostRouteSlug
isPostRoute : Route -> Bool
isPostRoute route =
case route of
PostRoute _ ->
True
_ ->
False
getPostRouteSlug : Route -> Maybe String
getPostRouteSlug route =
case route of
PostRoute slug ->
Just slug
_ ->
Nothing
Url.Codec.parsePath [codec] "post/hello"
--> Ok (PostRoute "hello")
Url.Codec.parsePath [codec] "post"
--> Err SegmentNotAvailable
Url.Codec.toString [codec] (PostRoute "hiya")
--> Just "post/hiya"An integer query parameter.
type Route
= UserRoute (Maybe Int)
| ...
codec : Codec Route
codec =
Url.Codec.succeed UserRoute isUserRoute
|> Url.Codec.s "user"
|> Url.Codec.queryInt "id" getUserRouteId
isUserRoute : Route -> Bool
isUserRoute route =
case route of
UserRoute _ ->
True
_ ->
False
getUserRouteId : Route -> Maybe Int
getUserRouteId route =
case route of
UserRoute maybeId ->
maybeId
_ ->
Nothing
Url.Codec.parsePath [codec] "user?id=123"
--> Ok (UserRoute (Just 123))
Url.Codec.parsePath [codec] "user"
--> Ok (UserRoute Nothing)
Url.Codec.toString [codec] (UserRoute (Just 999))
--> Just "user?id=999"
Url.Codec.toString [codec] (UserRoute Nothing)
--> Just "user"Will fail if there are multiple query parameters with the same key:
Url.Codec.parsePath [codec] "user?id=1&id=2"
--> Err (NeededSingleQueryParameterValueGotMultiple { got = ["1","2"], key = "id" })Will succeed with Nothing if the query parameter contains a non-integer string:
Url.Codec.parsePath [codec] "user?id=martin"
--> Ok (UserRoute Nothing)A string query parameter.
type Route
= UserRoute (Maybe String)
| ...
codec : Codec Route
codec =
Url.Codec.succeed UserRoute isUserRoute
|> Url.Codec.s "user"
|> Url.Codec.queryString "name" getUserRouteName
isUserRoute : Route -> Bool
isUserRoute route =
case route of
UserRoute _ ->
True
_ ->
False
getUserRouteName : Route -> Maybe String
getUserRouteName route =
case route of
UserRoute name ->
name
_ ->
Nothing
Url.Codec.parsePath [codec] "user?name=martin"
--> Ok (UserRoute (Just "martin"))
Url.Codec.toString [codec] (UserRoute (Just "john")
--> Just "user?name=john"Will fail if there are multiple query parameters with the same key:
Url.Codec.parsePath [codec] "user?name=a&name=b"
--> Err (NeededSingleQueryParameterValueGotMultiple { got = ["a","b"], key = "name" })A repeated integer query parameter.
type Route
= UserListingRoute (List Int)
| ...
codec : Codec Route
codec =
Url.Codec.succeed UserListingRoute isUserListingRoute
|> Url.Codec.s "users"
|> Url.Codec.queryInts "id" getUserListingRouteIds
isUserListingRoute : Route -> Bool
isUserListingRoute route =
case route of
UserListingRoute _ ->
True
_ ->
False
getUserListingRouteIds : Route -> List Int
getUserListingRouteIds route =
case route of
UserListingRoute ids ->
ids
_ ->
[]
Url.Codec.parsePath [codec] "users?id=1"
--> Ok (UserListingRoute [1])
Url.Codec.parsePath [codec] "users?id=1&id=2&id=3"
--> Ok (UserListingRoute [1,2,3])
Url.Codec.parsePath [codec] "users"
--> Ok (UserListingRoute [])
Url.Codec.toString [codec] (UserListingRoute [])
--> Just "user"
Url.Codec.toString [codec] (UserListingRoute [1])
--> Just "user?id=1"
Url.Codec.toString [codec] (UserListingRoute [1,2])
--> Just "user?id=1&id=2"Will fail if given a query parameter with an empty value:
Url.Codec.parsePath [codec] "users?id="
--> Err (NotAllQueryParameterValuesWereInts { got = [ "" ] , key = "id" })Will fail if any of the query parameters has a non-integer value:
Url.Codec.parsePath [codec] "users?id=1&id=hello"
--> Err (NotAllQueryParameterValuesWereInts { got = [ "1", "hello" ] , key = "id" })A repeated string query parameter.
type Route
= UserListingRoute (List String)
| ...
codec : Codec Route
codec =
Url.Codec.succeed UserListingRoute isUserListingRoute
|> Url.Codec.s "users"
|> Url.Codec.queryInts "tags" getUserListingRouteTags
isUserListingRoute : Route -> Bool
isUserListingRoute route =
case route of
UserListingRoute _ ->
True
_ ->
False
getUserListingRouteTags : Route -> List String
getUserListingRouteTags route =
case route of
UserListingRoute tags ->
tags
_ ->
[]
Url.Codec.parsePath [codec] "users?tags=Foo"
--> Ok (UserListingRoute ["Foo"])
Url.Codec.parsePath [codec] "users?tags=Foo&tags=Bar&tags=999"
--> Ok (UserListingRoute ["Foo", "Bar", "999"])
Url.Codec.parsePath [codec] "users"
--> Ok (UserListingRoute [])
Url.Codec.toString [codec] (UserListingRoute [])
--> Just "user"
Url.Codec.toString [codec] (UserListingRoute ["hello"])
--> Just "user?tags=hello
Url.Codec.toString [codec] (UserListingRoute ["hello", "111"])
--> Just "user?tags=hello&tags=111"Will succeed with an empty string if given a query parameter with an empty value:
Url.Codec.parsePath [codec] "users?tags="
--> Ok (UserListingRoute [""])A query flag (parameter without = and a value), like eg. /settings?admin.
type Route
= SettingsRoute { admin : Bool }
| ...
codec : Codec Route
codec =
Url.Codec.succeed (\admin -> SettingsRoute { admin = admin }) isSettingsRoute
|> Url.Codec.s "settings"
|> Url.Codec.queryFlag "admin" getSettingsAdminFlag
isSettingsRoute : Route -> Bool
isSettingsRoute route =
case route of
SettingsRoute _ ->
True
_ ->
False
getSettingsAdminFlag : Route -> Bool
getSettingsAdminFlag route =
case route of
SettingsRoute {admin} ->
admin
_ ->
False
Url.Codec.parsePath [codec] "settings?admin"
--> Ok (SettingsRoute { admin = True })
Url.Codec.parsePath [codec] "settings"
--> Ok (SettingsRoute { admin = False })
Url.Codec.toString [codec] (SettingsRoute { admin = False })
--> Just "settings"
Url.Codec.toString [codec] (SettingsRoute { admin = True })
--> Just "settings?admin"All query flags, like eg. /settings?admin&no-exports.
type Route
= SettingsRoute (List String)
| ...
codec : Codec Route
codec =
Url.Codec.succeed SettingsRoute isSettingsRoute
|> Url.Codec.s "settings"
|> Url.Codec.allQueryFlags getSettingsFlags
isSettingsRoute : Route -> Bool
isSettingsRoute route =
case route of
SettingsRoute _ ->
True
_ ->
False
getSettingsFlags : Route -> List String
getSettingsFlags route =
case route of
SettingsRoute flags ->
flags
_ ->
[]
Url.Codec.parsePath [codec] "settings?admin"
--> Ok (SettingsRoute ["admin"])
Url.Codec.parsePath [codec] "settings"
--> Ok (SettingsRoute [])
Url.Codec.parsePath [codec] "settings?admin&no-exports"
--> Ok (SettingsRoute ["admin", "no-exports"])
Url.Codec.toString [codec] (SettingsRoute [])
--> Just "settings"
Url.Codec.toString [codec] (SettingsRoute ["foo"])
--> Just "settings?foo"
Url.Codec.toString [codec] (SettingsRoute ["foo", "bar"])
--> Just "settings?foo&bar"Fragment part of the URL, eg. /settings#HelloThereWorld.
type Route
= SettingsRoute (Maybe String)
| ...
codec : Codec Route
codec =
Url.Codec.succeed SettingsRoute isSettingsRoute
|> Url.Codec.s "settings"
|> Url.Codec.fragment getSettingsFragment
isSettingsRoute : Route -> Bool
isSettingsRoute route =
case route of
SettingsRoute _ ->
True
_ ->
False
getSettingsFragment : Route -> Maybe String
getSettingsFragment route =
case route of
SettingsRoute fragment ->
fragment
_ ->
[]
Url.Codec.parsePath [codec] "settings#abc"
--> Ok (SettingsRoute (Just "abc"))
Url.Codec.parsePath [codec] "settings"
--> Ok (SettingsRoute Nothing)
Url.Codec.parsePath [codec] "settings#"
--> Ok (SettingsRoute (Just ""))
Url.Codec.toString [codec] (SettingsRoute (Just "abc"))
--> Just "settings#abc"
Url.Codec.toString [codec] (SettingsRoute Nothing)
--> Just "settings"
Url.Codec.toString [codec] (SettingsRoute (Just ""))
--> Just "settings#"
Codec knows both:
Create it with the combinators:
succeed,s,int,stringqueryInt,queryString,queryInts,queryStrings,queryFlag,allQueryFlagsUse it to parse URLs with the functions
parsePathandparseUrl.Use it to build URLs with the function
toString.Leading and trailing slashes don't matter.