packages feed

by-other-names 1.2.0.1 → 1.2.1.0

raw patch · 4 files changed

+142/−44 lines, 4 filesPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

API changes (from Hackage documentation)

- ByOtherNames: type family AliasType k :: Type;
- ByOtherNames.Internal: type family AliasType k :: Type;
+ ByOtherNames: type AliasType k :: Type;
+ ByOtherNames.Aeson: JSONEnum :: r -> JSONEnum r
+ ByOtherNames.Aeson: instance (ByOtherNames.Internal.Aliased 'ByOtherNames.Aeson.JSON r, ByOtherNames.Internal.GSum ByOtherNames.Aeson.Impossible (GHC.Generics.Rep r)) => Data.Aeson.Types.ToJSON.ToJSON (ByOtherNames.Aeson.JSONEnum r)
+ ByOtherNames.Aeson: instance (ByOtherNames.Internal.Aliased 'ByOtherNames.Aeson.JSON r, ByOtherNames.Internal.GSum Data.Aeson.Types.FromJSON.FromJSON (GHC.Generics.Rep r)) => Data.Aeson.Types.FromJSON.FromJSON (ByOtherNames.Aeson.JSONEnum r)
+ ByOtherNames.Aeson: instance (x GHC.Types.~ Data.Void.Void) => ByOtherNames.Aeson.Impossible x
+ ByOtherNames.Aeson: instance GHC.Base.Functor ByOtherNames.Aeson.EnumBranchParser
+ ByOtherNames.Aeson: newtype JSONEnum r
+ ByOtherNames.Internal: type AliasType k :: Type;

Files

CHANGELOG.md view
@@ -1,4 +1,9 @@ +1.2.1.0+=======++- JSONEnum: FromJSON / ToJSON instances for enum-like types without fields.+ 1.2.0.1 ======= 
by-other-names.cabal view
@@ -1,6 +1,6 @@ cabal-version:       3.0 name:                by-other-names-version:             1.2.0.1+version:             1.2.1.0 synopsis:            Give aliases to record fields.  description:         Give aliases to record fields.
lib/ByOtherNames/Aeson.hs view
@@ -2,20 +2,17 @@ {-# LANGUAGE ApplicativeDo #-} {-# LANGUAGE BlockArguments #-} {-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveFunctor #-} {-# LANGUAGE DerivingVia #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE LambdaCase #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneKindSignatures #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeOperators #-}-{-# LANGUAGE TypeSynonymInstances #-} {-# LANGUAGE UndecidableInstances #-}-{-# LANGUAGE ViewPatterns #-}-{-# LANGUAGE DeriveFunctor #-}-{-# LANGUAGE LambdaCase #-}  -- | A 'Rubric' for JSON serialization using Aeson, along with some helper -- newtypes and re-exports.@@ -65,16 +62,39 @@ -- :} -- -- Some limitations:--- +-- -- - 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.--- --- - For sum types, only the "object with a single key consisting in the branch tag" style of serialization is supported. --+-- - 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 (..),      -- * Re-exports from ByOtherNames     Aliased (aliases),@@ -89,15 +109,16 @@ where  import ByOtherNames-import Control.Applicative 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 Data.Proxy-import Data.Foldable  -- | Aliases for JSON serialization fall under this 'Rubric'. -- The constructor 'JSON' is used as a type, with DataKinds.@@ -121,41 +142,53 @@ type JSONSum :: Symbol -> Type -> Type newtype JSONSum objectName r = JSONSum 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+ ---instance (KnownSymbol objectName, Aliased JSON r, GSum FromJSON (Rep r)) => FromJSON (JSONSum objectName r) where+--+instance (Aliased JSON r, GSum FromJSON (Rep r)) => FromJSON (JSONEnum r) where   parseJSON v =-    let parsers = gToSum @FromJSON (aliases @JSONRubric @JSON @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 JSONSum . to <$> withObject (symbolVal (Proxy @objectName)) parserForObject v-newtype BranchParser v = BranchParser { runBranchParser :: Object -> Parser v}-  deriving stock Functor+    let parsers =+          gToSum @FromJSON+            (aliases @JSONRubric @JSON @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 JSONEnum . to <$> parserForValue v -newtype ProductInBranchParser1 v = ProductInBranchParser1 { runProductInBranchParser1 :: Value -> Parser v }-  deriving stock Functor-  deriving Applicative via (Compose ((->) Value) Parser)+newtype EnumBranchParser v = EnumBranchParser {runEnumBranchParser :: Value -> Parser v}+  deriving stock (Functor) -newtype ProductInBranchParser v = ProductInBranchParser { runProductInBranchParser :: [Value] -> Parser (v, [Value]) }-  deriving stock Functor+instance (Aliased JSON r, GSum Impossible (Rep r)) => ToJSON (JSONEnum r) where+  toJSON (JSONEnum o) =+    let (key, slots) = gFromSum @Impossible @(Rep r) @Key @Value @Value (aliases @JSONRubric @JSON @r) absurd (from @r o)+     in case slots of+          [] -> String (toText key)+          [_] -> error "never happens"+          _ -> error "never happens" +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 =@@ -167,15 +200,50 @@  -- --+class (x ~ Void) => Impossible x++instance (x ~ Void) => Impossible x++instance (KnownSymbol objectName, Aliased JSON r, GSum FromJSON (Rep r)) => FromJSON (JSONSum objectName r) where+  parseJSON v =+    let parsers =+          gToSum @FromJSON+            (aliases @JSONRubric @JSON @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 JSONSum . to <$> withObject (symbolVal (Proxy @objectName)) parserForObject v++--+-- instance (KnownSymbol objectName, Aliased JSON r, GRecord FromJSON (Rep r)) => FromJSON (JSONRecord objectName r) where   parseJSON v =-    let FieldParser parser = gToRecord @FromJSON (aliases @JSONRubric @JSON @r) -          (\fieldName -> FieldParser (\o ->explicitParseField parseJSON o fieldName))+    let FieldParser parser =+          gToRecord @FromJSON+            (aliases @JSONRubric @JSON @r)+            (\fieldName -> FieldParser (\o -> explicitParseField parseJSON o fieldName))         objectName = symbolVal (Proxy @objectName)      in JSONRecord . to <$> withObject objectName parser v+ newtype FieldParser a = FieldParser (Object -> Parser a)   deriving (Functor, Applicative) via ((->) Object `Compose` Parser)-  -- --
tests/tests.hs view
@@ -18,6 +18,7 @@     JSONRecord (..),     JSONRubric (JSON),     JSONSum (..),+    JSONEnum (..),     alias,     aliasListBegin,     aliasListEnd,@@ -127,6 +128,22 @@     ("Eex", [typeRep (Proxy @Int)])   ] ++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++ -- >>> enumSummy  roundtrip :: forall t. (Eq t, Show t, FromJSON t, ToJSON t) => t -> IO ()@@ -175,6 +192,14 @@           testCase "d" $ roundtrip $ Dd 'f' True 0,           testCase "e" $ roundtrip $ Ee 3         ],+      testGroup+        "enumRoundtrip"+        [+          testCase "x" $ roundtrip Xx,+          testCase "y" $ roundtrip Yy,+          testCase "z" $ roundtrip Zz+        ]+       ,       testGroup         "enums"         [ testCase "prod typeReps" $ assertEqual "prod typeReps match" expectedEnumFoo enumFoo,