-- 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 DefaultSignatures #-} {-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeOperators #-} {-# LANGUAGE UndecidableInstances #-} -- | Support for simple sum types. module Libjwt.Flag ( Flag(..) , AFlag(..) ) where import Libjwt.ASCII import Control.Applicative ( (<|>) ) import Data.Char ( toLower ) import Data.Coerce ( coerce ) import Data.Proxied ( conNameProxied ) import Data.Proxy import GHC.Generics import GHC.TypeLits import Text.Casing -- | Value that is encoded and decoded as 'AFlag' -- -- Flags provide a way to automatically encode and decode simple sum types. -- -- @ -- data Scope = Login | Extended | UserRead | UserWrite | AccountRead | AccountWrite -- deriving stock (Show, Eq, Generic) -- -- instance AFlag Scope -- -- mkPayload = jwtPayload -- (withIssuer "myApp" <> withRecipient "https://myApp.com" <> setTtl 300) -- ( #user_name ->> "John Doe" -- , #is_root ->> False -- , #user_id ->> (12345 :: Int) -- , #scope ->> Flag Login -- ) -- @ newtype Flag a = Flag { getFlag :: a } deriving stock (Show, Eq) -- | Types that can be used as /flags/ . That is, they support conversion to/from ASCII values, -- for example, simple sum types are good candidates that can even be generically derived -- -- -- @ -- data Scope = Login | Extended | UserRead | UserWrite | AccountRead | AccountWrite -- deriving stock (Show, Eq, Generic) -- -- instance AFlag Scope -- @ -- -- >>> getFlagValue UserWrite -- ASCII {getASCII = "userWrite"} -- -- >>> setFlagValue (ASCII "userWrite") :: Maybe Scope -- Just UserWrite class AFlag a where getFlagValue :: a -> ASCII default getFlagValue :: (Generic a, GFlag (Rep a)) => a -> ASCII getFlagValue = ggetFlagValue . from setFlagValue :: ASCII -> Maybe a default setFlagValue :: (Generic a, GFlag (Rep a)) => ASCII -> Maybe a setFlagValue = fmap to . gsetFlagValue instance AFlag a => AFlag (Flag a) where getFlagValue = getFlagValue . getFlag setFlagValue = coerce . setFlagValue @a class GFlag f where ggetFlagValue :: f p -> ASCII gsetFlagValue :: ASCII -> Maybe (f p) instance GFlag f => GFlag (D1 d f) where ggetFlagValue (M1 x) = ggetFlagValue x gsetFlagValue = fmap M1 . gsetFlagValue instance (GFlag l, GFlag r) => GFlag (l :+: r) where ggetFlagValue (L1 x) = ggetFlagValue x ggetFlagValue (R1 x) = ggetFlagValue x gsetFlagValue string = tryL <|> tryR where tryL = L1 <$> gsetFlagValue string tryR = R1 <$> gsetFlagValue string instance Constructor c => GFlag (C1 c U1) where ggetFlagValue _ = ASCII $ conNameToFlagValue c where c = conNameProxied (Proxy :: Proxy (C1 c U1 p)) gsetFlagValue (ASCII flag) = if lower flag == lower c then Just (M1 U1) else Nothing where lower = map toLower c = conNameProxied (Proxy :: Proxy (C1 c U1 p)) conNameToFlagValue :: String -> String conNameToFlagValue = toCamel . fromHumps instance ( TypeError ( 'Text "Only sum types with empty constructors can be flags. For instance," ':$$: 'Text "data Good = A | B | C is ok, but" ':$$: 'Text "data Bad = A Int String | B | C Char is not" ) ) => GFlag (C1 c (any :*: thing)) where ggetFlagValue = error "unreachable" gsetFlagValue = error "unreachable"