by-other-names-1.2.2.0: lib/ByOtherNames/Aeson.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE ApplicativeDo #-}
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneKindSignatures #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
-- | A 'Rubric' for JSON serialization using Aeson, along with some helper
-- newtypes and re-exports.
--
-- Required extensions:
--
-- - DataKinds
-- - DeriveGeneric
-- - DerivingVia
-- - FlexibleInstances
-- - MultiParamTypeClasses
-- - OverloadedStrings
-- - TypeApplications
-- - ScopedTypeVariables
--
-- Example of use for a record type:
--
-- >>> :{
-- data Foo = Foo {aa :: Int, bb :: Bool, cc :: Char}
-- deriving (Read, Show, Eq, Generic)
-- deriving (FromJSON, ToJSON) via (JSONRecord "obj" Foo)
-- instance Aliased JSON Foo where
-- aliases =
-- aliasListBegin
-- $ alias @"aa" "aax"
-- $ alias @"bb" "bbx"
-- $ alias @"cc" "ccx"
-- $ aliasListEnd
-- :}
--
-- Example of use for a sum type:
--
-- >>> :{
-- data Summy
-- = Aa Int
-- | Bb Bool
-- | Cc
-- deriving (Read, Show, Eq, Generic)
-- deriving (FromJSON, ToJSON) via (JSONSum "sum" Summy)
-- instance Aliased JSON Summy where
-- aliases =
-- aliasListBegin
-- $ alias @"Aa" "Aax"
-- $ alias @"Bb" "Bbx"
-- $ alias @"Cc" "Ccx"
-- $ aliasListEnd
-- :}
--
-- Some observations:
--
-- - Fields in branches of sum types can't have selectors. When there is more than one field in a branch, they are parsed as a JSON Array.
--
-- - The "object with a single key consisting in the branch tag" style of serialization is used.
--
-- Sometimes we have enum-like sum types without any fields, and want to
-- serialize them to simple JSON strings, instead of to objects. In that case,
-- we can do the following:
--
-- >>> :{
-- data Enumy
-- = Xx
-- | Yy
-- | Zz
-- deriving (Read, Show, Eq, Generic)
-- deriving (FromJSON, ToJSON) via (JSONEnum Enumy)
-- instance Aliased JSON Enumy where
-- aliases =
-- aliasListBegin
-- $ alias @"Xx" "x"
-- $ alias @"Yy" "y"
-- $ alias @"Zz" "z"
-- $ aliasListEnd
-- :}
--
--
module ByOtherNames.Aeson
( -- * JSON helpers
JSONRubric (..),
JSONRecord (..),
JSONSum (..),
JSONEnum (..),
-- ** Advanced JSON helpers
GeneralJSONRecord (..),
GeneralJSONSum (..),
GeneralJSONEnum (..),
-- * Re-exports from ByOtherNames
Aliased (aliases),
aliasListBegin,
alias,
aliasListEnd,
-- * Re-exports from Data.Aeson
FromJSON,
ToJSON,
)
where
import ByOtherNames
import Data.Aeson
import Data.Aeson.Key (fromText, toText)
import Data.Aeson.Types
import Data.Foldable
import Data.Functor.Compose
import Data.Kind
import Data.Proxy
import Data.Void
import GHC.Generics
import GHC.TypeLits
import ByOtherNames.Constraint
-- | Aliases for JSON serialization fall under this 'Rubric'.
-- The constructor 'JSON' is used as a type, with DataKinds.
data JSONRubric = JSON
-- | The aliases will be of type "Data.Aeson.Key".
instance Rubric JSON where
type AliasType JSON = Key
-- | Helper newtype for deriving 'FromJSON' and 'ToJSON' for record types,
-- using DerivingVia.
--
-- The @objectName@ type parameter of kind 'Symbol' is used in parse error messages.
type JSONRecord :: Symbol -> Type -> Type
newtype JSONRecord objectName r = JSONRecord r
deriving via (GeneralJSONRecord 'JSON objectName r) instance (KnownSymbol objectName, Aliased 'JSON r, GRecord FromJSON (Rep r)) => FromJSON (JSONRecord objectName r)
deriving via (GeneralJSONRecord 'JSON objectName r) instance (Aliased 'JSON r, GRecord ToJSON (Rep r)) => ToJSON (JSONRecord objectName r)
-- | Helper newtype for deriving 'FromJSON' and 'ToJSON' for sum types,
-- using DerivingVia.
--
-- The 'Symbol' type parameter is used in parse error messages.
type JSONSum :: Symbol -> Type -> Type
newtype JSONSum objectName r = JSONSum r
deriving via (GeneralJSONSum 'JSON objectName r) instance (KnownSymbol objectName, Aliased 'JSON r, GSum FromJSON (Rep r)) => FromJSON (JSONSum objectName r)
deriving via (GeneralJSONSum 'JSON objectName r) instance (Aliased 'JSON r, GSum ToJSON (Rep r)) => ToJSON (JSONSum objectName r)
-- | Helper newtype for deriving 'FromJSON' and 'ToJSON' for enum-like sum types,
-- using DerivingVia.
--
-- Each constructor is serialized to a JSON string.
type JSONEnum :: Type -> Type
newtype JSONEnum r = JSONEnum r
deriving via (GeneralJSONEnum 'JSON r) instance (Aliased 'JSON r, GSum Impossible (Rep r)) => FromJSON (JSONEnum r)
deriving via (GeneralJSONEnum 'JSON r) instance (Aliased 'JSON r, GSum Impossible (Rep r)) => ToJSON (JSONEnum r)
newtype EnumBranchParser v = EnumBranchParser {runEnumBranchParser :: Value -> Parser v}
deriving stock (Functor)
newtype BranchParser v = BranchParser {runBranchParser :: Object -> Parser v}
deriving stock (Functor)
newtype ProductInBranchParser1 v = ProductInBranchParser1 {runProductInBranchParser1 :: Value -> Parser v}
deriving stock (Functor)
deriving (Applicative) via (Compose ((->) Value) Parser)
newtype ProductInBranchParser v = ProductInBranchParser {runProductInBranchParser :: [Value] -> Parser (v, [Value])}
deriving stock (Functor)
instance Applicative ProductInBranchParser where
pure v = ProductInBranchParser \vs -> pure (v, vs)
ProductInBranchParser left <*> ProductInBranchParser right =
ProductInBranchParser \vs0 -> do
(f, vs1) <- left vs0
(x, vs2) <- right vs1
pure (f x, vs2)
--
--
newtype FieldParser a = FieldParser (Object -> Parser a)
deriving (Functor, Applicative) via ((->) Object `Compose` Parser)
-- | A more flexible version of 'JSONSum' that lets you use any 'Rubric' whose
-- 'AliasType' is 'Data.Aeson.Key'.
--
-- It allows deriving 'FromJSON' and 'ToJSON' for a newtype, using the generic
-- 'Rep' and the aliases of the underlying type, but __without__ defining
-- 'FromJSON' and 'ToJSON' instances for the underlying type.
--
-- >>> :{
-- data Summy
-- = Aa Int
-- | Bb Bool
-- | Cc
-- deriving (Read, Show, Eq, Generic)
-- data JSONLocal
-- -- We define a local rubric type to avoid colliding "Aliased" instances over Foo.
-- instance Rubric JSONLocal where
-- type AliasType JSONLocal = Key
-- instance Aliased JSONLocal Summy where
-- aliases =
-- aliasListBegin
-- $ alias @"Aa" "Aax"
-- $ alias @"Bb" "Bbx"
-- $ alias @"Cc" "Cc1"
-- $ aliasListEnd
-- newtype SummyN = SummyN Summy
-- deriving (FromJSON, ToJSON) via (GeneralJSONSum JSONLocal "obj" Summy)
-- :}
--
--
type GeneralJSONSum :: rubric -> Symbol -> Type -> Type
newtype GeneralJSONSum rubric objectName r = GeneralJSONSum r
instance (
KnownSymbol objectName,
Rubric rubric,
Aliased rubric r,
AliasType rubric ~ Key,
GSum FromJSON (Rep r)) => FromJSON (GeneralJSONSum rubric objectName r) where
parseJSON v =
let parsers =
gToSum @FromJSON
(aliases @_ @rubric @r)
( \a -> \case
ZeroSlots v -> BranchParser \o -> do
Null :: Value <- o .: a
pure v
SingleSlot p -> BranchParser \o -> do
value <- o .: a
runProductInBranchParser1 p value
ManySlots p -> BranchParser \o -> do
valueList <- o .: a
(prod, _) <- runProductInBranchParser p valueList
pure prod
)
(ProductInBranchParser1 parseJSON)
( ProductInBranchParser \case
[] -> parseFail "not enough field values for branch"
v : vs -> do
r <- parseJSON v
pure (r, vs)
)
parserForObject o = asum $ fmap (($ o) . runBranchParser) parsers
in GeneralJSONSum . to <$> withObject (symbolVal (Proxy @objectName)) parserForObject v
instance (
Rubric rubric,
Aliased rubric r,
AliasType rubric ~ Key,
GSum ToJSON (Rep r)) => ToJSON (GeneralJSONSum rubric objectName r) where
toJSON (GeneralJSONSum o) =
let (key, slots) = gFromSum @ToJSON @(Rep r) @Key @Value @Value (aliases @_ @rubric @r) toJSON (from @r o)
in case slots of
[] -> object [(key, Null)]
[x] -> object [(key, toJSON x)]
xs -> object [(key, toJSON xs)]
-- | A more flexible version of 'JSONRecord' that lets you use any 'Rubric' whose
-- 'AliasType' is 'Data.Aeson.Key'.
--
-- It allows deriving 'FromJSON' and 'ToJSON' for a newtype, using the generic
-- 'Rep' and the aliases of the underlying type, but __without__ defining
-- 'FromJSON' and 'ToJSON' instances for the underlying type.
--
-- >>> :{
-- data Foo = Foo {aa :: Int, bb :: Bool, cc :: Char}
-- deriving (Read, Show, Eq, Generic)
-- data JSONLocal
-- -- We define a local rubric type to avoid colliding "Aliased" instances over Foo.
-- instance Rubric JSONLocal where
-- type AliasType JSONLocal = Key
-- instance Aliased JSONLocal Foo where
-- aliases =
-- aliasListBegin
-- $ alias @"aa" "aax"
-- $ alias @"bb" "bbx"
-- $ alias @"cc" "ccx"
-- $ aliasListEnd
-- newtype FooN = FooN Foo
-- deriving (FromJSON, ToJSON) via (GeneralJSONRecord JSONLocal "obj" Foo)
-- :}
--
--
type GeneralJSONRecord :: rubric -> Symbol -> Type -> Type
newtype GeneralJSONRecord rubric objectName r = GeneralJSONRecord r
instance (KnownSymbol objectName,
Rubric rubric,
Aliased rubric r,
AliasType rubric ~ Key,
GRecord FromJSON (Rep r))
=> FromJSON (GeneralJSONRecord rubric objectName r) where
parseJSON v =
let FieldParser parser =
gToRecord @FromJSON
(aliases @_ @rubric @r)
(\fieldName -> FieldParser (\o -> explicitParseField parseJSON o fieldName))
objectName = symbolVal (Proxy @objectName)
in GeneralJSONRecord . to <$> withObject objectName parser v
instance (Rubric rubric,
AliasType rubric ~ Key,
Aliased rubric r,
GRecord ToJSON (Rep r)) => ToJSON (GeneralJSONRecord rubric objectName r) where
toJSON (GeneralJSONRecord o) =
object $ Data.Foldable.toList $ gFromRecord @ToJSON @(Rep r) @Key (aliases @_ @rubric @r) (\a v -> (a, toJSON v)) (from @r o)
-- | A more flexible version of 'JSONEnum' that lets you use any 'Rubric' whose
-- 'AliasType' is 'Data.Aeson.Key'.
--
-- It allows deriving 'FromJSON' and 'ToJSON' for a newtype, using the generic
-- 'Rep' and the aliases of the underlying type, but __without__ defining
-- 'FromJSON' and 'ToJSON' instances for the underlying type.
--
-- >>> :{
-- data Enumy
-- = Xx
-- | Yy
-- | Zz
-- deriving (Read, Show, Eq, Generic)
-- data JSONLocal
-- -- We define a local rubric type to avoid colliding "Aliased" instances over Enumy.
-- instance Rubric JSONLocal where
-- type AliasType JSONLocal = Key
-- instance Aliased JSONLocal Enumy where
-- aliases =
-- aliasListBegin
-- $ alias @"Xx" "x"
-- $ alias @"Yy" "y"
-- $ alias @"Zz" "z"
-- $ aliasListEnd
-- -- We use the underlying Enumy type in DerivingVia.
-- newtype EnumyN = EnumyN Enumy
-- deriving (FromJSON, ToJSON) via (GeneralJSONEnum JSONLocal Enumy)
-- :}
--
--
type GeneralJSONEnum :: rubric -> Type -> Type
newtype GeneralJSONEnum rubric r = GeneralJSONEnum r
--
--
instance (
Rubric rubric,
AliasType rubric ~ Key,
Aliased rubric r,
GSum Impossible (Rep r)) => FromJSON (GeneralJSONEnum rubric r) where
parseJSON v =
let parsers =
gToSum @Impossible
(aliases @_ @rubric @r)
( \a -> \case
ZeroSlots x -> EnumBranchParser \case
String a' | a == fromText a' -> pure x
_ -> mempty
SingleSlot _ -> EnumBranchParser mempty
ManySlots _ -> EnumBranchParser mempty
)
Proxy
Proxy
parserForValue v = asum $ fmap (($ v) . runEnumBranchParser) parsers
in GeneralJSONEnum . to <$> parserForValue v
instance (
Rubric rubric,
AliasType rubric ~ Key,
Aliased rubric r,
GSum Impossible (Rep r))
=> ToJSON (GeneralJSONEnum rubric r) where
toJSON (GeneralJSONEnum o) =
let (key, slots) = gFromSum @Impossible @(Rep r) @Key @Value @Value (aliases @_ @rubric @r) absurd (from @r o)
in case slots of
[] -> String (toText key)
[_] -> error "never happens"
_ -> error "never happens"
-- $setup
--
-- >>> :set -XBlockArguments
-- >>> :set -XTypeApplications
-- >>> :set -XDerivingStrategies
-- >>> :set -XDerivingVia
-- >>> :set -XDataKinds
-- >>> :set -XMultiParamTypeClasses
-- >>> :set -XDeriveGeneric
-- >>> :set -XOverloadedStrings
-- >>> :set -XTypeFamilies
-- >>> :set -XDerivingStrategies
-- >>> :set -XDerivingVia
-- >>> import ByOtherNames.Aeson
-- >>> import Data.Aeson
-- >>> import Data.Aeson.Types
-- >>> import GHC.Generics
-- >>> import GHC.TypeLits