@@ -1,14 +1,24 @@
1{-# LANGUAGE DeriveDataTypeable #-}
1{-# LANGUAGE DeriveDataTypeable #-}
2{-# LANGUAGE DeriveGeneric #-}
2{-# LANGUAGE DeriveGeneric #-}
3+{-# LANGUAGE DeriveLift #-}
3{-# LANGUAGE ViewPatterns #-}
4{-# LANGUAGE ViewPatterns #-}
4module Servant.Client.Core.Internal.BaseUrl where
5module Servant.Client.Core.Internal.BaseUrl where
6import Control.Monad.Catch
7import Control.Monad.Catch
7 (Exception, MonadThrow, throwM)
8 (Exception, MonadThrow, throwM)
10+ (FromJSON (..), FromJSONKey (..), ToJSON (..), ToJSONKey (..))
11+import Data.Aeson.Types
12+ (FromJSONKeyFunction (..), contramapToJSONKeyFunction,
8import Data.List
16import Data.List
9−import Data.Typeable
17+import qualified Data.Text as T
10import GHC.Generics
18import GHC.Generics
19+import Language.Haskell.TH.Syntax
11−import Network.URI hiding
21+import Network.URI hiding
13import Safe
23import Safe
14import Text.Read
24import Text.Read
@@ -17,16 +27,18 @@ import Text.Read
17data Scheme =
27data Scheme =
18 Http -- ^ http://
28 Http -- ^ http://
19 | Https -- ^ https://
29 | Https -- ^ https://
20− deriving (Show, Eq, Ord, Generic)
30+ deriving (Show, Eq, Ord, Generic, Lift, Data)
22-- | Simple data type to represent the target of HTTP requests
32-- | Simple data type to represent the target of HTTP requests
23-- for servant's automatically-generated clients.
33-- for servant's automatically-generated clients.
24data BaseUrl = BaseUrl
34data BaseUrl = BaseUrl
25− { baseUrlScheme :: Scheme -- ^ URI scheme to use
35+ { baseUrlScheme :: Scheme -- ^ URI scheme to use
26 , baseUrlHost :: String -- ^ host (eg "haskell.org")
36 , baseUrlHost :: String -- ^ host (eg "haskell.org")
27 , baseUrlPort :: Int -- ^ port (eg 80)
37 , baseUrlPort :: Int -- ^ port (eg 80)
28 , baseUrlPath :: String -- ^ path (eg "/a/b/c")
38 , baseUrlPath :: String -- ^ path (eg "/a/b/c")
29− } deriving (Show, Ord, Generic)
39+ } deriving (Show, Ord, Generic, Lift, Data)
40+-- TODO: Ord is more precise than Eq
41+-- TODO: Add Hashable instance?
31instance Eq BaseUrl where
43instance Eq BaseUrl where
32 BaseUrl a b c path == BaseUrl a' b' c' path'
44 BaseUrl a b c path == BaseUrl a' b' c' path'
@@ -34,6 +46,36 @@ instance Eq BaseUrl where
34 where s ('/':x) = x
46 where s ('/':x) = x
49+-- | >>> traverse_ (LBS8.putStrLn . encode) $ parseBaseUrl "api.example.com"
50+-- "http://api.example.com"
51+instance ToJSON BaseUrl where
52+ toJSON = toJSON . showBaseUrl
53+ toEncoding = toEncoding . showBaseUrl
55+-- | >>> parseBaseUrl "api.example.com" >>= decode . encode :: Maybe BaseUrl
56+-- Just (BaseUrl {baseUrlScheme = Http, baseUrlHost = "api.example.com", baseUrlPort = 80, baseUrlPath = ""})
57+instance FromJSON BaseUrl where
58+ parseJSON = withText "BaseUrl" $ \t -> case parseBaseUrl (T.unpack t) of
60+ Nothing -> fail $ "Invalid base url: " ++ T.unpack t
63+-- traverse_ (LBS8.putStrLn . encode) $ do
64+-- u1 <- parseBaseUrl "api.example.com"
65+-- u2 <- parseBaseUrl "example.com"
66+-- return $ Map.fromList [(u1, 'x'), (u2, 'y')]
68+-- {"http://api.example.com":"x","http://example.com":"y"}
69+instance ToJSONKey BaseUrl where
70+ toJSONKey = contramapToJSONKeyFunction showBaseUrl toJSONKey
72+instance FromJSONKey BaseUrl where
73+ fromJSONKey = FromJSONKeyTextParser $ \t -> case parseBaseUrl (T.unpack t) of
75+ Nothing -> fail $ "Invalid base url: " ++ T.unpack t
77+-- | >>> showBaseUrl <$> parseBaseUrl "api.example.com"
78+-- "http://api.example.com"
37showBaseUrl :: BaseUrl -> String
79showBaseUrl :: BaseUrl -> String
38showBaseUrl (BaseUrl urlscheme host port path) =
80showBaseUrl (BaseUrl urlscheme host port path) =
39 schemeString ++ "//" ++ host ++ (portString </> path)
81 schemeString ++ "//" ++ host ++ (portString </> path)
@@ -47,9 +89,22 @@ showBaseUrl (BaseUrl urlscheme host port path) =
47 (Https, 443) -> ""
89 (Https, 443) -> ""
48 _ -> ":" ++ show port
90 _ -> ":" ++ show port
50−data InvalidBaseUrlException = InvalidBaseUrlException String deriving (Show, Typeable)
92+newtype InvalidBaseUrlException = InvalidBaseUrlException String deriving (Show)
51instance Exception InvalidBaseUrlException
93instance Exception InvalidBaseUrlException
97+-- >>> parseBaseUrl "api.example.com"
98+-- BaseUrl {baseUrlScheme = Http, baseUrlHost = "api.example.com", baseUrlPort = 80, baseUrlPath = ""}
100+-- /Note:/ trailing slash is removed
102+-- >>> parseBaseUrl "api.example.com/"
103+-- BaseUrl {baseUrlScheme = Http, baseUrlHost = "api.example.com", baseUrlPort = 80, baseUrlPath = ""}
105+-- >>> parseBaseUrl "api.example.com/dir/"
106+-- BaseUrl {baseUrlScheme = Http, baseUrlHost = "api.example.com", baseUrlPort = 80, baseUrlPath = "/dir"}
53parseBaseUrl :: MonadThrow m => String -> m BaseUrl
108parseBaseUrl :: MonadThrow m => String -> m BaseUrl
54parseBaseUrl s = case parseURI (removeTrailingSlash s) of
109parseBaseUrl s = case parseURI (removeTrailingSlash s) of
55 -- This is a rather hacky implementation and should be replaced with something
110 -- This is a rather hacky implementation and should be replaced with something
@@ -69,3 +124,10 @@ parseBaseUrl s = case parseURI (removeTrailingSlash s) of
69 removeTrailingSlash str = case lastMay str of
124 removeTrailingSlash str = case lastMay str of
70 Just '/' -> init str
125 Just '/' -> init str
130+-- >>> import Data.Aeson
131+-- >>> import Data.Foldable (traverse_)
132+-- >>> import qualified Data.ByteString.Lazy.Char8 as LBS8
133+-- >>> import qualified Data.Map.Strict as Map