{-# 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 ------------------------------------------------------------------------------------------ -- Main class ------------------------------------------------------------------------------------------ -- | A class for defining 'Options' for Aeson's Generic deriving support. -- It is generally instantiated by overriding specific fields using the instance -- for (type-level) list values. It can also be instantiated in a more exhaustive way -- using the 'GenericOptions' type. In both cases fields are specified in a record-like -- form using the '(:=)' data type for explicitness. -- -- See the ReadMe or tests for examples. -- -- Users may also provide instances for their own phantom data types if desired. 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 } -- Its easy to get confusing errors if you forget to tick the list syntax. Hence the custom error 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 "" ) -- | A class that knows about fields of aeson's 'Options'. class ToAesonOptionsField x where toAesonOptionsField :: Proxy x -> Options -> Options -- | Represents an aeson 'Options' field to be set with `(:=)`. See `ToAesonOptions` data FieldLabelModifier -- | Represents an aeson 'Options' field to be set with `(:=)`. See `ToAesonOptions` data ConstructorTagModifier -- | Represents an aeson 'Options' field to be set with `(:=)`. See `ToAesonOptions` data AllNullaryToStringTag -- | Represents an aeson 'Options' field to be set with `(:=)`. See `ToAesonOptions` data OmitNothingFields -- SumEncoding type name already exists in aeson. We repurpose it. -- | Represents an aeson 'Options' field to be set with `(:=)`. See `ToAesonOptions` data UnwrapUnaryRecords -- | Represents an aeson 'Options' field to be set with `(:=)`. See `ToAesonOptions` 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} ------------------------------------------------------------------------------------------ -- A Single type for all Options fields ------------------------------------------------------------------------------------------ -- | Type-level representation of the Aeson Generic deriving 'Options'. -- This representation is useful for explicitly setting all options. 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 } -- | Specify your encoding scheme in terms of aeson's out-of-the box Generic -- functionality. This type is never used directly, only "coerced through". -- Use some of the pre-defined types supplied here for the @opts@ phantom parameter, -- or define your with an instance of 'ToAesonOptions'. 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 -- | Used in FromJSON/ToJSON superclass constraints for newtypes that recursively modify -- the instances. A guard against the common mistake of deriving encoders in terms -- of such a newtype over the naked base type instead of the 'GenericEncoded' version. -- This can lead to nasty runtime bugs. -- -- This error can be disabled by wrapping your type in 'DisableLoopWarning'. -- This should never be necessary to use the functionality of this package. It may be -- required if you, for example, combine our newtypes with another library's types -- for generating aeson instances. 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 "" ) -- | Assert that you know what you're doing and to nullify the 'LoopWarning' constraint -- family. This should not be necessary. newtype DisableLoopWarning a = DisableLoopWarning a deriving newtype (FromJSON, ToJSON) ------------------------------------------------------------------------------------------ -- Sums over records ------------------------------------------------------------------------------------------ -- | An encoding scheme for sums of records that are defined as distinct data types. -- If we have a number of record types we want to combine under a sum, a straightforward -- solution is to ensure that each each inner type uses a constructor tag, and then -- derive the sum with @SumEncoding := UntaggedValue@. This works fine for the happy -- path, but makes for very bad error messages, since it means that decoding proceeds by -- trying each case in sequence. Thus error messages always pertain to the last type in -- the sum, even when it wasn't the intended payload. This newtype improves on that -- solution by providing the relevant error messages, by remembering the correspondence -- between the constructor tag and the intended inner type/parser. -- -- In order to work correctly, the inner types must use the 'TaggedObject' encoding. -- The same tag field name and 'ConstructorTagModifier' must be supplied to this type. 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 ------------------------------------------------------------------------------------------ -- String functions ------------------------------------------------------------------------------------------ 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 -- | Applies 'snakeCase' data SnakeCase data Uppercase data Lowercase data FirstChar a -- | Applies 'dropLowercasePrefix', dropping until the first uppercase character. 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 instance StringFunction a => StringFunction (FirstChar a) where stringFunction Proxy = \case [] -> [] c:cs -> stringFunction (Proxy @a) [c] ++ cs ------------------------------------------------------------------------------------------ -- Sum type encodings ------------------------------------------------------------------------------------------ -- | Type-level encoding for 'SumEncoding' class ToSumEncoding a where toSumEncoding :: Proxy a -> SumEncoding data UntaggedValue data ObjectWithSingleField data TwoElemArray -- | A constructor will be encoded to an object with a field tagFieldName which specifies -- the constructor tag (modified by the constructorTagModifier). If the constructor is -- a record the encoded record fields will be unpacked into this object. So make sure -- that your record doesn't have a field with the same label as the tagFieldName. -- Otherwise the tag gets overwritten by the encoded value of that field! If the -- constructor is not a record the encoded constructor contents will be stored under -- the contentsFieldName field. data TaggedObject (tagFieldName :: Symbol) (contentsFieldName :: Symbol) -- Would be nice to have separate types for records versus ordinary constructors -- rather than conflating them with the conditional interpretation of this type. -- However, this module is just about modeling what aeson gives us. 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) ------------------------------------------------------------------------------------------ -- Utilities ------------------------------------------------------------------------------------------ -- | Field name modifier function that separates camel-case words by underscores -- (i.e. on capital letters). Also knows to handle a consecutive sequence of -- capitals as a single word. snakeCase :: String -> String snakeCase = camelTo2 '_' -- | Drop the first lowercase sequence (i.e. until 'isUpper' returns True) from the start -- of a string. Used for the common idiom where fields are prefixed by the type name in -- all lowercase. The definition is taken from the aeson-casing package. 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)