Janiczek/elm-url-codec/1.0.0

Url.Codec

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.

type alias Codec target =
CodecInProgress target target

Codec knows both:

  • how to parse an URL string into Elm data
  • how to build an URL from Elm data.

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.

type CodecInProgress target parseResult

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 :)

URL parsing

= SegmentMismatch
{ expected : String
, available : String
}
| SegmentNotAvailable
| WasNotInt String
| DidNotConsumeEverything (List String)
| NeededSingleQueryParameterValueGotMultiple
{ key : String
, got : List String
}
| NotAllQueryParameterValuesWereInts
{ key : String
, got : List String
}
| NoCodecs

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.

URL building

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

Combinators

succeed : parseResult -> (target -> 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 Int)
-> CodecInProgress target (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"
(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"

Query parameters

String
-> (target -> Maybe Int)
-> CodecInProgress target (Maybe 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)
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" })
String
-> (target -> List Int)
-> CodecInProgress target (List 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" })
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 [""])
String
-> (target -> Bool)
-> CodecInProgress target (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"
(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#"