{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
module Data.Aeson.Deriving.Internal.Generic where
import Data.Aeson
import Data.Aeson.Deriving.Known
import Data.Aeson.Deriving.Internal.RecordSum
import Data.Aeson.Deriving.Utils
import Data.Aeson.Types (modifyFailure)
import Data.Char (isUpper, toLower, toUpper)
import Data.Function ((&))
import qualified Data.HashMap.Strict as HashMap
import Data.Kind (Constraint, Type)
import Data.List (intercalate, stripPrefix)
import Data.Maybe (fromMaybe)
import Data.Proxy (Proxy (..))
import Data.Text (pack)
import GHC.Generics
import GHC.TypeLits
class ToAesonOptions a where
toAesonOptions :: Proxy a -> Options
instance ToAesonOptions '[] where toAesonOptions Proxy = defaultOptions
instance (ToAesonOptionsField x, ToAesonOptions xs) => ToAesonOptions (x ': xs) where
toAesonOptions Proxy =
let
patch = toAesonOptionsField (Proxy @x)
opts = toAesonOptions (Proxy @xs)
in
patch $ defaultOptions
{ fieldLabelModifier = fieldLabelModifier opts
, constructorTagModifier = constructorTagModifier opts
, allNullaryToStringTag = allNullaryToStringTag opts
, omitNothingFields = omitNothingFields opts
, sumEncoding = sumEncoding opts
, unwrapUnaryRecords = unwrapUnaryRecords opts
, tagSingleConstructors = tagSingleConstructors opts
}
instance TypeError ToAesonOptionsListError => ToAesonOptions [] where toAesonOptions = undefined
instance TypeError ToAesonOptionsListError => ToAesonOptions [a] where toAesonOptions = undefined
type ToAesonOptionsListError =
( 'Text "aeson-deriving constraint error for ToAesonOptions class:"
':$$: 'Text "Don't forget to \"tick\" your opening list bracket."
':$$: 'Text "There is no ToAesonOptions instance for list types."
':$$: 'Text "Rather, there are instances for promoted list values."
':$$: 'Text ""
':$$: 'Text "You likely should correct your deriving declaration to something like:"
':$$: 'Text ""
':$$: 'Text " via GenericEncoded '[myVal1,..]"
':$$: 'Text ""
':$$: 'Text "Instead of:"
':$$: 'Text ""
':$$: 'Text " via GenericEncoded [myVal1,..]"
':$$: 'Text ""
':$$: 'Text "For explanation, see GHC documentation on datatype promotion:"
':$$: 'Text "https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/glasgow_exts.html#datatype-promotion"
':$$: 'Text ""
)
class ToAesonOptionsField x where
toAesonOptionsField :: Proxy x -> Options -> Options
data FieldLabelModifier
data ConstructorTagModifier
data AllNullaryToStringTag
data OmitNothingFields
data UnwrapUnaryRecords
data TagSingleConstructors
instance StringFunction f => ToAesonOptionsField (FieldLabelModifier := f) where
toAesonOptionsField Proxy opts = opts {fieldLabelModifier = stringFunction $ Proxy @f}
instance StringFunction f => ToAesonOptionsField (ConstructorTagModifier := f) where
toAesonOptionsField Proxy opts = opts {constructorTagModifier = stringFunction $ Proxy @f}
instance KnownBool b => ToAesonOptionsField (AllNullaryToStringTag := b) where
toAesonOptionsField Proxy opts = opts {allNullaryToStringTag = boolVal $ Proxy @b}
instance KnownBool b => ToAesonOptionsField (OmitNothingFields := b) where
toAesonOptionsField Proxy opts = opts {omitNothingFields = boolVal $ Proxy @b}
instance ToSumEncoding se => ToAesonOptionsField (SumEncoding := se) where
toAesonOptionsField Proxy opts = opts {sumEncoding = toSumEncoding $ Proxy @se}
instance KnownBool b => ToAesonOptionsField (UnwrapUnaryRecords := b) where
toAesonOptionsField Proxy opts = opts {unwrapUnaryRecords = boolVal $ Proxy @b}
instance KnownBool b => ToAesonOptionsField (TagSingleConstructors := b) where
toAesonOptionsField Proxy opts = opts {tagSingleConstructors = boolVal $ Proxy @b}
data GenericOptions
:: fieldLabelModifier
-> constructorTagModifier
-> allNullaryToStringTag
-> omitNothingFields
-> sumEncoding
-> unwrapUnaryRecords
-> tagSingleConstructors
-> Type
instance
( All StringFunction [fieldLabelModifier, constructorTagModifier]
, ToSumEncoding sumEncoding
, All KnownBool
[ allNullaryToStringTag
, omitNothingFields
, unwrapUnaryRecords
, tagSingleConstructors
]
) => ToAesonOptions
(GenericOptions
(FieldLabelModifier := fieldLabelModifier)
(ConstructorTagModifier := constructorTagModifier)
(AllNullaryToStringTag := allNullaryToStringTag)
(OmitNothingFields := omitNothingFields)
(SumEncoding := sumEncoding)
(UnwrapUnaryRecords := unwrapUnaryRecords)
(TagSingleConstructors := tagSingleConstructors)) where
toAesonOptions _ = defaultOptions
{ fieldLabelModifier = stringFunction $ Proxy @fieldLabelModifier
, constructorTagModifier = stringFunction $ Proxy @constructorTagModifier
, allNullaryToStringTag = boolVal $ Proxy @allNullaryToStringTag
, omitNothingFields = boolVal $ Proxy @omitNothingFields
, sumEncoding = toSumEncoding $ Proxy @sumEncoding
, unwrapUnaryRecords = boolVal $ Proxy @unwrapUnaryRecords
, tagSingleConstructors = boolVal $ Proxy @tagSingleConstructors
}
newtype GenericEncoded opts a = GenericEncoded a
instance
( ToAesonOptions opts
, Generic a
, GFromJSON Zero (Rep a))
=> FromJSON (GenericEncoded opts a) where
parseJSON = fmap GenericEncoded . genericParseJSON (toAesonOptions $ Proxy @opts)
instance
( ToAesonOptions opts
, Generic a
, GToJSON Zero (Rep a))
=> ToJSON (GenericEncoded opts a) where
toJSON (GenericEncoded x)
= genericToJSON (toAesonOptions (Proxy @opts)) x
type family LoopWarning (n :: Type -> Type) (a :: Type) :: Constraint where
LoopWarning n (GenericEncoded opts a) = ()
LoopWarning n (RecordSumEncoded tagKey tagValMod a) = ()
LoopWarning n (DisableLoopWarning a) = ()
LoopWarning n (x & f) = LoopWarning n (f x)
LoopWarning n (f x) = LoopWarning n x
LoopWarning n x = TypeError
( 'Text "Uh oh! Watch out for those infinite loops!"
':$$: 'Text "Newtypes that recursively modify aeson instances, namely:"
':$$: 'Text ""
':$$: 'Text " " ':<>: 'ShowType n
':$$: 'Text ""
':$$: 'Text "must only be used atop a type that creates the instances non-recursively: "
':$$: 'Text ""
':$$: 'Text " ○ GenericEncoded"
':$$: 'Text " ○ RecordSumEncoded"
':$$: 'Text ""
':$$: 'Text "We observe instead the inner type: "
':$$: 'Text ""
':$$: 'Text " " ':<>: 'ShowType x
':$$: 'Text ""
':$$: 'Text "You probably created an infinitely recursive encoder/decoder pair."
':$$: 'Text "See `LoopWarning` for details."
':$$: 'Text "This check can be disabled by wrapping the inner type in `DisableLoopWarning`."
':$$: 'Text ""
)
newtype DisableLoopWarning a = DisableLoopWarning a
deriving newtype (FromJSON, ToJSON)
newtype RecordSumEncoded (tagKey :: Symbol) (tagModifier :: k) (a :: Type) = RecordSumEncoded a
instance
( Generic a
, GFromJSON Zero (Rep a)
, GTagParserMap (Rep a)
, Rep a ~ D1 meta cs
, Datatype meta
, StringFunction tagModifier
, KnownSymbol tagKey)
=> FromJSON (RecordSumEncoded tagKey tagModifier a) where
parseJSON val = prependErrMsg outerErrorMsg . flip (withObject "Object") val $ \hm -> do
tagVal <- hm .: pack tagKeyStr
case HashMap.lookup tagVal parserMap of
Nothing -> fail . mconcat $
[ "We are not expecting a payload with tag value " <> backticks tagVal
, " under the " <> backticks tagKeyStr <> " key here. "
, "Expected tag values: "
, intercalate ", " $ backticks <$> HashMap.keys parserMap
, "."
]
Just parser -> RecordSumEncoded . to <$> parser val
& prependErrMsg
("Failed parsing the case with tag value "
<> backticks tagVal <> " under the "
<> backticks tagKeyStr <> " key: ")
where
tagKeyStr = symbolVal $ Proxy @tagKey
ParserMap parserMap
= unsafeMapKeys (stringFunction $ Proxy @tagModifier)
. gParserMap
$ Proxy @(Rep a)
backticks str = "`" <> str <> "`"
prependErrMsg str = modifyFailure (str <>)
outerErrorMsg = "Failed to parse a " <> datatypeName @meta undefined <> ": "
instance
( Generic a
, GToJSON Zero (Rep a))
=> ToJSON (RecordSumEncoded tagKey tagModifier a) where
toJSON (RecordSumEncoded x) =
toJSON $ GenericEncoded @'[SumEncoding := UntaggedValue] x
stripSuffix :: Eq a => [a] -> [a] -> Maybe [a]
stripSuffix a b = reverse <$> stripPrefix (reverse a) (reverse b)
dropPrefix :: Eq a => [a] -> [a] -> [a]
dropPrefix a b = fromMaybe b $ stripPrefix a b
dropSuffix :: Eq a => [a] -> [a] -> [a]
dropSuffix a b = fromMaybe b $ stripSuffix a b
class StringFunction (a :: k) where
stringFunction :: Proxy a -> String -> String
data Id
data SnakeCase
data Uppercase
data Lowercase
data DropLowercasePrefix
data DropPrefix (str :: Symbol)
data DropSuffix (str :: Symbol)
instance StringFunction Id where stringFunction _ = id
instance StringFunction SnakeCase where stringFunction _ = snakeCase
instance StringFunction Uppercase where stringFunction _ = map toUpper
instance StringFunction Lowercase where stringFunction _ = map toLower
instance StringFunction DropLowercasePrefix where stringFunction _ = dropLowercasePrefix
instance KnownSymbol str => StringFunction (DropPrefix str) where
stringFunction Proxy = dropPrefix (symbolVal $ Proxy @str)
instance KnownSymbol str => StringFunction (DropSuffix str) where
stringFunction Proxy = dropSuffix (symbolVal $ Proxy @str)
instance StringFunction '[] where stringFunction _ = id
instance (StringFunction x, StringFunction xs) => StringFunction (x ': xs) where
stringFunction Proxy = stringFunction (Proxy @x) . stringFunction (Proxy @xs)
instance All KnownSymbol [a, b] => StringFunction (a ==> b) where
stringFunction Proxy x
| x == symbolVal (Proxy @a) = symbolVal (Proxy @b)
| otherwise = x
class ToSumEncoding a where
toSumEncoding :: Proxy a -> SumEncoding
data UntaggedValue
data ObjectWithSingleField
data TwoElemArray
data TaggedObject (tagFieldName :: Symbol) (contentsFieldName :: Symbol)
instance ToSumEncoding UntaggedValue where toSumEncoding _ = UntaggedValue
instance ToSumEncoding ObjectWithSingleField where toSumEncoding _ = ObjectWithSingleField
instance ToSumEncoding TwoElemArray where toSumEncoding _ = TwoElemArray
instance (KnownSymbol tag, KnownSymbol contents) => ToSumEncoding (TaggedObject tag contents) where
toSumEncoding _ = TaggedObject
(symbolVal $ Proxy @tag)
(symbolVal $ Proxy @contents)
snakeCase :: String -> String
snakeCase = camelTo2 '_'
dropLowercasePrefix :: String -> String
dropLowercasePrefix [] = []
dropLowercasePrefix (x:xs)
| isUpper x = x : xs
| otherwise = dropLowercasePrefix xs
infixl 2 &
newtype (x & f) = Ampersand {unAmpersand :: f x }
deriving newtype (FromJSON, ToJSON)