{-# LANGUAGE DataKinds #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE UndecidableInstances #-}
--------------------
-- | Type-directed aeson instance CustomJSONisation
--------------------
module Deriving.Aeson
( CustomJSON(..)
, FieldLabelModifier
, ConstrctorTagModifier
, OmitNothingFields
, TagSingleConstructors
, NoAllNullaryToStringTag
-- * Name modifiers
, StripPrefix
, CamelToKebab
, CamelToSnake
-- * Interface
, AesonOptions(..)
, StringModifier(..)
-- * Reexports
, FromJSON
, ToJSON
, Generic
)where
import Data.Aeson
import Data.Coerce
import Data.List (stripPrefix)
import Data.Maybe (fromMaybe)
import Data.Proxy
import GHC.Generics
import GHC.TypeLits
-- | A newtype wrapper which gives FromJSON/ToJSON instances with modified options.
newtype CustomJSON t a = CustomJSON { unCustomJSON :: a }
instance (AesonOptions t, Generic a, GFromJSON Zero (Rep a)) => FromJSON (CustomJSON t a) where
parseJSON = (coerce `asTypeOf` fmap CustomJSON) . genericParseJSON (aesonOptions @t)
{-# INLINE parseJSON #-}
instance (AesonOptions t, Generic a, GToJSON Zero (Rep a)) => ToJSON (CustomJSON t a) where
toJSON = genericToJSON (aesonOptions @t) . unCustomJSON
{-# INLINE toJSON #-}
-- | Function applied to field labels. Handy for removing common record prefixes for example.
data FieldLabelModifier t
-- | Function applied to constructor tags which could be handy for lower-casing them for example.
data ConstrctorTagModifier t
-- | Record fields with a Nothing value will be omitted from the resulting object.
data OmitNothingFields
-- | Encode types with a single constructor as sums, so that allNullaryToStringTag and sumEncoding apply.
data TagSingleConstructors
-- | the encoding will always follow the 'sumEncoding'.
data NoAllNullaryToStringTag
-- | Strip prefix @t@. If it doesn't have the prefix, keep it as-is.
data StripPrefix t
-- | CamelCase to snake_case
data CamelToSnake
-- | CamelCase to kebab-case
data CamelToKebab
-- | Reify a function which modifies names
class StringModifier t where
getStringModifier :: String -> String
instance KnownSymbol k => StringModifier (StripPrefix k) where
getStringModifier = fromMaybe <*> stripPrefix (symbolVal (Proxy @k))
-- | Left-to-right (@'flip' '.'@) composition
instance (StringModifier a, StringModifier b) => StringModifier (a, b) where
getStringModifier = getStringModifier @b . getStringModifier @a
instance StringModifier CamelToKebab where
getStringModifier = camelTo2 '-'
instance StringModifier CamelToSnake where
getStringModifier = camelTo2 '_'
-- | Reify 'Options' from a type-level list
class AesonOptions xs where
aesonOptions :: Options
instance AesonOptions '[] where
aesonOptions = defaultOptions
instance AesonOptions xs => AesonOptions (OmitNothingFields ': xs) where
aesonOptions = (aesonOptions @xs) { omitNothingFields = True }
instance (StringModifier f, AesonOptions xs) => AesonOptions (FieldLabelModifier f ': xs) where
aesonOptions = (aesonOptions @xs) { fieldLabelModifier = getStringModifier @f }
instance (StringModifier f, AesonOptions xs) => AesonOptions (ConstrctorTagModifier f ': xs) where
aesonOptions = (aesonOptions @xs) { constructorTagModifier = getStringModifier @f }
instance AesonOptions xs => AesonOptions (TagSingleConstructors ': xs) where
aesonOptions = (aesonOptions @xs) { tagSingleConstructors = True }
instance AesonOptions xs => AesonOptions (NoAllNullaryToStringTag ': xs) where
aesonOptions = (aesonOptions @xs) { allNullaryToStringTag = False }