packages feed

generic-override-aeson 0.3.0.0 → 0.4.0.0

raw patch · 5 files changed

+253/−78 lines, 5 filesdep ~generic-overridePVP ok

version bump matches the API change (PVP)

Dependency ranges changed: generic-override

API changes (from Hackage documentation)

- Data.Override.Aeson: instance (GHC.Types.Coercible a (Data.Override.Internal.Using ms a xs), Data.Aeson.Types.FromJSON.FromJSON (Data.Override.Internal.Using ms a xs)) => Data.Aeson.Types.FromJSON.FromJSON (Data.Override.Internal.Overridden ms a xs)
- Data.Override.Aeson: instance (GHC.Types.Coercible a (Data.Override.Internal.Using ms a xs), Data.Aeson.Types.ToJSON.ToJSON (Data.Override.Internal.Using ms a xs)) => Data.Aeson.Types.ToJSON.ToJSON (Data.Override.Internal.Overridden ms a xs)
+ Data.Override.Aeson: AllNullaryToStringTag :: Bool -> AesonOption
+ Data.Override.Aeson: OmitNothingFields :: AesonOption
+ Data.Override.Aeson: SumEncodingObjectWithSingleField :: AesonOption
+ Data.Override.Aeson: SumEncodingTaggedObject :: Symbol -> Symbol -> AesonOption
+ Data.Override.Aeson: SumEncodingTwoElemArray :: AesonOption
+ Data.Override.Aeson: SumEncodingUntaggedValue :: AesonOption
+ Data.Override.Aeson: TagSingleConstructors :: AesonOption
+ Data.Override.Aeson: UnwrapUnaryRecords :: AesonOption
+ Data.Override.Aeson: WithAesonOptions :: a -> WithAesonOptions (a :: *) (options :: [AesonOption])
+ Data.Override.Aeson: data AesonOption
+ Data.Override.Aeson: newtype WithAesonOptions (a :: *) (options :: [AesonOption])
+ Data.Override.Aeson.Options.Internal: AllNullaryToStringTag :: Bool -> AesonOption
+ Data.Override.Aeson.Options.Internal: OmitNothingFields :: AesonOption
+ Data.Override.Aeson.Options.Internal: SumEncodingObjectWithSingleField :: AesonOption
+ Data.Override.Aeson.Options.Internal: SumEncodingTaggedObject :: Symbol -> Symbol -> AesonOption
+ Data.Override.Aeson.Options.Internal: SumEncodingTwoElemArray :: AesonOption
+ Data.Override.Aeson.Options.Internal: SumEncodingUntaggedValue :: AesonOption
+ Data.Override.Aeson.Options.Internal: TagSingleConstructors :: AesonOption
+ Data.Override.Aeson.Options.Internal: UnwrapUnaryRecords :: AesonOption
+ Data.Override.Aeson.Options.Internal: WithAesonOptions :: a -> WithAesonOptions (a :: *) (options :: [AesonOption])
+ Data.Override.Aeson.Options.Internal: applyAesonOption :: ApplyAesonOption option => Proxy option -> Options -> Options
+ Data.Override.Aeson.Options.Internal: applyAesonOptions :: ApplyAesonOptions options => Proxy options -> Options -> Options
+ Data.Override.Aeson.Options.Internal: class ApplyAesonOption (option :: AesonOption)
+ Data.Override.Aeson.Options.Internal: class ApplyAesonOptions (options :: [AesonOption])
+ Data.Override.Aeson.Options.Internal: data AesonOption
+ Data.Override.Aeson.Options.Internal: instance (Data.Override.Aeson.Options.Internal.ApplyAesonOption option, Data.Override.Aeson.Options.Internal.ApplyAesonOptions options) => Data.Override.Aeson.Options.Internal.ApplyAesonOptions (option : options)
+ Data.Override.Aeson.Options.Internal: instance (Data.Override.Aeson.Options.Internal.ApplyAesonOptions options, GHC.Generics.Generic a, Data.Aeson.Types.Class.GToJSON Data.Aeson.Types.Generic.Zero (GHC.Generics.Rep a), Data.Aeson.Types.Class.GToEncoding Data.Aeson.Types.Generic.Zero (GHC.Generics.Rep a)) => Data.Aeson.Types.ToJSON.ToJSON (Data.Override.Aeson.Options.Internal.WithAesonOptions a options)
+ Data.Override.Aeson.Options.Internal: instance (Data.Override.Aeson.Options.Internal.ApplyAesonOptions options, GHC.Generics.Generic a, Data.Aeson.Types.FromJSON.GFromJSON Data.Aeson.Types.Generic.Zero (GHC.Generics.Rep a)) => Data.Aeson.Types.FromJSON.FromJSON (Data.Override.Aeson.Options.Internal.WithAesonOptions a options)
+ Data.Override.Aeson.Options.Internal: instance (GHC.TypeLits.KnownSymbol k, GHC.TypeLits.KnownSymbol v) => Data.Override.Aeson.Options.Internal.ApplyAesonOption ('Data.Override.Aeson.Options.Internal.SumEncodingTaggedObject k v)
+ Data.Override.Aeson.Options.Internal: instance Data.Override.Aeson.Options.Internal.ApplyAesonOption 'Data.Override.Aeson.Options.Internal.OmitNothingFields
+ Data.Override.Aeson.Options.Internal: instance Data.Override.Aeson.Options.Internal.ApplyAesonOption 'Data.Override.Aeson.Options.Internal.SumEncodingObjectWithSingleField
+ Data.Override.Aeson.Options.Internal: instance Data.Override.Aeson.Options.Internal.ApplyAesonOption 'Data.Override.Aeson.Options.Internal.SumEncodingTwoElemArray
+ Data.Override.Aeson.Options.Internal: instance Data.Override.Aeson.Options.Internal.ApplyAesonOption 'Data.Override.Aeson.Options.Internal.SumEncodingUntaggedValue
+ Data.Override.Aeson.Options.Internal: instance Data.Override.Aeson.Options.Internal.ApplyAesonOption 'Data.Override.Aeson.Options.Internal.TagSingleConstructors
+ Data.Override.Aeson.Options.Internal: instance Data.Override.Aeson.Options.Internal.ApplyAesonOption 'Data.Override.Aeson.Options.Internal.UnwrapUnaryRecords
+ Data.Override.Aeson.Options.Internal: instance Data.Override.Aeson.Options.Internal.ApplyAesonOption ('Data.Override.Aeson.Options.Internal.AllNullaryToStringTag 'GHC.Types.False)
+ Data.Override.Aeson.Options.Internal: instance Data.Override.Aeson.Options.Internal.ApplyAesonOption ('Data.Override.Aeson.Options.Internal.AllNullaryToStringTag 'GHC.Types.True)
+ Data.Override.Aeson.Options.Internal: instance Data.Override.Aeson.Options.Internal.ApplyAesonOptions '[]
+ Data.Override.Aeson.Options.Internal: newtype WithAesonOptions (a :: *) (options :: [AesonOption])

Files

CHANGELOG.md view
@@ -1,5 +1,14 @@ # Changelog for generic-override-aeson +## 0.4.0.0++* Add `WithAesonOptions` support+* Bumping dependency bounds to support generic-override 0.4+* Because this version of generic-override no longer uses `Overridden` under+    the hood, this solves a problem where `omitNothingFields` had no effect+    due to aeson's incoherent `Maybe` instance not being solved. The new+    encoding solves this problem and `omitNothingFields` works as expected.+ ## 0.3.0.0  * Bumping dependency bounds to support generic-override 0.3
generic-override-aeson.cabal view
@@ -1,6 +1,6 @@ cabal-version:      1.12 name:               generic-override-aeson-version:            0.3.0.0+version:            0.4.0.0 license:            BSD3 license-file:       LICENSE copyright:          2020 Estatico Studios LLC@@ -25,14 +25,18 @@     location: https://github.com/estatico/generic-override  library-    exposed-modules:  Data.Override.Aeson+    exposed-modules:+        Data.Override.Aeson+        Data.Override.Aeson.Options.Internal+     hs-source-dirs:   src     other-modules:    Paths_generic_override_aeson     default-language: Haskell2010+    ghc-options:      -Wall     build-depends:         aeson >=1.4 && <3,         base >=4.7 && <5,-        generic-override >=0.3.0.0 && <0.4+        generic-override >=0.4.0.0 && <0.5  test-suite generic-override-aeson-test     type:             exitcode-stdio-1.0@@ -43,11 +47,11 @@         Paths_generic_override_aeson      default-language: Haskell2010-    ghc-options:      -threaded -rtsopts -with-rtsopts=-N+    ghc-options:      -Wall -threaded -rtsopts -with-rtsopts=-N     build-depends:         aeson >=1.4 && <3,         base >=4.7 && <5,-        generic-override >=0.3.0.0 && <0.4,+        generic-override >=0.4.0.0 && <0.5,         generic-override-aeson -any,         hspec >=2.7.1 && <2.8,         text >=1.2.3.1 && <1.3
src/Data/Override/Aeson.hs view
@@ -1,46 +1,27 @@--- | This module contains only orphan instances. It is only needed to--- be imported where you are overriding instances for aeson generic derivation.--{-# LANGUAGE AllowAmbiguousTypes #-}-{-# LANGUAGE DataKinds #-}+-- | The public, stable @generic-override-aeson@ API.+-- Provides orphan instances for 'Override' as well as customization+-- for aeson's 'Options' when using @DerivingVia@. {-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeApplications #-}-{-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE MonoLocalBinds #-} {-# LANGUAGE UndecidableInstances #-} {-# OPTIONS_GHC -fno-warn-orphans #-}-module Data.Override.Aeson where+module Data.Override.Aeson+  ( WithAesonOptions(..)+  , AesonOption(..)+  ) where -import Data.Coerce (Coercible, coerce)-import Data.Override.Internal (Override, Overridden(Overridden), Using)+import Data.Aeson+import Data.Override (Override(..))+import Data.Override.Aeson.Options.Internal (AesonOption(..), WithAesonOptions(..)) import GHC.Generics (Generic, Rep)-import qualified Data.Aeson as Aeson  instance   ( Generic (Override a xs)-  , Aeson.GToJSON Aeson.Zero (Rep (Override a xs))-  , Aeson.GToEncoding Aeson.Zero (Rep (Override a xs))-  ) => Aeson.ToJSON (Override a xs)--instance-  ( Coercible a (Using ms a xs)-  , Aeson.ToJSON (Using ms a xs)-  ) => Aeson.ToJSON (Overridden ms a xs)-  where-  toJSON = Aeson.toJSON @(Using ms a xs) . coerce-  toEncoding = Aeson.toEncoding @(Using ms a xs) . coerce+  , GToJSON Zero (Rep (Override a xs))+  , GToEncoding Zero (Rep (Override a xs))+  ) => ToJSON (Override a xs)  instance   ( Generic (Override a xs)-  , Aeson.GFromJSON Aeson.Zero (Rep (Override a xs))-  ) => Aeson.FromJSON (Override a xs)--instance-  ( Coercible a (Using ms a xs)-  , Aeson.FromJSON (Using ms a xs)-  ) => Aeson.FromJSON (Overridden ms a xs)-  where-  parseJSON = coerce . Aeson.parseJSON @(Using ms a xs)+  , GFromJSON Zero (Rep (Override a xs))+  ) => FromJSON (Override a xs)
+ src/Data/Override/Aeson/Options/Internal.hs view
@@ -0,0 +1,99 @@+-- | This is the internal generic-override-aeson API and should be considered+-- unstable and subject to change. In general, you should prefer to use the+-- public, stable API provided by "Data.Override.Aeson".+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE KindSignatures #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE UndecidableInstances #-}+module Data.Override.Aeson.Options.Internal where++import Data.Aeson+import Data.Coerce (coerce)+import Data.Proxy (Proxy(..))+import GHC.Generics (Generic, Rep)+import GHC.TypeLits (KnownSymbol, Symbol, symbolVal)++import qualified Data.Aeson as Aeson++-- | Use with @DerivingVia@ to override Aeson @Options@ with a type-level+-- list of 'AesonOption'.+newtype WithAesonOptions (a :: *) (options :: [AesonOption]) = WithAesonOptions a++instance+  ( ApplyAesonOptions options+  , Generic a+  , Aeson.GToJSON Aeson.Zero (Rep a)+  , Aeson.GToEncoding Aeson.Zero (Rep a)+  ) => ToJSON (WithAesonOptions a options)+  where+  toJSON = coerce $ genericToJSON @a $ applyAesonOptions (Proxy @options) defaultOptions+  toEncoding = coerce $ genericToEncoding @a $ applyAesonOptions (Proxy @options) defaultOptions++instance+  ( ApplyAesonOptions options+  , Generic a+  , Aeson.GFromJSON Aeson.Zero (Rep a)+  ) => FromJSON (WithAesonOptions a options)+  where+  parseJSON = coerce $ genericParseJSON @a $ applyAesonOptions (Proxy @options) defaultOptions++-- | Provides a type-level subset of fields from 'Options'+data AesonOption =+    AllNullaryToStringTag Bool -- ^ Equivalient to @'allNullaryToStringTag' = b@+  | OmitNothingFields -- ^ Equivalient to @'omitNothingFields' = True@+  | SumEncodingTaggedObject Symbol Symbol -- ^ Equivalient to @'sumEncoding' = 'TaggedObject' k v@+  | SumEncodingUntaggedValue -- ^ Equivalient to @'sumEncoding' = 'UntaggedValue'@+  | SumEncodingObjectWithSingleField -- ^ Equivalient to @'sumEncoding' = 'ObjectWithSingleField'@+  | SumEncodingTwoElemArray -- ^ Equivalient to @'sumEncoding' = 'TwoElemArray'@+  | UnwrapUnaryRecords -- ^ Equivalient to @'unwrapUnaryRecords' = True@+  | TagSingleConstructors -- ^ Equivalient to @'tagSingleConstructors' = True@++-- | Updates 'Options' given a type-level list of 'AesonOption'.+class ApplyAesonOptions (options :: [AesonOption]) where+  applyAesonOptions :: Proxy options -> Options -> Options++instance ApplyAesonOptions '[] where+  applyAesonOptions _ = id++instance+  ( ApplyAesonOption option+  , ApplyAesonOptions options+  ) => ApplyAesonOptions (option ': options)+  where+  applyAesonOptions _ =+    applyAesonOption (Proxy @option) . (applyAesonOptions (Proxy @options))++-- | Updates 'Options' given a single type-level 'AesonOption'.+class ApplyAesonOption (option :: AesonOption) where+  applyAesonOption :: Proxy option -> Options -> Options++instance ApplyAesonOption ('AllNullaryToStringTag 'True) where+  applyAesonOption _ o = o { allNullaryToStringTag = True }++instance ApplyAesonOption ('AllNullaryToStringTag 'False) where+  applyAesonOption _ o = o { allNullaryToStringTag = False }++instance ApplyAesonOption 'OmitNothingFields where+  applyAesonOption _ o = o { omitNothingFields = True }++instance (KnownSymbol k, KnownSymbol v) => ApplyAesonOption ('SumEncodingTaggedObject k v) where+  applyAesonOption _ o = o { sumEncoding = TaggedObject (symbolVal (Proxy @k)) (symbolVal (Proxy @v)) }++instance ApplyAesonOption 'SumEncodingUntaggedValue where+  applyAesonOption _ o = o { sumEncoding = UntaggedValue }++instance ApplyAesonOption 'SumEncodingObjectWithSingleField where+  applyAesonOption _ o = o { sumEncoding = ObjectWithSingleField }++instance ApplyAesonOption 'SumEncodingTwoElemArray where+  applyAesonOption _ o = o { sumEncoding = TwoElemArray }++instance ApplyAesonOption 'UnwrapUnaryRecords where+  applyAesonOption _ o = o { unwrapUnaryRecords = True }++instance ApplyAesonOption 'TagSingleConstructors where+  applyAesonOption _ o = o { tagSingleConstructors = True }
test/Test.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE BlockArguments #-} {-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveFunctor #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DerivingStrategies #-}@@ -13,17 +14,18 @@ {-# LANGUAGE TypeOperators #-} module Main where -import Data.Aeson (FromJSON(parseJSON), ToJSON(toJSON), Result(Success), fromJSON)+import Data.Aeson (FromJSON(parseJSON), Result(Success), ToJSON(toJSON), Value, fromJSON) import Data.Aeson.QQ.Simple (aesonQQ)-import Data.Override (Override(Override), As, With)-import Data.Override.Aeson ()+import Data.Override (Override(Override), As, At, With)+import Data.Override.Aeson (AesonOption(..), WithAesonOptions(..)) import Data.Text (Text) import GHC.Generics (Generic) import LispCaseAeson (LispCase(LispCase))-import qualified Data.Text as Text import Test.Hspec import Text.Read (readMaybe) +import qualified Data.Text as Text+ main :: IO () main = hspec do   describe "Override ToJSON machinery" do@@ -34,6 +36,8 @@     it "Rec5" testRec5     it "Rec6" testRec6     it "Rec7" testRec6+    it "Sum1" testSum1+    it "Options1" testOptions1  newtype Uptext = Uptext { unUptext :: Text } @@ -44,7 +48,6 @@  instance (Show a) => ToJSON (Shown a) where   toJSON = toJSON . show . unShown- instance (Read a) => FromJSON (Shown a) where   parseJSON v = do     s <- parseJSON v@@ -80,8 +83,8 @@  testRec1 :: IO () testRec1 = do-  let r = Rec1 { foo = 12, bar = "hi", baz = "bye" }-  toJSON r `shouldBe` [aesonQQ|+  toJSON Rec1 { foo = 12, bar = "hi", baz = "bye" }+    `shouldBe` [aesonQQ|     {       "foo": "12",       "bar": "hi",@@ -103,8 +106,8 @@  testRec2 :: IO () testRec2 = do-  let r = Rec2 { foo = 12, bar = "hi", baz = "bye" }-  toJSON r `shouldBe` [aesonQQ|+  toJSON Rec2 { foo = 12, bar = "hi", baz = "bye" }+    `shouldBe` [aesonQQ|     {       "foo": 12,       "bar": "HI",@@ -127,8 +130,8 @@  testRec3 :: IO () testRec3 = do-  let r = Rec3 { foo = 12, bar = "hi", baz = "bye" }-  toJSON r `shouldBe` [aesonQQ|+  toJSON Rec3 { foo = 12, bar = "hi", baz = "bye" }+    `shouldBe` [aesonQQ|     {       "foo": "12",       "bar": ["h", "i"],@@ -151,8 +154,8 @@  testRec4 :: IO () testRec4 = do-  let r = Rec4 { foo = "go", bar = "hi", baz = "bye" }-  toJSON r `shouldBe` [aesonQQ|+  toJSON Rec4 { foo = "go", bar = "hi", baz = "bye" }+    `shouldBe` [aesonQQ|     {       "foo": ["g", "o"],       "bar": ["h", "i"],@@ -175,14 +178,14 @@  testRec5 :: IO () testRec5 = do-  let r = Rec5 { fooBar = 1, baz = "hi", quuxSpamEggs = "bye" }-  toJSON r `shouldBe` [aesonQQ|-    {-      "foo-bar": "1",-      "baz": "HI",-      "quux-spam-eggs": ["b", "y", "e"]-    }-  |]+  toJSON Rec5 { fooBar = 1, baz = "hi", quuxSpamEggs = "bye" }+    `shouldBe` [aesonQQ|+      {+        "foo-bar": "1",+        "baz": "HI",+        "quux-spam-eggs": ["b", "y", "e"]+      }+    |]  -- Test 'Override' for both 'ToJSON' and 'FromJSON'. data Rec6 = Rec6@@ -198,16 +201,14 @@  testRec6 :: IO () testRec6 = do-  let r = Rec6 { foo = 1, bar = "hi", baz = "bye" }-  let j = [aesonQQ|-    {-      "foo": "1",-      "bar": ["h", "i"],-      "baz": "bye"-    }-  |]-  toJSON r `shouldBe` j-  fromJSON j `shouldBe` Success r+  Rec6 { foo = 1, bar = "hi", baz = "bye" }+    `shouldRoundtripAs` [aesonQQ|+      {+        "foo": "1",+        "bar": ["h", "i"],+        "baz": "bye"+      }+    |]  -- Test 'Override' for both 'ToJSON' and 'FromJSON'. data Rec7 = Rec7@@ -223,13 +224,94 @@  testRec7 :: IO () testRec7 = do-  let r = Rec7 { foo = 1, bar = "hi", baz = "bye" }-  let j = [aesonQQ|+  Rec7 { foo = 1, bar = "hi", baz = "bye" }+    `shouldRoundtripAs` [aesonQQ|+      {+        "foo": "1",+        "bar": ["h", "i"],+        "baz": "bye"+      }+    |]++newtype Reverse a = Reverse [a]++instance (ToJSON a) => ToJSON (Reverse a) where+  toJSON (Reverse xs) = toJSON $ reverse xs++instance (FromJSON a) => FromJSON (Reverse a) where+  parseJSON = fmap (Reverse . reverse) . parseJSON++newtype Not = Not Bool++instance ToJSON Not where+  toJSON (Not b) = toJSON $ not b++instance FromJSON Not where+  parseJSON = fmap (Not . not) . parseJSON++data Sum1 a =+    Sum1List [a]+  | Sum1Trip a Char Bool+  | Sum1Null+  deriving stock (Show, Eq, Generic)+  deriving (ToJSON, FromJSON)+    via Override (Sum1 a)+          '[ At "Sum1List" 0 (Reverse a)+           , At "Sum1Trip" 2 Not+           ]++testSum1 :: IO ()+testSum1 = do+  Sum1List ['a', 'b'] `shouldRoundtripAs` [aesonQQ|     {-      "foo": "1",-      "bar": ["h", "i"],-      "baz": "bye"+      "tag": "Sum1List",+      "contents": "ba"     }   |]-  toJSON r `shouldBe` j-  fromJSON j `shouldBe` Success r+  Sum1Trip 'a' 'b' True `shouldRoundtripAs` [aesonQQ|+    {+      "tag": "Sum1Trip",+      "contents": ["a", "b", false]+    }+  |]+  Sum1Null @Char `shouldRoundtripAs` [aesonQQ|+    {+      "tag": "Sum1Null"+    }+  |]+++data Options1 =+    Options1A { foo :: Maybe Int, bar :: String }+  | Options1B Options1BBody+  | Options1C+    deriving stock (Eq, Show, Generic)+    deriving (FromJSON, ToJSON)+      via Override Options1+            '[ "foo" `As` Maybe (Shown Int)+             ] `WithAesonOptions`+            '[ 'OmitNothingFields+             , 'SumEncodingTaggedObject "type" "data"+             ]++data Options1BBody = Options1BBody { baz :: Int }+  deriving stock (Eq, Show, Generic)+  deriving anyclass (FromJSON, ToJSON)++testOptions1 :: IO ()+testOptions1 = do+  Options1A { foo = Nothing, bar = "boo" }+    `shouldRoundtripAs` [aesonQQ| { "type": "Options1A", "bar": "boo" } |]+  Options1A { foo = Just 1, bar = "boo" }+    `shouldRoundtripAs` [aesonQQ| { "type": "Options1A", "foo": "1", "bar": "boo" } |]+  Options1B Options1BBody { baz = 2 }+    `shouldRoundtripAs` [aesonQQ| { "type": "Options1B", "data": { "baz": 2 } } |]+  Options1C+    `shouldRoundtripAs` [aesonQQ| { "type": "Options1C" } |]++shouldRoundtripAs+  :: (ToJSON a, FromJSON a, Eq a, Show a, HasCallStack)+  => a -> Value -> IO ()+shouldRoundtripAs x j = do+  toJSON x `shouldBe` j+  fromJSON j `shouldBe` Success x