graphql-engine/server/src-dc-api/Autodocodec/Extended.hs
Brandon Simmons 6e8da71ece server: migrate to aeson-2 in preparation for ghc 9.2 upgrade
(Work here originally done by awjchen, rebased and fixed up for merge by
jberryman)

This is part of a merge train towards GHC 9.2 compatibility. The main
issue is the use of the new abstract `KeyMap` in 2.0. See:
https://hackage.haskell.org/package/aeson-2.0.3.0/changelog

Alex's original work is here:
#4305

BEHAVIOR CHANGE NOTE: This change causes a different arbitrary ordering
of serialized Json, for example during metadata export. CLI users care
about this in particular, and so we need to call it out as a _behavior
change_ as we did in v2.5.0. The good news though is that after this
change ordering should be more stable (alphabetical key order).

See: https://hasurahq.slack.com/archives/C01M20G1YRW/p1654012632634389

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/4611
Co-authored-by: awjchen <13142944+awjchen@users.noreply.github.com>
GitOrigin-RevId: 700265162c782739b2bb88300ee3cda3819b2e87
2022-06-08 15:32:27 +00:00

205 lines
7.0 KiB
Haskell

{-# LANGUAGE DeriveAnyClass #-}
module Autodocodec.Extended
( disjointEnumCodec,
HasObjectCodec (..),
DisjunctCodec (..),
disjointMatchChoicesNECodec,
disjointStringConstCodec,
TypeAlternative (..),
sumTypeCodec,
ValueWrapper (..),
ValueWrapper2 (..),
ValueWrapper3 (..),
module Autodocodec,
)
where
import Autodocodec
import Control.DeepSeq (NFData)
import Control.Lens (Prism', review, (<&>), (^?))
import Control.Monad (void)
import Data.Data (Data)
import Data.Hashable (Hashable)
import Data.List.NonEmpty (NonEmpty (..), nonEmpty)
import Data.List.NonEmpty qualified as NE
import Data.Maybe (fromJust)
import Data.Proxy (Proxy (..))
import Data.Text (Text)
import Data.Text qualified as T
import GHC.Generics (Generic)
import GHC.TypeLits (KnownSymbol, Symbol, symbolVal)
import Prelude
-- | A codec for an enum that can be written each with their own codec.
-- Unlike enumCodec, disjointEnumCodec assumes that each provided codec is disjoint.
--
-- === WARNING
--
-- If you don't provide a string for one of the type's constructors, the last codec in the list will be used instead.
disjointEnumCodec ::
forall enum context.
Eq enum =>
NonEmpty (enum, Codec context enum enum) ->
Codec context enum enum
disjointEnumCodec = go
where
go :: NonEmpty (enum, Codec context enum enum) -> Codec context enum enum
go ((e, c) :| rest) = case NE.nonEmpty rest of
Nothing -> c
Just ne -> disjointMatchChoiceCodec c (go ne) $ \i ->
if e == i
then Left e
else Right i
-- | A codec for an enum that can be written as constant string values
-- Unlike stringConstCodec, this function assumes that the provided values and strings are disjoint.
--
-- === Example usage
--
-- >>> data Fruit = Apple | Orange deriving (Show, Eq)
-- >>> let c = disjointStringConstCodec [(Apple, "foo"), (Orange, "bar")]
-- >>> toJSONVia c Orange
-- String "bar"
-- >>> JSON.parseMaybe (parseJSONVia c) (String "foo") :: Maybe Fruit
-- Just Apple
--
-- === WARNING
--
-- If you don't provide a string for one of the type's constructors, the last string in the list will be used instead:
--
-- >>> let c = disjointStringConstCodec [(Apple, "foo")]
-- >>> toJSONVia c Orange
-- String "foo"
disjointStringConstCodec ::
forall constant.
Eq constant =>
NonEmpty (constant, Text) ->
JSONCodec constant
disjointStringConstCodec =
disjointEnumCodec
. NE.map
( \(constant, text) ->
( constant,
literalTextValueCodec constant text
)
)
-- | Class for types that have a `JSONObjectCodec`, but not necessarily
-- a `JSONValueCodec`.
-- Used for providec codecs for sum type alternatives via `TypeAlternative`.
class HasObjectCodec a where
objectCodec :: JSONObjectCodec a
requiredTypeField :: Text -> ObjectCodec void ()
requiredTypeField typeName =
void $ lmapCodec (const typeName) $ requiredFieldWith' "type" $ literalTextCodec typeName
altCodec :: HasObjectCodec a => Text -> Text -> JSONCodec a
altCodec typeName typeFieldValue = object typeName $ requiredTypeField typeFieldValue *> objectCodec
data DisjunctCodec context newInput output where
DisjunctCodec :: (newInput -> Maybe input) -> Codec context input output -> DisjunctCodec context newInput output
-- | A choice codec for a disjoint non-empty list of options
-- Note that this list of options must be complete.
-- There is a variant of newInput for which a DisjunctCodec is not provided
-- then encoding may fail with a call to `error` (via `fromJust`)
disjointMatchChoicesNECodec ::
-- | Codecs, each which their own rendering matcher
NonEmpty (DisjunctCodec context newInput output) ->
Codec context newInput output
disjointMatchChoicesNECodec l = go l
where
go (DisjunctCodec m c :| rest) = case nonEmpty rest of
Nothing -> lmapCodec (fromJust . m) c
Just l' ->
disjointMatchChoiceCodec c (go l') $ \i -> case m i of
Just j -> Left j
Nothing -> Right i
-- | Data needed to generate a codec for one alternative of a sum type `a`.
-- The existenstially quantified type `b` represents the object type
-- contained within the alternative.
data TypeAlternative a where
TypeAlternative ::
HasObjectCodec b =>
-- | Name of the object type for the alternative
Text ->
-- | Value to require in the "type" field of the object
Text ->
-- | Prism to access values of the alternative
Prism' a b ->
TypeAlternative a
-- | A codec for a sum type.
-- Note: the list of `TypeAlternative`s must cover all constructors of the sum type
-- Otherwise encoding may fail with a call to error.
-- This is not checked by the compiler.
-- Example:
-- >
-- > data Field
-- > = ColumnField (ValueWrapper "column" API.V0.ColumnName)
-- > | RelationshipField RelField
-- > deriving stock (Eq, Ord, Show, Generic, Data)
-- >
-- > $(makePrisms ''Field)
-- >
-- > instance HasCodec Field where
-- > codec =
-- > named "Field" $
-- > sumTypeCodec
-- > [ TypeAlternative "ColumnField" "column" _ColumnField,
-- > TypeAlternative "RelationshipField" "relationship" _RelationshipField
-- > ]
sumTypeCodec :: NonEmpty (TypeAlternative a) -> JSONCodec a
sumTypeCodec l =
disjointMatchChoicesNECodec l'
where
l' =
l <&> \(TypeAlternative typeName typeFieldValue p) ->
DisjunctCodec (^? p) $ review p <$> altCodec typeName typeFieldValue
-- Some wrappers to help with sum types.
-- TODO: can we generalize this using HList or something?
-- TODO: add some usage examples
newtype ValueWrapper (t :: Symbol) a = ValueWrapper {getValue :: a}
deriving stock (Eq, Ord, Show, Generic, Data)
deriving anyclass (Hashable, NFData)
instance (KnownSymbol t, HasCodec a) => HasObjectCodec (ValueWrapper t a) where
objectCodec =
ValueWrapper
<$> requiredField' (T.pack $ symbolVal (Proxy :: Proxy t)) .= getValue
data ValueWrapper2 (t1 :: Symbol) a1 (t2 :: Symbol) a2 = ValueWrapper2
{ getValue1 :: a1,
getValue2 :: a2
}
deriving stock (Eq, Ord, Show, Generic, Data)
deriving anyclass (Hashable, NFData)
instance (KnownSymbol t1, KnownSymbol t2, HasCodec a1, HasCodec a2) => HasObjectCodec (ValueWrapper2 t1 a1 t2 a2) where
objectCodec =
ValueWrapper2
<$> requiredField' (T.pack $ symbolVal (Proxy :: Proxy t1)) .= getValue1
<*> requiredField' (T.pack $ symbolVal (Proxy :: Proxy t2)) .= getValue2
data ValueWrapper3 (t1 :: Symbol) a1 (t2 :: Symbol) a2 (t3 :: Symbol) a3 = ValueWrapper3
{ getValue1_ :: a1,
getValue2_ :: a2,
getValue3_ :: a3
}
deriving stock (Eq, Ord, Show, Generic, Data)
deriving anyclass (Hashable, NFData)
instance
(KnownSymbol t1, KnownSymbol t2, KnownSymbol t3, HasCodec a1, HasCodec a2, HasCodec a3) =>
HasObjectCodec (ValueWrapper3 t1 a1 t2 a2 t3 a3)
where
objectCodec =
ValueWrapper3
<$> requiredField' (T.pack $ symbolVal (Proxy :: Proxy t1)) .= getValue1_
<*> requiredField' (T.pack $ symbolVal (Proxy :: Proxy t2)) .= getValue2_
<*> requiredField' (T.pack $ symbolVal (Proxy :: Proxy t3)) .= getValue3_