{-# LANGUAGE DataKinds #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE InstanceSigs #-} {-# LANGUAGE PolyKinds #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeOperators #-} {-# LANGUAGE UndecidableInstances #-} {-# OPTIONS_GHC -Wno-orphans #-} -------------------------------------------------------------------------------- -- | -- Module : Data.Extensible.Envy -- Copyright : (c) 2020 IIJ Innovation Institute, Inc. -- License : BSD3 -- Maintainer: YAMAMOTO Yuji -- Stability : Experimental -- -- Provides 'Env.FromEnv' instance for 'Ex.Record' and functions to -- create 'Ex.Record' from environment variable using 'Env.Parser'. -------------------------------------------------------------------------------- module Data.Extensible.Envy ( recordFromEnvWith , recordFromEnv , FieldLabelToEnvName , defaultFieldLabelToEnvName , camelToUpperSnakeCase ) where import qualified Data.Char as C import Data.Extensible (Forall, Instance1) import qualified Data.Extensible as Ex import Data.Functor.Identity (Identity (Identity), runIdentity) import Data.Kind (Type) import Data.Proxy (Proxy (Proxy)) import GHC.TypeLits (KnownSymbol, Symbol) import qualified System.Envy as Env -- | Function to convert field labels of 'Ex.Record' into the -- name of environment variable. -- Applied to each field label before passing it to 'Env.env' type FieldLabelToEnvName = String -> String -- | The default of 'FieldLabelToEnvName'. -- -- If the first argument is empty, just convert the field label -- (second argument) into @UPPER_SNAKE_CASE@. -- -- >>> defaultFieldLabelToEnvName "" "thisIsATest" -- "THIS_IS_A_TEST" -- -- Otherwise, convert the field label into @UPPER_SNAKE_CASE@, -- then prepend the first argument with an underscore @_@. -- -- >>> defaultFieldLabelToEnvName "PREFIXED" "thisIsATest" -- "PREFIXED_THIS_IS_A_TEST" defaultFieldLabelToEnvName :: String -> FieldLabelToEnvName defaultFieldLabelToEnvName "" s = camelToUpperSnakeCase s defaultFieldLabelToEnvName prefix s = prefix ++ "_" ++ camelToUpperSnakeCase s {-# INLINE defaultFieldLabelToEnvName #-} -- | Used internally in 'defaultFieldLabelToEnvName'. -- Published for your convenience. camelToUpperSnakeCase :: FieldLabelToEnvName camelToUpperSnakeCase = foldMap (\c -> if C.isUpper c then '_' : [c] else [C.toUpper c]) {-# INLINE camelToUpperSnakeCase #-} -- | -- @ -- recordFromEnv = recordFromEnvWith $ defaultFieldLabelToEnvName "" -- @ recordFromEnv :: forall (xs :: [Ex.Assoc Symbol Type]) h . Forall (Ex.KeyTargetAre KnownSymbol (Instance1 Env.Var h)) xs => Env.Parser (Ex.RecordOf h xs) recordFromEnv = recordFromEnvWith $ defaultFieldLabelToEnvName "" {-# INLINE recordFromEnv #-} -- | 'Env.Parser' for 'Ex.Record' recordFromEnvWith :: forall (xs :: [Ex.Assoc Symbol Type]) h . Forall (Ex.KeyTargetAre KnownSymbol (Instance1 Env.Var h)) xs => FieldLabelToEnvName -> Env.Parser (Ex.RecordOf h xs) recordFromEnvWith fl2en = Ex.hgenerateFor (Proxy :: Proxy (Ex.KeyTargetAre KnownSymbol (Instance1 Env.Var h))) f where f membership = Ex.Field <$> Env.env (fl2en $ Ex.stringKeyOf membership) {-# INLINE recordFromEnvWith #-} -- | Returns 'recordFromEnv' when the first argument of 'Env.fromEnv' is @Nothing@. instance Forall (Ex.KeyTargetAre KnownSymbol (Instance1 Env.Var h)) xs => Env.FromEnv (Ex.RecordOf (h :: Type -> Type) xs) where fromEnv (Just r) = pure r fromEnv Nothing = recordFromEnv {-# INLINE fromEnv #-} -- | Necessary to make 'Ex.Record' an instance of 'Env.FromEnv' instance Env.Var a => Env.Var (Identity a) where toVar = Env.toVar . runIdentity {-# INLINE toVar #-} fromVar = fmap Identity . Env.fromVar {-# INLINE fromVar #-}