packages feed

libjwt-typed-0.1: src/Libjwt/Decoding.hs

--   This Source Code Form is subject to the terms of the Mozilla Public
--   License, v. 2.0. If a copy of the MPL was not distributed with this
--   file, You can obtain one at http://mozilla.org/MPL/2.0/.

{-# OPTIONS_HADDOCK show-extensions #-}

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}

-- | JWT decoding definition
--   
--   __This module can be considered internal to the library__
--   Users should never need to implement the `Decode` typeclass or use any of the exported functions or types directly.
--   You'll only need to know of `Decode` typeclass if you want to write a function polymorphic in the type of payloads. 
--
--   If you want to extend the types supported by the library, see "Libjwt.Classes"
module Libjwt.Decoding
  ( DecodeResult(..)
  , hoistResult
  , ClaimDecoder(..)
  , Decode(..)
  , getOrEmpty
  , decodeClaimOrThrow
  , decodeClaimProxied
  , Decodable
  , JwtIO
  )
where

import           Libjwt.Classes
import           Libjwt.Exceptions              ( MissingClaim(..) )
import           Libjwt.FFI.Jwt
import           Libjwt.JsonByteString
import           Libjwt.NumericDate

import           Control.Applicative            ( Alternative )
import           Control.Monad                  ( (<=<) )

import           Control.Monad.Catch            ( throwM )

import           Control.Monad.Trans.Maybe

import           Data.ByteString                ( ByteString )

import           Data.Coerce
import           Data.Kind                      ( Constraint )
import           Data.Maybe                     ( fromMaybe )
import           Data.Proxy

newtype DecodeResult t = Result { getOptional :: JwtIO (Maybe t) }
  deriving (Functor, Applicative, Monad, Alternative) via (MaybeT JwtIO)

-- | Lift pure value
hoistResult :: Maybe a -> DecodeResult a
hoistResult = Result . pure

-- | Action that returns 'mempty' if decoding has failed
getOrEmpty :: (Monoid a) => DecodeResult a -> JwtIO a
getOrEmpty (Result x) = fromMaybe mempty <$> x

-- | 'decodeClaim' through proxy
decodeClaimProxied
  :: (ClaimDecoder t) => String -> proxy t -> JwtT -> DecodeResult t
decodeClaimProxied name _ = decodeClaim name

-- | Action that throws 'MissingClaim' if decoding has failed
decodeClaimOrThrow :: (ClaimDecoder t) => String -> proxy t -> JwtT -> JwtIO t
decodeClaimOrThrow name p =
  maybe (throwM $ Missing name) return
    <=< getOptional
    .   decodeClaimProxied name p

data DecoderType = Native | Derived

type family DecoderDef a :: DecoderType where
  DecoderDef ByteString     = 'Native
  DecoderDef Bool           = 'Native
  DecoderDef Int            = 'Native
  DecoderDef NumericDate    = 'Native
  DecoderDef JsonByteString = 'Native
  DecoderDef String         = 'Derived
  DecoderDef [a]            = 'Native
  DecoderDef _              = 'Derived

-- | Low-level definition of claims decoding.
class ClaimDecoder t where
  -- | Given a pointer to /jwt_t/, try to decode the value of type @t@
  decodeClaim :: String -> JwtT -> DecodeResult t

instance (DecoderDef a ~ ty, ClaimDecoder' ty a) => ClaimDecoder a where
  decodeClaim = decodeClaim' (Proxy :: Proxy ty)

class ClaimDecoder' (ty :: DecoderType) t where
  decodeClaim' :: proxy ty -> String -> JwtT -> DecodeResult t

instance ClaimDecoder' 'Native ByteString where
  decodeClaim' _ name = Result . getGrant name
  {-# INLINE decodeClaim' #-}

instance ClaimDecoder' 'Native Bool where
  decodeClaim' _ name = Result . getGrantBool name
  {-# INLINE decodeClaim' #-}

instance ClaimDecoder' 'Native Int where
  decodeClaim' _ name = Result . getGrantInt name
  {-# INLINE decodeClaim' #-}

instance ClaimDecoder' 'Native NumericDate where
  decodeClaim' _ name = coerce . getGrantInt64 name
  {-# INLINE decodeClaim' #-}

instance ClaimDecoder' 'Native JsonByteString where
  decodeClaim' _ name = fmap jsonFromStrict . Result . getGrantAsJson name
  {-# INLINE decodeClaim' #-}

fromJsonNative
  :: (JsonByteString -> JwtIO (Maybe a)) -> String -> JwtT -> DecodeResult a
fromJsonNative f name =
  (Result . f) <=< decodeClaim' (Proxy :: Proxy 'Native) name
{-# INLINE fromJsonNative #-}

instance JsonParser a => ClaimDecoder' 'Native [a] where
  decodeClaim' _ =
    fromJsonNative
      $ fmap (sequence =<<)
      . unsafeMapTokenizedJsonArray jsonParser
      . toJsonStrict
  {-# INLINE decodeClaim' #-}

instance (JwtRep b a, DecoderDef b ~ ty, ClaimDecoder' ty b) => ClaimDecoder' 'Derived a where
  decodeClaim' _ name =
    (hoistResult . unRep) <=< decodeClaim' (Proxy :: Proxy ty) name

type family Decodable t :: Constraint where
  Decodable t = ClaimDecoder' (DecoderDef t) t

-- | Definition of claims decoding.
--   
--   The only use for the user is probably to write a function that is polymorphic in the payload type
class Decode c where
  -- | Construct an action that decodes the value of type @c@, given a pointer to /jwt_t/. The action may fail.
  decode :: JwtT -> JwtIO c