Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add sum encoding TaggedFlatObject #828

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
31 changes: 31 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
inputs.nixpkgs.url = github:NixOS/nixpkgs/nixos-unstable;
inputs.flake-utils.url = github:poscat0x04/flake-utils;

outputs = { self, nixpkgs, flake-utils, ... }: with flake-utils;
eachDefaultSystem (
system:
let
pkgs = import nixpkgs { inherit system; overlays = [ self.overlay ]; };
in
with pkgs;
{
devShell = aeson-dev.envFunc { withHoogle = true; };
defaultPackage = aeson;
}
) // {
overlay = self: super:
let
hpkgs = super.haskellPackages;
aeson = hpkgs.callCabal2nix "aeson" ./. {};
in
with super; with haskell.lib;
{
inherit aeson;
aeson-dev = addBuildTools aeson [
haskell-language-server
cabal-install
];
};
};
}
124 changes: 121 additions & 3 deletions src/Data/Aeson/TH.hs
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,7 @@ sumToValue target opts multiCons nullary conName value pairs
content = pairs contentsFieldName
in fromPairsE $
if nullary then tag else infixApp tag [|(Monoid.<>)|] content
TaggedFlatObject {} -> error "TaggedFlatObject: Should be handled already"
ObjectWithSingleField ->
objectE [(conString opts conName, value)]
UntaggedValue | nullary -> conStr target opts conName
Expand All @@ -434,7 +435,21 @@ argsToValue :: ToJSONFun -> JSONClass -> TyVarMap -> Options -> Bool -> Construc
argsToValue target jc tvMap opts multiCons
ConstructorInfo { constructorName = conName
, constructorVariant = NormalConstructor
, constructorFields = argTys } = do
, constructorFields = argTys }
| TaggedFlatObject{tagFieldName} <- sumEncoding opts
, multiCons = do
let tag = (tagFieldName, conStr target opts conName)
argTys' <- mapM resolveTypeSynonyms argTys
let len = length argTys'
args <- newNameList "arg" len
let os = zipWith (\arg argTy -> dispatchToJSON target jc conName tvMap argTy `appE` varE arg) args argTys'
pairs = zip (fmap (show :: Int -> String) [0..]) os
obj = objectE (tag : pairs)
match (conP conName $ map varP args)
(normalB obj)
[]
| otherwise =
do
argTys' <- mapM resolveTypeSynonyms argTys
let len = length argTys'
args <- newNameList "arg" len
Expand Down Expand Up @@ -491,14 +506,33 @@ argsToValue target jc tvMap opts multiCons
else e arg

match (conP conName $ map varP args)
(normalB $ recordSumToValue target opts multiCons (null argTys) conName pairs)
(normalB $ case () of
()
| TaggedFlatObject {tagFieldName} <- sumEncoding opts -> do
let tag = pairE tagFieldName (conStr target opts conName)
fromPairsE $ infixApp tag [|(Monoid.<>)|] pairs
| otherwise -> recordSumToValue target opts multiCons (null argTys) conName pairs)
[]

-- Infix constructors.
argsToValue target jc tvMap opts multiCons
ConstructorInfo { constructorName = conName
, constructorVariant = InfixConstructor
, constructorFields = argTys } = do
, constructorFields = argTys }
| TaggedFlatObject {tagFieldName} <- sumEncoding opts
, multiCons = do
[alTy, arTy] <- mapM resolveTypeSynonyms argTys
al <- newName "argL"
ar <- newName "argR"
let tag = (tagFieldName, conStr target opts conName)
os = zipWith (\arg argTy -> dispatchToJSON target jc conName tvMap argTy `appE` varE arg) [al, ar] [alTy, arTy]
pairs = zip (fmap (show :: Int -> String) [0..]) os
obj = objectE (tag : pairs)
match (infixP (varP al) conName (varP ar))
(normalB obj)
[]
| otherwise =
do
[alTy, arTy] <- mapM resolveTypeSynonyms argTys
al <- newName "argL"
ar <- newName "argR"
Expand Down Expand Up @@ -729,6 +763,8 @@ consFromJSON jc tName opts instTys cons = do
case sumEncoding opts of
TaggedObject {tagFieldName, contentsFieldName} ->
parseObject $ parseTaggedObject tvMap tagFieldName contentsFieldName
TaggedFlatObject {tagFieldName} ->
parseObject $ parseTaggedFlatObject tvMap tagFieldName
UntaggedValue -> error "UntaggedValue: Should be handled already"
ObjectWithSingleField ->
parseObject $ parseObjectWithSingleField tvMap
Expand Down Expand Up @@ -779,6 +815,88 @@ consFromJSON jc tName opts instTys cons = do
, noBindS $ parseContents tvMap conKey (Left (valFieldName, obj)) 'conNotFoundFailTaggedObject
]

parseTaggedFlatObject tvMap tagFieldName obj = do
conKey <- newName "conKey"
doE [ bindS (varP conKey)
(infixApp (varE obj) [|(.:)|] ([|T.pack|] `appE` stringE tagFieldName))
, noBindS $
caseE (varE conKey)
[ match wildP
( guardedB $
[ do g <- normalG $ infixApp (varE conKey)
[|(==)|]
([|T.pack|] `appE`
conNameExp opts con)
argTys <- mapM resolveTypeSynonyms (constructorFields con)
let conName = constructorName con
e <- case constructorVariant con of
RecordConstructor fields ->
parseRecord jc tvMap argTys opts tName conName fields obj False
_ ->
parseNumRec tvMap argTys conName obj
return (g, e)
| con <- cons
]
++
[ liftM2 (,)
(normalG [e|otherwise|])
( varE 'conNotFoundFailTaggedObject
`appE` litE (stringL $ show tName)
`appE` listE (map ( litE
. stringL
. constructorTagModifier opts
. nameBase
. constructorName
) cons
)
`appE` ([|T.unpack|] `appE` varE conKey)
)
]
)
[]
]
]

parseNumRec :: TyVarMap
-> [Type]
-> Name
-> Name
-> ExpQ
parseNumRec tvMap argTys conName obj =
(if rejectUnknownFields opts
then infixApp checkUnknownRecords [|(>>)|]
else id) $
if null argTys
then [|pure|] `appE` conE conName
else
foldl' (\a b -> infixApp a [|(<*>)|] b)
(infixApp (conE conName) [|(<$>)|] x)
xs
where
fields = map (show :: Int -> String) $ take (length argTys) [0..]
knownFields = appE [|H.fromList|] $ listE $
map (\knownName -> tupE [appE [|T.pack|] $ litE $ stringL knownName, [|()|]]) fields
checkUnknownRecords =
caseE (appE [|H.keys|] $ infixApp (varE obj) [|H.difference|] knownFields)
[ match (listP []) (normalB [|return ()|]) []
, newName "unknownFields" >>=
\unknownFields -> match (varP unknownFields)
(normalB $ appE [|fail|] $ infixApp
(litE (stringL "Unknown fields: "))
[|(++)|]
(appE [|show|] (varE unknownFields)))
[]
]
x:xs = [ [|lookupField|]
`appE` dispatchParseJSON jc conName tvMap argTy
`appE` litE (stringL $ show tName)
`appE` litE (stringL $ constructorTagModifier opts $ nameBase conName)
`appE` varE obj
`appE` ( [|T.pack|] `appE` stringE field
)
| (field, argTy) <- zip fields argTys
]

parseUntaggedValue tvMap cons' conVal =
foldr1 (\e e' -> infixApp e [|(<|>)|] e')
(map (\x -> parseValue tvMap x conVal) cons')
Expand Down
76 changes: 76 additions & 0 deletions src/Data/Aeson/Types/FromJSON.hs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
Expand Down Expand Up @@ -158,6 +159,7 @@ import qualified Data.Primitive.Types as PM
import qualified Data.Primitive.PrimArray as PM

import Data.Coerce (Coercible, coerce)
import GHC.TypeLits

parseIndexedJSON :: (Value -> Parser a) -> Int -> Value -> Parser a
parseIndexedJSON p idx value = p value <?> Index idx
Expand Down Expand Up @@ -1010,6 +1012,7 @@ instance ( ConstructorNames f
, FromPair arity f
, FromTaggedObject arity f
, FromUntaggedValue arity f
, FromTaggedFlatObject arity f
) => ParseSum arity f True where
parseSum p@(tname :* opts :* _)
| allNullaryToStringTag opts = Tagged . parseAllNullarySum tname opts
Expand All @@ -1019,6 +1022,7 @@ instance ( ConstructorNames f
, FromPair arity f
, FromTaggedObject arity f
, FromUntaggedValue arity f
, FromTaggedFlatObject arity f
) => ParseSum arity f False where
parseSum p = Tagged . parseNonAllNullarySum p

Expand Down Expand Up @@ -1101,6 +1105,7 @@ parseNonAllNullarySum :: forall f c arity.
( FromPair arity f
, FromTaggedObject arity f
, FromUntaggedValue arity f
, FromTaggedFlatObject arity f
, ConstructorNames f
) => TypeName :* Options :* FromArgs arity c
-> Value -> Parser (f c)
Expand All @@ -1118,6 +1123,17 @@ parseNonAllNullarySum p@(tname :* opts :* _) =
", but found tag " ++ show tag
cnames_ = unTagged2 (constructorTags (constructorTagModifier opts) :: Tagged2 f [String])

TaggedFlatObject{..} ->
withObject tname $ \obj -> do
let tagKey = pack tagFieldName
badTag tag = failWith_ $ \cnames ->
"expected tag field to be one of " ++ show cnames ++
", but found tag " ++ show tag
cnames_ = unTagged2 (constructorTags (constructorTagModifier opts) :: Tagged2 f [String])
tag <- contextType tname . contextTag tagKey cnames_ $ obj .: tagKey
fromMaybe (badTag tag <?> Key tagKey) $
parseTaggedFlatObject (tag :* p) obj

ObjectWithSingleField ->
withObject tname $ \obj -> case H.toList obj of
[(tag, v)] -> maybe (badTag tag) (<?> Key tag) $
Expand Down Expand Up @@ -1401,6 +1417,66 @@ instance ( Constructor c

--------------------------------------------------------------------------------

class FromTaggedFlatObject arity f where
parseTaggedFlatObject :: Text :* TypeName :* Options :* FromArgs arity a
-> Object
-> Maybe (Parser (f a))

instance ( FromTaggedFlatObject arity f
, FromTaggedFlatObject arity g
) => FromTaggedFlatObject arity (f :+: g) where
parseTaggedFlatObject p obj =
(fmap L1 <$> parseTaggedFlatObject p obj) <|>
(fmap R1 <$> parseTaggedFlatObject p obj)

instance ( IsRecord f isRecord
, FromTaggedFlatObject' arity f isRecord
, Constructor c
) => FromTaggedFlatObject arity (C1 c f) where
parseTaggedFlatObject :: Text :* TypeName :* Options :* FromArgs arity a
-> Object
-> Maybe (Parser (C1 c f a))
parseTaggedFlatObject (tag :* p@(_ :* opts :* _)) obj
| tag == tag' = Just $ fmap M1 $ (unTagged :: Tagged isRecord (Parser (f a)) -> Parser (f a)) $ parseTaggedFlatObject' (cname :* p) obj
| otherwise = Nothing
where
tag' = pack $ constructorTagModifier opts cname
cname = conName (undefined :: M1 i c f p)

class FromTaggedFlatObject' arity f isRecord where
parseTaggedFlatObject' :: ConName :* TypeName :* Options :* FromArgs arity a
-> Object
-> Tagged isRecord (Parser (f a))

instance (RecordFromJSON arity f, FieldNames f) => FromTaggedFlatObject' arity f True where
parseTaggedFlatObject' p = Tagged . recordParseJSON (True :* p)

instance FromTaggedFlatObject' arity U1 False where
parseTaggedFlatObject' _ _ = Tagged (pure U1)

instance OVERLAPPABLE_ PositionFromObject 0 arity f => FromTaggedFlatObject' arity f False where
parseTaggedFlatObject' (_ :* p) obj = Tagged (positionFromObject (Proxy :: Proxy 0) p obj)

class KnownNat n => PositionFromObject n arity f where
positionFromObject :: Proxy n
-> TypeName :* Options :* FromArgs arity a
-> Object
-> Parser (f a)

instance (KnownNat n, GFromJSON arity a) => PositionFromObject n arity (S1 m a) where
positionFromObject _ (_ :* opts :* fargs) obj =
explicitParseField (gParseJSON opts fargs) obj $ pack $ show $ natVal (Proxy :: Proxy n)

instance ( PositionFromObject n arity f
, PositionFromObject (n+1) arity g
) => PositionFromObject n arity (f :*: g) where
positionFromObject _ p obj =
(:*:)
<$> positionFromObject (Proxy :: Proxy n) p obj
<*> positionFromObject (Proxy :: Proxy (n+1)) p obj

--------------------------------------------------------------------------------

class FromUntaggedValue arity f where
parseUntaggedValue :: TypeName :* Options :* FromArgs arity a
-> Value
Expand Down
33 changes: 33 additions & 0 deletions src/Data/Aeson/Types/Internal.hs
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,39 @@ data SumEncoding =
-- 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.
| TaggedFlatObject { tagFieldName :: String }
-- ^ Conceptually, this option will allow data types to be encoded to an object
-- with an additional field 'tagFieldName' which specifies the constructor tag.
-- This option differs from 'TaggedObject' in that the fields are encoded
-- in the same object as the tag, instead of in another object under the
-- field @contentsFieldName@.
--
-- The detailed behavior is as follows:
--
-- 1. If the data type has only a single constructor and has field names
-- (a record), it will be encoded as an object without any additional fields.
-- For example, given @A@ defined as
-- @data A = A {field1 :: Int, field2 :: Int}@,
-- this option will encode @A 1 2@ as @{"field1": 1, "field2": 2}@
-- 2. If the data type has only a single constructor but does not have any fields,
-- it will be encoded as an array.
-- For example, given @A@ defined as
-- @data A = A Int Int@,
-- this option will encode @A 1 2@ as @[1, 2]@
-- 3. If the data type has multiple constructors and the constructor has field names,
-- it will be encoded as an object with an additional field '$tagFieldName'.
-- For example, given @A@ defined as
-- @data A = A {field1 :: Int, field2 :: Int} | B@,
-- this option will encode @A 1 2@ as @{"field1": 1, "field2": 2, "$tagFieldName": \"A"}@
-- 4. If the data type has multiple constructors and the constructor does not have
-- any feild names, it will be encoded as an object whose keys are the position of the value
-- in that data type with an additional field '$tagFieldName'.
-- For example, given @A@ defined as
-- @data A = A Int Int | B@,
-- this option will encode @A 1 2@ as @{"0": 1, "1": 2, "$tagFieldName": \"A"}@
-- 5. The behavior is undefined when the '$tagFieldName' collides with another field name and should
-- not be relied upon. It may or may not overwite the field.
-- It may or may not throw an runtime exception. It may or may not raise an compile time error.
| UntaggedValue
-- ^ Constructor names won't be encoded. Instead only the contents of the
-- constructor will be encoded as if the type had a single constructor. JSON
Expand Down
Loading