{-|
Module : Url.Builder
Description : Build URLs.
License : BSD 3
Maintainer : terezasokol@gmail.com
Stability : experimental
Portability : POSIX
In [the URI spec](https://tools.ietf.org/html/rfc3986), Tim Berners-Lee
says a URL looks like this:
```
https://example.com:8042/over/there?name=ferret#nose
\___/ \______________/\_________/ \_________/ \__/
| | | | |
scheme authority path query fragment
```
This module helps you create these!
-}
module Url.Builder
( absolute, relative, crossOrigin, custom, Root(..)
, QueryParameter, string, int, toQuery
) where
import qualified Url
import qualified Prelude
import qualified Maybe
import qualified String
import qualified List
import qualified Tuple
import qualified Result
import qualified Dict
import qualified Task
import qualified Terminal
import Cherry.Prelude
-- BUILDERS
{-| Create an absolute URL:
absolute [] []
-- "/"
absolute [ "packages", "elm", "core" ] []
-- "/packages/elm/core"
absolute [ "blog", String.fromInt 42 ] []
-- "/blog/42"
absolute [ "products" ] [ string "search" "hat", int "page" 2 ]
-- "/products?search=hat&page=2"
Notice that the URLs start with a slash!
-}
absolute :: List String -> List QueryParameter -> String
absolute pathSegments parameters =
"/" ++ String.join "/" pathSegments ++ toQuery parameters
{-| Create a relative URL:
relative [] []
-- ""
relative [ "elm", "core" ] []
-- "elm/core"
relative [ "blog", String.fromInt 42 ] []
-- "blog/42"
relative [ "products" ] [ string "search" "hat", int "page" 2 ]
-- "products?search=hat&page=2"
Notice that the URLs **do not** start with a slash!
-}
relative :: List String -> List QueryParameter -> String
relative pathSegments parameters =
String.join "/" pathSegments ++ toQuery parameters
{-| Create a cross-origin URL.
crossOrigin "https://example.com" [ "products" ] []
-- "https://example.com/products"
crossOrigin "https://example.com" [] []
-- "https://example.com/"
crossOrigin
"https://example.com:8042"
[ "over", "there" ]
[ string "name" "ferret" ]
-- "https://example.com:8042/over/there?name=ferret"
**Note:** Cross-origin requests are slightly restricted for security.
For example, the [same-origin policy][sop] applies when sending HTTP requests,
so the appropriate `Access-Control-Allow-Origin` header must be enabled on the
*server* to get things working. Read more about the security rules [here][cors].
[sop]: https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy
[cors]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS
-}
crossOrigin :: String -> List String -> List QueryParameter -> String
crossOrigin prePath pathSegments parameters =
prePath ++ "/" ++ String.join "/" pathSegments ++ toQuery parameters
-- CUSTOM BUILDER
{-| Specify whether a [`custom`](#custom) URL is absolute, relative, or
cross-origin.
-}
data Root = Absolute | Relative | CrossOrigin String
{-| Create custom URLs that may have a hash on the end:
custom Absolute
[ "packages", "elm", "core", "latest", "String" ]
[]
(Just "length")
-- "/packages/elm/core/latest/String#length"
custom Relative [ "there" ] [ string "name" "ferret" ] Nothing
-- "there?name=ferret"
custom
(CrossOrigin "https://example.com:8042")
[ "over", "there" ]
[ string "name" "ferret" ]
(Just "nose")
-- "https://example.com:8042/over/there?name=ferret#nose"
-}
custom :: Root -> List String -> List QueryParameter -> Maybe String -> String
custom root pathSegments parameters maybeFragment =
let
fragmentless =
rootToPrePath root ++ String.join "/" pathSegments ++ toQuery parameters
in
case maybeFragment of
Nothing ->
fragmentless
Just fragment ->
fragmentless ++ "#" ++ fragment
rootToPrePath :: Root -> String
rootToPrePath root =
case root of
Absolute ->
"/"
Relative ->
""
CrossOrigin prePath ->
prePath ++ "/"
-- QUERY PARAMETERS
{-| Represents query parameter. Builder functions like `absolute` percent-encode
all the query parameters they get, so you do not need to worry about it!
-}
data QueryParameter =
QueryParameter String String
{-| Create a percent-encoded query parameter.
absolute ["products"] [ string "search" "hat" ]
-- "/products?search=hat"
absolute ["products"] [ string "search" "coffee table" ]
-- "/products?search=coffee%20table"
-}
string :: String -> String -> QueryParameter
string key value =
QueryParameter (Url.percentEncode key) (Url.percentEncode value)
{-| Create a percent-encoded query parameter.
absolute ["products"] [ string "search" "hat", int "page" 2 ]
-- "/products?search=hat&page=2"
Writing `int key n` is the same as writing `string key (String.fromInt n)`.
So this is just a convenience function, making your code a bit shorter!
-}
int :: String -> Int -> QueryParameter
int key value =
QueryParameter (Url.percentEncode key) (String.fromInt value)
{-| Convert a list of query parameters to a percent-encoded query. This
function is used by `absolute`, `relative`, etc.
toQuery [ string "search" "hat" ]
-- "?search=hat"
toQuery [ string "search" "coffee table" ]
-- "?search=coffee%20table"
toQuery [ string "search" "hat", int "page" 2 ]
-- "?search=hat&page=2"
toQuery []
-- ""
-}
toQuery :: List QueryParameter -> String
toQuery parameters =
case parameters of
[] ->
""
_ ->
"?" ++ String.join "&" (List.map toQueryPair parameters)
toQueryPair :: QueryParameter -> String
toQueryPair (QueryParameter key value) =
key ++ "=" ++ value