{-# LANGUAGE CPP #-}
-- |
-- Module:      Servant.Server.RFC7807
-- Description: Servant support for RFC7807 style error response messages
-- Copyright:   (c) 2020 Peter Trško
-- License:     BSD3
--
-- Maintainer:  peter.trsko@gmail.com
-- Stability:   experimental
-- Portability: GHC specific language extensions.
--
-- Servant support for [RFC7807 — Problem Details for HTTP APIs
-- ](https://tools.ietf.org/html/rfc7807) style response messages.
module Servant.Server.RFC7807
    (
    -- $intro
      rfc7807ServerError

    -- * Mime Type @application\/problem+json@
    , ProblemJSON

    -- * Usage Examples
    --
    -- $usageExamples

    -- ** Direct Use Example
    --
    -- $directUseExample

    -- * Re-exported
    --
    -- | When using 'Rfc7807Error' in more complex way, please, depend on
    -- "Network.HTTP.RFC7807" module directly. More information and more
    -- detailed usage examples can be found in "Network.HTTP.RFC7807" module
    -- documentation.
    , Rfc7807Error(..)

#if !MIN_VERSION_servant_server(0,16,0)
    -- * Servant 0.15 Compatibility
    --
    -- | In @servant-server@ version 0.16 'ServantErr' was renamed to
    -- 'ServerError'. This package provides compatiblity for that version, but
    -- it may be dropped in the near future.
    --
    -- For more information see [@servant-server-1.16 ChangeLog
    -- ](https://hackage.haskell.org/package/servant-server-0.16/changelog).
    , ServerError
#endif
    )
  where

import Data.Function (($))
import Data.List.NonEmpty (NonEmpty((:|)))
import Data.Maybe (Maybe(Just))
import Data.Proxy (Proxy)
import Data.Semigroup ((<>))
import Data.String (fromString)

import qualified Data.Aeson as Aeson (FromJSON, ToJSON, encode)
import Network.HTTP.Media ((//), (/:), renderHeader)
import Network.HTTP.Types (hContentType)
import Servant.API.ContentTypes
  ( Accept(contentTypes)
  , MimeRender(mimeRender)
  , MimeUnrender(mimeUnrender)
  , contentType
  , eitherDecodeLenient
  )
import Servant.Server

import Network.HTTP.RFC7807 (Rfc7807Error(..), rfc7807Error)

-- | Media type defined by
-- <https://tools.ietf.org/html/rfc7807#section-6.1 RFC7807>:
-- @application/problem+json@
--
-- The way how this mime type is handled is the same as
-- 'Servant.API.ContentTypes.JSON'.
data ProblemJSON

-- TODO: This mime type is specifically designed for RFC7807 representation.
-- Should we enforce that in the encoding and decoding?

-- | @application/problem+json; charset=utf-8@
instance Accept ProblemJSON where
    contentTypes :: Proxy ProblemJSON -> NonEmpty MediaType
contentTypes Proxy ProblemJSON
_ = MediaType
ct MediaType -> (ByteString, ByteString) -> MediaType
/: (ByteString
"charset", ByteString
"utf-8") MediaType -> [MediaType] -> NonEmpty MediaType
forall a. a -> [a] -> NonEmpty a
:| [MediaType
ct]
      where
        ct :: MediaType
ct = ByteString
"application" ByteString -> ByteString -> MediaType
// ByteString
"problem+json"

-- | 'Aeson.encode'
instance Aeson.ToJSON a => MimeRender ProblemJSON a where
    mimeRender :: Proxy ProblemJSON -> a -> ByteString
mimeRender Proxy ProblemJSON
_ = a -> ByteString
forall a. ToJSON a => a -> ByteString
Aeson.encode

-- | 'eitherDecodeLenient'
instance Aeson.FromJSON a => MimeUnrender ProblemJSON a where
    mimeUnrender :: Proxy ProblemJSON -> ByteString -> Either String a
mimeUnrender Proxy ProblemJSON
_ = ByteString -> Either String a
forall a. FromJSON a => ByteString -> Either String a
eitherDecodeLenient

#if !MIN_VERSION_servant_server(0,16,0)
-- | Compatibility with newer @servant-server@ versions as 'ServantErr' was
-- renamed to 'ServerError' in version 0.16.
type ServerError = ServantErr
#endif

-- | Construct Servant 'ServerError' with RFC7807 style response body.
--
-- By using Servant abstractions (like 'MimeRender' and 'Accept') we are able
-- to easily integrate with existing code bases.
--
-- === Usage Example
--
-- @
-- data ErrorType
--     = ValidationError
--     -- ...
--
-- instance 'Aeson.ToJSON' ErrorType where
--     toJSON = \\case
--         ValidationError ->
--              'Aeson.String' \"/errors#validation-error\"
--
-- {- ... -} = do
--     {- ... -}
--     unless validationSuccessful do
--         throwError $ 'rfc7807ServerError' (Proxy \@'ProblemJSON') 'err400' ValidationError \\e ->
--             e  { 'title' = \"Request failed to pass data validation\"
--                -- ...
--                }
-- @
rfc7807ServerError
    :: forall body ctype errorType errorInfo context
    .  (MimeRender ctype body)
    => Proxy ctype
    -- ^ Media type to use when encoding the error response body. This allows
    -- us to select appropriate mime type, e.g. 'Servant.API.ContentTypes.JSON'
    -- or 'ProblemJSON'.
    -> ServerError
    -- ^ One of Servant error values e.g. 'err400'.
    -> errorType
    -- ^ Value of the 'type_' field (@\"type\"@ in JSON), the only mandatory
    -- parameter for RFC7807 content.
    -> (Rfc7807Error errorType errorInfo context -> body)
    -- ^ Modify the 'Rfc7807Error' type to your hearts desire.
    --
    -- The @'Rfc7807Error' errorType errorInfo context@ given to this function
    -- will have @type@, @title@, and @status@ set. Values for @title@ and
    -- @status@ are taken from Servant's 'ServerError'. It is highly advised
    -- to modify the @title@ to something more useful.
    --
    -- Reason for the return type to be polymorphic (i.e. @body@) is that we
    -- may want to use a newtype to use a different encoding. This still allows
    -- us to use the @'Rfc7807Error' errorType errorInfo context@ type as a
    -- return type if @errorType@, @errorInfo@, and @context@ can be encoded
    -- into JSON. In other words, 'Data.Function.id' is a valid fit.
    -> ServerError
rfc7807ServerError :: Proxy ctype
-> ServerError
-> errorType
-> (Rfc7807Error errorType errorInfo context -> body)
-> ServerError
rfc7807ServerError
  Proxy ctype
ctype
#if MIN_VERSION_servant_server(0,16,0)
  serverError :: ServerError
serverError@ServerError{Int
errHTTPCode :: ServerError -> Int
errHTTPCode :: Int
errHTTPCode, [Header]
errHeaders :: ServerError -> [Header]
errHeaders :: [Header]
errHeaders, String
errReasonPhrase :: ServerError -> String
errReasonPhrase :: String
errReasonPhrase}
#else
  serverError@ServantErr{errHTTPCode, errHeaders, errReasonPhrase}
#endif
  errorType
errorType'
  Rfc7807Error errorType errorInfo context -> body
f =
    ServerError
serverError
        { errBody :: ByteString
errBody =
            Proxy ctype -> body -> ByteString
forall k (ctype :: k) a.
MimeRender ctype a =>
Proxy ctype -> a -> ByteString
mimeRender Proxy ctype
ctype (body -> ByteString) -> body -> ByteString
forall a b. (a -> b) -> a -> b
$ Rfc7807Error errorType errorInfo context -> body
f (errorType -> Rfc7807Error errorType errorInfo context
forall errorType errorInfo context.
errorType -> Rfc7807Error errorType errorInfo context
rfc7807Error errorType
errorType')
                { $sel:status:Rfc7807Error :: Maybe Int
status = Int -> Maybe Int
forall a. a -> Maybe a
Just Int
errHTTPCode
                , $sel:title:Rfc7807Error :: Maybe Text
title = Text -> Maybe Text
forall a. a -> Maybe a
Just (String -> Text
forall a. IsString a => String -> a
fromString String
errReasonPhrase)
                }

        , errHeaders :: [Header]
errHeaders = [Header]
errHeaders
            [Header] -> [Header] -> [Header]
forall a. Semigroup a => a -> a -> a
<>  [ (HeaderName
hContentType, MediaType -> ByteString
forall h. RenderHeader h => h -> ByteString
renderHeader (Proxy ctype -> MediaType
forall k (ctype :: k). Accept ctype => Proxy ctype -> MediaType
contentType Proxy ctype
ctype))
                ]
        }

-- $intro
--
-- The main functionality of this module is 'rfc7807ServerError', which allows
-- us to create Servant's 'ServerError' values with RFC7807 style body.
-- Implementation is more abstract than strictly necessary to account for the
-- fact that @application/problem+json@ may not always be the best mime type to
-- use. This is especially true if we are migrating existing error responses.
-- Another benefit of the abstract way it's defined is that we can potentially
-- use different encoding or serialisation libraries.
--
-- If you're interested in using this module right away then jump straight to
-- [Usage Examples section](#usage-examples).

-- $usageExamples
--
-- #usage-examples#
--
-- These examples focus on usage of 'rfc7807ServerError', to see examples more
-- related to the 'Rfc7807Error' messages go to "Network.HTTP.RFC7807" module.
--
-- Haskell\/GHC language extensions being used in the examples:
--
-- * @RecordWildCards@ and @NamedFieldPuns@ — please read this great article
--   if you're not familiar with these extensions: [The Power of RecordWildCards
--   by Dmitrii Kovanikov](https://kodimensional.dev/recordwildcards).
--
-- * @OverloadedStrings@ — allows us to define string literals for types like
--   'Text' without needing to manually pack\/convert 'String' values. See
--   [GHC User's Guide — Overloaded string literals
--   ](https://downloads.haskell.org/ghc/latest/docs/html/users_guide/glasgow_exts.html#overloaded-string-literals)
--   for more information.

-- $directUseExample
--
-- This example is intended to illustrate how we can start producing RFC7807
-- style responses without too much fuss. No complex abstractions, no custom
-- wrappers for 'Rfc7807Error', no custom serialisation, and no extra @context@.
--
-- @
-- -- | Servant definition of an endpoint.
-- type SomeEndpoint = {- ... -}
--
-- -- | This code is not complex enough to actually need to be in a function,
-- -- but it makes some things more obious and easier to change.
-- badRequest
--     :: ( MonadError 'ServerError' m
--        , 'Aeson.ToJSON' errorType
--        , 'Aeson.ToJSON' errorInfo
--        )
--     => errorType
--     -> ( 'Rfc7807Error' errorType errorInfo ()
--        -> 'Rfc7807Error' errorType errorInfo ()
--        )
--     -> m a
-- badRequest errorType =
--     throwError . 'rfc7807ServerError' (Proxy \@'ProblemJSON') 'err400' errorType
--
-- -- | See "Network.HTTP.RFC7807" module for more information and examples on
-- -- how to use and define data types to be used for @errorType@.
-- data ErrorType
--     = ValidationError
--     -- ...
--
-- instance 'Aeson.ToJSON' ErrorType where
--     toJSON = \\case
--         ValidationError ->
--              'Data.Aeson.String' \"/errors#some-endpoint-validation-error\"
--
-- someHandler :: 'ServerT' SomeEndpoint m
-- someHandler request = do
--     response <- doTheEndpointStuffBasedOn request
--
--     case response of
--         Success r ->
--             pure r
--
--         InvalidRequest error_@DataValidationFailed ->
--             badRequest ValidationError \\e -> e
--                 { title = \"Request data validation failed\"
--                 , detail = \"One or more members of request's 'data' field\\
--                     \\ failed validation, see 'error' field\"
--
--                 -- If we've used something like \@{\"error\": TheError}\@
--                 -- before switching to RFC7807 then this will be backward
--                 -- compatible. We can also play with the serialisation if we
--                 -- need to preserve backward compatibility. It won't work
--                 -- all the time though.
--                 --
--                 -- Huge downside of this approach is that the error is
--                 -- directly serialised into JSON. API contract can easily be
--                 -- affected by changes that seem unrelated. Please, consider
--                 -- having a separate data type for this purpose or use JSON
--                 -- combinators.
--                 , error_
--                 }
--
--         {- ... -}
-- @