packages feed

cherry-core-alpha-0.1.0.0: src/Url/Builder.hs

{-|

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