packages feed

servant-zeppelin-client-0.1.0.0: test/Servant/Zeppelin/ClientSpec.hs

{-# LANGUAGE DataKinds                  #-}
{-# LANGUAGE DeriveGeneric              #-}
{-# LANGUAGE FlexibleContexts           #-}
{-# LANGUAGE FlexibleInstances          #-}
{-# LANGUAGE GADTs                      #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses      #-}
{-# LANGUAGE OverloadedStrings          #-}
{-# LANGUAGE ScopedTypeVariables        #-}
{-# LANGUAGE TypeApplications           #-}
{-# LANGUAGE TypeFamilies               #-}
{-# LANGUAGE TypeOperators              #-}

{-# OPTIONS_GHC -fno-warn-unused-binds#-}

module Servant.Zeppelin.ClientSpec (spec) where

import           Control.Monad.Except
import           Data.Aeson
import           Data.Either
import qualified Data.List                as L
import           Data.Singletons.Prelude hiding ((:>))
import           Data.String.Conversions
import           GHC.Generics
import           Servant.Server
import           Servant.Zeppelin
import           Servant.Zeppelin.Client
import           Servant.Zeppelin.Server

import           Network.Wai.Handler.Warp (testWithApplication)
import           System.IO.Unsafe         (unsafePerformIO)


import           Network.HTTP.Client      (Manager, defaultManagerSettings,
                                           newManager)
import           Servant
import           Servant.Client
import           Test.Hspec
import           Test.QuickCheck


spec :: Spec
spec = do
  hasClientSpec
  return ()

hasClientSpec :: Spec
hasClientSpec = describe "HasClient" $ around (testWithApplication $ return app) $ do

  it "succeeds when we ask for the inflated data" $ \port -> quickCheckWith qcArgs $ property
                                                  $ \aid -> do
    ealbum <- getAlbumClientFull mgr (BaseUrl Http "localhost" port "") aid
    let Just (album, person, photos) = do
          a <- getAlbumById aid
          p <- getPersonById (albumOwner a)
          let phs = getPhotosByIds (albumPhotos a)
          return (a, p, phs)
    ealbum `shouldSatisfy` isRight
    let Right (SideLoaded a deps) = ealbum
    person `shouldBe` projectDependency deps
    photos `shouldBe` projectDependency deps
    a `shouldBe` album

  it "succeeds when we ask for the uninflated data" $ \port -> quickCheckWith qcArgs $ property
                                                    $ \aid -> do
    ealbum <- getAlbumClient mgr (BaseUrl Http "localhost" port "") aid
    ealbum `shouldSatisfy` isRight
    let Right album = ealbum
        Just album' = getAlbumById aid
    album `shouldBe` album'

qcArgs :: Args
qcArgs = stdArgs {maxSuccess = 20}

--------------------------------------------------------------------------------
-- | Client
--------------------------------------------------------------------------------

mgr :: Manager
mgr = unsafePerformIO $ newManager defaultManagerSettings
{-# NOINLINE mgr #-}

type AlbumDeps =  '[Person, [Photo]]

getAlbumClientFull :: Manager
                   -> BaseUrl
                   -> AlbumId
                   -> IO (Either ServantError (SideLoaded Album AlbumDeps))
getAlbumClientFull m burl aid =
  flip runClientM (ClientEnv m burl) $
    runDepClient (client api aid) STrue

getAlbumClient :: Manager
               -> BaseUrl
               -> AlbumId
               -> IO (Either ServantError Album)
getAlbumClient m burl aid =
  flip runClientM (ClientEnv m burl) $
    runDepClient (client api aid) SFalse

--------------------------------------------------------------------------------
-- | Application
--------------------------------------------------------------------------------

type API = "albums" :> Capture "albumId" AlbumId
                    :> Get '[JSON, PlainText] Album
                    :> SideLoad '[Person, [Photo]]

api :: Proxy API
api = Proxy @API

newtype QueryError = LookupError String

newtype DB a = DB { runDB :: ExceptT QueryError IO a }
  deriving (Functor, Applicative, Monad, MonadIO, MonadError QueryError)

server :: ServerT API Handler
server = albumHandler

albumHandler :: AlbumId -> Handler Album
albumHandler aid =
  case L.find (\album -> albumId album == aid) albumTable of
    Nothing    -> throwError err404
    Just album -> return album

phi :: DB :~> Handler
phi = NT $ \ha -> do
  ea <- liftIO . runExceptT . runDB $ ha
  case ea of
    Left (LookupError msg) -> throwError err404 {errBody = cs msg}
    Right a                -> return a

app :: Application
app = serveWithContext api ctxt server
  where
    ctxt :: Context '[DB :~> Handler]
    ctxt = phi :. EmptyContext

--------------------------------------------------------------------------------
-- | Data
--------------------------------------------------------------------------------
-- | Photo

newtype PhotoId = PhotoId Int
  deriving (Eq, Show, Num, Generic, ToJSON, FromJSON)

type instance NamedDependency [Photo] = "photos"
type instance NamedDependency [PhotoId] = "photoIds"

data Photo =
  Photo { photoId      :: PhotoId
        , photoCaption :: String
        , artistId     :: PersonId
        } deriving (Eq, Show, Generic)

instance ToJSON Photo
instance FromJSON Photo

photosTable :: [Photo]
photosTable = [ Photo 1 "At the Beach." 1
              , Photo 2 "At the Mountain." 1
              , Photo 3 "With Friends." 1
              , Photo 4 "Bow Wow." 2
              ]

getPhotosByIds :: [PhotoId] -> [Photo]
getPhotosByIds pids = filter (\photo -> photoId photo `elem` pids) photosTable

instance Inflatable DB [PhotoId] where
  type Full DB [PhotoId] = [Photo]
  inflator = return . getPhotosByIds

-- | Person

newtype PersonId = PersonId Int
  deriving (Eq, Show, Num, Generic, ToJSON, FromJSON)

type instance NamedDependency Person = "person"

data Person =
  Person { personId   :: PersonId
         , personName :: String
         } deriving (Eq, Show, Generic)

instance ToJSON Person
instance FromJSON Person

personTable :: [Person]
personTable = [ Person 1 "Alice"
              , Person 2 "Fido"
              ]

getPersonById :: PersonId -> Maybe Person
getPersonById pid = L.find (\person -> personId person == pid) personTable

instance Inflatable DB PersonId where
  type Full DB PersonId = Person
  inflator pid =
    case getPersonById pid of
      Nothing -> throwError . LookupError $ "Could not find person with id: " <> show pid
      Just person -> return person

-- | Albums

newtype AlbumId = AlbumId Int
  deriving (Eq, Show, Num, ToJSON, FromJSON, FromHttpApiData, ToHttpApiData)

instance Arbitrary AlbumId where
  arbitrary = elements $ map AlbumId [1..3]

data Album =
  Album { albumId     :: AlbumId
        , albumName   :: String
        , albumOwner  :: PersonId
        , albumPhotos :: [PhotoId]
        } deriving (Eq, Show, Generic)

instance ToJSON Album
instance FromJSON Album

albumTable :: [Album]
albumTable = [ Album 1 "Vacations" 1 [1,2]
             , Album 2 "In the City" 1 [3]
             , Album 3 "Howl" 2 [4]
             ]

getAlbumById :: AlbumId -> Maybe Album
getAlbumById aid = L.find (\album -> albumId album == aid) albumTable

instance HasDependencies DB Album '[PersonId, [PhotoId]] where
  getDependencies (Album _ _ owner pIds) = owner :&: pIds :&: NilDeps

instance MimeRender PlainText Album where
  mimeRender _ _ = "Album"