packages feed

cookie-tray-0.0.0.0: library/CookieTray.hs

module CookieTray
  ( -- * Command
    render,
    renderLBS,
    ToCommandList (..),
    Command,
    Action (..),
    renderCommand,
    BinaryCommand,
    binaryCommandByteStringLazy,

    -- * Tray
    Tray (..),
    parse,
    lookup,
    fromList,
    toList,

    -- * Name
    Name (..),
    Named (..),

    -- * Value
    Value (..),

    -- * Expiry
    Expiry (..),
    Expiring (..),

    -- * Meta

    -- ** Security
    Security (..),
    Secured (..),
    Origin (..),
    TransportEncryption (..),
    SameSiteOptions (..),
    SameSiteStrictness (..),
    JavascriptAccess (..),

    -- ** Scope
    Scope (..),
    Domain (..),
    Path (..),
    Meta (..),
  )
where

import CookieTray.Command (Command, ToCommandList (..))
import CookieTray.Command qualified as Command
import CookieTray.Types
import Data.Binary.Builder qualified as Binary
import Data.ByteString qualified as BS
import Data.ByteString.Char8 qualified as BS.Char8
import Data.ByteString.Lazy qualified as LBS
import Data.Functor (Functor, fmap, (<&>))
import Data.Map (Map)
import Data.Map qualified as Map
import Data.Monoid (Endo (..), Monoid (mempty))
import Data.Semigroup (Semigroup ((<>)))
import Data.Time.Clock.POSIX qualified as Time
import GHC.Exts (IsList, Item)
import GHC.Exts qualified as IsList (IsList (..))
import Web.Cookie qualified as Web
import Prelude (Bool (..), Eq, Maybe (..), Ord, Show, ($), (.))

---  Tray  ---

newtype Tray a = Tray (Map Name a)
  deriving (Eq, Ord, Show, Functor)

-- | Left-biased map union
instance Semigroup (Tray a) where
  Tray x <> Tray y = Tray (x <> y)

instance Monoid (Tray a) where
  mempty = Tray mempty

instance IsList (Tray a) where
  type Item (Tray a) = Named a
  fromList = fromList
  toList = toList

parse :: BS.ByteString -> Tray Value
parse =
  fromList
    . fmap (\(a, b) -> Named {name = Name a, value = Value b})
    . Web.parseCookies

toList :: Tray a -> [Named a]
toList (Tray m) =
  Map.toList m <&> \(a, b) ->
    Named {name = a, value = b}

fromList :: [Named a] -> Tray a
fromList = Tray . Map.fromList . fmap (\x -> (name x, value x))

lookup :: Name -> Tray a -> Maybe a
lookup x (Tray m) = Map.lookup x m

---  Command  ---

renderCommand :: Command -> BinaryCommand
renderCommand = renderSetCookie . applyToSetCookie

render :: (ToCommandList a) => a -> [BinaryCommand]
render = fmap renderCommand . toCommandList

renderLBS :: (ToCommandList a) => a -> [LBS.ByteString]
renderLBS = fmap binaryCommandByteStringLazy . render

---  Rendering internals  ---

renderSetCookie :: Endo Web.SetCookie -> BinaryCommand
renderSetCookie f =
  BinaryCommand $ Binary.toLazyByteString $ Web.renderSetCookie $ appEndo f Web.def

class ApplyToSetCookie a where
  applyToSetCookie :: a -> Endo Web.SetCookie

instance ApplyToSetCookie Command where
  applyToSetCookie x =
    applyToSetCookie (Command.name x)
      <> applyToSetCookie (Command.meta x)
      <> applyToSetCookie (Command.action x)

instance (ApplyToSetCookie a) => ApplyToSetCookie (Named a) where
  applyToSetCookie x =
    applyToSetCookie (name x)
      <> applyToSetCookie (value x)

instance ApplyToSetCookie Name where
  applyToSetCookie x = Endo \sc -> sc {Web.setCookieName = nameByteString x}

instance ApplyToSetCookie Value where
  applyToSetCookie x = Endo \sc -> sc {Web.setCookieValue = valueByteString x}

instance ApplyToSetCookie TransportEncryption where
  applyToSetCookie x = Endo \sc -> sc {Web.setCookieSecure = y}
    where
      y = case x of
        RequireEncryptedTransport -> True
        AllowUnencryptedTransport -> False

instance ApplyToSetCookie Security where
  applyToSetCookie x =
    applyToSetCookie (jsAccess x)
      <> applyToSetCookie (origin x)

instance ApplyToSetCookie Origin where
  applyToSetCookie = \case
    SameSite o -> applyToSetCookie o
    CrossSite -> Endo \sc ->
      sc
        { Web.setCookieSameSite = Just Web.sameSiteNone,
          Web.setCookieSecure = True -- When SameSite=None, Secure is required
        }

instance ApplyToSetCookie SameSiteOptions where
  applyToSetCookie x =
    applyToSetCookie (sameSiteStrictness x)
      <> applyToSetCookie (transportEncryption x)

instance ApplyToSetCookie SameSiteStrictness where
  applyToSetCookie x = Endo \sc ->
    sc
      { Web.setCookieSameSite = Just
          case x of
            SameSiteStrict -> Web.sameSiteStrict
            SameSiteLax -> Web.sameSiteLax
      }

instance ApplyToSetCookie JavascriptAccess where
  applyToSetCookie x = Endo \sc -> sc {Web.setCookieHttpOnly = y}
    where
      y = case x of
        HiddenFromJavascript -> True
        AccessibleFromJavascript -> False

instance ApplyToSetCookie Domain where
  applyToSetCookie x = Endo \sc -> sc {Web.setCookieDomain = y}
    where
      y = case x of
        Domain z -> Just z
        CurrentHostExcludingSubdomains -> Nothing

instance ApplyToSetCookie Expiry where
  applyToSetCookie = \case
    ExpiryTime x -> Endo \sc -> sc {Web.setCookieExpires = Just x}
    ExpiryAge x -> Endo \sc -> sc {Web.setCookieMaxAge = Just x}

instance ApplyToSetCookie Path where
  applyToSetCookie x = Endo \sc -> sc {Web.setCookiePath = y}
    where
      y = case x of
        Path z -> Just z
        CurrentPath -> Nothing

instance ApplyToSetCookie Scope where
  applyToSetCookie x =
    applyToSetCookie (domain x)
      <> applyToSetCookie (path x)

instance ApplyToSetCookie Meta where
  applyToSetCookie x =
    applyToSetCookie (metaScope x)
      <> applyToSetCookie (metaSecurity x)

instance (ApplyToSetCookie a) => ApplyToSetCookie (Secured a) where
  applyToSetCookie x =
    applyToSetCookie (security x)
      <> applyToSetCookie (secured x)

instance (ApplyToSetCookie a) => ApplyToSetCookie (Action a) where
  applyToSetCookie = \case
    Put x -> applyToSetCookie x
    Delete ->
      applyToSetCookie (ExpiryTime (Time.posixSecondsToUTCTime 0))
        <> applyToSetCookie (Value (BS.Char8.pack "x"))

instance (ApplyToSetCookie a) => ApplyToSetCookie (Expiring a) where
  applyToSetCookie x =
    applyToSetCookie (expiry x)
      <> applyToSetCookie (expiring x)