{-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE KindSignatures #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TupleSections #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeOperators #-} {-# LANGUAGE UndecidableInstances #-} -- | -- Module : Network.Google.Types -- Copyright : (c) 2015-2016 Brendan Hay -- License : Mozilla Public License, v. 2.0. -- Maintainer : Brendan Hay -- Stability : provisional -- Portability : non-portable (GHC extensions) -- module Network.Google.Types where import Control.Exception.Lens (exception) import Control.Lens import Control.Monad.Catch import Control.Monad.Trans.Resource import Data.Aeson import Data.ByteString (ByteString) import Data.Coerce import Data.Conduit import Data.Data import Data.DList (DList) import Data.Foldable (foldMap, foldl') import Data.String import Data.Semigroup import Data.Text (Text) import Data.Text.Lazy.Builder (Builder) import GHC.Generics import GHC.TypeLits import Network.HTTP.Client (HttpException, RequestBody (..)) import Network.HTTP.Media hiding (Accept) import Network.HTTP.Types hiding (Header) import Servant.API hiding (Stream) import qualified Data.ByteString.Char8 as BS8 import qualified Data.ByteString.Lazy as LBS import qualified Data.CaseInsensitive as CI import qualified Data.Conduit.Combinators as Conduit import qualified Data.DList as DList import qualified Data.Text.Encoding as Text import qualified Data.Text.Lazy.Builder as Build import qualified Network.HTTP.Types as HTTP data AltJSON = AltJSON deriving (Eq, Ord, Show, Read, Generic, Typeable) data AltMedia = AltMedia deriving (Eq, Ord, Show, Read, Generic, Typeable) data Multipart = Multipart deriving (Eq, Ord, Show, Read, Generic, Typeable) instance ToHttpApiData AltJSON where toQueryParam = const "json" instance ToHttpApiData AltMedia where toQueryParam = const "media" instance ToHttpApiData Multipart where toQueryParam = const "multipart" -- | An OAuth2 scope. newtype OAuthScope = OAuthScope Text deriving ( Eq , Ord , Show , Read , IsString , Generic , Typeable , FromHttpApiData , ToHttpApiData , FromJSON , ToJSON ) -- | An OAuth2 access token. newtype AccessToken = AccessToken Text deriving ( Eq , Ord , Show , Read , IsString , Generic , Typeable , FromHttpApiData , ToHttpApiData , FromJSON , ToJSON ) -- | An OAuth2 refresh token. newtype RefreshToken = RefreshToken Text deriving ( Eq , Ord , Show , Read , IsString , Generic , Typeable , FromHttpApiData , ToHttpApiData , FromJSON , ToJSON ) -- | A client identifier. newtype ClientId = ClientId Text deriving ( Eq , Ord , Show , Read , IsString , Generic , Typeable , FromHttpApiData , ToHttpApiData , FromJSON , ToJSON ) -- | A service identifier. newtype ServiceId = ServiceId Text deriving ( Eq , Ord , Show , Read , IsString , Generic , Typeable , FromHttpApiData , ToHttpApiData , FromJSON , ToJSON ) -- | An opaque client secret. newtype GSecret = GSecret Text deriving ( Eq , Ord , Read , IsString , Generic , Typeable , FromHttpApiData , ToHttpApiData , FromJSON , ToJSON ) instance Show GSecret where show = const "*****" newtype MediaDownload a = MediaDownload a data MediaUpload a = MediaUpload a GBody _Coerce :: (Coercible a b, Coercible b a) => Iso' a b _Coerce = iso coerce coerce -- | Invalid Iso, exists for ease of composition with the current 'Lens . Iso' -- chaining to hide internal types from the user. _Default :: Monoid a => Iso' (Maybe a) a _Default = iso f Just where f (Just x) = x f Nothing = mempty type Stream = ConduitM () ByteString (ResourceT IO) () data Error = TransportError HttpException | SerializeError SerializeError | ServiceError ServiceError deriving (Show, Typeable) instance Exception Error data SerializeError = SerializeError' { _serializeId :: !ServiceId , _serializeHeaders :: [HTTP.Header] , _serializeStatus :: !Status , _serializeMessage :: !String , _serializeBody :: !(Maybe LBS.ByteString) } deriving (Eq, Show, Typeable) data ServiceError = ServiceError' { _serviceId :: !ServiceId , _serviceStatus :: !Status , _serviceHeaders :: ![HTTP.Header] , _serviceBody :: !(Maybe LBS.ByteString) } deriving (Eq, Show, Typeable) class AsError a where -- | A general Amazonka error. _Error :: Prism' a Error {-# MINIMAL _Error #-} -- | An error occured while communicating over HTTP with a remote service. _TransportError :: Prism' a HttpException -- | A serialisation error occured when attempting to deserialise a response. _SerializeError :: Prism' a SerializeError -- | A service specific error returned by the remote service. _ServiceError :: Prism' a ServiceError _TransportError = _Error . _TransportError _SerializeError = _Error . _SerializeError _ServiceError = _Error . _ServiceError instance AsError SomeException where _Error = exception instance AsError Error where _Error = id _TransportError = prism TransportError $ \case TransportError e -> Right e x -> Left x _SerializeError = prism SerializeError $ \case SerializeError e -> Right e x -> Left x _ServiceError = prism ServiceError $ \case ServiceError e -> Right e x -> Left x data ServiceConfig = ServiceConfig { _svcId :: !ServiceId , _svcHost :: !ByteString , _svcPath :: !Builder , _svcPort :: !Int , _svcSecure :: !Bool , _svcTimeout :: !(Maybe Seconds) } defaultService :: ServiceId -> ByteString -> ServiceConfig defaultService i h = ServiceConfig { _svcId = i , _svcHost = h , _svcPath = mempty , _svcPort = 443 , _svcSecure = True , _svcTimeout = Just 70 } -- | The remote host name, used for both the IP address to connect to and the -- host request header. serviceHost :: Lens' ServiceConfig ByteString serviceHost = lens _svcHost (\s a -> s { _svcHost = a }) -- | The remote port to connect to. -- -- Defaults to @443@. servicePort :: Lens' ServiceConfig Int servicePort = lens _svcPort (\s a -> s { _svcPort = a }) -- | A path prefix that is prepended to any sent HTTP request. -- -- Defaults to @mempty@. servicePath :: Lens' ServiceConfig Builder servicePath = lens _svcPath (\s a -> s { _svcPath = a }) -- | Whether to use HTTPS/SSL. -- -- Defaults to @True@. serviceSecure :: Lens' ServiceConfig Bool serviceSecure = lens _svcSecure (\s a -> s { _svcSecure = a }) -- | Number of seconds to wait for a response. serviceTimeout :: Lens' ServiceConfig (Maybe Seconds) serviceTimeout = lens _svcTimeout (\s a -> s { _svcTimeout = a }) -- | A single part of a (potentially multipart) request body. -- -- /Note:/ The 'IsString' instance defaults to a @text/plain@ MIME type. data GBody = GBody !MediaType !RequestBody instance IsString GBody where fromString = GBody ("text" // "plain") . fromString -- | A lens into the 'MediaType' of a request 'Body'. bodyContentType :: Lens' GBody MediaType bodyContentType = lens (\(GBody m _) -> m) (\(GBody _ b) m -> GBody m b) -- | An intermediary request builder. data Request = Request { _rqPath :: !Builder , _rqQuery :: !(DList (ByteString, Maybe ByteString)) , _rqHeaders :: !(DList (HeaderName, ByteString)) , _rqBody :: ![GBody] } instance Monoid Request where mempty = Request mempty mempty mempty mempty mappend = (<>) instance Semigroup Request where a <> b = Request (_rqPath a <> "/" <> _rqPath b) (_rqQuery a <> _rqQuery b) (_rqHeaders a <> _rqHeaders b) (_rqBody b <> _rqBody a) appendPath :: Request -> Builder -> Request appendPath rq x = rq { _rqPath = _rqPath rq <> "/" <> x } appendPaths :: ToHttpApiData a => Request -> [a] -> Request appendPaths rq = appendPath rq . foldMap (mappend "/" . buildText) appendQuery :: Request -> ByteString -> Maybe Text -> Request appendQuery rq k v = rq { _rqQuery = DList.snoc (_rqQuery rq) (k, Text.encodeUtf8 <$> v) } appendHeader :: Request -> HeaderName -> Maybe Text -> Request appendHeader rq _ Nothing = rq appendHeader rq k (Just v) = rq { _rqHeaders = DList.snoc (_rqHeaders rq) (k, Text.encodeUtf8 v) } setBody :: Request -> [GBody] -> Request setBody rq bs = rq { _rqBody = bs } -- | A materialised 'http-client' request and associated response parser. data GClient a = GClient { _cliAccept :: !(Maybe MediaType) , _cliMethod :: !Method , _cliCheck :: !(Status -> Bool) , _cliService :: !ServiceConfig , _cliRequest :: !Request , _cliResponse :: !(Stream -> ResourceT IO (Either (String, LBS.ByteString) a)) } clientService :: Lens' (GClient a) ServiceConfig clientService = lens _cliService (\s a -> s { _cliService = a }) mime :: FromStream c a => Proxy c -> Method -> [Int] -> Request -> ServiceConfig -> GClient a mime p = gClient (fromStream p) (Just (contentType p)) discard :: Method -> [Int] -> Request -> ServiceConfig -> GClient () discard = gClient (\b -> runConduit (b .| Conduit.sinkNull) >> pure (Right ())) Nothing gClient :: (Stream -> ResourceT IO (Either (String, LBS.ByteString) a)) -> Maybe MediaType -> Method -> [Int] -> Request -> ServiceConfig -> GClient a gClient f cs m statuses rq s = GClient { _cliAccept = cs , _cliMethod = m , _cliCheck = \status -> fromEnum status `elem` statuses , _cliService = s , _cliRequest = rq , _cliResponse = f } class Accept c => ToBody c a where toBody :: Proxy c -> a -> GBody instance ToBody OctetStream ByteString where toBody p = GBody (contentType p) . RequestBodyBS instance ToBody OctetStream LBS.ByteString where toBody p = GBody (contentType p) . RequestBodyLBS instance ToBody PlainText ByteString where toBody p = GBody (contentType p) . RequestBodyBS instance ToBody PlainText LBS.ByteString where toBody p = GBody (contentType p) . RequestBodyLBS instance ToJSON a => ToBody JSON a where toBody p = GBody (contentType p) . RequestBodyLBS . encode class Accept c => FromStream c a where fromStream :: Proxy c -> Stream -> ResourceT IO (Either (String, LBS.ByteString) a) instance FromStream OctetStream Stream where fromStream Proxy = pure . Right instance FromJSON a => FromStream JSON a where fromStream Proxy s = do bs <- sinkLBS s case eitherDecode bs of Left e -> pure $! Left (e, bs) Right x -> pure $! Right x class GoogleRequest a where type Rs a :: * type Scopes a :: [Symbol] requestClient :: a -> GClient (Rs a) class GoogleClient fn where type Fn fn :: * buildClient :: Proxy fn -> Request -> Fn fn -- | Multiple path captures, with @[xs]@ forming @////...@. data Captures (s :: Symbol) a deriving (Typeable) -- | Form a Google style sub-resource, such as @/:@. data CaptureMode (s :: Symbol) (m :: Symbol) a deriving (Typeable) data MultipartRelated (cs :: [*]) m deriving (Typeable) instance ( ToBody c m , GoogleClient fn ) => GoogleClient (MultipartRelated (c ': cs) m :> fn) where type Fn (MultipartRelated (c ': cs) m :> fn) = m -> GBody -> Fn fn buildClient Proxy rq m b = buildClient (Proxy :: Proxy fn) $ setBody rq [toBody (Proxy :: Proxy c) m, b] instance GoogleClient fn => GoogleClient (AltMedia :> fn) where type Fn (AltMedia :> fn) = GBody -> Fn fn buildClient Proxy rq b = buildClient (Proxy :: Proxy fn) $ setBody rq [b] instance (KnownSymbol s, GoogleClient fn) => GoogleClient (s :> fn) where type Fn (s :> fn) = Fn fn buildClient Proxy rq = buildClient (Proxy :: Proxy fn) $ appendPath rq (buildSymbol (Proxy :: Proxy s)) instance (GoogleClient a, GoogleClient b) => GoogleClient (a :<|> b) where type Fn (a :<|> b) = Fn a :<|> Fn b buildClient Proxy rq = buildClient (Proxy :: Proxy a) rq :<|> buildClient (Proxy :: Proxy b) rq instance ( KnownSymbol s , ToHttpApiData a , GoogleClient fn ) => GoogleClient (Capture s a :> fn) where type Fn (Capture s a :> fn) = a -> Fn fn buildClient Proxy rq = buildClient (Proxy :: Proxy fn) . appendPath rq . buildText instance ( KnownSymbol s , ToHttpApiData a , GoogleClient fn ) => GoogleClient (Captures s a :> fn) where type Fn (Captures s a :> fn) = [a] -> Fn fn buildClient Proxy rq = buildClient (Proxy :: Proxy fn) . appendPaths rq instance ( KnownSymbol s , KnownSymbol m , ToHttpApiData a , GoogleClient fn ) => GoogleClient (CaptureMode s m a :> fn) where type Fn (CaptureMode s m a :> fn) = a -> Fn fn buildClient Proxy rq x = buildClient (Proxy :: Proxy fn) . appendPath rq $ buildText x <> ":" <> buildSymbol (Proxy :: Proxy m) instance ( KnownSymbol s , ToHttpApiData a , GoogleClient fn ) => GoogleClient (QueryParam s a :> fn) where type Fn (QueryParam s a :> fn) = Maybe a -> Fn fn buildClient Proxy rq mx = buildClient (Proxy :: Proxy fn) $ case mx of Nothing -> rq Just x -> appendQuery rq k v where k = byteSymbol (Proxy :: Proxy s) v = Just (toQueryParam x) instance ( KnownSymbol s , ToHttpApiData a , GoogleClient fn ) => GoogleClient (QueryParams s a :> fn) where type Fn (QueryParams s a :> fn) = [a] -> Fn fn buildClient Proxy rq = buildClient (Proxy :: Proxy fn) . foldl' go rq where go r = appendQuery r k . Just . toQueryParam k = byteSymbol (Proxy :: Proxy s) instance ( KnownSymbol s , ToHttpApiData a , GoogleClient fn ) => GoogleClient (Header s a :> fn) where type Fn (Header s a :> fn) = Maybe a -> Fn fn buildClient Proxy rq mx = buildClient (Proxy :: Proxy fn) $ case mx of Nothing -> rq Just x -> appendHeader rq (CI.mk k) v where k = byteSymbol (Proxy :: Proxy s) v = Just (toQueryParam x) instance ( ToBody c a , GoogleClient fn ) => GoogleClient (ReqBody (c ': cs) a :> fn) where type Fn (ReqBody (c ': cs) a :> fn) = a -> Fn fn buildClient Proxy rq x = buildClient (Proxy :: Proxy fn) $ setBody rq [toBody (Proxy :: Proxy c) x] instance {-# OVERLAPPABLE #-} FromStream c a => GoogleClient (Get (c ': cs) a) where type Fn (Get (c ': cs) a) = ServiceConfig -> GClient a buildClient Proxy = mime (Proxy :: Proxy c) methodGet [200, 203] instance {-# OVERLAPPING #-} GoogleClient (Get (c ': cs) ()) where type Fn (Get (c ': cs) ()) = ServiceConfig -> GClient () buildClient Proxy = discard methodGet [204] instance {-# OVERLAPPABLE #-} (FromStream c a, cs' ~ (c ': cs)) => GoogleClient (Post cs' a) where type Fn (Post cs' a) = ServiceConfig -> GClient a buildClient Proxy = mime (Proxy :: Proxy c) methodPost [200, 201] instance {-# OVERLAPPING #-} GoogleClient (Post cs ()) where type Fn (Post cs ()) = ServiceConfig -> GClient () buildClient Proxy = discard methodPost [204] instance {-# OVERLAPPABLE #-} FromStream c a => GoogleClient (Put (c ': cs) a) where type Fn (Put (c ': cs) a) = ServiceConfig -> GClient a buildClient Proxy = mime (Proxy :: Proxy c) methodPut [200, 201] instance {-# OVERLAPPING #-} GoogleClient (Put (c ': cs) ()) where type Fn (Put (c ': cs) ()) = ServiceConfig -> GClient () buildClient Proxy = discard methodPut [204] instance {-# OVERLAPPABLE #-} FromStream c a => GoogleClient (Patch (c ': cs) a) where type Fn (Patch (c ': cs) a) = ServiceConfig -> GClient a buildClient Proxy = mime (Proxy :: Proxy c) methodPatch [200, 201] instance {-# OVERLAPPING #-} GoogleClient (Patch (c ': cs) ()) where type Fn (Patch (c ': cs) ()) = ServiceConfig -> GClient () buildClient Proxy = discard methodPatch [204] instance {-# OVERLAPPABLE #-} FromStream c a => GoogleClient (Delete (c ': cs) a) where type Fn (Delete (c ': cs) a) = ServiceConfig -> GClient a buildClient Proxy = mime (Proxy :: Proxy c) methodDelete [200, 202] instance {-# OVERLAPPING #-} GoogleClient (Delete (c ': cs) ()) where type Fn (Delete (c ': cs) ()) = ServiceConfig -> GClient () buildClient Proxy = discard methodDelete [204] sinkLBS :: Stream -> ResourceT IO LBS.ByteString sinkLBS = runConduit . (.| Conduit.sinkLazy) buildText :: ToHttpApiData a => a -> Builder buildText = Build.fromText . toQueryParam buildSymbol :: forall n proxy. KnownSymbol n => proxy n -> Builder buildSymbol = Build.fromString . symbolVal byteSymbol :: forall n proxy. KnownSymbol n => proxy n -> ByteString byteSymbol = BS8.pack . symbolVal -- | An integral value representing seconds. newtype Seconds = Seconds Int deriving ( Eq , Ord , Read , Show , Enum , Num , Bounded , Integral , Real , Data , Typeable , Generic ) seconds :: Seconds -> Int seconds (Seconds n) | n < 0 = 0 | otherwise = n microseconds :: Seconds -> Int microseconds = (1000000 *) . seconds newtype GFieldMask = GFieldMask Text deriving ( Eq , Ord , Show , Read , IsString , Generic , Data , Typeable , FromHttpApiData , ToHttpApiData , FromJSON , ToJSON )