packages feed

cachix-1.7: src/Cachix/Client/URI.hs

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE QuasiQuotes #-}

module Cachix.Client.URI
  ( URI,
    fromURIRef,
    getScheme,
    getHostname,
    appendSubdomain,
    getPortFor,
    getPath,
    getQueryParam,
    requiresSSL,
    parseURI,
    serialize,
    toByteString,
    getBaseUrl,
    defaultCachixURI,
    defaultCachixBaseUrl,
    UBS.Host (..),
    UBS.Scheme (..),
    UBS.Port (..),
  )
where

import Control.Monad (fail)
import qualified Data.Aeson as Aeson
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as Char8
import Data.Either.Validation (Validation (Failure, Success))
import qualified Data.Map.Strict as Map
import Data.Maybe (fromJust)
import Data.String
import qualified Dhall
import qualified Dhall.Core
import Protolude hiding (toS)
import Protolude.Conv
import Servant.Client
import qualified URI.ByteString as UBS
import qualified URI.ByteString.QQ as UBS

-- Default URIs

defaultCachixURI :: URI
defaultCachixURI = fromURIRef [UBS.uri|https://cachix.org|]

defaultCachixBaseUrl :: BaseUrl
defaultCachixBaseUrl = getBaseUrl defaultCachixURI

newtype URI = URI {getUri :: UBS.URIRef UBS.Absolute}
  deriving stock (Eq, Show)

fromURIRef :: UBS.URIRef UBS.Absolute -> URI
fromURIRef = URI

toByteString :: URI -> ByteString
toByteString = UBS.serializeURIRef' . getUri

getScheme :: URI -> UBS.Scheme
getScheme = UBS.uriScheme . getUri

getHostname :: URI -> UBS.Host
getHostname = UBS.authorityHost . fromJust . UBS.uriAuthority . getUri

-- TODO: lenses?
appendSubdomain :: Text -> URI -> URI
appendSubdomain domain uri =
  let UBS.URI uScheme uAuthority uPath uQuery uFragment = getUri uri
      UBS.Authority aUserInfo aHost aPort = fromJust uAuthority
      newHost = UBS.Host $ toS domain <> "." <> UBS.hostBS aHost
   in URI $
        UBS.URI
          uScheme
          (Just (UBS.Authority aUserInfo newHost aPort))
          uPath
          uQuery
          uFragment

getPortFor :: UBS.Scheme -> Maybe UBS.Port
getPortFor scheme = Map.lookup scheme UBS.httpDefaultPorts

getPath :: URI -> ByteString
getPath = UBS.uriPath . getUri

getQueryParam :: URI -> ByteString -> Maybe ByteString
getQueryParam uri param =
  let query = UBS.uriQuery $ getUri uri
   in map snd $ head $ filter (\(key, _) -> key == param) $ UBS.queryPairs query

requiresSSL :: UBS.Scheme -> Bool
requiresSSL (UBS.Scheme "https") = True
requiresSSL _ = False

parseURI :: ByteString -> Either UBS.URIParseError URI
parseURI bs = fromURIRef <$> UBS.parseURI UBS.strictURIParserOptions bs

serialize :: (StringConv BS.ByteString s) => URI -> s
serialize = toS . UBS.serializeURIRef' . getUri

instance IsString URI where
  fromString = either (panic . show) identity . parseURI . toS

instance Aeson.ToJSON URI where
  toJSON (URI uri) = Aeson.String . toS . UBS.serializeURIRef' $ uri

instance Aeson.FromJSON URI where
  parseJSON = Aeson.withText "URI" $ \text ->
    either (fail . show) (return . URI) $
      UBS.parseURI UBS.strictURIParserOptions (toS text)

instance Dhall.FromDhall URI where
  autoWith opts =
    Dhall.Decoder extract expected
    where
      textDecoder :: Dhall.Decoder Text
      textDecoder = Dhall.autoWith opts

      extract expression =
        case Dhall.extract textDecoder expression of
          Success x -> case UBS.parseURI UBS.strictURIParserOptions (toS x) of
            Left exception -> Dhall.extractError (show exception)
            Right path -> Success (fromURIRef path)
          Failure e -> Failure e

      expected = Dhall.expected textDecoder

instance Dhall.ToDhall URI where
  injectWith opts = Dhall.Encoder embed declared
    where
      textEncoder :: Dhall.Encoder Text
      textEncoder = Dhall.injectWith opts

      embed (URI uri) = Dhall.embed textEncoder $ toS (UBS.serializeURIRef' uri)

      declared = Dhall.Core.Text

-- | Partial function from URI to BaseUrl
--
-- TODO: We should error out during the parsing stage with a nice error.
-- TODO: make getBaseUrl internal
getBaseUrl :: URI -> BaseUrl
getBaseUrl (URI uriref) =
  case UBS.uriAuthority uriref of
    Nothing -> panic "missing host in url"
    Just authority ->
      BaseUrl scheme hostname port path
      where
        scheme :: Scheme
        scheme = case UBS.uriScheme uriref of
          UBS.Scheme "http" -> Http
          UBS.Scheme "https" -> Https
          _ -> panic "uri can only be http/https"

        hostname = toS $ UBS.hostBS (UBS.authorityHost authority)

        port :: Int
        port = maybe defaultPort UBS.portNumber $ UBS.authorityPort authority

        defaultPort :: Int
        defaultPort = case scheme of
          Http -> 80
          Https -> 443

        path = toS $ removeTrailingSlash (UBS.uriPath uriref)

        -- Servant expects the trailing slash to be removed
        -- https://hackage.haskell.org/package/servant-client-core-0.19/docs/Servant-Client-Core.html#v:parseBaseUrl
        removeTrailingSlash :: ByteString -> ByteString
        removeTrailingSlash "" = ""
        removeTrailingSlash str = case Char8.last str of
          '/' -> Char8.init str
          _ -> str