packages feed

fb 0.14.6 → 0.14.7

raw patch · 14 files changed

+275/−127 lines, 14 filesdep ~hspecPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: hspec

API changes (from Hackage documentation)

+ Facebook: DebugToken :: Maybe Text -> Maybe Text -> Maybe UTCTime -> Maybe Bool -> Maybe UTCTime -> Maybe [Permission] -> Maybe Id -> Maybe UserAccessToken -> DebugToken
+ Facebook: FbUTCTime :: UTCTime -> FbUTCTime
+ Facebook: data DebugToken
+ Facebook: debugToken :: (MonadBaseControl IO m, MonadResource m) => AppAccessToken -> AccessTokenData -> FacebookT Auth m DebugToken
+ Facebook: dtAccessToken :: DebugToken -> Maybe UserAccessToken
+ Facebook: dtAppId :: DebugToken -> Maybe Text
+ Facebook: dtAppName :: DebugToken -> Maybe Text
+ Facebook: dtExpiresAt :: DebugToken -> Maybe UTCTime
+ Facebook: dtIsValid :: DebugToken -> Maybe Bool
+ Facebook: dtIssuedAt :: DebugToken -> Maybe UTCTime
+ Facebook: dtScopes :: DebugToken -> Maybe [Permission]
+ Facebook: dtUserId :: DebugToken -> Maybe Id
+ Facebook: newtype FbUTCTime
+ Facebook: unFbUTCTime :: FbUTCTime -> UTCTime

Files

fb.cabal view
@@ -1,5 +1,5 @@ name:              fb-version:           0.14.6+version:           0.14.7 license:           BSD3 license-file:      LICENSE author:            Felipe Lessa@@ -51,6 +51,7 @@     Facebook.Monad     Facebook.Base     Facebook.Auth+    Facebook.Pager     Facebook.Graph     Facebook.Object.Action     Facebook.Object.Checkin@@ -116,7 +117,7 @@     , data-default     , HUnit     , QuickCheck-    , hspec >= 1.4 && < 1.6+    , hspec >= 1.4 && < 1.7     , fb   extensions:     TypeFamilies
src/Facebook.hs view
@@ -30,6 +30,8 @@     , getUserAccessTokenStep2     , getUserLogoutUrl     , extendUserAccessToken+    , debugToken+    , DebugToken(..)       -- ** Signed requests     , parseSignedRequest @@ -66,6 +68,7 @@       -- ** Simple types     , (#=)     , SimpleType(..)+    , FbUTCTime(..)       -- ** Complex types     , Place(..)     , Location(..)@@ -128,6 +131,7 @@ import Facebook.Monad import Facebook.Base import Facebook.Auth+import Facebook.Pager import Facebook.Graph import Facebook.Object.Page import Facebook.Object.User
src/Facebook/Auth.hs view
@@ -11,6 +11,8 @@     , hasExpired     , isValid     , parseSignedRequest+    , debugToken+    , DebugToken(..)     ) where  import Control.Applicative@@ -26,6 +28,7 @@ import Data.Maybe (fromMaybe) import Data.Text (Text) import Data.Time (getCurrentTime, addUTCTime, UTCTime)+import Data.Typeable (Typeable) import Data.String (IsString(..))  import qualified Control.Exception.Lifted as E@@ -211,7 +214,7 @@     -- ^ Retrieves the 'Text' back from a 'Permission'.  Most of     -- the time you won't need to use this function, but you may     -- need it if you're a library author.-  }+  } deriving (Eq, Ord)  instance Show Permission where     show = show . unPermission@@ -348,3 +351,62 @@   | drem == 3 = bs `B.append` "="   | otherwise = bs   where drem = B.length bs `mod` 4+++-- | Get detailed information about an access token.+debugToken :: (MonadBaseControl IO m, C.MonadResource m) =>+              AppAccessToken  -- ^ Your app access token.+           -> AccessTokenData -- ^ The access token you want to debug.+           -> FacebookT Auth m DebugToken+debugToken appToken userTokenData  = do+  req <- fbreq "/debug_token" (Just appToken) $+           [ ("input_token", TE.encodeUtf8 userTokenData) ]+  ret <- undata <$> (asJson =<< fbhttp req)+  let muserToken = UserAccessToken <$> dtUserId ret+                                   <*> return userTokenData+                                   <*> dtExpiresAt ret+  return ret { dtAccessToken = muserToken }+++-- | Helper used in 'debugToken'.  Unfortunately, we can't use 'Pager' here.+data Undata a =+  Undata {+    undata :: a+  }++instance AE.FromJSON a => AE.FromJSON (Undata a) where+  parseJSON (AE.Object v) =+    Undata <$> v AE..: "data"+  parseJSON _ = mzero+++-- | Detailed information about an access token (cf. 'debugToken').+data DebugToken =+  DebugToken+    { dtAppId       :: Maybe Text+    , dtAppName     :: Maybe Text+    , dtExpiresAt   :: Maybe UTCTime+    , dtIsValid     :: Maybe Bool+    , dtIssuedAt    :: Maybe UTCTime+    , dtScopes      :: Maybe [Permission]+    , dtUserId      :: Maybe Id+    , dtAccessToken :: Maybe UserAccessToken+    } deriving (Eq, Ord, Show, Typeable)+++-- | Note: this instance always sets 'dtAccessToken' to+-- 'Nothing', but 'debugToken' will update this field before+-- returning the final 'DebugToken'.  This is done because we+-- need the 'AccessTokenData', which is not part of FB's+-- response.+instance AE.FromJSON DebugToken where+  parseJSON (AE.Object v) =+    DebugToken <$> (fmap idCode <$> v AE..:? "app_id")+               <*> v AE..:? "application"+               <*> (fmap unFbUTCTime <$> v AE..:? "expires_at")+               <*> v AE..:? "is_valid"+               <*> (fmap unFbUTCTime <$> v AE..:? "issued_at")+               <*> (fmap (map Permission) <$> v AE..:? "scopes")+               <*> v AE..:? "user_id"+               <*> pure Nothing+  parseJSON _ = mzero
src/Facebook/Base.hs view
@@ -13,6 +13,7 @@  import Control.Applicative import Control.Monad (mzero)+import Control.Monad.IO.Class (MonadIO) import Data.ByteString.Char8 (ByteString) import Data.Text (Text) import Data.Typeable (Typeable)@@ -23,6 +24,7 @@ import qualified Data.Aeson as A import qualified Data.Attoparsec.Char8 as AT import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as L import qualified Data.Conduit as C import qualified Data.Conduit.Attoparsec as C import qualified Data.Conduit.List as CL@@ -32,7 +34,7 @@ import qualified Network.HTTP.Types as HT  #if DEBUG-import Control.Monad.IO.Class (liftIO)+import Control.Monad.IO.Class (MonadIO, liftIO) import Text.Printf (printf) #endif @@ -83,16 +85,22 @@  -- | Converts a plain 'H.Response' coming from 'H.http' into a -- JSON value.-asJson :: (MonadTrans t, C.MonadThrow m, A.FromJSON a) =>+asJson :: (MonadIO m, MonadTrans t, C.MonadThrow m, A.FromJSON a) =>           H.Response (C.ResumableSource m ByteString)        -> t m a asJson = lift . asJsonHelper -asJsonHelper :: (C.MonadThrow m, A.FromJSON a) =>+asJsonHelper :: (MonadIO m, C.MonadThrow m, A.FromJSON a) =>                 H.Response (C.ResumableSource m ByteString)              -> m a asJsonHelper response = do+#if DEBUG+  bs <- H.responseBody response C.$$+- fmap L.fromChunks CL.consume+  _ <- liftIO $ printf "asJsonHelper: %s\n" (show bs)+  val <- either (fail . ("asJsonHelper: A.decode returned " ++)) return (A.eitherDecode bs)+#else   val <- H.responseBody response C.$$+- C.sinkParser A.json'+#endif   case A.fromJSON val of     A.Success r -> return r     A.Error str ->
src/Facebook/FQL.hs view
@@ -21,6 +21,7 @@ import Facebook.Monad import Facebook.Base import Facebook.Graph+import Facebook.Pager   -- | Query the Facebook Graph using FQL.@@ -44,6 +45,8 @@                    . posixSecondsToUTCTime                    . fromInteger)             . A.parseJSON++{-# DEPRECATED FQLTime "Deprecated since fb 0.14.7, please use FbUTCTime instead." #-}   -- | @newtype@ wrapper around lists that works around FQL's
src/Facebook/Graph.hs view
@@ -4,11 +4,6 @@     , postObject     , deleteObject     , searchObjects-    , Pager(..)-    , fetchNextPage-    , fetchPreviousPage-    , fetchAllNextPages-    , fetchAllPreviousPages     , (#=)     , SimpleType(..)     , Place(..)@@ -20,9 +15,7 @@  import Control.Applicative import Control.Monad (mzero)-import Control.Monad.IO.Class (liftIO) import Control.Monad.Trans.Control (MonadBaseControl)-import Control.Monad.Trans.Resource (MonadResourceBase) import Data.ByteString.Char8 (ByteString) import Data.Int (Int8, Int16, Int32, Int64) import Data.List (intersperse)@@ -47,6 +40,7 @@ import Facebook.Base import Facebook.Monad import Facebook.Types+import Facebook.Pager   -- | Make a raw @GET@ request to Facebook's Graph API.@@ -101,111 +95,6 @@               -> FacebookT anyAuth m (Pager a) searchObjects objectType keyword query = getObject "/search" query'   where query' = ("q" #= keyword) : ("type" #= objectType) : query------------------------------------------------------------------------------ | Many Graph API results are returned as a JSON object with--- the following structure:------ @--- {---   \"data\": [---     ...item 1...,---          :---     ...item n...---   ],---   \"paging\": {---     \"previous\": \"http://...link to previous page...\",---     \"next\":     \"http://...link to next page...\"---   }--- }--- @------ Only the @\"data\"@ field is required, the others may or may--- not appear.------ A @Pager a@ datatype encodes such result where each item has--- type @a@.  You may use functions 'fetchNextPage' and--- 'fetchPreviousPage' to navigate through the results.-data Pager a =-  Pager {-      pagerData     :: [a]-    , pagerPrevious :: Maybe String-    , pagerNext     :: Maybe String-  } deriving (Eq, Ord, Show, Read, Typeable)--instance A.FromJSON a => A.FromJSON (Pager a) where-  parseJSON (A.Object v) =-    let paging f = v A..:? "paging" >>= maybe (return Nothing) (A..:? f)-    in Pager <$> v A..: "data"-             <*> paging "previous"-             <*> paging "next"-  parseJSON _ = mzero----- | Tries to fetch the next page of a 'Pager'.  Returns--- 'Nothing' whenever the current @Pager@ does not have a--- 'pagerNext'.-fetchNextPage :: (C.MonadResource m, MonadBaseControl IO m, A.FromJSON a) =>-                 Pager a -> FacebookT anyAuth m (Maybe (Pager a))-fetchNextPage = fetchHelper pagerNext----- | Tries to fetch the previous page of a 'Pager'.  Returns--- 'Nothing' whenever the current @Pager@ does not have a--- 'pagerPrevious'.-fetchPreviousPage :: (C.MonadResource m, MonadBaseControl IO m, A.FromJSON a) =>-                     Pager a -> FacebookT anyAuth m (Maybe (Pager a))-fetchPreviousPage = fetchHelper pagerPrevious----- | (Internal) See 'fetchNextPage' and 'fetchPreviousPage'.-fetchHelper :: (C.MonadResource m, MonadBaseControl IO m, A.FromJSON a) =>-               (Pager a -> Maybe String) -> Pager a -> FacebookT anyAuth m (Maybe (Pager a))-fetchHelper pagerRef pager =-  case pagerRef pager of-    Nothing  -> return Nothing-    Just url -> do-      req <- liftIO (H.parseUrl url)-      Just <$> (asJson =<< fbhttp req { H.redirectCount = 3 })----- | Tries to fetch all next pages and returns a 'C.Source' with--- all results.  The 'C.Source' will include the results from--- this page as well.  Previous pages will not be considered.--- Next pages will be fetched on-demand.-fetchAllNextPages ::-  (Monad m, MonadResourceBase n, A.FromJSON a) =>-  Pager a -> FacebookT anyAuth m (C.Source n a)-fetchAllNextPages = fetchAllHelper pagerNext----- | Tries to fetch all previous pages and returns a 'C.Source'--- with all results.  The 'C.Source' will include the results--- from this page as well.  Next pages will not be--- considered.  Previous pages will be fetched on-demand.-fetchAllPreviousPages ::-  (Monad m, MonadResourceBase n, A.FromJSON a) =>-  Pager a -> FacebookT anyAuth m (C.Source n a)-fetchAllPreviousPages = fetchAllHelper pagerPrevious----- | (Internal) See 'fetchAllNextPages' and 'fetchAllPreviousPages'.-fetchAllHelper ::-  (Monad m, MonadResourceBase n, A.FromJSON a) =>-  (Pager a -> Maybe String) -> Pager a -> FacebookT anyAuth m (C.Source n a)-fetchAllHelper pagerRef pager = do-  manager <- getManager-  let go (x:xs) mnext   = C.yield x >> go xs mnext-      go [] Nothing     = return ()-      go [] (Just next) = do-        req <- liftIO (H.parseUrl next)-        let get = fbhttpHelper manager req { H.redirectCount = 3 }-        start =<< lift (C.runResourceT $ asJsonHelper =<< get)-      start p = go (pagerData p) $! pagerRef p-  return (start pager)   ----------------------------------------------------------------------
src/Facebook/Object/Checkin.hs view
@@ -21,6 +21,7 @@ import Facebook.Types import Facebook.Monad import Facebook.Graph+import Facebook.Pager   -- | A Facebook check-in (see
src/Facebook/Object/Page.hs view
@@ -20,6 +20,7 @@ import Facebook.Graph import Facebook.Monad import Facebook.Types+import Facebook.Pager  -- | A Facebook page (see -- <https://developers.facebook.com/docs/reference/api/page/>).
src/Facebook/Object/User.hs view
@@ -24,6 +24,7 @@ import Facebook.Types import Facebook.Monad import Facebook.Graph+import Facebook.Pager import Facebook.Object.Checkin  
+ src/Facebook/Pager.hs view
@@ -0,0 +1,126 @@+{-# LANGUAGE ConstraintKinds, DeriveDataTypeable, FlexibleContexts, OverloadedStrings #-}+module Facebook.Pager+    ( Pager(..)+    , fetchNextPage+    , fetchPreviousPage+    , fetchAllNextPages+    , fetchAllPreviousPages+    ) where+++import Control.Applicative+import Control.Monad (mzero)+import Control.Monad.IO.Class (liftIO)+import Control.Monad.Trans.Control (MonadBaseControl)+import Control.Monad.Trans.Resource (MonadResourceBase)+import Data.Typeable (Typeable)++import qualified Data.Aeson as A+import qualified Data.Conduit as C+import qualified Network.HTTP.Conduit as H+++import Facebook.Base+import Facebook.Monad+++-- | Many Graph API results are returned as a JSON object with+-- the following structure:+--+-- @+-- {+--   \"data\": [+--     ...item 1...,+--          :+--     ...item n...+--   ],+--   \"paging\": {+--     \"previous\": \"http://...link to previous page...\",+--     \"next\":     \"http://...link to next page...\"+--   }+-- }+-- @+--+-- Only the @\"data\"@ field is required, the others may or may+-- not appear.+--+-- A @Pager a@ datatype encodes such result where each item has+-- type @a@.  You may use functions 'fetchNextPage' and+-- 'fetchPreviousPage' to navigate through the results.+data Pager a =+  Pager {+      pagerData     :: [a]+    , pagerPrevious :: Maybe String+    , pagerNext     :: Maybe String+  } deriving (Eq, Ord, Show, Read, Typeable)++instance A.FromJSON a => A.FromJSON (Pager a) where+  parseJSON (A.Object v) =+    let paging f = v A..:? "paging" >>= maybe (return Nothing) (A..:? f)+    in Pager <$> v A..: "data"+             <*> paging "previous"+             <*> paging "next"+  parseJSON _ = mzero+++-- | Tries to fetch the next page of a 'Pager'.  Returns+-- 'Nothing' whenever the current @Pager@ does not have a+-- 'pagerNext'.+fetchNextPage :: (C.MonadResource m, MonadBaseControl IO m, A.FromJSON a) =>+                 Pager a -> FacebookT anyAuth m (Maybe (Pager a))+fetchNextPage = fetchHelper pagerNext+++-- | Tries to fetch the previous page of a 'Pager'.  Returns+-- 'Nothing' whenever the current @Pager@ does not have a+-- 'pagerPrevious'.+fetchPreviousPage :: (C.MonadResource m, MonadBaseControl IO m, A.FromJSON a) =>+                     Pager a -> FacebookT anyAuth m (Maybe (Pager a))+fetchPreviousPage = fetchHelper pagerPrevious+++-- | (Internal) See 'fetchNextPage' and 'fetchPreviousPage'.+fetchHelper :: (C.MonadResource m, MonadBaseControl IO m, A.FromJSON a) =>+               (Pager a -> Maybe String) -> Pager a -> FacebookT anyAuth m (Maybe (Pager a))+fetchHelper pagerRef pager =+  case pagerRef pager of+    Nothing  -> return Nothing+    Just url -> do+      req <- liftIO (H.parseUrl url)+      Just <$> (asJson =<< fbhttp req { H.redirectCount = 3 })+++-- | Tries to fetch all next pages and returns a 'C.Source' with+-- all results.  The 'C.Source' will include the results from+-- this page as well.  Previous pages will not be considered.+-- Next pages will be fetched on-demand.+fetchAllNextPages ::+  (Monad m, MonadResourceBase n, A.FromJSON a) =>+  Pager a -> FacebookT anyAuth m (C.Source n a)+fetchAllNextPages = fetchAllHelper pagerNext+++-- | Tries to fetch all previous pages and returns a 'C.Source'+-- with all results.  The 'C.Source' will include the results+-- from this page as well.  Next pages will not be+-- considered.  Previous pages will be fetched on-demand.+fetchAllPreviousPages ::+  (Monad m, MonadResourceBase n, A.FromJSON a) =>+  Pager a -> FacebookT anyAuth m (C.Source n a)+fetchAllPreviousPages = fetchAllHelper pagerPrevious+++-- | (Internal) See 'fetchAllNextPages' and 'fetchAllPreviousPages'.+fetchAllHelper ::+  (Monad m, MonadResourceBase n, A.FromJSON a) =>+  (Pager a -> Maybe String) -> Pager a -> FacebookT anyAuth m (C.Source n a)+fetchAllHelper pagerRef pager = do+  manager <- getManager+  let go (x:xs) mnext   = C.yield x >> go xs mnext+      go [] Nothing     = return ()+      go [] (Just next) = do+        req <- liftIO (H.parseUrl next)+        let get = fbhttpHelper manager req { H.redirectCount = 3 }+        start =<< lift (C.runResourceT $ asJsonHelper =<< get)+      start p = go (pagerData p) $! pagerRef p+  return (start pager)
src/Facebook/RealTime.hs view
@@ -37,6 +37,7 @@ import Facebook.Monad import Facebook.Base import Facebook.Graph+import Facebook.Pager   -- | The type of objects that a real-time update refers to.
src/Facebook/TestUsers.hs view
@@ -31,6 +31,7 @@ import Facebook.Graph import Facebook.Monad import Facebook.Types+import Facebook.Pager   -- | A Facebook test user.@@ -120,10 +121,11 @@ -- | Remove an existing test user. removeTestUser :: (C.MonadResource m, MonadBaseControl IO m)                   => TestUser       -- ^ The TestUser to be removed.-                  -> AppAccessToken -- ^ Access token for your app.+                  -> AppAccessToken -- ^ Access token for your app (ignored since fb 0.14.7).                   -> FacebookT Auth m Bool-removeTestUser testUser token =-  getObjectBool ("/" <> idCode (tuId testUser)) [("method","delete")] (Just token)+removeTestUser testUser _token =+  getObjectBool ("/" <> idCode (tuId testUser)) [("method","delete")] token+  where token = incompleteTestUserAccessToken testUser   -- | Make a friend connection between two test users.
src/Facebook/Types.hs view
@@ -26,6 +26,7 @@ import Data.String (IsString) import Data.Text (Text) import Data.Time (UTCTime, parseTime)+import Data.Time.Clock.POSIX (posixSecondsToUTCTime) import Data.Typeable (Typeable, Typeable1) import System.Locale (defaultTimeLocale) @@ -145,15 +146,22 @@ (<>) = mappend  --- | (Internal) @newtype@ for 'UTCTime' that follows Facebook's--- conventions of JSON parsing.  While @aeson@ expects a format--- of @%FT%T%Q@, Facebook gives time values formatted as--- @%FT%T%z@.+-- | @newtype@ for 'UTCTime' that follows Facebook's+-- conventions of JSON parsing.+--+--  * As a string, while @aeson@ expects a format of @%FT%T%Q@,+--    Facebook gives time values formatted as @%FT%T%z@.+--+--  * As a number, 'FbUTCTime' accepts a number of seconds since+--    the Unix epoch. newtype FbUTCTime = FbUTCTime { unFbUTCTime :: UTCTime }+  deriving (Eq, Ord, Show, Read, Typeable)  instance A.FromJSON FbUTCTime where   parseJSON (A.String t) =     case parseTime defaultTimeLocale "%FT%T%z" (T.unpack t) of       Just d -> return (FbUTCTime d)       _      -> fail $ "could not parse FbUTCTime string " ++ show t-  parseJSON _ = fail "could not parse FbUTCTime from something which is not a string"+  parseJSON (A.Number n) =+    return $ FbUTCTime $ posixSecondsToUTCTime $ fromInteger $ floor n+  parseJSON _ = fail "could not parse FbUTCTime from something which is not a string or number"
tests/Main.hs view
@@ -1,6 +1,7 @@ {-# LANGUAGE OverloadedStrings            , Rank2Types            , ScopedTypeVariables+           , GADTs            , FlexibleContexts #-} module Main (main, getCredentials) where @@ -97,11 +98,13 @@   hspec $ do     -- Run the tests twice, once in Facebook's production tier...     facebookTests "Production tier: "+                  creds                   manager                   (C.runResourceT . FB.runFacebookT creds manager)                   (C.runResourceT . FB.runNoAuthFacebookT manager)     -- ...and the other in Facebook's beta tier.     facebookTests "Beta tier: "+                  creds                   manager                   (C.runResourceT . FB.beta_runFacebookT creds manager)                   (C.runResourceT . FB.beta_runNoAuthFacebookT manager)@@ -110,11 +113,12 @@     libraryTests manager  facebookTests :: String+              -> FB.Credentials               -> H.Manager               -> (forall a. FB.FacebookT FB.Auth   (C.ResourceT IO) a -> IO a)               -> (forall a. FB.FacebookT FB.NoAuth (C.ResourceT IO) a -> IO a)               -> Spec-facebookTests pretitle manager runAuth runNoAuth = do+facebookTests pretitle creds manager runAuth runNoAuth = do   let describe' = describe . (pretitle ++)    describe' "getAppAccessToken" $ do@@ -136,6 +140,34 @@     it "returns False on a clearly invalid app access token" $       runNoAuth $ FB.isValid invalidAppAccessToken  #?= False +  describe' "debugToken" $ do+    it "works on a test user access token" $ do+      runAuth $+        withTestUser D.def $ \testUser -> do+          Just testUserAccessTokenData <- return (FB.tuAccessToken testUser)+          appToken <- FB.getAppAccessToken+          ret <- FB.debugToken appToken testUserAccessTokenData+          now <- liftIO TI.getCurrentTime+          FB.dtAppId ret &?= Just (FB.appId creds)+          FB.dtAppName ret &?= Just (FB.appName creds)+          case FB.dtExpiresAt ret of+            Nothing -> fail "dtExpiresAt is Nothing"+            Just t  -> compare t now &?= GT+          FB.dtIsValid ret &?= Just True+          case FB.dtIssuedAt ret of+            Nothing -> return () -- ok since it's a test user+            Just t  -> compare t now &?= LT+          isJust (FB.dtScopes ret) &?= True+          FB.dtUserId ret &?= Just (FB.tuId testUser)+{-+          case FB.dtAccessToken ret of+            Nothing -> fail "dtAccessToken is Nothing"+            Just (FB.UserAccessToken uid dt exps) -> do+              uid &?= FB.tuId testUser+              dt  &?= testUserAccessTokenData+              Just exps &?= FB.dtExpiresAt ret+-}+   describe' "getObject" $ do     it "is able to fetch Facebook's own page" $       runNoAuth $ do@@ -379,6 +411,15 @@     it "seems to work" $ do       let input  = "[1348678357]"           output = FB.FQLTime (read "2012-09-26 16:52:37 UTC")+      A.decode input @?= Just [output]++  describe "FbUTCTime" $ do+    let output = FB.FbUTCTime (read "2012-09-26 16:52:37 UTC")+    it "seems to work (string)" $ do+      let input  = "[\"2012-09-26T16:52:37+0000\"]"+      A.decode input @?= Just [output]+    it "seems to work (unix epoch)" $ do+      let input  = "[1348678357]"       A.decode input @?= Just [output]    describe "FQLList" $ do