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 target target
Codec knows both:
Create it with the combinators:
Use it to parse URLs with the functions parsePath
and parseUrl
.
Use it to build URLs with the function toString
.
Leading and trailing slashes don't matter.
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 isComment
At 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 getCommentPageNumber
Now 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
_ ->
Nothing
Sidenote: 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.
parsePath : List (Codec parseResult) -> String -> Result ParseError parseResult
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 NoCodecs
parseUrl : List (Codec parseResult) -> Url -> Result ParseError parseResult
A variant of parsePath
that accepts an
Url
.
toString : List (Codec target) -> target -> Maybe String
Convert 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)
--> Nothing
succeed : parseResult -> (target -> Basics.Bool) -> CodecInProgress target parseResult
A 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
_ ->
False
You'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 ""
s : String -> CodecInProgress target parseResult -> CodecInProgress target parseResult
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"
int : (target -> Maybe Basics.Int) -> CodecInProgress target (Basics.Int -> parseResult) -> CodecInProgress target parseResult
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"
string : (target -> Maybe String) -> CodecInProgress target (String -> parseResult) -> CodecInProgress target parseResult
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"
queryInt : String -> (target -> Maybe Basics.Int) -> CodecInProgress target (Maybe Basics.Int -> parseResult) -> CodecInProgress target parseResult
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)
queryString : String -> (target -> Maybe String) -> CodecInProgress target (Maybe String -> parseResult) -> CodecInProgress target parseResult
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" })
queryInts : String -> (target -> List Basics.Int) -> CodecInProgress target (List Basics.Int -> parseResult) -> CodecInProgress target parseResult
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" })
queryStrings : String -> (target -> List String) -> CodecInProgress target (List String -> parseResult) -> CodecInProgress target parseResult
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 [""])
queryFlag : String -> (target -> Basics.Bool) -> CodecInProgress target (Basics.Bool -> parseResult) -> CodecInProgress target parseResult
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"
allQueryFlags : (target -> List String) -> CodecInProgress target (List String -> parseResult) -> CodecInProgress target parseResult
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 : (target -> Maybe String) -> CodecInProgress target (Maybe String -> parseResult) -> CodecInProgress target parseResult
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#"