{-# 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")