{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE UndecidableInstances #-}
module Versioning.Servant
( VersionedJSON
, VersionedJSONFrom
)
where
import Data.Aeson (FromJSON, ToJSON, encode,
parseJSON)
import Data.Aeson.Parser (value)
import Data.Aeson.Types (parseEither)
import Data.Attoparsec.ByteString.Char8 (endOfInput, parseOnly,
skipSpace, (<?>))
import qualified Data.ByteString.Lazy as LazyBS
import Data.Kind (Type)
import qualified Data.List.NonEmpty as NonEmpty
import Data.Typeable (Typeable)
import qualified Network.HTTP.Media as Media
import Servant.API (Accept (..), MimeRender (..),
MimeUnrender (..))
import Versioning.Base
import Versioning.Internal.Decoding
import Versioning.JSON
-- | Drop-in replacement for the 'JSON' data-type
-- for seamless integration with servant.
type VersionedJSON = VersionedJSONFrom V0
-- | Like 'VersionedJSON', with an additional type-parameter
-- indicating the oldest version you want to be able to decode
data VersionedJSONFrom (v :: V) deriving Typeable
instance Accept (VersionedJSONFrom from) where
contentTypes _ =
"application" Media.// "json" Media./: ("charset", "utf-8") NonEmpty.:|
[ "application" Media.// "json" ]
-- We add a redundant 'JsonDecodableTo' constraint to minimize the risk
-- of using the 'VersionedJSON' type in the wrong place
instance {-# OVERLAPPABLE #-} (JsonDecodableToFrom from v a, ToJSON (a v))
=> MimeRender (VersionedJSONFrom from) (a v) where
mimeRender _ = encode
instance JsonDecodableToFrom from v a => MimeUnrender (VersionedJSONFrom from) (a v) where
mimeUnrender _ = fromJsonAnyVersionLenientFrom @from
-- | Like 'fromJsonAnyVersionEither', but it uses 'eitherDecodeLenient' for decoding
fromJsonAnyVersionLenientFrom
:: forall from v a
. JsonDecodableToFrom from v a
=> LazyBS.ByteString
-> Either String (a v)
fromJsonAnyVersionLenientFrom = decodeAnyVersionFrom @from jsonDecodeLenient
-- | Lenient JSON decoder
jsonDecodeLenient
:: Decoder FromJSON LazyBS.ByteString (Either String) (a :: V -> Type)
jsonDecodeLenient = Decoder eitherDecodeLenient
-- Copied and pasted from Servant.API.ContentTypes:
-- | Like 'Data.Aeson.eitherDecode' but allows all JSON values instead of just
-- objects and arrays.
--
-- Will handle trailing whitespace, but not trailing junk. ie.
--
-- >>> eitherDecodeLenient "1 " :: Either String Int
-- Right 1
--
-- >>> eitherDecodeLenient "1 junk" :: Either String Int
-- Left "trailing junk after valid JSON: endOfInput"
eitherDecodeLenient :: FromJSON a => LazyBS.ByteString -> Either String a
eitherDecodeLenient input = parseOnly parser (LazyBS.toStrict input)
>>= parseEither parseJSON
where
parser =
skipSpace
*> value
<* skipSpace
<* (endOfInput <?> "trailing junk after valid JSON")