2015-05-27 22:25:08 +02:00
|
|
|
{-# LANGUAGE DeriveDataTypeable #-}
|
|
|
|
{-# LANGUAGE DeriveGeneric #-}
|
2018-09-18 12:46:45 +02:00
|
|
|
{-# LANGUAGE DeriveLift #-}
|
2015-05-27 22:25:08 +02:00
|
|
|
{-# LANGUAGE ViewPatterns #-}
|
2019-02-18 18:08:13 +01:00
|
|
|
module Servant.Client.Core.BaseUrl (
|
|
|
|
BaseUrl (..),
|
|
|
|
Scheme (..),
|
|
|
|
showBaseUrl,
|
|
|
|
parseBaseUrl,
|
|
|
|
InvalidBaseUrlException (..),
|
|
|
|
) where
|
2014-11-27 18:28:01 +01:00
|
|
|
|
2019-02-03 17:18:55 +01:00
|
|
|
import Control.DeepSeq
|
|
|
|
(NFData (..))
|
2018-06-30 21:17:08 +02:00
|
|
|
import Control.Monad.Catch
|
|
|
|
(Exception, MonadThrow, throwM)
|
2018-09-18 12:46:45 +02:00
|
|
|
import Data.Aeson
|
|
|
|
(FromJSON (..), FromJSONKey (..), ToJSON (..), ToJSONKey (..))
|
|
|
|
import Data.Aeson.Types
|
|
|
|
(FromJSONKeyFunction (..), contramapToJSONKeyFunction,
|
|
|
|
withText)
|
|
|
|
import Data.Data
|
|
|
|
(Data)
|
2015-08-17 23:56:29 +02:00
|
|
|
import Data.List
|
2018-09-18 12:46:45 +02:00
|
|
|
import qualified Data.Text as T
|
2015-08-17 23:56:29 +02:00
|
|
|
import GHC.Generics
|
2018-09-18 12:46:45 +02:00
|
|
|
import Language.Haskell.TH.Syntax
|
|
|
|
(Lift)
|
|
|
|
import Network.URI hiding
|
2018-06-30 21:17:08 +02:00
|
|
|
(path)
|
2015-08-17 23:56:29 +02:00
|
|
|
import Safe
|
|
|
|
import Text.Read
|
2014-11-27 18:28:01 +01:00
|
|
|
|
|
|
|
-- | URI scheme to use
|
|
|
|
data Scheme =
|
|
|
|
Http -- ^ http://
|
|
|
|
| Https -- ^ https://
|
2018-09-18 12:46:45 +02:00
|
|
|
deriving (Show, Eq, Ord, Generic, Lift, Data)
|
2014-11-27 18:28:01 +01:00
|
|
|
|
|
|
|
-- | Simple data type to represent the target of HTTP requests
|
|
|
|
-- for servant's automatically-generated clients.
|
|
|
|
data BaseUrl = BaseUrl
|
2018-09-18 12:46:45 +02:00
|
|
|
{ baseUrlScheme :: Scheme -- ^ URI scheme to use
|
2015-08-17 23:56:29 +02:00
|
|
|
, baseUrlHost :: String -- ^ host (eg "haskell.org")
|
|
|
|
, baseUrlPort :: Int -- ^ port (eg 80)
|
2015-08-25 04:26:15 +02:00
|
|
|
, baseUrlPath :: String -- ^ path (eg "/a/b/c")
|
2018-09-18 12:46:45 +02:00
|
|
|
} deriving (Show, Ord, Generic, Lift, Data)
|
|
|
|
-- TODO: Ord is more precise than Eq
|
|
|
|
-- TODO: Add Hashable instance?
|
2019-02-03 17:18:55 +01:00
|
|
|
--
|
|
|
|
instance NFData BaseUrl where
|
|
|
|
rnf (BaseUrl a b c d) = a `seq` rnf b `seq` rnf c `seq` rnf d
|
2015-10-07 21:07:07 +02:00
|
|
|
|
|
|
|
instance Eq BaseUrl where
|
|
|
|
BaseUrl a b c path == BaseUrl a' b' c' path'
|
|
|
|
= a == a' && b == b' && c == c' && s path == s path'
|
|
|
|
where s ('/':x) = x
|
|
|
|
s x = x
|
2014-11-27 18:28:01 +01:00
|
|
|
|
2018-09-18 12:46:45 +02:00
|
|
|
-- | >>> traverse_ (LBS8.putStrLn . encode) $ parseBaseUrl "api.example.com"
|
|
|
|
-- "http://api.example.com"
|
|
|
|
instance ToJSON BaseUrl where
|
|
|
|
toJSON = toJSON . showBaseUrl
|
|
|
|
toEncoding = toEncoding . showBaseUrl
|
|
|
|
|
|
|
|
-- | >>> parseBaseUrl "api.example.com" >>= decode . encode :: Maybe BaseUrl
|
|
|
|
-- Just (BaseUrl {baseUrlScheme = Http, baseUrlHost = "api.example.com", baseUrlPort = 80, baseUrlPath = ""})
|
|
|
|
instance FromJSON BaseUrl where
|
|
|
|
parseJSON = withText "BaseUrl" $ \t -> case parseBaseUrl (T.unpack t) of
|
|
|
|
Just u -> return u
|
|
|
|
Nothing -> fail $ "Invalid base url: " ++ T.unpack t
|
|
|
|
|
|
|
|
-- | >>> :{
|
|
|
|
-- traverse_ (LBS8.putStrLn . encode) $ do
|
|
|
|
-- u1 <- parseBaseUrl "api.example.com"
|
|
|
|
-- u2 <- parseBaseUrl "example.com"
|
|
|
|
-- return $ Map.fromList [(u1, 'x'), (u2, 'y')]
|
|
|
|
-- :}
|
|
|
|
-- {"http://api.example.com":"x","http://example.com":"y"}
|
|
|
|
instance ToJSONKey BaseUrl where
|
|
|
|
toJSONKey = contramapToJSONKeyFunction showBaseUrl toJSONKey
|
|
|
|
|
|
|
|
instance FromJSONKey BaseUrl where
|
|
|
|
fromJSONKey = FromJSONKeyTextParser $ \t -> case parseBaseUrl (T.unpack t) of
|
|
|
|
Just u -> return u
|
|
|
|
Nothing -> fail $ "Invalid base url: " ++ T.unpack t
|
|
|
|
|
|
|
|
-- | >>> showBaseUrl <$> parseBaseUrl "api.example.com"
|
|
|
|
-- "http://api.example.com"
|
2014-11-27 18:28:01 +01:00
|
|
|
showBaseUrl :: BaseUrl -> String
|
2015-08-25 04:26:15 +02:00
|
|
|
showBaseUrl (BaseUrl urlscheme host port path) =
|
2015-10-07 21:07:07 +02:00
|
|
|
schemeString ++ "//" ++ host ++ (portString </> path)
|
2014-11-27 18:28:01 +01:00
|
|
|
where
|
2015-10-07 21:07:07 +02:00
|
|
|
a </> b = if "/" `isPrefixOf` b || null b then a ++ b else a ++ '/':b
|
2014-11-27 18:28:01 +01:00
|
|
|
schemeString = case urlscheme of
|
|
|
|
Http -> "http:"
|
|
|
|
Https -> "https:"
|
|
|
|
portString = case (urlscheme, port) of
|
|
|
|
(Http, 80) -> ""
|
|
|
|
(Https, 443) -> ""
|
|
|
|
_ -> ":" ++ show port
|
|
|
|
|
2018-09-18 12:46:45 +02:00
|
|
|
newtype InvalidBaseUrlException = InvalidBaseUrlException String deriving (Show)
|
2015-05-27 15:29:56 +02:00
|
|
|
instance Exception InvalidBaseUrlException
|
|
|
|
|
2018-09-18 12:46:45 +02:00
|
|
|
-- |
|
|
|
|
--
|
|
|
|
-- >>> parseBaseUrl "api.example.com"
|
|
|
|
-- BaseUrl {baseUrlScheme = Http, baseUrlHost = "api.example.com", baseUrlPort = 80, baseUrlPath = ""}
|
|
|
|
--
|
|
|
|
-- /Note:/ trailing slash is removed
|
|
|
|
--
|
|
|
|
-- >>> parseBaseUrl "api.example.com/"
|
|
|
|
-- BaseUrl {baseUrlScheme = Http, baseUrlHost = "api.example.com", baseUrlPort = 80, baseUrlPath = ""}
|
|
|
|
--
|
|
|
|
-- >>> parseBaseUrl "api.example.com/dir/"
|
|
|
|
-- BaseUrl {baseUrlScheme = Http, baseUrlHost = "api.example.com", baseUrlPort = 80, baseUrlPath = "/dir"}
|
|
|
|
--
|
2015-05-20 21:54:10 +02:00
|
|
|
parseBaseUrl :: MonadThrow m => String -> m BaseUrl
|
2014-11-27 18:28:01 +01:00
|
|
|
parseBaseUrl s = case parseURI (removeTrailingSlash s) of
|
|
|
|
-- This is a rather hacky implementation and should be replaced with something
|
|
|
|
-- implemented in attoparsec (which is already a dependency anyhow (via aeson)).
|
2015-08-25 04:26:15 +02:00
|
|
|
Just (URI "http:" (Just (URIAuth "" host (':' : (readMaybe -> Just port)))) path "" "") ->
|
|
|
|
return (BaseUrl Http host port path)
|
|
|
|
Just (URI "http:" (Just (URIAuth "" host "")) path "" "") ->
|
|
|
|
return (BaseUrl Http host 80 path)
|
|
|
|
Just (URI "https:" (Just (URIAuth "" host (':' : (readMaybe -> Just port)))) path "" "") ->
|
|
|
|
return (BaseUrl Https host port path)
|
|
|
|
Just (URI "https:" (Just (URIAuth "" host "")) path "" "") ->
|
|
|
|
return (BaseUrl Https host 443 path)
|
2014-11-27 18:28:01 +01:00
|
|
|
_ -> if "://" `isInfixOf` s
|
2015-05-27 15:29:56 +02:00
|
|
|
then throwM (InvalidBaseUrlException $ "Invalid base URL: " ++ s)
|
2014-11-27 18:28:01 +01:00
|
|
|
else parseBaseUrl ("http://" ++ s)
|
|
|
|
where
|
|
|
|
removeTrailingSlash str = case lastMay str of
|
|
|
|
Just '/' -> init str
|
|
|
|
_ -> str
|
2018-09-18 12:46:45 +02:00
|
|
|
|
|
|
|
-- $setup
|
|
|
|
--
|
|
|
|
-- >>> import Data.Aeson
|
|
|
|
-- >>> import Data.Foldable (traverse_)
|
|
|
|
-- >>> import qualified Data.ByteString.Lazy.Char8 as LBS8
|
|
|
|
-- >>> import qualified Data.Map.Strict as Map
|