cfg-0.0.2.0: src/Cfg/Deriving/KeyModifier.hs
-- |
-- Module : Cfg.Deriving.KeyModifier
-- Copyright : © Jonathan Lorimer, 2023
-- License : MIT
-- Maintainer : jonathanlorimer@pm.me
-- Stability : stable
--
-- @since 0.0.2.0
--
-- This module provides type level tags that can be used to configure string
-- transformations against configuration keys derived from things like record
-- fields.
module Cfg.Deriving.KeyModifier
( -- * Key Modifiers
KeyModifier (..)
, Identity
, ToLower
, ToUpper
, LowerFirst
, UpperFirst
, StripPrefix
, StripSuffix
, CamelTo
, CamelToSnake
, CamelToKebab
-- * Helper Functions
, mapFirst
, camelTo
, camelToText
)
where
import Cfg.Options (RootKey (..))
import Data.Char (isLower, isUpper, toLower, toUpper)
import Data.Data (Proxy (..))
import Data.Functor
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import Data.Text qualified as T
import GHC.TypeLits
-- | Identity transformation, corresponds to 'id', does not change the string.
--
-- @since 0.0.2.0
data Identity
-- | Lower cases all alphabetical characters, corresponds to 'Data.Text.toLower'.
--
-- @since 0.0.2.0
data ToLower
-- | Upper cases all alphabetical characters, corresponds to 'Data.Text.toUpper'.
--
-- @since 0.0.2.0
data ToUpper
-- | Lower cases the first character, corresponds to 'Data.Char.toLower'.
--
-- @since 0.0.2.0
data LowerFirst
-- | Upper cases the first character, corresponds to 'Data.Char.toUpper'.
--
-- @since 0.0.2.0
data UpperFirst
-- | Takes a type level string and removes that from the beginning of the text,
-- corresponds to 'Data.Text.stripPrefix'.
--
-- @since 0.0.2.0
data StripPrefix (prefix :: Symbol)
-- | Takes a type level string and removes that from the end of the text,
-- corresponds to 'Data.Text.stripSuffix.
--
-- @since 0.0.2.0
data StripSuffix (suffix :: Symbol)
-- | Takes a type level character known as the \"separator"\ and will break the
-- camel case string on its \"humps\" and then rejoin the string with the
-- separator.
--
-- @since 0.0.2.0
data CamelTo (separator :: Char)
-- | Specialized version of `CamelTo` where the separator is \"_\". Results in
-- snake cased strings.
--
-- @since 0.0.2.0
type CamelToSnake = CamelTo '_'
-- | Specialized version of `CamelTo` where the separator is \"-\". Results in
-- kebab cased strings.
--
-- @since 0.0.2.0
type CamelToKebab = CamelTo '-'
-- | This typeclass turns a type level \"tag\" into a function from @Text ->
-- Text@. In addition to the instances for the \"tags\", there are also
-- instances for type level lists and tuples up to an arity of 4.
--
-- __important__: For type level lists and tuples the modifiers are applied in
-- order from left to right.
--
-- >>> getKeyModifier @'[ToUpper, ToLower] "Hello World"
-- "hello world"
--
-- >>> getKeyModifier @(ToLower, ToUpper) "Hello World"
-- "HELLO WORLD"
--
-- >>> getKeyModifier @CamelToSnake "iLoveCFGProject"
-- "i_love_cfg_project"
--
-- @since 0.0.2.0
class KeyModifier t where
getKeyModifier :: Text -> Text
instance (KeyModifier k) => KeyModifier ('TypeName k) where
getKeyModifier = getKeyModifier @k
instance (KeyModifier k) => KeyModifier ('ConstructorName k) where
getKeyModifier = getKeyModifier @k
instance KeyModifier Identity where
getKeyModifier = id
instance KeyModifier '() where
getKeyModifier = id
instance KeyModifier '[] where
getKeyModifier = id
instance (KeyModifier a, KeyModifier as) => KeyModifier (a ': as) where
getKeyModifier = getKeyModifier @as . getKeyModifier @a
instance (KeyModifier a, KeyModifier b) => KeyModifier (a, b) where
getKeyModifier = getKeyModifier @b . getKeyModifier @a
instance (KeyModifier a, KeyModifier b, KeyModifier c) => KeyModifier (a, b, c) where
getKeyModifier = getKeyModifier @c . getKeyModifier @b . getKeyModifier @a
instance (KeyModifier a, KeyModifier b, KeyModifier c, KeyModifier d) => KeyModifier (a, b, c, d) where
getKeyModifier = getKeyModifier @d . getKeyModifier @c . getKeyModifier @b . getKeyModifier @a
instance KeyModifier ToLower where
getKeyModifier = T.toLower
instance KeyModifier ToUpper where
getKeyModifier = T.toUpper
instance KeyModifier LowerFirst where
getKeyModifier t = fromMaybe t $ mapFirst toLower t
instance KeyModifier UpperFirst where
getKeyModifier t = fromMaybe t $ mapFirst toUpper t
instance (KnownSymbol prefix) => KeyModifier (StripPrefix prefix) where
getKeyModifier label =
fromMaybe label . T.stripPrefix (T.pack . symbolVal $ Proxy @prefix) $ label
instance (KnownSymbol prefix) => KeyModifier (StripSuffix prefix) where
getKeyModifier label =
fromMaybe label . T.stripSuffix (T.pack . symbolVal $ Proxy @prefix) $ label
instance (KnownChar separator) => KeyModifier (CamelTo separator) where
getKeyModifier = camelToText (charVal $ Proxy @separator)
-- | Map over the first character of a stream of 'Data.Text.Text'
--
-- @since 0.0.2.0
mapFirst :: (Char -> Char) -> Text -> Maybe Text
mapFirst f text = T.uncons text <&> \(first, rest) -> f first `T.cons` rest
-- | Function for breaking a camel case string on its \"humps\" and re-joining
-- on a provided separator char.
--
-- @since 0.0.2.0
camelTo
:: Char
-- ^ Separator character
-> String
-- ^ Camel cased string
-> String
camelTo c = map toLower . go2 . go1
where
go1 "" = ""
go1 (x : u : l : xs) | isUpper u && isLower l = x : c : u : l : go1 xs
go1 (x : xs) = x : go1 xs
go2 "" = ""
go2 (l : u : xs) | isLower l && isUpper u = l : c : u : go2 xs
go2 (x : xs) = x : go2 xs
-- | "Data.Text.Text" version of 'camelTo'
--
-- @since 0.0.2.0
camelToText :: Char -> Text -> Text
camelToText c = T.pack . camelTo c . T.unpack