packages feed

libjwt-typed-0.1: src/Libjwt/Encoding.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 FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}

-- | JWT encoding definition
--   
--   __This module can be considered internal to the library__
--   Users should never need to implement the `Encode` typeclass or use any of the exported functions or types directly.
--   You'll only need to know of `Encode` 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.Encoding
  ( EncodeResult
  , Encode(..)
  , ClaimEncoder(..)
  , nullEncode
  )
where

import           Libjwt.Classes
import           Libjwt.FFI.Jwt
import           Libjwt.JsonByteString
import           Libjwt.NumericDate

import           Data.ByteString                ( ByteString )
import           Data.ByteString.Builder        ( Builder
                                                , char7
                                                , string7
                                                , lazyByteString
                                                )
import           Data.ByteString.Builder.Extra  ( toLazyByteStringWith
                                                , safeStrategy
                                                )
import           Data.ByteString.Lazy           ( toStrict )

import           Data.Coerce                    ( coerce )
import           Data.Proxy                     ( Proxy(..) )

type EncodeResult = JwtIO ()

-- | Do not perform any action. It is used to encode things like empty lists or /Nothing/
nullEncode :: b -> EncodeResult
nullEncode = const $ return ()

data EncoderType = Native | Spec | Derived

type family EncoderDef a :: EncoderType where
  EncoderDef (Maybe a)      = 'Spec
  EncoderDef ByteString     = 'Native
  EncoderDef Bool           = 'Native
  EncoderDef Int            = 'Native
  EncoderDef NumericDate    = 'Native
  EncoderDef JsonByteString = 'Native
  EncoderDef String         = 'Derived
  EncoderDef [a]            = 'Spec
  EncoderDef _              = 'Derived

-- | Low-level definition of JWT claims encoding.
class ClaimEncoder t where
  -- | Given a pointer to /jwt_t/, mutate the structure it points to to encode the value as a named claim
  --   It relies on the functions exported from "Libjwt.FFI.Jwt" to perform an /impure/ effect of /encoding/
  encodeClaim :: String -> t -> JwtT -> EncodeResult

instance (EncoderDef a ~ ty, ClaimEncoder' ty a) => ClaimEncoder a where
  encodeClaim = encodeClaim' (Proxy :: Proxy ty)

class ClaimEncoder' (ty :: EncoderType) t where
  encodeClaim' :: proxy ty -> String -> t -> JwtT -> EncodeResult

instance ClaimEncoder a => ClaimEncoder' 'Spec (Maybe a) where
  encodeClaim' _ name (Just val) = encodeClaim name val
  encodeClaim' _ _    Nothing    = nullEncode

instance JsonBuilder a => ClaimEncoder' 'Spec [a] where
  encodeClaim' _ _    [] = nullEncode
  encodeClaim' _ name as = fromJson name $ jsonBuilder as

instance ClaimEncoder' 'Native ByteString where
  encodeClaim' _ = addGrant

instance ClaimEncoder' 'Native Bool where
  encodeClaim' _ = addGrantBool

instance ClaimEncoder' 'Native Int where
  encodeClaim' _ = addGrantInt

instance ClaimEncoder' 'Native NumericDate where
  encodeClaim' _ name = addGrantInt64 name . coerce
  {-# INLINE encodeClaim' #-}

fromJson :: String -> Builder -> JwtT -> JwtIO ()
fromJson name =
  addGrantsFromJson
    . toStrict
    . toLazyByteStringWith (safeStrategy 64 512) mempty
    . encodeAsObject1
 where
  encodeAsObject1 = objectBrackets . ((fieldName <> char7 ':') <>)
   where
    objectBrackets bs = char7 '{' <> bs <> char7 '}'
    fieldName = char7 '"' <> string7 name <> char7 '"'

instance ClaimEncoder' 'Native JsonByteString where
  encodeClaim' _ name = fromJson name . lazyByteString . toJson

instance (JwtRep b a, EncoderDef b ~ ty, ClaimEncoder' ty b) => ClaimEncoder' 'Derived a where
  encodeClaim' _ name = encodeClaim' (Proxy :: Proxy ty) name . rep

-- | Definition of claims encoding.
--   
--   The only use for the user is probably to write a function that is polymorphic in the payload type.
class Encode c where
  -- | Perform the encoding as /impure/ action
  encode :: c -> JwtT -> EncodeResult