-- Copyright 2020 Fernando Rincon Martin -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. ------------------------------------------------------------------------------- {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE ImpredicativeTypes #-} {-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE RecordWildCards #-} module Main where import Logic import Network.Wai.Routing.Purescheme.Core import Prelude hiding (id) import Data.Aeson import qualified Data.ByteString as BS import Data.ByteString.Lazy (ByteString) import Data.Maybe (catMaybes) import Data.Text (Text) import GHC.Generics import Network.HTTP.Types (status200, StdMethod(..), Header, hContentType, statusCode) import Network.Wai.Handler.Warp (run) import Network.Wai (responseLBS, Response, Request, Application) data ErrorResponse = ErrorResponse { code :: Int , message :: Text } deriving (Generic, Show) instance ToJSON ErrorResponse main :: IO () main = do context <- initialContext run 8080 $ api context -- The API api :: Context -> Application api context = handleException handleRejections $ alternatives [restApiV1, v1 context] handleRejections :: Rejection -> Application handleRejections Rejection{..} = withContentNegotiationJson $ \negotiate -> complete $ negotiate $ entityResponse status headers $ ErrorResponse (statusCode status) message -- Simple Api restApiV1 :: Application restApiV1 = path "hello" $ method GET $ complete $ responseLBS status200 [(hContentType, "text/html")] "

Hellow World!

" v1 :: Context -> Application v1 context = pathSegment "v1" $ alternatives [ pathSegment "pets" $ alternatives [ pathEnd $ method GET $ listPetsEndpointV1 context , pathVar $ \petId -> pathEnd $ alternatives [ method GET $ showPetByIdEndpointV1 context petId , method PUT $ createOrUpdatePetEndpointV1 context petId ] ] ] listPetsEndpointV1 :: Context -> GenericApplication Response listPetsEndpointV1 context = withContentNegotiationJson $ \negotiate -> mapResponse negotiate $ withOffsetPagination $ \pagination -> completeIO $ ok <$> listPets context pagination showPetByIdEndpointV1 :: Context -> Int -> GenericApplication Response showPetByIdEndpointV1 context petId = withContentNegotiationJson $ \negotiate -> completeIO $ do maybePet <- getPet context petId return $ case maybePet of Nothing -> negotiate $ notFound $ ErrorResponse 404 "Pet nof found" Just pet -> negotiate $ ok pet createOrUpdatePetEndpointV1 :: Context -> Int -> GenericApplication Response createOrUpdatePetEndpointV1 context petId = entityJson $ \pet -> withContentNegotiationJson $ \negotiate -> completeIO $ if petId /= id pet then return $ negotiate $ badRequest $ ErrorResponse 400 "Pet id does not match with the URL" else do isNew <- createOrReplacePet context pet if isNew then return $ negotiate $ created pet else return $ negotiate $ ok pet -- Common abstraction for offset based pagination withOffsetPagination :: (OffsetPagination -> GenericApplication (EntityResponse (ListResult a))) -> GenericApplication (EntityResponse [a]) withOffsetPagination f = maybeSingleParameter "limit" $ \mLimit -> maybeSingleParameter "page" $ \mPage -> withRequest $ \req -> mapResponse (transformResponse req) $ f $ OffsetPagination { limit = mLimit, page = mPage } where transformResponse :: Request -> EntityResponse (ListResult a) -> EntityResponse [a] transformResponse req resp = mapResponseHeaders insertLinkHeader $ mapEntity resultList resp where listResult = entity resp insertLinkHeader :: [Header] -> [Header] insertLinkHeader headers = ("link", linkHeaderValue):filter (\(k, _) -> k /= "link" ) headers linkHeaderValue :: BS.ByteString linkHeaderValue = BS.intercalate "," $ catMaybes [nextHeader, lastHeader, firstHeader, prevHeader] -- TODO Make Link headers nextHeader = Nothing lastHeader = Nothing firstHeader = Nothing prevHeader = Nothing {- -- XmlAndJson Support -- V2 Api: -- Supports JSON and XML v2 :: Context -> Application v2 context = pathSegment "v2" $ alternatives [ pathSegment "pets" $ alternatives [ pathEnd $ method GET $ listPetsEndpointV1 context , pathVar $ \petId -> pathEnd $ alternatives [ method GET $ showPetByIdEndpointV1 context petId , method PUT $ createOrUpdatePetEndpointV1 context petId ] ] ] listPetsEndpointV2 :: Context -> GenericApplication Response listPetsEndpointV2 context = withXmlAndJson $ \negotiate -> mapResponse negotiate $ withOffsetPagination $ \pagination -> completeIO $ ok <$> listPets context pagination showPetByIdEndpointV2 :: Context -> Int -> GenericApplication Response showPetByIdEndpointV2 context petId = withXmlAndJson $ \negotiate -> completeIO $ do maybePet <- getPet context petId return $ case maybePet of Nothing -> negotiate $ notFound $ ErrorResponse 404 "Pet nof found" Just pet -> negotiate $ ok pet createOrUpdatePetEndpointV2 :: Context -> Int -> GenericApplication Response createOrUpdatePetEndpointV2 context petId = requestEntity jsonOrXml $ \pet -> withXmlAndJson $ \negotiate -> completeIO $ if petId /= id pet then return $ negotiate $ badRequest $ ErrorResponse 400 "Pet id does not match with the URL" else do isNew <- createOrReplacePet context pet if isNew then return $ negotiate $ created pet else return $ negotiate $ ok pet jsonOrXml :: (FromJSON a, FromXML a) => [(BS.ByteString, ByteString -> Either String a)] jsonOrXml = [ ("application/json", eitherDecode) , ("application/xml", fromXML) ] withXmlAndJson :: ((forall a. (ToJSON a, ToXML a) => EntityResponse a -> NegotiatedResponse) -> GenericApplication NegotiatedResponse) -> GenericApplication Response withXmlAndJson f = withCustomNegotiation $ f $ negotiated negotiationXmlAndJson negotiationXmlAndJson :: (ToJSON a, ToXML a) => [(BS.ByteString, a -> ByteString)] negotiationXmlAndJson = [ ("application/json", encode) , ("application/xml", toXML) ] class FromXML a where fromXML :: ByteString -> Either String a instance FromXML Pet where fromXML bs = $notImplemented class ToXML a where toXML :: a -> ByteString instance ToXML a => ToXML [a] where toXML a = $notImplemented instance ToXML Pet where toXML a = $notImplemented instance ToXML ErrorResponse where toXML a = $notImplemented -}