packages feed

hyperbole-0.7.0: src/Web/Hyperbole/Data/URI.hs

module Web.Hyperbole.Data.URI
  ( -- * URI
    URI (..)
  , URIAuth (..)
  , uri

    -- ** Path
  , Path (..)
  , Segment
  , path
  , parseURIReference
  , parseURI
  , pathUri
  , uriToText
  , pathToText

    -- ** Query String
  , queryString
  , parseQuery
  , queryInsert
  , renderQuery
  , Query
  , QueryItem
  , (./.)
  , (.?.)
  , cleanSegment
  , Endpoint (..)
  )
where

import Data.ByteString (ByteString)
import Data.String (IsString (..))
import Data.String.Conversions (cs)
import Data.Text (Text)
import Data.Text qualified as T
import GHC.Exts (IsList (..))
import Network.HTTP.Types (Query, QueryItem, parseQuery, renderQuery)
import Network.URI (URI (..), URIAuth (..), parseURI, parseURIReference, uriToString)
import Network.URI qualified as Network
import Network.URI.Static (uri)
import System.FilePath (normalise, (</>))


-- Constructors ------------------------------------------
-- see `uri` for static URIs

-- Operators -----------------------------------------------

-- maybe lets not care about leading slashes at all until rendering
(./.) :: URI -> Path -> URI
u ./. p =
  u{Network.uriPath = addLeadingSlash $ normalise (u.uriPath </> newPath)}
 where
  newPath = cs $ pathToText False p

  addLeadingSlash pth =
    case take 1 pth of
      "/" -> pth
      _ -> '/' : pth


infixl 5 ./.


(.?.) :: URI -> QueryItem -> URI
u .?. (k, mv) = u{uriQuery = queryInsert k mv u.uriQuery}


-- Query ---------------------------------------------------

type QueryString = String


queryInsert :: ByteString -> Maybe ByteString -> QueryString -> QueryString
queryInsert k mv s =
  queryString $ parseQuery (cs s) <> [(k, mv)]


queryString :: [(ByteString, Maybe ByteString)] -> QueryString
queryString = cs . renderQuery True


-- Path -----------------------------------------------------

newtype Path = Path {segments :: [Segment]}
  deriving (Show, Eq)
instance IsList Path where
  type Item Path = Segment
  fromList = Path . filter (not . T.null)
  toList p = p.segments
instance IsString Path where
  fromString = path . cs


type Segment = Text


cleanSegment :: Segment -> Segment
cleanSegment = T.dropWhileEnd (== '/') . T.dropWhile (== '/')


path :: Text -> Path
path p =
  fromList $ T.splitOn "/" $ T.dropWhile (== '/') p


pathUri :: Path -> URI
pathUri p =
  URI
    { uriPath = cs $ pathToText True p
    , uriScheme = mempty
    , uriAuthority = Nothing
    , uriQuery = mempty
    , uriFragment = mempty
    }


uriToText :: URI -> Text
uriToText u = cs $ uriToString id u ""


pathToText :: Bool -> Path -> Text
pathToText isRoot p =
  pathPrefix <> T.intercalate "/" (fmap cleanSegment p.segments)
 where
  pathPrefix :: Text
  pathPrefix =
    if isRoot then "/" else ""


-- | A URI with a phantom type to distinguish different endpoints
newtype Endpoint a = Endpoint {uri :: URI}