@@ -1,7 +1,16 @@
1+{-# LANGUAGE DataKinds #-}
12{-# LANGUAGE DeriveDataTypeable #-}
23{-# LANGUAGE DeriveGeneric #-}
4+{-# LANGUAGE FlexibleInstances #-}
35{-# LANGUAGE GeneralizedNewtypeDeriving #-}
6+{-# LANGUAGE LambdaCase #-}
7+{-# LANGUAGE MultiParamTypeClasses #-}
48{-# LANGUAGE OverloadedStrings #-}
9+{-# LANGUAGE RecordWildCards #-}
10+{-# LANGUAGE ScopedTypeVariables #-}
11+{-# LANGUAGE TypeFamilies #-}
12+{-# LANGUAGE TypeOperators #-}
13+{-# LANGUAGE UndecidableInstances #-}
514
615-- |
716-- Module : Network.Google.Types
@@ -13,14 +22,36 @@
1322--
1423module Network.Google.Types where
1524
16−import Control.Lens hiding (coerce)
25+import Control.Lens hiding (coerce)
26+import Control.Monad.Catch
27+import Control.Monad.Trans.Resource
28+import Data.Aeson
29+import Data.Bifunctor
30+import Data.ByteString (ByteString)
31+import qualified Data.ByteString.Lazy as LBS
1732import Data.Coerce
33+import Data.Conduit
34+import qualified Data.Conduit.List as CL
1835import Data.Data
36+import Data.Default.Class (def)
37+import Data.DList (DList)
38+import qualified Data.DList as DList
39+import Data.Foldable (foldMap, foldl')
40+import Data.List (intersperse)
41+import Data.Maybe
1942import Data.Monoid
20−import Data.Text (Text)
43+import Data.Proxy
44+import Data.Text (Text)
45+import qualified Data.Text as Text
46+import Data.Text.Lazy.Builder (Builder)
47+import qualified Data.Text.Lazy.Builder as Build
2148import GHC.Generics
49+import GHC.TypeLits
50+import Network.HTTP.Client (HttpException, RequestBody (..))
51+import qualified Network.HTTP.Client as Client
52+import Network.HTTP.Media hiding (Accept)
2253import Network.HTTP.Types
23−import Servant.API
54+import Servant.API hiding (Header)
2455import Servant.Utils.Links
2556
2657data AltJSON = AltJSON
@@ -65,3 +96,426 @@ _Default = iso f Just
6596 where
6697 f (Just x) = x
6798 f Nothing = mempty
99+
100+-- | A convenience alias to avoid type ambiguity.
101+type ClientRequest = Client.Request
102+
103+-- | A convenience alias encapsulating the common 'Response'.
104+type ClientResponse = Client.Response Stream
105+
106+type Stream = ResumableSource (ResourceT IO) ByteString
107+
108+data Error
109+ = TransportError HttpException
110+ | SerializeError SerializeError
111+ | ServiceError ServiceError
112+ deriving (Show, Typeable)
113+
114+instance Exception Error
115+
116+data SerializeError = SerializeError'
117+ -- , _serializeHeaders :: [Header]
118+ { -- _serializeAbbrev :: !Abbrev
119+ -- , _serializeStatus :: !Status
120+ -- , _serializeMessage :: String
121+ } deriving (Eq, Show, Typeable)
122+
123+data ServiceError = ServiceError' !Status !MediaType [Header] LBS.ByteString
124+ -- _serviceAbbrev :: !Abbrev
125+ -- , _serviceStatus :: !Status
126+ -- , _serviceHeaders :: [Header]
127+ -- , _serviceCode :: !ErrorCode
128+ -- , _serviceMessage :: Maybe ErrorMessage
129+ -- , _serviceRequestId :: Maybe RequestId
130+ deriving (Eq, Show, Typeable)
131+
132+class AsError a where
133+ -- | A general Amazonka error.
134+ _Error :: Prism' a Error
135+ {-# MINIMAL _Error #-}
136+
137+ -- | An error occured while communicating over HTTP with a remote service.
138+ _TransportError :: Prism' a HttpException
139+
140+ -- | A serialisation error occured when attempting to deserialise a response.
141+ _SerializeError :: Prism' a SerializeError
142+
143+ -- | A service specific error returned by the remote service.
144+ _ServiceError :: Prism' a ServiceError
145+
146+ _TransportError = _Error . _TransportError
147+ _SerializeError = _Error . _SerializeError
148+ _ServiceError = _Error . _ServiceError
149+
150+instance AsError Error where
151+ _Error = id
152+
153+ _TransportError = prism TransportError $ \case
154+ TransportError e -> Right e
155+ x -> Left x
156+
157+ _SerializeError = prism SerializeError $ \case
158+ SerializeError e -> Right e
159+ x -> Left x
160+
161+ _ServiceError = prism ServiceError $ \case
162+ ServiceError e -> Right e
163+ x -> Left x
164+
165+-- | An intermediary request builder.
166+data RequestBuilder = RequestBuilder
167+ { _rqHost :: ByteString
168+ , _rqBasePath :: ByteString
169+ , _rqPath :: Builder
170+ , _rqQuery :: DList (Text, Maybe Text)
171+ , _rqBody :: Maybe (MediaType, Stream)
172+ }
173+
174+-- | Construct a default request builder. Since the Google service descriptions
175+-- have http/https in the host (rootUrl), the scheme is assumed.
176+defaultRequest :: ByteString -- ^ Host. (rootUrl)
177+ -> ByteString -- ^ Path. (servicePath)
178+ -> RequestBuilder
179+defaultRequest h p = RequestBuilder
180+ { _rqHost = h
181+ , _rqBasePath = p
182+ , _rqPath = mempty
183+ , _rqQuery = mempty
184+ , _rqBody = Nothing
185+ }
186+
187+appendPath :: RequestBuilder -> Builder -> RequestBuilder
188+appendPath rq x = rq { _rqPath = _rqPath rq <> "/" <> x }
189+
190+appendPaths :: ToText a => RequestBuilder -> [a] -> RequestBuilder
191+appendPaths rq = appendPath rq . foldMap (mappend "/" . buildText)
192+
193+appendQuery :: RequestBuilder -> Text -> Maybe Text -> RequestBuilder
194+appendQuery rq k v = rq { _rqQuery = DList.snoc (_rqQuery rq) (k, v) }
195+
196+setStream :: RequestBuilder -> MediaType -> Stream -> RequestBuilder
197+setStream rq c x = rq { _rqBody = Just (c, x) }
198+
199+-- | A materialised 'http-client' request and associated response parser.
200+data Client a = Client
201+ { _clientAccept :: Maybe MediaType
202+ , _clientCheck :: Int -> Bool
203+ , _clientResponse :: Stream -> ResourceT IO (Either SerializeError a)
204+ , _clientRequest :: ClientRequest
205+ }
206+
207+clientRequest :: (Stream -> ResourceT IO (Either SerializeError a))
208+ -> Maybe MediaType
209+ -> Method
210+ -> [Int]
211+ -> RequestBuilder
212+ -> Client a
213+clientRequest f cs m ns rq = Client
214+ { _clientAccept = cs
215+ , _clientCheck = (`elem` ns)
216+ , _clientResponse = f
217+ , _clientRequest = def
218+ { Client.method = m
219+ , Client.checkStatus = \_ _ _ -> Nothing
220+ }
221+ }
222+
223+clientMime :: FromStream c a
224+ => Proxy c
225+ -> Method
226+ -> [Int]
227+ -> RequestBuilder
228+ -> Client a
229+clientMime p = clientRequest (fromStream p) (Just (contentType p))
230+
231+clientEmpty :: Method -> [Int] -> RequestBuilder -> Client ()
232+clientEmpty = clientRequest go Nothing
233+ where
234+ go b = closeResumableSource b >> pure (Right ())
235+
236+class Accept c => ToStream c a where
237+ toStream :: Proxy c -> a -> Stream
238+
239+instance ToJSON a => ToStream JSON a where
240+ toStream Proxy = newResumableSource . CL.sourceList . LBS.toChunks . encode
241+
242+class Accept c => FromStream c a where
243+ fromStream :: Proxy c
244+ -> Stream
245+ -> ResourceT IO (Either SerializeError a)
246+
247+instance FromStream OctetStream Stream where
248+ fromStream Proxy = pure . Right
249+
250+instance FromJSON a => FromStream JSON a where
251+ fromStream Proxy b = do
252+ lbs <- sinkLBS b
253+ case eitherDecode lbs of
254+ Left e -> undefined
255+ Right x -> pure (Right x)
256+
257+class GoogleRequest a where
258+ type Rs a :: *
259+
260+ request :: a -> Client (Rs a)
261+ requestWith :: RequestBuilder -> a -> Client (Rs a)
262+
263+class GoogleClient fn where
264+ type Fn fn :: *
265+
266+ clientBuild :: Proxy fn -> RequestBuilder -> Fn fn
267+
268+-- | Multiple path captures, with @[xs]@ forming @/<x1>/<x2>/<x2>/...@.
269+data Captures (s :: Symbol) a
270+ deriving (Typeable)
271+
272+-- | Form a Google style sub-resource, such as @/<capture>:<mode>@.
273+data CaptureMode (s :: Symbol) (m :: Symbol) a
274+ deriving (Typeable)
275+
276+data MultipartRelated (cs :: [*]) m b
277+ deriving (Typeable)
278+
279+instance ( MimeRender c m
280+ , GoogleClient fn
281+ ) => GoogleClient (MultipartRelated (c ': cs) m b :> fn) where
282+ type Fn (MultipartRelated (c ': cs) m b :> fn) = m -> b -> Fn fn
283+
284+ -- requestBuild Proxy rq m b = requestBuild (Proxy :: Proxy fn) $
285+ -- mimeRender p m
286+ -- mimeRender (Proxy :: Proxy OctetStream) b
287+ -- where
288+ -- p = Proxy :: Proxy c
289+
290+-- POST /upload/drive/v2/files?uploadType=multipart HTTP/1.1
291+-- Host: www.googleapis.com
292+-- Authorization: Bearer your_auth_token
293+-- Content-Type: multipart/related; boundary="foo_bar_baz"
294+-- Content-Length: number_of_bytes_in_entire_request_body
295+--
296+-- --foo_bar_baz
297+-- Content-Type: application/json; charset=UTF-8
298+-- {
299+-- "title": "My File"
300+-- }
301+-- --foo_bar_baz
302+-- Content-Type: image/jpeg
303+-- JPEG data
304+-- --foo_bar_baz--
305+
306+-- "mediaType" param also comes/calculated from the request body?
307+--
308+-- Resumable endpoints - Not supported to begin with, need to figure
309+-- out how to return session id etc.
310+--
311+-- Assume initially that every service supports multipart upload.
312+--
313+-- When downloading media you must set the alt query parameter
314+-- to media in the request URL.
315+--
316+-- "resumable" or "multipart" needs to go into the "uploadType" param
317+--
318+
319+-- if c.protocol_ == "resumable" {
320+-- if c.mediaType_ == "" {
321+-- c.mediaType_ = googleapi.DetectMediaType(c.resumable_)
322+-- }
323+-- req.Header.Set("X-Upload-Content-Type", c.mediaType_)
324+-- req.Header.Set("Content-Type", "application/json; charset=utf-8")
325+-- } else {
326+-- req.Header.Set("Content-Type", ctype)
327+-- }
328+
329+instance (KnownSymbol s, GoogleClient fn) => GoogleClient (s :> fn) where
330+ type Fn (s :> fn) = Fn fn
331+
332+ clientBuild Proxy rq = clientBuild (Proxy :: Proxy fn) $
333+ appendPath rq (buildSymbol (Proxy :: Proxy s))
334+
335+instance (GoogleClient a, GoogleClient b) => GoogleClient (a :<|> b) where
336+ type Fn (a :<|> b) = Fn a :<|> Fn b
337+
338+ clientBuild Proxy rq =
339+ clientBuild (Proxy :: Proxy a) rq
340+ :<|> clientBuild (Proxy :: Proxy b) rq
341+
342+instance ( KnownSymbol s
343+ , ToText a
344+ , GoogleClient fn
345+ ) => GoogleClient (Capture s a :> fn) where
346+ type Fn (Capture s a :> fn) = a -> Fn fn
347+
348+ clientBuild Proxy rq = clientBuild (Proxy :: Proxy fn)
349+ . appendPath rq
350+ . buildText
351+
352+instance ( KnownSymbol s
353+ , ToText a
354+ , GoogleClient fn
355+ ) => GoogleClient (Captures s a :> fn) where
356+ type Fn (Captures s a :> fn) = [a] -> Fn fn
357+
358+ clientBuild Proxy rq = clientBuild (Proxy :: Proxy fn)
359+ . appendPaths rq
360+
361+instance ( KnownSymbol s
362+ , KnownSymbol m
363+ , ToText a
364+ , GoogleClient fn
365+ ) => GoogleClient (CaptureMode s m a :> fn) where
366+ type Fn (CaptureMode s m a :> fn) = a -> Fn fn
367+
368+ clientBuild Proxy rq x = clientBuild (Proxy :: Proxy fn)
369+ . appendPath rq
370+ $ buildText x <> buildSymbol (Proxy :: Proxy m)
371+
372+instance ( KnownSymbol s
373+ , ToText a
374+ , GoogleClient fn
375+ ) => GoogleClient (QueryParam s a :> fn) where
376+ type Fn (QueryParam s a :> fn) = Maybe a -> Fn fn
377+
378+ clientBuild Proxy rq mx = clientBuild (Proxy :: Proxy fn) $
379+ case mx of
380+ Nothing -> rq
381+ Just x -> appendQuery rq k v
382+ where
383+ k = textSymbol (Proxy :: Proxy s)
384+ v = Just (toText x)
385+
386+instance ( KnownSymbol s
387+ , ToText a
388+ , GoogleClient fn
389+ ) => GoogleClient (QueryParams s a :> fn) where
390+ type Fn (QueryParams s a :> fn) = [a] -> Fn fn
391+
392+ clientBuild Proxy rq = clientBuild (Proxy :: Proxy fn) . foldl' go rq
393+ where
394+ go r = appendQuery r k . Just . toText
395+
396+ k = textSymbol (Proxy :: Proxy s)
397+
398+instance ( ToStream c a
399+ , GoogleClient fn
400+ ) => GoogleClient (ReqBody (c ': cs) a :> fn) where
401+ type Fn (ReqBody (c ': cs) a :> fn) = a -> Fn fn
402+
403+ clientBuild Proxy rq = clientBuild (Proxy :: Proxy fn)
404+ . setStream rq (contentType p)
405+ . toStream p
406+ where
407+ p = Proxy :: Proxy c
408+
409+instance {-# OVERLAPPABLE #-}
410+ FromStream c a => GoogleClient (Get (c ': cs) a) where
411+ type Fn (Get (c ': cs) a) = Client a
412+
413+ clientBuild Proxy = clientMime (Proxy :: Proxy c) methodGet [200, 203]
414+
415+instance {-# OVERLAPPING #-}
416+ GoogleClient (Get (c ': cs) ()) where
417+ type Fn (Get (c ': cs) ()) = Client ()
418+
419+ clientBuild Proxy = clientEmpty methodGet [204]
420+
421+instance {-# OVERLAPPABLE #-}
422+ (FromStream c a, cs' ~ (c ': cs)) => GoogleClient (Post cs' a) where
423+ type Fn (Post cs' a) = Client a
424+
425+ clientBuild Proxy = clientMime (Proxy :: Proxy c) methodPost [200, 201]
426+
427+instance {-# OVERLAPPING #-}
428+ GoogleClient (Post cs ()) where
429+ type Fn (Post cs ()) = Client ()
430+
431+ clientBuild Proxy = clientEmpty methodPost [204]
432+
433+instance {-# OVERLAPPABLE #-}
434+ FromStream c a => GoogleClient (Put (c ': cs) a) where
435+ type Fn (Put (c ': cs) a) = Client a
436+
437+ clientBuild Proxy = clientMime (Proxy :: Proxy c) methodPut [200, 201]
438+
439+instance {-# OVERLAPPING #-}
440+ GoogleClient (Put (c ': cs) ()) where
441+ type Fn (Put (c ': cs) ()) = Client ()
442+
443+ clientBuild Proxy = clientEmpty methodPut [204]
444+
445+instance {-# OVERLAPPABLE #-}
446+ FromStream c a => GoogleClient (Patch (c ': cs) a) where
447+ type Fn (Patch (c ': cs) a) = Client a
448+
449+ clientBuild Proxy = clientMime (Proxy :: Proxy c) methodPatch [200, 201]
450+
451+instance {-# OVERLAPPING #-}
452+ GoogleClient (Patch (c ': cs) ()) where
453+ type Fn (Patch (c ': cs) ()) = Client ()
454+
455+ clientBuild Proxy = clientEmpty methodPatch [204]
456+
457+instance {-# OVERLAPPABLE #-}
458+ FromStream c a => GoogleClient (Delete (c ': cs) a) where
459+ type Fn (Delete (c ': cs) a) = Client a
460+
461+ clientBuild Proxy = clientMime (Proxy :: Proxy c) methodDelete [200, 202]
462+
463+instance {-# OVERLAPPING #-}
464+ GoogleClient (Delete (c ': cs) ()) where
465+ type Fn (Delete (c ': cs) ()) = Client ()
466+
467+ clientBuild Proxy = clientEmpty methodDelete [204]
468+
469+-- perform :: Accept
470+-- -> (Stream -> Client (Either SerializeError a))
471+-- -> Method
472+-- -> [Int]
473+-- -> RequestBuilder
474+-- -> Client a
475+-- perform a meth ns f rq = undefined -- do
476+ -- m <- ask
477+ -- x <- lift . try $ Client.http m (clientRequest meth a rq)
478+ -- either (failure . TransportError) success x
479+ -- where
480+ -- failure = pure . Left
481+
482+ -- success rs = do
483+ -- let s = Client.responseStatus rs
484+ -- b = Client.responseBody rs
485+ -- hs = Client.responseHeaders rs
486+ -- case content hs of
487+ -- unless (matches respCT (acceptCT)) $
488+ -- left $ UnsupportedContentType respCT respBody
489+
490+ -- Nothing -> failure (SerializeError' (SerializeError hs))
491+
492+ -- Just _ | statusInvalid s -> do
493+ -- lbs <- sinkLBS b
494+ -- failure (ServiceError' (ServiceError s c hs lbs))
495+
496+ -- Just c | contentInvalid c -> do
497+ -- lbs <- sinkLBS b
498+ -- failure (SerializeError' (SerializeError s c hs lbs))
499+
500+
501+ -- Just c -> first SerializeError' <$> f b
502+
503+ -- content :: [Header] -> Maybe c
504+ -- content = parseAccept
505+ -- . fromMaybe "application/octet-stream"
506+ -- . lookup hContentType
507+
508+ -- statusCheck s = fromEnum s `elem` ns
509+ -- contentCheck = matches a
510+
511+sinkLBS :: Stream -> ResourceT IO LBS.ByteString
512+sinkLBS = fmap LBS.fromChunks . ($$+- CL.consume)
513+
514+buildText :: ToText a => a -> Builder
515+buildText = Build.fromText . toText
516+
517+buildSymbol :: forall n proxy. KnownSymbol n => proxy n -> Builder
518+buildSymbol = Build.fromString . symbolVal
519+
520+textSymbol :: forall n proxy. KnownSymbol n => proxy n -> Text
521+textSymbol = Text.pack . symbolVal