project-m36-0.8.1: src/lib/ProjectM36/Tupleable/Deriving.hs
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE UndecidableInstances #-}
-- | Newtypes for deriving Tupleable instances with customization using
-- @DerivingVia@.
--
-- Inspired by
-- [Dhall.Deriving](https://hackage.haskell.org/package/dhall-1.33.1/docs/Dhall-Deriving.html)
-- which in turn was inspired by Matt Parson's blog post
-- [Mirror Mirror: Reflection and Encoding Via](https://www.parsonsmatt.org/2020/02/04/mirror_mirror.html).
--
-- required extensions:
--
-- * DerivingVia
-- * DeriveGenerics
-- * TypeOperators (for @('<<<')@ and @('>>>')@)
-- * DataKinds (for types that take a string argument)
module ProjectM36.Tupleable.Deriving
( -- * DerivingVia Newtype
Codec(..)
-- * Type-level Options
, ModifyOptions(..)
, Field
-- * Type-level 'T.Text' -> 'T.Text' Functions
, ModifyText(..)
, AddPrefix
, DropPrefix
, AddSuffix
, DropSuffix
, UpperCase
, LowerCase
, TitleCase
, CamelCase
, PascalCase
, SnakeCase
, SpinalCase
, TrainCase
-- * Composition
, AsIs
, type (<<<)
, type (>>>)
-- * Re-Exports
, Generic
, module ProjectM36.Tupleable
) where
import Data.Maybe (fromMaybe)
import Data.Proxy
import qualified Data.Text as T
import Data.Text.Manipulate
import GHC.TypeLits
import GHC.Generics (Generic, Rep)
import ProjectM36.Tupleable
-- | A newtype wrapper to allow for easier deriving of 'Tupleable' instances
-- with customization.
--
-- The @tag@ type variable can be used to specify options for converting the
-- datatype to and from a 'RelationTuple'. For example,
--
-- > data Example = Example
-- > { exampleFoo :: Int
-- > , exampleBar :: Int
-- > }
-- > deriving stock (Generic)
-- > deriving (Tupleable)
-- > via Codec (Field (DropPrefix "example" >>> CamelCase)) Example
--
-- will derive an instance of 'Tupleable' where field names are translated into
-- attribute names by dropping the prefix @"example"@ and then converting the
-- result to camelCase. So @"exampleFoo"@ becomes @"foo"@ and @"exampleBar"@
-- becomes @"bar"@.
--
-- Requires the @DerivingGeneric@ and @DerivingVia@ extensions to be enabled.
newtype Codec tag a = Codec { unCodec :: a }
instance (ModifyOptions tag, Generic a, TupleableG (Rep a)) => Tupleable (Codec tag a) where
toTuple v = genericToTuple opts (unCodec v)
where
opts = modifyOptions (Proxy :: Proxy tag) defaultTupleableOptions
fromTuple tup = Codec <$> genericFromTuple opts tup
where
opts = modifyOptions (Proxy :: Proxy tag) defaultTupleableOptions
toAttributes _ = genericToAttributes opts (Proxy :: Proxy a)
where
opts = modifyOptions (Proxy :: Proxy tag) defaultTupleableOptions
-- | Types that can be used as tags for 'Codec'.
class ModifyOptions a where
modifyOptions :: proxy a -> TupleableOptions -> TupleableOptions
-- | Change how record field names are translated into attribute names. For
-- example,
--
-- > Field SnakeCase
--
-- will translate the field name @fooBar@ into the attribute name @foo_bar@.
data Field a
instance ModifyText a => ModifyOptions (Field a) where
modifyOptions _ opts = opts { fieldModifier = newFieldModifier }
where
newFieldModifier = modifyText (Proxy :: Proxy a) . fieldModifier opts
-- | Types that can be used in options that modify 'T.Text' such as in 'Field'.
class ModifyText a where
modifyText :: proxy a -> T.Text -> T.Text
-- | Add a prefix. @AddPrefix "foo"@ will transform @"bar"@ into @"foobar"@.
data AddPrefix (prefix :: Symbol)
instance KnownSymbol prefix => ModifyText (AddPrefix prefix) where
modifyText _ oldText = prefixText <> oldText
where
prefixText = T.pack (symbolVal (Proxy :: Proxy prefix))
-- | Drop a prefix. @DropPrefix "bar"@ will transform @"foobar"@ into @"foo"@.
data DropPrefix (prefix :: Symbol)
instance KnownSymbol prefix => ModifyText (DropPrefix prefix) where
modifyText _ oldText = fromMaybe oldText (T.stripPrefix prefixText oldText)
where
prefixText = T.pack (symbolVal (Proxy :: Proxy prefix))
-- | Add a suffix. @AddSuffix "bar"@ will transform @"foo"@ into @"foobar"@.
data AddSuffix (suffix :: Symbol)
instance KnownSymbol suffix => ModifyText (AddSuffix suffix) where
modifyText _ oldText = oldText <> suffixText
where
suffixText = T.pack (symbolVal (Proxy :: Proxy suffix))
-- | Drop a suffix. @DropSuffix "bar"@ will transform @"foobar"@ into @"foo"@.
data DropSuffix (suffix :: Symbol)
instance KnownSymbol suffix => ModifyText (DropSuffix suffix) where
modifyText _ oldText = fromMaybe oldText (T.stripSuffix suffixText oldText)
where
suffixText = T.pack (symbolVal (Proxy :: Proxy suffix))
-- | Convert to UPPERCASE. Will transform @"foobar"@ into @\"FOOBAR\"@.
data UpperCase
instance ModifyText UpperCase where
modifyText _ = T.toUpper
-- | Convert to lowercase. Will transform @\"FOOBAR\"@ into @"foobar"@.
data LowerCase
instance ModifyText LowerCase where
modifyText _ = T.toLower
-- | Convert to Title Case. Will transform @"fooBar"@ into @\"Foo Bar\"@.
data TitleCase
instance ModifyText TitleCase where
modifyText _ = toTitle
-- | Convert to camelCase. Will transform @"foo_bar"@ into @"fooBar"@.
data CamelCase
instance ModifyText CamelCase where
modifyText _ = toCamel
-- | Convert to PascalCase. Will transform @"foo_bar"@ into @\"FooBar\"@.
data PascalCase
instance ModifyText PascalCase where
modifyText _ = toPascal
-- | Convert to snake_case. Will transform @"fooBar"@ into @"foo_bar"@.
data SnakeCase
instance ModifyText SnakeCase where
modifyText _ = toSnake
-- | Convert to spinal-case. will transform @"fooBar"@ into @"foo-bar"@.
data SpinalCase
instance ModifyText SpinalCase where
modifyText _ = toSpinal
-- | Convert to Train-Case. Will transform @"fooBar"@ into @\"Foo-Bar\"@.
data TrainCase
instance ModifyText TrainCase where
modifyText _ = toTrain
-- | Identity option.
type AsIs = ()
instance ModifyOptions () where
modifyOptions _ = id
instance ModifyText () where
modifyText _ = id
-- | Right to left composition.
--
-- Requires the @TypeOperators@ extension to be enabled.
data a <<< b
instance (ModifyOptions a, ModifyOptions b) => ModifyOptions (a <<< b) where
modifyOptions _ = modifyOptions (Proxy :: Proxy a) . modifyOptions (Proxy :: Proxy b)
instance (ModifyText a, ModifyText b) => ModifyText (a <<< b) where
modifyText _ = modifyText (Proxy :: Proxy a) . modifyText (Proxy :: Proxy b)
-- | Left to right composition.
--
-- Requires the @TypeOperators@ extension to be enabled.
data a >>> b
instance (ModifyOptions a, ModifyOptions b) => ModifyOptions (a >>> b) where
modifyOptions _ = modifyOptions (Proxy :: Proxy b) . modifyOptions (Proxy :: Proxy a)
instance (ModifyText a, ModifyText b) => ModifyText (a >>> b) where
modifyText _ = modifyText (Proxy :: Proxy b) . modifyText (Proxy :: Proxy a)