From b8b3b22cb5fec7c5d9b74b72b8e68cef23a5e0d1 Mon Sep 17 00:00:00 2001 From: Robert Klotzner Date: Sat, 23 Jan 2016 18:49:16 +0100 Subject: [PATCH 01/16] Added support for aeson like encoding/decoding Needs more testing & cleanup before being ready for a PR --- src/Data/Argonaut/Decode.purs | 56 +++++++++++++++++++++++++++++++-- src/Data/Argonaut/Encode.purs | 56 ++++++++++++++++++++++++++++++++- src/Data/Argonaut/Internal.purs | 16 ++++++++++ 3 files changed, 125 insertions(+), 3 deletions(-) create mode 100644 src/Data/Argonaut/Internal.purs diff --git a/src/Data/Argonaut/Decode.purs b/src/Data/Argonaut/Decode.purs index 1d9b303..d586c21 100644 --- a/src/Data/Argonaut/Decode.purs +++ b/src/Data/Argonaut/Decode.purs @@ -11,10 +11,11 @@ import Prelude import Control.Alt ((<|>)) import Control.Bind ((=<<)) import Data.Argonaut.Core (Json(), isNull, foldJsonNull, foldJsonBoolean, foldJsonNumber, foldJsonString, toArray, toNumber, toObject, toString, toBoolean) -import Data.Array (zipWithA) +import Data.Argonaut.Internal +import Data.Array (zipWithA, length) import Data.Either (either, Either(..)) import Data.Foldable (find) -import Data.Generic (Generic, GenericSpine(..), GenericSignature(..), fromSpine, toSignature) +import Data.Generic (Generic, GenericSpine(..), GenericSignature(..), DataConstructor(), fromSpine, toSignature) import Data.Int (fromNumber) import Data.List (List(..), toList) import Data.Map as Map @@ -65,6 +66,57 @@ gDecodeJson' signature json = case signature of mFail :: forall a. String -> Maybe a -> Either String a mFail msg = maybe (Left msg) Right +-- | Decode `Json` representation of a value which has a `Generic` type. +gAesonDecodeJson :: forall a. (Generic a) => Json -> Either String a +gAesonDecodeJson json = maybe (Left "fromSpine failed") Right <<< fromSpine + =<< gAesonDecodeJson' (toSignature (Proxy :: Proxy a)) json + +-- | Decode `Json` representation of a `GenericSpine`. +gAesonDecodeJson' :: GenericSignature -> Json -> Either String GenericSpine +gAesonDecodeJson' signature json = case signature of + SigNumber -> SNumber <$> mFail "Expected a number" (toNumber json) + SigInt -> SInt <$> mFail "Expected an integer number" (fromNumber =<< toNumber json) + SigString -> SString <$> mFail "Expected a string" (toString json) + SigChar -> SChar <$> mFail "Expected a char" (toChar =<< toString json) + SigBoolean -> SBoolean <$> mFail "Expected a boolean" (toBoolean json) + SigArray thunk -> do + jArr <- mFail "Expected an array" $ toArray json + SArray <$> traverse (map const <<< gDecodeJson' (thunk unit)) jArr + SigRecord props -> do + jObj <- mFail "Expected an object" $ toObject json + SRecord <$> for props \({recLabel: lbl, recValue: val}) -> do + pf <- mFail ("'" <> lbl <> "' property missing") (M.lookup lbl jObj) + sp <- gDecodeJson' (val unit) pf + pure { recLabel: lbl, recValue: const sp } + SigProd typeConstr constrSigns -> gAesonDecodeProdJson' typeConstr constrSigns json + +mFail :: forall a. String -> Maybe a -> Either String a +mFail msg = maybe (Left msg) Right + + +gAesonDecodeProdJson' :: String -> Array DataConstructor -> Json -> Either String GenericSpine +gAesonDecodeProdJson' tname constrSigns json = if allConstrNullary constrSigns + then decodeFromString + else decodeTagged + where + decodeFromString = do + tag <- mFail (decodingErr "Constructor name as string expected") (toString json) + pure (SProd tag []) + decodeTagged = do + jObj <- mFail (decodingErr "expected an object") (toObject json) + tagJson <- mFail (decodingErr "'tag' property is missing") (M.lookup "tag" jObj) + tag <- mFail (decodingErr "'tag' property is not a string") (toString tagJson) + case find ((tag ==) <<< fixConstr <<< _.sigConstructor) constrSigns of + Nothing -> Left (decodingErr ("'" <> tag <> "' isn't a valid constructor")) + Just { sigValues: sigValues } -> do + jVals <- mFail (decodingErr "'contents' property is missing") (M.lookup "contents" jObj) + vals <- case length sigValues of + 1 -> pure [jVals] + _ -> mFail (decodingErr "Expected array") (toArray jVals) + sps <- zipWithA (\k -> gAesonDecodeJson' (k unit)) sigValues vals + pure (SProd tag (const <$> sps)) + decodingErr msg = "When decoding a " ++ tname ++ ": " ++ msg + instance decodeJsonMaybe :: (DecodeJson a) => DecodeJson (Maybe a) where decodeJson j | isNull j = pure Nothing diff --git a/src/Data/Argonaut/Encode.purs b/src/Data/Argonaut/Encode.purs index 5351d52..5cf770c 100644 --- a/src/Data/Argonaut/Encode.purs +++ b/src/Data/Argonaut/Encode.purs @@ -8,9 +8,10 @@ module Data.Argonaut.Encode import Prelude import Data.Argonaut.Core (Json(), jsonNull, fromBoolean, fromNumber, fromString, fromArray, fromObject) +import Data.Argonaut.Internal import Data.Either (Either(), either) import Data.Foldable (foldr) -import Data.Generic (Generic, GenericSpine(..), toSpine) +import Data.Generic (Generic, GenericSpine(..), toSpine, GenericSignature(..), DataConstructor(), toSignature) import Data.Int (toNumber) import Data.List (List(..), fromList) import Data.Map as M @@ -18,6 +19,9 @@ import Data.Maybe (Maybe(..)) import Data.String (fromChar) import Data.StrMap as SM import Data.Tuple (Tuple(..)) +import Type.Proxy (Proxy(..)) +import Data.Tuple (uncurry) +import Data.Array (null, concatMap, filter, zip) class EncodeJson a where encodeJson :: a -> Json @@ -42,6 +46,56 @@ gEncodeJson' spine = case spine of where addField field = SM.insert field.recLabel (gEncodeJson' $ field.recValue unit) + +gAesonEncodeJson :: forall a. (Generic a) => a -> Json +gAesonEncodeJson = gAesonEncodeJson' sign <<< toSpine + where sign = toSignature (Proxy :: Proxy a) + +-- | Encode `GenericSpine` into `Json`. +gAesonEncodeJson' :: GenericSignature -> GenericSpine -> Json +gAesonEncodeJson' sign spine = case spine of + SInt x -> fromNumber $ toNumber x + SString x -> fromString x + SChar x -> fromString $ fromChar x + SNumber x -> fromNumber x + SBoolean x -> fromBoolean x + SArray thunks -> fromArray (gAesonEncodeJson' sign <<< (unit #) <$> thunks) + SProd constr args -> case sign of + SigProd _ constrSigns -> gAesonEncodeProdJson' constrSigns constr args + -- _ -> unsafeCrashWith "Signature does not match value, please don't do that!" -- Not yet supported, waiting for purescript 0.8 + SRecord fields -> case sign of + SigRecord sigs -> gAesonEncodeRecordJson' sigs fields + -- _ -> unsafeCrashWith "Signature does not match value, please don't do that!" -- Not yet supported, waiting for purescript 0.8 + +gAesonEncodeRecordJson' :: Array { recLabel :: String, recValue :: Unit -> GenericSignature } + -> Array { recLabel :: String, recValue :: Unit -> GenericSpine } + -> Json +gAesonEncodeRecordJson' sigs fields = fromObject <<< foldr (uncurry addField) SM.empty $ zip sigs fields + where + addField sig field = SM.insert field.recLabel (gAesonEncodeJson' (sig.recValue unit) (field.recValue unit)) + +gAesonEncodeProdJson' :: Array DataConstructor -> String -> Array (Unit -> GenericSpine) -> Json +gAesonEncodeProdJson' constrSigns constr args = fromObject + $ SM.insert "tag" (encodeJson (fixConstr constr)) + $ SM.singleton "contents" flattenedArgs + where + contents = if allConstrNullary constrSigns -- If no constructor has any values - serialize as string ... + then fromString constr + else flattenedArgs + encodedArgs = gAesonEncodeProdArgs constrSigns constr args + flattenedArgs = case encodedArgs of + [a] -> a + as -> encodeJson as + +gAesonEncodeProdArgs :: Array DataConstructor -> String -> Array (Unit -> GenericSpine) -> Array (Json) +gAesonEncodeProdArgs constrSigns constr args = gAesonEncodeJson' <$> sigValues <*> values + where + lSigValues = concatMap (\c -> c.sigValues) + <<< filter (\c -> c.sigConstructor == constr) $ constrSigns + sigValues = (unit #) <$> lSigValues + values = (unit #) <$> args + + instance encodeJsonMaybe :: (EncodeJson a) => EncodeJson (Maybe a) where encodeJson Nothing = jsonNull encodeJson (Just a) = encodeJson a diff --git a/src/Data/Argonaut/Internal.purs b/src/Data/Argonaut/Internal.purs new file mode 100644 index 0000000..cf3e2f1 --- /dev/null +++ b/src/Data/Argonaut/Internal.purs @@ -0,0 +1,16 @@ +module Data.Argonaut.Internal where + +import Prelude +import Data.Foldable (foldr) +import Data.String (lastIndexOf, drop) +import Data.Generic (DataConstructor()) +import Data.Array (null) +import Data.Maybe (Maybe(..)) + +allConstrNullary :: Array DataConstructor -> Boolean +allConstrNullary constrSigns = foldr (&&) true <<< map (\c -> null c.sigValues) $ constrSigns + +fixConstr :: String -> String +fixConstr constr = case lastIndexOf "." constr of + Nothing -> constr + Just i -> drop (i+1) constr From d87bf9833ec325f2821c7393d367442fc5e3bfa6 Mon Sep 17 00:00:00 2001 From: Robert Klotzner Date: Mon, 25 Jan 2016 15:54:43 +0100 Subject: [PATCH 02/16] Made encoding/decoding configurable Tests for existing functionality passing. Tests for aeson encoding missing - coming soon. --- src/Data/Argonaut/Decode.purs | 98 +++++++++++++-------------------- src/Data/Argonaut/Encode.purs | 90 +++++++++++++++--------------- src/Data/Argonaut/Internal.purs | 16 ------ src/Data/Argonaut/Options.purs | 59 ++++++++++++++++++++ test/Test/Main.purs | 8 +-- 5 files changed, 147 insertions(+), 124 deletions(-) delete mode 100644 src/Data/Argonaut/Internal.purs create mode 100644 src/Data/Argonaut/Options.purs diff --git a/src/Data/Argonaut/Decode.purs b/src/Data/Argonaut/Decode.purs index d586c21..ae3971a 100644 --- a/src/Data/Argonaut/Decode.purs +++ b/src/Data/Argonaut/Decode.purs @@ -2,8 +2,11 @@ module Data.Argonaut.Decode ( DecodeJson , decodeJson , gDecodeJson - , gDecodeJson' + , gAesonDecodeJson + , genericDecodeJson + , genericDecodeJson' , decodeMaybe + , module Data.Argonaut.Options ) where import Prelude @@ -11,7 +14,7 @@ import Prelude import Control.Alt ((<|>)) import Control.Bind ((=<<)) import Data.Argonaut.Core (Json(), isNull, foldJsonNull, foldJsonBoolean, foldJsonNumber, foldJsonString, toArray, toNumber, toObject, toString, toBoolean) -import Data.Argonaut.Internal +import Data.Argonaut.Options import Data.Array (zipWithA, length) import Data.Either (either, Either(..)) import Data.Foldable (find) @@ -29,51 +32,25 @@ import Type.Proxy (Proxy(..)) class DecodeJson a where decodeJson :: Json -> Either String a --- | Decode `Json` representation of a value which has a `Generic` type. +-- | Decode `Json` representation of a value which has a `Generic` type +-- | with Argonaut options. gDecodeJson :: forall a. (Generic a) => Json -> Either String a -gDecodeJson json = maybe (Left "fromSpine failed") Right <<< fromSpine - =<< gDecodeJson' (toSignature (Proxy :: Proxy a)) json +gDecodeJson = genericDecodeJson argonautOptions --- | Decode `Json` representation of a `GenericSpine`. -gDecodeJson' :: GenericSignature -> Json -> Either String GenericSpine -gDecodeJson' signature json = case signature of - SigNumber -> SNumber <$> mFail "Expected a number" (toNumber json) - SigInt -> SInt <$> mFail "Expected an integer number" (fromNumber =<< toNumber json) - SigString -> SString <$> mFail "Expected a string" (toString json) - SigChar -> SChar <$> mFail "Expected a char" (toChar =<< toString json) - SigBoolean -> SBoolean <$> mFail "Expected a boolean" (toBoolean json) - SigArray thunk -> do - jArr <- mFail "Expected an array" $ toArray json - SArray <$> traverse (map const <<< gDecodeJson' (thunk unit)) jArr - SigRecord props -> do - jObj <- mFail "Expected an object" $ toObject json - SRecord <$> for props \({recLabel: lbl, recValue: val}) -> do - pf <- mFail ("'" <> lbl <> "' property missing") (M.lookup lbl jObj) - sp <- gDecodeJson' (val unit) pf - pure { recLabel: lbl, recValue: const sp } - SigProd typeConstr alts -> do - let decodingErr msg = "When decoding a " ++ typeConstr ++ ": " ++ msg - jObj <- mFail (decodingErr "expected an object") (toObject json) - tagJson <- mFail (decodingErr "'tag' property is missing") (M.lookup "tag" jObj) - tag <- mFail (decodingErr "'tag' property is not a string") (toString tagJson) - case find ((tag ==) <<< _.sigConstructor) alts of - Nothing -> Left (decodingErr ("'" <> tag <> "' isn't a valid constructor")) - Just { sigValues: sigValues } -> do - vals <- mFail (decodingErr "'values' array is missing") (toArray =<< M.lookup "values" jObj) - sps <- zipWithA (\k -> gDecodeJson' (k unit)) sigValues vals - pure (SProd tag (const <$> sps)) - where - mFail :: forall a. String -> Maybe a -> Either String a - mFail msg = maybe (Left msg) Right +-- | Decode `Json` representation of a value which has a `Generic` type +-- | with Aeson options. Data from Haskell, with Aeson default options can be +-- | decoded with gAesonDecodJson. +gAesonDecodeJson :: forall a. (Generic a) => Json -> Either String a +gAesonDecodeJson = genericDecodeJson aesonOptions -- | Decode `Json` representation of a value which has a `Generic` type. -gAesonDecodeJson :: forall a. (Generic a) => Json -> Either String a -gAesonDecodeJson json = maybe (Left "fromSpine failed") Right <<< fromSpine - =<< gAesonDecodeJson' (toSignature (Proxy :: Proxy a)) json +genericDecodeJson :: forall a. (Generic a) => Options -> Json -> Either String a +genericDecodeJson opts json = maybe (Left "fromSpine failed") Right <<< fromSpine + =<< genericDecodeJson' opts (toSignature (Proxy :: Proxy a)) json -- | Decode `Json` representation of a `GenericSpine`. -gAesonDecodeJson' :: GenericSignature -> Json -> Either String GenericSpine -gAesonDecodeJson' signature json = case signature of +genericDecodeJson' :: Options -> GenericSignature -> Json -> Either String GenericSpine +genericDecodeJson' opts signature json = case signature of SigNumber -> SNumber <$> mFail "Expected a number" (toNumber json) SigInt -> SInt <$> mFail "Expected an integer number" (fromNumber =<< toNumber json) SigString -> SString <$> mFail "Expected a string" (toString json) @@ -81,41 +58,41 @@ gAesonDecodeJson' signature json = case signature of SigBoolean -> SBoolean <$> mFail "Expected a boolean" (toBoolean json) SigArray thunk -> do jArr <- mFail "Expected an array" $ toArray json - SArray <$> traverse (map const <<< gDecodeJson' (thunk unit)) jArr + SArray <$> traverse (map const <<< genericDecodeJson' opts (thunk unit)) jArr SigRecord props -> do jObj <- mFail "Expected an object" $ toObject json SRecord <$> for props \({recLabel: lbl, recValue: val}) -> do pf <- mFail ("'" <> lbl <> "' property missing") (M.lookup lbl jObj) - sp <- gDecodeJson' (val unit) pf + sp <- genericDecodeJson' opts (val unit) pf pure { recLabel: lbl, recValue: const sp } - SigProd typeConstr constrSigns -> gAesonDecodeProdJson' typeConstr constrSigns json - -mFail :: forall a. String -> Maybe a -> Either String a -mFail msg = maybe (Left msg) Right - + SigProd typeConstr constrSigns -> genericDecodeProdJson' opts typeConstr constrSigns json -gAesonDecodeProdJson' :: String -> Array DataConstructor -> Json -> Either String GenericSpine -gAesonDecodeProdJson' tname constrSigns json = if allConstrNullary constrSigns - then decodeFromString - else decodeTagged +genericDecodeProdJson' :: Options -> String -> Array DataConstructor -> Json -> Either String GenericSpine +genericDecodeProdJson' opts tname constrSigns json = if opts.allNullaryToStringTag && allConstructorsNullary constrSigns + then decodeFromString + else decodeTagged where decodeFromString = do tag <- mFail (decodingErr "Constructor name as string expected") (toString json) pure (SProd tag []) decodeTagged = do jObj <- mFail (decodingErr "expected an object") (toObject json) - tagJson <- mFail (decodingErr "'tag' property is missing") (M.lookup "tag" jObj) - tag <- mFail (decodingErr "'tag' property is not a string") (toString tagJson) + tagJson <- mFail (decodingErr "'" ++ tagL ++ "' property is missing") (M.lookup tagL jObj) + tag <- mFail (decodingErr "'" ++ tagL ++ "' property is not a string") (toString tagJson) case find ((tag ==) <<< fixConstr <<< _.sigConstructor) constrSigns of Nothing -> Left (decodingErr ("'" <> tag <> "' isn't a valid constructor")) Just { sigValues: sigValues } -> do - jVals <- mFail (decodingErr "'contents' property is missing") (M.lookup "contents" jObj) - vals <- case length sigValues of - 1 -> pure [jVals] - _ -> mFail (decodingErr "Expected array") (toArray jVals) - sps <- zipWithA (\k -> gAesonDecodeJson' (k unit)) sigValues vals + jVals <- mFail (decodingErr "'" ++ contL ++ "' property is missing") (M.lookup contL jObj) + vals <- if opts.flattenContentsArray && (length sigValues == 1) + then pure [jVals] + else mFail (decodingErr "Expected array") (toArray jVals) + sps <- zipWithA (\k -> genericDecodeJson' opts (k unit)) sigValues vals pure (SProd tag (const <$> sps)) decodingErr msg = "When decoding a " ++ tname ++ ": " ++ msg + fixConstr = opts.constructorTagModifier + sumConf = case opts.sumEncoding of TaggedObject conf -> conf + tagL = sumConf.tagFieldName + contL = sumConf.contentsFieldName instance decodeJsonMaybe :: (DecodeJson a) => DecodeJson (Maybe a) where decodeJson j @@ -191,3 +168,6 @@ instance decodeMap :: (Ord a, DecodeJson a, DecodeJson b) => DecodeJson (Map.Map decodeMaybe :: forall a. (DecodeJson a) => Json -> Maybe a decodeMaybe json = either (const Nothing) pure $ decodeJson json + +mFail :: forall a. String -> Maybe a -> Either String a +mFail msg = maybe (Left msg) Right diff --git a/src/Data/Argonaut/Encode.purs b/src/Data/Argonaut/Encode.purs index 5cf770c..c103711 100644 --- a/src/Data/Argonaut/Encode.purs +++ b/src/Data/Argonaut/Encode.purs @@ -2,13 +2,16 @@ module Data.Argonaut.Encode ( EncodeJson , encodeJson , gEncodeJson - , gEncodeJson' + , gAesonEncodeJson + , genericEncodeJson + , genericEncodeJson' + , module Data.Argonaut.Options ) where import Prelude import Data.Argonaut.Core (Json(), jsonNull, fromBoolean, fromNumber, fromString, fromArray, fromObject) -import Data.Argonaut.Internal +import Data.Argonaut.Options import Data.Either (Either(), either) import Data.Foldable (foldr) import Data.Generic (Generic, GenericSpine(..), toSpine, GenericSignature(..), DataConstructor(), toSignature) @@ -21,74 +24,71 @@ import Data.StrMap as SM import Data.Tuple (Tuple(..)) import Type.Proxy (Proxy(..)) import Data.Tuple (uncurry) -import Data.Array (null, concatMap, filter, zip) +import Data.Array (length, concatMap, filter, zip, zipWith) +import qualified Data.Array.Unsafe as Unsafe class EncodeJson a where encodeJson :: a -> Json --- | Encode any `Generic` data structure into `Json`. +-- | Encode any `Generic` data structure into `Json`, +-- | formatted according to argonautOptions gEncodeJson :: forall a. (Generic a) => a -> Json -gEncodeJson = gEncodeJson' <<< toSpine - --- | Encode `GenericSpine` into `Json`. -gEncodeJson' :: GenericSpine -> Json -gEncodeJson' spine = case spine of - SInt x -> fromNumber $ toNumber x - SString x -> fromString x - SChar x -> fromString $ fromChar x - SNumber x -> fromNumber x - SBoolean x -> fromBoolean x - SArray thunks -> fromArray (gEncodeJson' <<< (unit #) <$> thunks) - SProd constr args -> fromObject - $ SM.insert "tag" (encodeJson constr) - $ SM.singleton "values" (encodeJson (gEncodeJson' <<< (unit #) <$> args)) - SRecord fields -> fromObject $ foldr addField SM.empty fields - where addField field = SM.insert field.recLabel - (gEncodeJson' $ field.recValue unit) - +gEncodeJson = genericEncodeJson argonautOptions +-- | Encode any `Generic` data structure into `Json`, +-- | formatted according to aesonOptions, which is compatible to the default +-- | encoding used by Aeson from Haskell. gAesonEncodeJson :: forall a. (Generic a) => a -> Json -gAesonEncodeJson = gAesonEncodeJson' sign <<< toSpine +gAesonEncodeJson = genericEncodeJson aesonOptions + +genericEncodeJson :: forall a. (Generic a) => Options -> a -> Json +genericEncodeJson opts = genericEncodeJson' opts sign <<< toSpine where sign = toSignature (Proxy :: Proxy a) -- | Encode `GenericSpine` into `Json`. -gAesonEncodeJson' :: GenericSignature -> GenericSpine -> Json -gAesonEncodeJson' sign spine = case spine of +genericEncodeJson' :: Options -> GenericSignature -> GenericSpine -> Json +genericEncodeJson' opts sign spine = case spine of SInt x -> fromNumber $ toNumber x SString x -> fromString x SChar x -> fromString $ fromChar x SNumber x -> fromNumber x SBoolean x -> fromBoolean x - SArray thunks -> fromArray (gAesonEncodeJson' sign <<< (unit #) <$> thunks) + SArray thunks -> case sign of + SigArray elemSign -> fromArray (genericEncodeJson' opts (elemSign unit) <<< (unit #) <$> thunks) + -- _ -> unsafeCrashWith "Signature does not match value, please don't do that!" -- Not yet supported, waiting for purescript 0.8 SProd constr args -> case sign of - SigProd _ constrSigns -> gAesonEncodeProdJson' constrSigns constr args + SigProd _ constrSigns -> genericEncodeProdJson' opts constrSigns constr args -- _ -> unsafeCrashWith "Signature does not match value, please don't do that!" -- Not yet supported, waiting for purescript 0.8 SRecord fields -> case sign of - SigRecord sigs -> gAesonEncodeRecordJson' sigs fields + SigRecord sigs -> genericEncodeRecordJson' opts sigs fields -- _ -> unsafeCrashWith "Signature does not match value, please don't do that!" -- Not yet supported, waiting for purescript 0.8 -gAesonEncodeRecordJson' :: Array { recLabel :: String, recValue :: Unit -> GenericSignature } +genericEncodeRecordJson' :: Options + -> Array { recLabel :: String, recValue :: Unit -> GenericSignature } -> Array { recLabel :: String, recValue :: Unit -> GenericSpine } -> Json -gAesonEncodeRecordJson' sigs fields = fromObject <<< foldr (uncurry addField) SM.empty $ zip sigs fields +genericEncodeRecordJson' opts sigs fields = fromObject <<< foldr (uncurry addField) SM.empty $ zip sigs fields where - addField sig field = SM.insert field.recLabel (gAesonEncodeJson' (sig.recValue unit) (field.recValue unit)) + addField sig field = SM.insert field.recLabel (genericEncodeJson' opts (sig.recValue unit) (field.recValue unit)) -gAesonEncodeProdJson' :: Array DataConstructor -> String -> Array (Unit -> GenericSpine) -> Json -gAesonEncodeProdJson' constrSigns constr args = fromObject - $ SM.insert "tag" (encodeJson (fixConstr constr)) - $ SM.singleton "contents" flattenedArgs +genericEncodeProdJson' :: Options -> Array DataConstructor -> String -> Array (Unit -> GenericSpine) -> Json +genericEncodeProdJson' opts constrSigns constr args = fromObject + $ SM.insert sumConf.tagFieldName (encodeJson fixedConstr) + $ SM.singleton sumConf.contentsFieldName flattenedArgs where - contents = if allConstrNullary constrSigns -- If no constructor has any values - serialize as string ... - then fromString constr - else flattenedArgs - encodedArgs = gAesonEncodeProdArgs constrSigns constr args - flattenedArgs = case encodedArgs of - [a] -> a - as -> encodeJson as - -gAesonEncodeProdArgs :: Array DataConstructor -> String -> Array (Unit -> GenericSpine) -> Array (Json) -gAesonEncodeProdArgs constrSigns constr args = gAesonEncodeJson' <$> sigValues <*> values + sumConf = case opts. sumEncoding of + TaggedObject conf -> conf + fixedConstr = opts.constructorTagModifier constr + contents = if opts.allNullaryToStringTag && allConstructorsNullary constrSigns + then fromString constr + else flattenedArgs + encodedArgs = genericEncodeProdArgs opts constrSigns constr args + flattenedArgs = if opts.flattenContentsArray && length encodedArgs == 1 + then Unsafe.head encodedArgs + else encodeJson encodedArgs + +genericEncodeProdArgs :: Options -> Array DataConstructor -> String -> Array (Unit -> GenericSpine) -> Array (Json) +genericEncodeProdArgs opts constrSigns constr args = zipWith (genericEncodeJson' opts) sigValues values where lSigValues = concatMap (\c -> c.sigValues) <<< filter (\c -> c.sigConstructor == constr) $ constrSigns diff --git a/src/Data/Argonaut/Internal.purs b/src/Data/Argonaut/Internal.purs deleted file mode 100644 index cf3e2f1..0000000 --- a/src/Data/Argonaut/Internal.purs +++ /dev/null @@ -1,16 +0,0 @@ -module Data.Argonaut.Internal where - -import Prelude -import Data.Foldable (foldr) -import Data.String (lastIndexOf, drop) -import Data.Generic (DataConstructor()) -import Data.Array (null) -import Data.Maybe (Maybe(..)) - -allConstrNullary :: Array DataConstructor -> Boolean -allConstrNullary constrSigns = foldr (&&) true <<< map (\c -> null c.sigValues) $ constrSigns - -fixConstr :: String -> String -fixConstr constr = case lastIndexOf "." constr of - Nothing -> constr - Just i -> drop (i+1) constr diff --git a/src/Data/Argonaut/Options.purs b/src/Data/Argonaut/Options.purs new file mode 100644 index 0000000..71bff0c --- /dev/null +++ b/src/Data/Argonaut/Options.purs @@ -0,0 +1,59 @@ +module Data.Argonaut.Options where + +import Prelude +import Data.Foldable (foldr) +import Data.String (lastIndexOf, drop) +import Data.Generic (DataConstructor()) +import Data.Array (null) +import Data.Maybe (Maybe(..)) + + +type Options = { + constructorTagModifier :: String -> String +, allNullaryToStringTag :: Boolean +, sumEncoding :: SumEncoding +, flattenContentsArray :: Boolean -- Flatten array to simple value, if constructor only takes a single value +} + +data SumEncoding = TaggedObject { + tagFieldName :: String +, contentsFieldName :: String +} + +argonautOptions :: Options +argonautOptions = { + constructorTagModifier : id +, allNullaryToStringTag : false +, sumEncoding : argonautSumEncoding +, flattenContentsArray : false +} + +argonautSumEncoding :: SumEncoding +argonautSumEncoding = TaggedObject { + tagFieldName : "tag" +, contentsFieldName : "values" +} + +aesonOptions :: Options +aesonOptions = { + constructorTagModifier : stripModulePath +, allNullaryToStringTag : true +, sumEncoding : aesonSumEncoding +, flattenContentsArray : true +} + +aesonSumEncoding :: SumEncoding +aesonSumEncoding = TaggedObject { + tagFieldName : "tag" +, contentsFieldName : "contents" +} + + + +allConstructorsNullary :: Array DataConstructor -> Boolean +allConstructorsNullary constrSigns = foldr (&&) true <<< map (\c -> null c.sigValues) $ constrSigns + +stripModulePath :: String -> String +stripModulePath constr = case lastIndexOf "." constr of + Nothing -> constr + Just i -> drop (i+1) constr diff --git a/test/Test/Main.purs b/test/Test/Main.purs index 02d26d1..788c4d5 100644 --- a/test/Test/Main.purs +++ b/test/Test/Main.purs @@ -3,8 +3,8 @@ module Test.Main where import Prelude import Data.Argonaut.Core -import Data.Argonaut.Decode (decodeJson, DecodeJson, gDecodeJson, gDecodeJson') -import Data.Argonaut.Encode (encodeJson, EncodeJson, gEncodeJson, gEncodeJson') +import Data.Argonaut.Decode (decodeJson, DecodeJson, gDecodeJson, genericDecodeJson', argonautOptions) +import Data.Argonaut.Encode (encodeJson, EncodeJson, gEncodeJson, genericEncodeJson', argonautOptions) import Data.Argonaut.Combinators ((:=), (~>), (?>>=), (.?)) import Data.Either import Data.Tuple @@ -145,12 +145,12 @@ derive instance genericUser :: Generic User prop_iso_generic :: GenericValue -> Boolean prop_iso_generic genericValue = - Right val.spine == gDecodeJson' val.signature (gEncodeJson' val.spine) + Right val.spine == genericDecodeJson' argonautOptions val.signature (genericEncodeJson' argonautOptions val.signature val.spine) where val = runGenericValue genericValue prop_decoded_spine_valid :: GenericValue -> Boolean prop_decoded_spine_valid genericValue = - Right true == (isValidSpine val.signature <$> gDecodeJson' val.signature (gEncodeJson' val.spine)) + Right true == (isValidSpine val.signature <$> genericDecodeJson' argonautOptions val.signature (genericEncodeJson' argonautOptions val.signature val.spine)) where val = runGenericValue genericValue genericsCheck = do From 8bf4ba9aa130f8a2691149c9ffafbb80f3398f8a Mon Sep 17 00:00:00 2001 From: Robert Klotzner Date: Mon, 25 Jan 2016 16:15:32 +0100 Subject: [PATCH 03/16] foldr (&&) true <<< map ... -> all --- src/Data/Argonaut/Options.purs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Data/Argonaut/Options.purs b/src/Data/Argonaut/Options.purs index 71bff0c..7c1db39 100644 --- a/src/Data/Argonaut/Options.purs +++ b/src/Data/Argonaut/Options.purs @@ -1,7 +1,7 @@ module Data.Argonaut.Options where import Prelude -import Data.Foldable (foldr) +import Data.Foldable (all) import Data.String (lastIndexOf, drop) import Data.Generic (DataConstructor()) import Data.Array (null) @@ -51,7 +51,7 @@ aesonSumEncoding = TaggedObject { allConstructorsNullary :: Array DataConstructor -> Boolean -allConstructorsNullary constrSigns = foldr (&&) true <<< map (\c -> null c.sigValues) $ constrSigns +allConstructorsNullary constrSigns = all (null <<< _.sigValues) $ constrSigns stripModulePath :: String -> String stripModulePath constr = case lastIndexOf "." constr of From 4fb35e7c6f8ca356699a94554331ebfce38c63b6 Mon Sep 17 00:00:00 2001 From: Robert Klotzner Date: Mon, 25 Jan 2016 17:08:36 +0100 Subject: [PATCH 04/16] Added tests for aeson encoding. --- test/Test/Main.purs | 59 +++++++++++++++++++++++++++------------------ 1 file changed, 36 insertions(+), 23 deletions(-) diff --git a/test/Test/Main.purs b/test/Test/Main.purs index 788c4d5..23220a2 100644 --- a/test/Test/Main.purs +++ b/test/Test/Main.purs @@ -3,8 +3,9 @@ module Test.Main where import Prelude import Data.Argonaut.Core -import Data.Argonaut.Decode (decodeJson, DecodeJson, gDecodeJson, genericDecodeJson', argonautOptions) -import Data.Argonaut.Encode (encodeJson, EncodeJson, gEncodeJson, genericEncodeJson', argonautOptions) +import Data.Argonaut.Options +import Data.Argonaut.Decode (decodeJson, DecodeJson, genericDecodeJson, genericDecodeJson', argonautOptions) +import Data.Argonaut.Encode (encodeJson, EncodeJson, genericEncodeJson, genericEncodeJson', argonautOptions) import Data.Argonaut.Combinators ((:=), (~>), (?>>=), (.?)) import Data.Either import Data.Tuple @@ -143,31 +144,37 @@ data User = Anonymous } derive instance genericUser :: Generic User -prop_iso_generic :: GenericValue -> Boolean -prop_iso_generic genericValue = - Right val.spine == genericDecodeJson' argonautOptions val.signature (genericEncodeJson' argonautOptions val.signature val.spine) +data AllNullary = Nullary1 | Nullary2 | Nullary3 +derive instance genericAllNullary :: Generic AllNullary + +data MultipleArgs = MArgs Int Int String | NArgs +derive instance genericMultipleArgs :: Generic MultipleArgs + +prop_iso_generic :: Options -> GenericValue -> Boolean +prop_iso_generic opts genericValue = + Right val.spine == genericDecodeJson' opts val.signature (genericEncodeJson' opts val.signature val.spine) where val = runGenericValue genericValue -prop_decoded_spine_valid :: GenericValue -> Boolean -prop_decoded_spine_valid genericValue = - Right true == (isValidSpine val.signature <$> genericDecodeJson' argonautOptions val.signature (genericEncodeJson' argonautOptions val.signature val.spine)) +prop_decoded_spine_valid :: Options -> GenericValue -> Boolean +prop_decoded_spine_valid opts genericValue = + Right true == (isValidSpine val.signature <$> genericDecodeJson' opts val.signature (genericEncodeJson' opts val.signature val.spine)) where val = runGenericValue genericValue -genericsCheck = do +genericsCheck opts= do log "Check that decodeJson' and encodeJson' form an isomorphism" - quickCheck prop_iso_generic + quickCheck (prop_iso_generic opts) log "Check that decodeJson' returns a valid spine" - quickCheck prop_decoded_spine_valid - log "Print samples of values encoded with gEncodeJson" - print $ gEncodeJson 5 - print $ gEncodeJson [1, 2, 3, 5] - print $ gEncodeJson (Just "foo") - print $ gEncodeJson (Right "foo" :: Either String String) - print $ gEncodeJson $ MyRecord { foo: "foo", bar: 2} - print $ gEncodeJson "foo" - print $ gEncodeJson Anonymous - print $ gEncodeJson $ Guest "guest's handle" - print $ gEncodeJson $ Registered { name: "user1" + quickCheck (prop_decoded_spine_valid opts) + log "Print samples of values encoded with genericEncodeJson" + print $ genericEncodeJson opts 5 + print $ genericEncodeJson opts [1, 2, 3, 5] + print $ genericEncodeJson opts (Just "foo") + print $ genericEncodeJson opts (Right "foo" :: Either String String) + print $ genericEncodeJson opts $ MyRecord { foo: "foo", bar: 2} + print $ genericEncodeJson opts "foo" + print $ genericEncodeJson opts Anonymous + print $ genericEncodeJson opts $ Guest "guest's handle" + print $ genericEncodeJson opts $ Registered { name: "user1" , age: 5 , balance: 26.6 , banned: false @@ -181,7 +188,10 @@ genericsCheck = do , tweets: ["Hi"] , followers: [] }]} - + print $ genericEncodeJson opts Nullary1 + print $ genericEncodeJson opts Nullary2 + print $ genericEncodeJson opts $ MArgs 9 22 "Test" + print $ genericEncodeJson opts NArgs eitherCheck = do log "Test EncodeJson/DecodeJson Either instance" @@ -197,4 +207,7 @@ main = do eitherCheck encodeDecodeCheck combinatorsCheck - genericsCheck + log "genericsCheck check for argonautOptions" + genericsCheck argonautOptions + log "genericsCheck check for aesonOptions" + genericsCheck aesonOptions From e54f5e65c296d3b8c441729a7b42036f190acedc Mon Sep 17 00:00:00 2001 From: Robert Klotzner Date: Tue, 26 Jan 2016 10:50:30 +0100 Subject: [PATCH 05/16] Added tests and fixed bugs in aeson encoding. Ouch! --- src/Data/Argonaut/Decode.purs | 22 ++++++++++++---------- src/Data/Argonaut/Encode.purs | 13 ++++++------- test/Test/Main.purs | 23 +++++++++++++++++++++-- 3 files changed, 39 insertions(+), 19 deletions(-) diff --git a/src/Data/Argonaut/Decode.purs b/src/Data/Argonaut/Decode.purs index ae3971a..e451de9 100644 --- a/src/Data/Argonaut/Decode.purs +++ b/src/Data/Argonaut/Decode.purs @@ -74,25 +74,27 @@ genericDecodeProdJson' opts tname constrSigns json = if opts.allNullaryToStringT where decodeFromString = do tag <- mFail (decodingErr "Constructor name as string expected") (toString json) - pure (SProd tag []) + foundConstr <- findConstrFail tag + pure (SProd foundConstr.sigConstructor []) decodeTagged = do jObj <- mFail (decodingErr "expected an object") (toObject json) tagJson <- mFail (decodingErr "'" ++ tagL ++ "' property is missing") (M.lookup tagL jObj) tag <- mFail (decodingErr "'" ++ tagL ++ "' property is not a string") (toString tagJson) - case find ((tag ==) <<< fixConstr <<< _.sigConstructor) constrSigns of - Nothing -> Left (decodingErr ("'" <> tag <> "' isn't a valid constructor")) - Just { sigValues: sigValues } -> do - jVals <- mFail (decodingErr "'" ++ contL ++ "' property is missing") (M.lookup contL jObj) - vals <- if opts.flattenContentsArray && (length sigValues == 1) - then pure [jVals] - else mFail (decodingErr "Expected array") (toArray jVals) - sps <- zipWithA (\k -> genericDecodeJson' opts (k unit)) sigValues vals - pure (SProd tag (const <$> sps)) + foundConstr <- findConstrFail tag + jVals <- mFail (decodingErr "'" ++ contL ++ "' property is missing") (M.lookup contL jObj) + vals <- if opts.flattenContentsArray && (length foundConstr.sigValues == 1) + then pure [jVals] + else mFail (decodingErr "Expected array") (toArray jVals) + sps <- zipWithA (\k -> genericDecodeJson' opts (k unit)) foundConstr.sigValues vals + pure (SProd foundConstr.sigConstructor (const <$> sps)) + decodingErr msg = "When decoding a " ++ tname ++ ": " ++ msg fixConstr = opts.constructorTagModifier sumConf = case opts.sumEncoding of TaggedObject conf -> conf tagL = sumConf.tagFieldName contL = sumConf.contentsFieldName + findConstrFail tag = mFail (decodingErr ("'" <> tag <> "' isn't a valid constructor")) (findConstr tag) + findConstr tag = find ((tag ==) <<< fixConstr <<< _.sigConstructor) constrSigns instance decodeJsonMaybe :: (DecodeJson a) => DecodeJson (Maybe a) where decodeJson j diff --git a/src/Data/Argonaut/Encode.purs b/src/Data/Argonaut/Encode.purs index c103711..912aa85 100644 --- a/src/Data/Argonaut/Encode.purs +++ b/src/Data/Argonaut/Encode.purs @@ -72,18 +72,17 @@ genericEncodeRecordJson' opts sigs fields = fromObject <<< foldr (uncurry addFie addField sig field = SM.insert field.recLabel (genericEncodeJson' opts (sig.recValue unit) (field.recValue unit)) genericEncodeProdJson' :: Options -> Array DataConstructor -> String -> Array (Unit -> GenericSpine) -> Json -genericEncodeProdJson' opts constrSigns constr args = fromObject - $ SM.insert sumConf.tagFieldName (encodeJson fixedConstr) - $ SM.singleton sumConf.contentsFieldName flattenedArgs +genericEncodeProdJson' opts constrSigns constr args = if opts.allNullaryToStringTag && allConstructorsNullary constrSigns + then fromString fixedConstr + else fromObject + $ SM.insert sumConf.tagFieldName (encodeJson fixedConstr) + $ SM.singleton sumConf.contentsFieldName contents where sumConf = case opts. sumEncoding of TaggedObject conf -> conf fixedConstr = opts.constructorTagModifier constr - contents = if opts.allNullaryToStringTag && allConstructorsNullary constrSigns - then fromString constr - else flattenedArgs encodedArgs = genericEncodeProdArgs opts constrSigns constr args - flattenedArgs = if opts.flattenContentsArray && length encodedArgs == 1 + contents = if opts.flattenContentsArray && length encodedArgs == 1 then Unsafe.head encodedArgs else encodeJson encodedArgs diff --git a/test/Test/Main.purs b/test/Test/Main.purs index 23220a2..054d10e 100644 --- a/test/Test/Main.purs +++ b/test/Test/Main.purs @@ -4,8 +4,8 @@ import Prelude import Data.Argonaut.Core import Data.Argonaut.Options -import Data.Argonaut.Decode (decodeJson, DecodeJson, genericDecodeJson, genericDecodeJson', argonautOptions) -import Data.Argonaut.Encode (encodeJson, EncodeJson, genericEncodeJson, genericEncodeJson', argonautOptions) +import Data.Argonaut.Decode (decodeJson, DecodeJson, genericDecodeJson, genericDecodeJson') +import Data.Argonaut.Encode (encodeJson, EncodeJson, genericEncodeJson, genericEncodeJson') import Data.Argonaut.Combinators ((:=), (~>), (?>>=), (.?)) import Data.Either import Data.Tuple @@ -14,12 +14,14 @@ import Data.Array import Data.Generic import Data.Foldable (foldl) import Data.List (toList, List(..)) + import Control.Monad.Eff.Console import qualified Data.StrMap as M import Test.StrongCheck import Test.StrongCheck.Gen import Test.StrongCheck.Generic +import Type.Proxy genJNull :: Gen Json genJNull = pure jsonNull @@ -144,11 +146,16 @@ data User = Anonymous } derive instance genericUser :: Generic User + data AllNullary = Nullary1 | Nullary2 | Nullary3 derive instance genericAllNullary :: Generic AllNullary +instance genericEqAllNullary :: Eq AllNullary where + eq = gEq data MultipleArgs = MArgs Int Int String | NArgs derive instance genericMultipleArgs :: Generic MultipleArgs +instance genericEqMArgs :: Eq MultipleArgs where + eq = gEq prop_iso_generic :: Options -> GenericValue -> Boolean prop_iso_generic opts genericValue = @@ -161,7 +168,11 @@ prop_decoded_spine_valid opts genericValue = where val = runGenericValue genericValue genericsCheck opts= do + let vNullary = Nullary2 + let mArgs = MArgs 9 20 "Hello" log "Check that decodeJson' and encodeJson' form an isomorphism" + logError " Check all nullary:" (aesonEncodeDecode vNullary) + logError " Check multiple args:" (aesonEncodeDecode mArgs) quickCheck (prop_iso_generic opts) log "Check that decodeJson' returns a valid spine" quickCheck (prop_decoded_spine_valid opts) @@ -193,6 +204,14 @@ genericsCheck opts= do print $ genericEncodeJson opts $ MArgs 9 22 "Test" print $ genericEncodeJson opts NArgs + where + aesonEncodeDecode :: forall a. (Eq a, Generic a) => a -> Boolean + aesonEncodeDecode val = ((Right val) ==) <<< genericDecodeJson aesonOptions <<< genericEncodeJson aesonOptions $ val + + logError message test = log $ message ++ result test + where result false = " ##########FAILED########!" + result true = " ok." + eitherCheck = do log "Test EncodeJson/DecodeJson Either instance" quickCheck \(x :: Either String String) -> From fa5520839f7c264da4c2bf619fc365ce2158c44a Mon Sep 17 00:00:00 2001 From: Robert Klotzner Date: Tue, 26 Jan 2016 11:14:09 +0100 Subject: [PATCH 06/16] Added documentation for aeson options --- src/Data/Argonaut/Options.purs | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/Data/Argonaut/Options.purs b/src/Data/Argonaut/Options.purs index 7c1db39..bbdf942 100644 --- a/src/Data/Argonaut/Options.purs +++ b/src/Data/Argonaut/Options.purs @@ -9,17 +9,28 @@ import Data.Maybe (Maybe(..)) type Options = { + -- | Modify the tag, e.g. strip module path with: `stripModulePath` constructorTagModifier :: String -> String + -- | If all constructors of a sum type are nullary, just serialize the constructor name as a string. , allNullaryToStringTag :: Boolean + -- | Options on how to do encoding of sum types. , sumEncoding :: SumEncoding + -- | If a constructor has exactly one field, do not serialize as array. , flattenContentsArray :: Boolean -- Flatten array to simple value, if constructor only takes a single value } -data SumEncoding = TaggedObject { - tagFieldName :: String -, contentsFieldName :: String -} +data SumEncoding = + -- | Serialize as tagged object. + -- | The Javascript object will have a tag field, with the + -- | `constructorTagModifier constructorName` name as contents + -- | and a contents field, which contains an array with the constructor + -- | parameters. + TaggedObject { + tagFieldName :: String + , contentsFieldName :: String + } +-- | Default for straight forward argonaut encoding. argonautOptions :: Options argonautOptions = { constructorTagModifier : id @@ -34,6 +45,7 @@ argonautSumEncoding = TaggedObject { , contentsFieldName : "values" } +-- | Options for aeson compatible encoding/decoding. aesonOptions :: Options aesonOptions = { constructorTagModifier : stripModulePath From 360ab2d8cf48921db5cc91b6474ff16becb0ada4 Mon Sep 17 00:00:00 2001 From: Robert Klotzner Date: Tue, 26 Jan 2016 11:20:13 +0100 Subject: [PATCH 07/16] A little cleanup --- src/Data/Argonaut/Decode.purs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Data/Argonaut/Decode.purs b/src/Data/Argonaut/Decode.purs index e451de9..c03931f 100644 --- a/src/Data/Argonaut/Decode.purs +++ b/src/Data/Argonaut/Decode.purs @@ -87,10 +87,12 @@ genericDecodeProdJson' opts tname constrSigns json = if opts.allNullaryToStringT else mFail (decodingErr "Expected array") (toArray jVals) sps <- zipWithA (\k -> genericDecodeJson' opts (k unit)) foundConstr.sigValues vals pure (SProd foundConstr.sigConstructor (const <$> sps)) - + decodingErr msg = "When decoding a " ++ tname ++ ": " ++ msg fixConstr = opts.constructorTagModifier - sumConf = case opts.sumEncoding of TaggedObject conf -> conf + sumConf = case opts.sumEncoding of + TaggedObject conf -> conf + -- _ -> unsafeCrashWith "Only TaggedObject encoding is supported - FIX ME!" -- Not yet supported, waiting for purescript 0.8 tagL = sumConf.tagFieldName contL = sumConf.contentsFieldName findConstrFail tag = mFail (decodingErr ("'" <> tag <> "' isn't a valid constructor")) (findConstr tag) From d10f081c990fc9107af77aee052f793455340c20 Mon Sep 17 00:00:00 2001 From: Robert Klotzner Date: Tue, 26 Jan 2016 14:04:15 +0100 Subject: [PATCH 08/16] Added unwrapUnaryRecords option Closing #5 --- src/Data/Argonaut/Decode.purs | 15 ++++++++++++--- src/Data/Argonaut/Encode.purs | 19 ++++++++++++++----- src/Data/Argonaut/Options.purs | 13 +++++++++++-- test/Test/Main.purs | 28 +++++++++++++++++++++++----- 4 files changed, 60 insertions(+), 15 deletions(-) diff --git a/src/Data/Argonaut/Decode.purs b/src/Data/Argonaut/Decode.purs index c03931f..f48d051 100644 --- a/src/Data/Argonaut/Decode.purs +++ b/src/Data/Argonaut/Decode.purs @@ -28,6 +28,7 @@ import Data.StrMap as M import Data.Traversable (traverse, for) import Data.Tuple (Tuple(..)) import Type.Proxy (Proxy(..)) +import qualified Data.Array.Unsafe as Unsafe class DecodeJson a where decodeJson :: Json -> Either String a @@ -68,9 +69,17 @@ genericDecodeJson' opts signature json = case signature of SigProd typeConstr constrSigns -> genericDecodeProdJson' opts typeConstr constrSigns json genericDecodeProdJson' :: Options -> String -> Array DataConstructor -> Json -> Either String GenericSpine -genericDecodeProdJson' opts tname constrSigns json = if opts.allNullaryToStringTag && allConstructorsNullary constrSigns - then decodeFromString - else decodeTagged +genericDecodeProdJson' opts tname constrSigns json = + if opts.unwrapUnaryRecords && isUnaryRecord constrSigns + then do + let constr = Unsafe.head constrSigns + let unwrapped = Unsafe.head constr.sigValues unit + r <- genericDecodeJson' opts unwrapped json + pure (SProd constr.sigConstructor [const r]) + else + if opts.allNullaryToStringTag && allConstructorsNullary constrSigns + then decodeFromString + else decodeTagged where decodeFromString = do tag <- mFail (decodingErr "Constructor name as string expected") (toString json) diff --git a/src/Data/Argonaut/Encode.purs b/src/Data/Argonaut/Encode.purs index 912aa85..7bf881b 100644 --- a/src/Data/Argonaut/Encode.purs +++ b/src/Data/Argonaut/Encode.purs @@ -72,11 +72,18 @@ genericEncodeRecordJson' opts sigs fields = fromObject <<< foldr (uncurry addFie addField sig field = SM.insert field.recLabel (genericEncodeJson' opts (sig.recValue unit) (field.recValue unit)) genericEncodeProdJson' :: Options -> Array DataConstructor -> String -> Array (Unit -> GenericSpine) -> Json -genericEncodeProdJson' opts constrSigns constr args = if opts.allNullaryToStringTag && allConstructorsNullary constrSigns - then fromString fixedConstr - else fromObject - $ SM.insert sumConf.tagFieldName (encodeJson fixedConstr) - $ SM.singleton sumConf.contentsFieldName contents +genericEncodeProdJson' opts constrSigns constr args = + if opts.unwrapUnaryRecords && isUnaryRecord constrSigns + then + genericEncodeJson' opts + (Unsafe.head (Unsafe.head constrSigns).sigValues unit) + (Unsafe.head args unit) + else + if opts.allNullaryToStringTag && allConstructorsNullary constrSigns + then fromString fixedConstr + else fromObject + $ SM.insert sumConf.tagFieldName (encodeJson fixedConstr) + $ SM.singleton sumConf.contentsFieldName contents where sumConf = case opts. sumEncoding of TaggedObject conf -> conf @@ -86,6 +93,8 @@ genericEncodeProdJson' opts constrSigns constr args = if opts.allNullaryToString then Unsafe.head encodedArgs else encodeJson encodedArgs + + genericEncodeProdArgs :: Options -> Array DataConstructor -> String -> Array (Unit -> GenericSpine) -> Array (Json) genericEncodeProdArgs opts constrSigns constr args = zipWith (genericEncodeJson' opts) sigValues values where diff --git a/src/Data/Argonaut/Options.purs b/src/Data/Argonaut/Options.purs index bbdf942..a00f8d2 100644 --- a/src/Data/Argonaut/Options.purs +++ b/src/Data/Argonaut/Options.purs @@ -4,7 +4,7 @@ import Prelude import Data.Foldable (all) import Data.String (lastIndexOf, drop) import Data.Generic (DataConstructor()) -import Data.Array (null) +import Data.Array (null, length) import Data.Maybe (Maybe(..)) @@ -17,6 +17,9 @@ type Options = { , sumEncoding :: SumEncoding -- | If a constructor has exactly one field, do not serialize as array. , flattenContentsArray :: Boolean -- Flatten array to simple value, if constructor only takes a single value + -- | You need a newtype wrapper encoding/decoding of records, set this + -- | to true if you want the plain Javascript object without a wrapping tagged object. +, unwrapUnaryRecords :: Boolean } data SumEncoding = @@ -37,6 +40,7 @@ argonautOptions = { , allNullaryToStringTag : false , sumEncoding : argonautSumEncoding , flattenContentsArray : false +, unwrapUnaryRecords : false } argonautSumEncoding :: SumEncoding @@ -52,6 +56,7 @@ aesonOptions = { , allNullaryToStringTag : true , sumEncoding : aesonSumEncoding , flattenContentsArray : true +, unwrapUnaryRecords : false } aesonSumEncoding :: SumEncoding @@ -63,7 +68,11 @@ aesonSumEncoding = TaggedObject { allConstructorsNullary :: Array DataConstructor -> Boolean -allConstructorsNullary constrSigns = all (null <<< _.sigValues) $ constrSigns +allConstructorsNullary = all (null <<< _.sigValues) + +isUnaryRecord :: Array DataConstructor -> Boolean +isUnaryRecord constrSigns = length constrSigns == 1 -- Only one constructor + && all ((== 1) <<< length <<< _.sigValues) constrSigns -- Only one parameter stripModulePath :: String -> String stripModulePath constr = case lastIndexOf "." constr of diff --git a/test/Test/Main.purs b/test/Test/Main.purs index 054d10e..fc82f60 100644 --- a/test/Test/Main.purs +++ b/test/Test/Main.purs @@ -157,6 +157,15 @@ derive instance genericMultipleArgs :: Generic MultipleArgs instance genericEqMArgs :: Eq MultipleArgs where eq = gEq +newtype NewTypeWrapper1 = NewTypeWrapper1 { test :: String } +derive instance genericNewTypeWrapper1 :: Generic NewTypeWrapper1 +instance eqNewTypeWrapper1 :: Eq NewTypeWrapper1 where + eq = gEq +data NewTypeWrapper2 = NewTypeWrapper2 {test :: Int} +derive instance genericNewTypeWrapper2 :: Generic NewTypeWrapper2 +instance eqNewTypeWrapper2 :: Eq NewTypeWrapper2 where + eq = gEq + prop_iso_generic :: Options -> GenericValue -> Boolean prop_iso_generic opts genericValue = Right val.spine == genericDecodeJson' opts val.signature (genericEncodeJson' opts val.signature val.spine) @@ -170,9 +179,13 @@ prop_decoded_spine_valid opts genericValue = genericsCheck opts= do let vNullary = Nullary2 let mArgs = MArgs 9 20 "Hello" + let ntw1 = NewTypeWrapper1 { test : "hello" } + let ntw2 = NewTypeWrapper2 { test : 9 } log "Check that decodeJson' and encodeJson' form an isomorphism" - logError " Check all nullary:" (aesonEncodeDecode vNullary) - logError " Check multiple args:" (aesonEncodeDecode mArgs) + logError " Check all nullary:" (valEncodeDecode opts vNullary) + logError " Check multiple args:" (valEncodeDecode opts mArgs) + logError " Check new type wrapper (1) encoding:" (valEncodeDecode opts ntw1) + logError " Check new type wrapper (2) encoding:" (valEncodeDecode opts ntw2) quickCheck (prop_iso_generic opts) log "Check that decodeJson' returns a valid spine" quickCheck (prop_decoded_spine_valid opts) @@ -203,15 +216,17 @@ genericsCheck opts= do print $ genericEncodeJson opts Nullary2 print $ genericEncodeJson opts $ MArgs 9 22 "Test" print $ genericEncodeJson opts NArgs + print $ genericEncodeJson opts ntw1 + print $ genericEncodeJson opts ntw2 where - aesonEncodeDecode :: forall a. (Eq a, Generic a) => a -> Boolean - aesonEncodeDecode val = ((Right val) ==) <<< genericDecodeJson aesonOptions <<< genericEncodeJson aesonOptions $ val + valEncodeDecode :: forall a. (Eq a, Generic a) => Options -> a -> Boolean + valEncodeDecode opts val = ((Right val) ==) <<< genericDecodeJson opts <<< genericEncodeJson opts $ val logError message test = log $ message ++ result test where result false = " ##########FAILED########!" result true = " ok." - + eitherCheck = do log "Test EncodeJson/DecodeJson Either instance" quickCheck \(x :: Either String String) -> @@ -230,3 +245,6 @@ main = do genericsCheck argonautOptions log "genericsCheck check for aesonOptions" genericsCheck aesonOptions + log "genericsCheck check for unwrapUnaryOptions" + let unwrapUnaryOptions = aesonOptions { unwrapUnaryRecords = true } + genericsCheck unwrapUnaryOptions From a4f1cd7ec360b98b0d5b203f92316d9d5a9ff796 Mon Sep 17 00:00:00 2001 From: Robert Klotzner Date: Tue, 26 Jan 2016 15:00:49 +0100 Subject: [PATCH 09/16] Cleaned up some warnings in Test/Main.purs --- test/Test/Main.purs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/test/Test/Main.purs b/test/Test/Main.purs index fc82f60..f114164 100644 --- a/test/Test/Main.purs +++ b/test/Test/Main.purs @@ -6,22 +6,24 @@ import Data.Argonaut.Core import Data.Argonaut.Options import Data.Argonaut.Decode (decodeJson, DecodeJson, genericDecodeJson, genericDecodeJson') import Data.Argonaut.Encode (encodeJson, EncodeJson, genericEncodeJson, genericEncodeJson') -import Data.Argonaut.Combinators ((:=), (~>), (?>>=), (.?)) +import Data.Argonaut.Combinators ((:=), (~>), (?>>=)) import Data.Either import Data.Tuple import Data.Maybe import Data.Array import Data.Generic import Data.Foldable (foldl) -import Data.List (toList, List(..)) +import Data.List (toList) +import Control.Monad.Eff (Eff()) +import Control.Monad.Eff.Exception (EXCEPTION()) +import Control.Monad.Eff.Random (RANDOM()) import Control.Monad.Eff.Console import qualified Data.StrMap as M import Test.StrongCheck import Test.StrongCheck.Gen import Test.StrongCheck.Generic -import Type.Proxy genJNull :: Gen Json genJNull = pure jsonNull @@ -70,7 +72,7 @@ prop_decode_then_encode (TestJson json) = let decoded = (decodeJson json) :: Either String Json in Right json == (decoded >>= (encodeJson >>> pure)) - +encodeDecodeCheck :: forall e. Eff ( err :: EXCEPTION, random :: RANDOM, console :: CONSOLE | e ) Unit encodeDecodeCheck = do log "Showing small sample of JSON" showSample (genJson 10) @@ -121,7 +123,7 @@ assert_maybe_msg = - +combinatorsCheck :: forall e. Eff ( err :: EXCEPTION, random :: RANDOM, console :: CONSOLE | e ) Unit combinatorsCheck = do log "Check assoc builder `:=`" quickCheck' 20 prop_assoc_builder_str @@ -176,6 +178,7 @@ prop_decoded_spine_valid opts genericValue = Right true == (isValidSpine val.signature <$> genericDecodeJson' opts val.signature (genericEncodeJson' opts val.signature val.spine)) where val = runGenericValue genericValue +genericsCheck :: forall e. Options -> Eff ( err :: EXCEPTION , random :: RANDOM , console :: CONSOLE | e) Unit genericsCheck opts= do let vNullary = Nullary2 let mArgs = MArgs 9 20 "Hello" @@ -227,6 +230,7 @@ genericsCheck opts= do where result false = " ##########FAILED########!" result true = " ok." +eitherCheck :: forall e. Eff ( err :: EXCEPTION, random :: RANDOM, console :: CONSOLE | e ) Unit eitherCheck = do log "Test EncodeJson/DecodeJson Either instance" quickCheck \(x :: Either String String) -> @@ -237,6 +241,7 @@ eitherCheck = do Left err -> false err +main:: forall e. Eff ( err :: EXCEPTION, random :: RANDOM, console :: CONSOLE | e ) Unit main = do eitherCheck encodeDecodeCheck From 8adfd362a642ef662a0b19c537c2a5f289a5d6ec Mon Sep 17 00:00:00 2001 From: Robert Klotzner Date: Tue, 1 Mar 2016 12:55:28 +0100 Subject: [PATCH 10/16] Use unsafeCrashWith - psc 0.8 --- bower.json | 4 +++- src/Data/Argonaut/Encode.purs | 7 ++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/bower.json b/bower.json index fb870e6..47ec231 100644 --- a/bower.json +++ b/bower.json @@ -24,7 +24,9 @@ "dependencies": { "purescript-argonaut-core": "^0.2.0", "purescript-generics": "^0.7.0", - "purescript-integers": "^0.2.1" + "purescript-integers": "^0.2.1", + "purescript-partial" : "^1.1.0" + }, "devDependencies": { "purescript-strongcheck-generics": "^0.3.0" diff --git a/src/Data/Argonaut/Encode.purs b/src/Data/Argonaut/Encode.purs index 7bf881b..a8b640a 100644 --- a/src/Data/Argonaut/Encode.purs +++ b/src/Data/Argonaut/Encode.purs @@ -26,6 +26,7 @@ import Type.Proxy (Proxy(..)) import Data.Tuple (uncurry) import Data.Array (length, concatMap, filter, zip, zipWith) import qualified Data.Array.Unsafe as Unsafe +import Partial.Unsafe (unsafeCrashWith) class EncodeJson a where encodeJson :: a -> Json @@ -55,13 +56,13 @@ genericEncodeJson' opts sign spine = case spine of SBoolean x -> fromBoolean x SArray thunks -> case sign of SigArray elemSign -> fromArray (genericEncodeJson' opts (elemSign unit) <<< (unit #) <$> thunks) - -- _ -> unsafeCrashWith "Signature does not match value, please don't do that!" -- Not yet supported, waiting for purescript 0.8 + _ -> unsafeCrashWith "Signature does not match value, please don't do that!" -- Not yet supported, waiting for purescript 0.8 SProd constr args -> case sign of SigProd _ constrSigns -> genericEncodeProdJson' opts constrSigns constr args - -- _ -> unsafeCrashWith "Signature does not match value, please don't do that!" -- Not yet supported, waiting for purescript 0.8 + _ -> unsafeCrashWith "Signature does not match value, please don't do that!" -- Not yet supported, waiting for purescript 0.8 SRecord fields -> case sign of SigRecord sigs -> genericEncodeRecordJson' opts sigs fields - -- _ -> unsafeCrashWith "Signature does not match value, please don't do that!" -- Not yet supported, waiting for purescript 0.8 + _ -> unsafeCrashWith "Signature does not match value, please don't do that!" -- Not yet supported, waiting for purescript 0.8 genericEncodeRecordJson' :: Options -> Array { recLabel :: String, recValue :: Unit -> GenericSignature } From 522b5c86a29a5c060be419f82c341c683a0118b1 Mon Sep 17 00:00:00 2001 From: Robert Klotzner Date: Tue, 1 Mar 2016 13:00:36 +0100 Subject: [PATCH 11/16] Bumped to psc 0.8.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2f825ca..c3bf507 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ }, "devDependencies": { "pulp": "^7.0.0", - "purescript": "^0.7.6", + "purescript": "^0.8.0", "rimraf": "^2.4.4" } } From a09a99e62f6fbd074ec2756ae10ae5e921f92a76 Mon Sep 17 00:00:00 2001 From: Robert Klotzner Date: Fri, 4 Mar 2016 13:40:51 +0100 Subject: [PATCH 12/16] Uncommented missing unsafeCrashWith --- src/Data/Argonaut/Decode.purs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Data/Argonaut/Decode.purs b/src/Data/Argonaut/Decode.purs index f48d051..d85bfe8 100644 --- a/src/Data/Argonaut/Decode.purs +++ b/src/Data/Argonaut/Decode.purs @@ -27,6 +27,7 @@ import Data.String (charAt, toChar) import Data.StrMap as M import Data.Traversable (traverse, for) import Data.Tuple (Tuple(..)) +import Partial.Unsafe (unsafeCrashWith) import Type.Proxy (Proxy(..)) import qualified Data.Array.Unsafe as Unsafe @@ -101,7 +102,7 @@ genericDecodeProdJson' opts tname constrSigns json = fixConstr = opts.constructorTagModifier sumConf = case opts.sumEncoding of TaggedObject conf -> conf - -- _ -> unsafeCrashWith "Only TaggedObject encoding is supported - FIX ME!" -- Not yet supported, waiting for purescript 0.8 + _ -> unsafeCrashWith "Only TaggedObject encoding is supported - FIX ME!" -- Not yet supported, waiting for purescript 0.8 tagL = sumConf.tagFieldName contL = sumConf.contentsFieldName findConstrFail tag = mFail (decodingErr ("'" <> tag <> "' isn't a valid constructor")) (findConstr tag) From ace1902cb3b86085a5fc4516d0e5c5668950c010 Mon Sep 17 00:00:00 2001 From: Robert Klotzner Date: Wed, 9 Mar 2016 17:30:53 +0100 Subject: [PATCH 13/16] Proper aeson encoding WIP Support for special cases --- src/Data/Argonaut/Aeson.purs | 88 ++++++++++++++++++++++++++++++++++ src/Data/Argonaut/Decode.purs | 13 ++--- src/Data/Argonaut/Encode.purs | 40 ++++++++-------- src/Data/Argonaut/Options.purs | 34 +++++++------ test/Test/Main.purs | 1 + 5 files changed, 129 insertions(+), 47 deletions(-) create mode 100644 src/Data/Argonaut/Aeson.purs diff --git a/src/Data/Argonaut/Aeson.purs b/src/Data/Argonaut/Aeson.purs new file mode 100644 index 0000000..9f79354 --- /dev/null +++ b/src/Data/Argonaut/Aeson.purs @@ -0,0 +1,88 @@ +-- Haskell Aeson compatible encoding/decoding: + +module Data.Argonaut.Aeson + ( gAesonEncodeJson + , gAesonDecodeJson + , aesonOptions + , aesonUserEncoding + , aesonUserDecoding + ) where + +import Prelude + +import Control.Alt ((<|>)) +import Data.Argonaut.Core (Json(), jsonNull, fromBoolean, fromNumber, fromString, fromArray, fromObject, JArray, jsonNull) +import Data.Argonaut.Options +import Data.Argonaut.Encode +import Data.Argonaut.Decode +import Data.Either (Either(), either) +import Data.Foldable (foldr) +import Data.Generic (Generic, GenericSpine(..), toSpine, GenericSignature(..), DataConstructor(), toSignature) +import Data.Int (toNumber) +import Data.List (List(..), fromList) +import Data.Map as M +import Data.Maybe (Maybe(..), fromMaybe) +import Data.String (fromChar) +import Data.StrMap as SM +import Data.Tuple (Tuple(..)) +import Type.Proxy (Proxy(..)) +import Data.Tuple (uncurry) +import Data.Array (length, concatMap, filter, zip, zipWith) +import qualified Data.Array.Unsafe as Unsafe +import Partial.Unsafe (unsafeCrashWith) + + +-- | Options for aeson compatible encoding/decoding. +aesonOptions :: Options +aesonOptions = Options { + constructorTagModifier : stripModulePath +, allNullaryToStringTag : true +, sumEncoding : aesonSumEncoding +, flattenContentsArray : true +, unwrapUnaryRecords : false +, userEncoding : aesonUserEncoding +, userDecoding : aesonUserDecoding +} + +aesonSumEncoding :: SumEncoding +aesonSumEncoding = TaggedObject { + tagFieldName : "tag" +, contentsFieldName : "contents" +} + +-- | Encode `Json` representation of a value which has a `Generic` type +-- | with Aeson options. The encoded data will be compatible with Haskell Aeson, +-- | if Aeson default options are used. +gAesonEncodeJson :: forall a. (Generic a) => a -> Json +gAesonEncodeJson = genericEncodeJson aesonOptions + +-- | Decode `Json` representation of a value which has a `Generic` type +-- | with Aeson options. Data from Haskell, with Aeson default options can be +-- | decoded with gAesonDecodJson. +gAesonDecodeJson :: forall a. (Generic a) => Json -> Either String a +gAesonDecodeJson = genericDecodeJson aesonOptions + + +aesonUserEncoding :: Options -> GenericSignature -> GenericSpine -> Maybe Json +aesonUserEncoding opts sig spine = fromArray <$> encodeTuple opts sig spine + +aesonUserDecoding :: Options -> GenericSignature -> Json -> Maybe GenericSpine +aesonUserDecoding _ _ _ = Nothing + + +encodeTuple :: Options -> GenericSignature -> GenericSpine -> Maybe JArray +encodeTuple opts (SigProd "Data.Tuple.Tuple" sigArr) (SProd "Data.Tuple.Tuple" arr) = + append + <$> encodeTuple opts (Unsafe.head signatures) (Unsafe.head spines) + <*> encodeTupleArgs opts (Unsafe.tail signatures) (Unsafe.tail spines) + <|> + encodeTupleArgs opts signatures spines -- Or just encode arguments + where + tupleC = Unsafe.head sigArr + signatures = ($ unit) <$> tupleC.sigValues + spines = ($ unit) <$> arr +encodeTuple _ _ _ = Nothing + + +encodeTupleArgs :: Options -> Array GenericSignature -> Array GenericSpine -> Maybe JArray +encodeTupleArgs opts sigs arr = return $ zipWith (genericUserEncodeJson' opts) sigs arr diff --git a/src/Data/Argonaut/Decode.purs b/src/Data/Argonaut/Decode.purs index d85bfe8..8efecc4 100644 --- a/src/Data/Argonaut/Decode.purs +++ b/src/Data/Argonaut/Decode.purs @@ -2,7 +2,6 @@ module Data.Argonaut.Decode ( DecodeJson , decodeJson , gDecodeJson - , gAesonDecodeJson , genericDecodeJson , genericDecodeJson' , decodeMaybe @@ -39,12 +38,6 @@ class DecodeJson a where gDecodeJson :: forall a. (Generic a) => Json -> Either String a gDecodeJson = genericDecodeJson argonautOptions --- | Decode `Json` representation of a value which has a `Generic` type --- | with Aeson options. Data from Haskell, with Aeson default options can be --- | decoded with gAesonDecodJson. -gAesonDecodeJson :: forall a. (Generic a) => Json -> Either String a -gAesonDecodeJson = genericDecodeJson aesonOptions - -- | Decode `Json` representation of a value which has a `Generic` type. genericDecodeJson :: forall a. (Generic a) => Options -> Json -> Either String a genericDecodeJson opts json = maybe (Left "fromSpine failed") Right <<< fromSpine @@ -70,12 +63,12 @@ genericDecodeJson' opts signature json = case signature of SigProd typeConstr constrSigns -> genericDecodeProdJson' opts typeConstr constrSigns json genericDecodeProdJson' :: Options -> String -> Array DataConstructor -> Json -> Either String GenericSpine -genericDecodeProdJson' opts tname constrSigns json = +genericDecodeProdJson' opts'@(Options opts) tname constrSigns json = if opts.unwrapUnaryRecords && isUnaryRecord constrSigns then do let constr = Unsafe.head constrSigns let unwrapped = Unsafe.head constr.sigValues unit - r <- genericDecodeJson' opts unwrapped json + r <- genericDecodeJson' opts' unwrapped json pure (SProd constr.sigConstructor [const r]) else if opts.allNullaryToStringTag && allConstructorsNullary constrSigns @@ -95,7 +88,7 @@ genericDecodeProdJson' opts tname constrSigns json = vals <- if opts.flattenContentsArray && (length foundConstr.sigValues == 1) then pure [jVals] else mFail (decodingErr "Expected array") (toArray jVals) - sps <- zipWithA (\k -> genericDecodeJson' opts (k unit)) foundConstr.sigValues vals + sps <- zipWithA (\k -> genericDecodeJson' opts' (k unit)) foundConstr.sigValues vals pure (SProd foundConstr.sigConstructor (const <$> sps)) decodingErr msg = "When decoding a " ++ tname ++ ": " ++ msg diff --git a/src/Data/Argonaut/Encode.purs b/src/Data/Argonaut/Encode.purs index a8b640a..def9581 100644 --- a/src/Data/Argonaut/Encode.purs +++ b/src/Data/Argonaut/Encode.purs @@ -2,9 +2,9 @@ module Data.Argonaut.Encode ( EncodeJson , encodeJson , gEncodeJson - , gAesonEncodeJson , genericEncodeJson , genericEncodeJson' + , genericUserEncodeJson' , module Data.Argonaut.Options ) where @@ -18,7 +18,7 @@ import Data.Generic (Generic, GenericSpine(..), toSpine, GenericSignature(..), D import Data.Int (toNumber) import Data.List (List(..), fromList) import Data.Map as M -import Data.Maybe (Maybe(..)) +import Data.Maybe (Maybe(..), fromMaybe) import Data.String (fromChar) import Data.StrMap as SM import Data.Tuple (Tuple(..)) @@ -36,17 +36,19 @@ class EncodeJson a where gEncodeJson :: forall a. (Generic a) => a -> Json gEncodeJson = genericEncodeJson argonautOptions --- | Encode any `Generic` data structure into `Json`, --- | formatted according to aesonOptions, which is compatible to the default --- | encoding used by Aeson from Haskell. -gAesonEncodeJson :: forall a. (Generic a) => a -> Json -gAesonEncodeJson = genericEncodeJson aesonOptions - genericEncodeJson :: forall a. (Generic a) => Options -> a -> Json -genericEncodeJson opts = genericEncodeJson' opts sign <<< toSpine +genericEncodeJson opts = genericUserEncodeJson' opts sign <<< toSpine where sign = toSignature (Proxy :: Proxy a) + +-- | Generically encode to json, using a supplied userEncoding, falling back to genericEncodeJson': +genericUserEncodeJson' :: Options -> GenericSignature -> GenericSpine -> Json +genericUserEncodeJson' opts'@(Options opts) sign spine = fromMaybe (genericEncodeJson' opts' sign spine) + (opts.userEncoding opts' sign spine) + -- | Encode `GenericSpine` into `Json`. +-- | This function is mutually recursive with `genericUserEncodeJson'`, as for all descendent spines +-- | `genericUserEncodeJson'` is invoked. genericEncodeJson' :: Options -> GenericSignature -> GenericSpine -> Json genericEncodeJson' opts sign spine = case spine of SInt x -> fromNumber $ toNumber x @@ -55,14 +57,14 @@ genericEncodeJson' opts sign spine = case spine of SNumber x -> fromNumber x SBoolean x -> fromBoolean x SArray thunks -> case sign of - SigArray elemSign -> fromArray (genericEncodeJson' opts (elemSign unit) <<< (unit #) <$> thunks) - _ -> unsafeCrashWith "Signature does not match value, please don't do that!" -- Not yet supported, waiting for purescript 0.8 + SigArray elemSign -> fromArray (genericUserEncodeJson' opts (elemSign unit) <<< (unit #) <$> thunks) + _ -> unsafeCrashWith "Signature does not match value, please don't do that!" SProd constr args -> case sign of SigProd _ constrSigns -> genericEncodeProdJson' opts constrSigns constr args - _ -> unsafeCrashWith "Signature does not match value, please don't do that!" -- Not yet supported, waiting for purescript 0.8 + _ -> unsafeCrashWith "Signature does not match value, please don't do that!" SRecord fields -> case sign of SigRecord sigs -> genericEncodeRecordJson' opts sigs fields - _ -> unsafeCrashWith "Signature does not match value, please don't do that!" -- Not yet supported, waiting for purescript 0.8 + _ -> unsafeCrashWith "Signature does not match value, please don't do that!" genericEncodeRecordJson' :: Options -> Array { recLabel :: String, recValue :: Unit -> GenericSignature } @@ -70,13 +72,13 @@ genericEncodeRecordJson' :: Options -> Json genericEncodeRecordJson' opts sigs fields = fromObject <<< foldr (uncurry addField) SM.empty $ zip sigs fields where - addField sig field = SM.insert field.recLabel (genericEncodeJson' opts (sig.recValue unit) (field.recValue unit)) + addField sig field = SM.insert field.recLabel (genericUserEncodeJson' opts (sig.recValue unit) (field.recValue unit)) genericEncodeProdJson' :: Options -> Array DataConstructor -> String -> Array (Unit -> GenericSpine) -> Json -genericEncodeProdJson' opts constrSigns constr args = +genericEncodeProdJson' opts'@(Options opts) constrSigns constr args = if opts.unwrapUnaryRecords && isUnaryRecord constrSigns then - genericEncodeJson' opts + genericUserEncodeJson' opts' (Unsafe.head (Unsafe.head constrSigns).sigValues unit) (Unsafe.head args unit) else @@ -86,10 +88,10 @@ genericEncodeProdJson' opts constrSigns constr args = $ SM.insert sumConf.tagFieldName (encodeJson fixedConstr) $ SM.singleton sumConf.contentsFieldName contents where - sumConf = case opts. sumEncoding of + sumConf = case opts.sumEncoding of TaggedObject conf -> conf fixedConstr = opts.constructorTagModifier constr - encodedArgs = genericEncodeProdArgs opts constrSigns constr args + encodedArgs = genericEncodeProdArgs opts' constrSigns constr args contents = if opts.flattenContentsArray && length encodedArgs == 1 then Unsafe.head encodedArgs else encodeJson encodedArgs @@ -97,7 +99,7 @@ genericEncodeProdJson' opts constrSigns constr args = genericEncodeProdArgs :: Options -> Array DataConstructor -> String -> Array (Unit -> GenericSpine) -> Array (Json) -genericEncodeProdArgs opts constrSigns constr args = zipWith (genericEncodeJson' opts) sigValues values +genericEncodeProdArgs opts constrSigns constr args = zipWith (genericUserEncodeJson' opts) sigValues values where lSigValues = concatMap (\c -> c.sigValues) <<< filter (\c -> c.sigConstructor == constr) $ constrSigns diff --git a/src/Data/Argonaut/Options.purs b/src/Data/Argonaut/Options.purs index a00f8d2..a9d79ff 100644 --- a/src/Data/Argonaut/Options.purs +++ b/src/Data/Argonaut/Options.purs @@ -1,14 +1,16 @@ module Data.Argonaut.Options where import Prelude +import Data.Argonaut.Core (Json()) import Data.Foldable (all) import Data.String (lastIndexOf, drop) import Data.Generic (DataConstructor()) import Data.Array (null, length) +import Data.Generic (Generic, GenericSpine(..), toSpine, GenericSignature(..), DataConstructor(), toSignature) import Data.Maybe (Maybe(..)) -type Options = { +newtype Options = Options { -- newtype necessary to avoid: https://github.com/purescript/purescript/wiki/Error-Code-CycleInTypeSynonym -- | Modify the tag, e.g. strip module path with: `stripModulePath` constructorTagModifier :: String -> String -- | If all constructors of a sum type are nullary, just serialize the constructor name as a string. @@ -20,6 +22,12 @@ type Options = { -- | You need a newtype wrapper encoding/decoding of records, set this -- | to true if you want the plain Javascript object without a wrapping tagged object. , unwrapUnaryRecords :: Boolean +-- | You can choose to encode some data types differently than the generic default. +-- | Just return Nothing if you want to relay to generic encoding. +, userEncoding :: Options -> GenericSignature -> GenericSpine -> Maybe Json +-- | You can choose to decode some data types differently than the generic default. +-- | Just return Nothing, to relay to generic decoding. +, userDecoding :: Options -> GenericSignature -> Json -> Maybe GenericSpine } data SumEncoding = @@ -35,12 +43,14 @@ data SumEncoding = -- | Default for straight forward argonaut encoding. argonautOptions :: Options -argonautOptions = { +argonautOptions = Options { constructorTagModifier : id , allNullaryToStringTag : false , sumEncoding : argonautSumEncoding , flattenContentsArray : false , unwrapUnaryRecords : false +, userEncoding : dummyUserEncoding +, userDecoding : dummyUserDecoding } argonautSumEncoding :: SumEncoding @@ -49,23 +59,11 @@ argonautSumEncoding = TaggedObject { , contentsFieldName : "values" } --- | Options for aeson compatible encoding/decoding. -aesonOptions :: Options -aesonOptions = { - constructorTagModifier : stripModulePath -, allNullaryToStringTag : true -, sumEncoding : aesonSumEncoding -, flattenContentsArray : true -, unwrapUnaryRecords : false -} - -aesonSumEncoding :: SumEncoding -aesonSumEncoding = TaggedObject { - tagFieldName : "tag" -, contentsFieldName : "contents" -} - +dummyUserEncoding :: Options -> GenericSignature -> GenericSpine -> Maybe Json +dummyUserEncoding _ _ _ = Nothing +dummyUserDecoding :: Options -> GenericSignature -> Json -> Maybe GenericSpine +dummyUserDecoding _ _ _ = Nothing allConstructorsNullary :: Array DataConstructor -> Boolean allConstructorsNullary = all (null <<< _.sigValues) diff --git a/test/Test/Main.purs b/test/Test/Main.purs index f114164..8afd034 100644 --- a/test/Test/Main.purs +++ b/test/Test/Main.purs @@ -4,6 +4,7 @@ import Prelude import Data.Argonaut.Core import Data.Argonaut.Options +import Data.Argonaut.Aeson import Data.Argonaut.Decode (decodeJson, DecodeJson, genericDecodeJson, genericDecodeJson') import Data.Argonaut.Encode (encodeJson, EncodeJson, genericEncodeJson, genericEncodeJson') import Data.Argonaut.Combinators ((:=), (~>), (?>>=)) From ee0ef3b95358c4bc3331b4f3b9eaeda56efc5428 Mon Sep 17 00:00:00 2001 From: Robert Klotzner Date: Thu, 10 Mar 2016 13:23:42 +0100 Subject: [PATCH 14/16] Aeson compatible encoding finished so far. --- bower.json | 6 +++--- src/Data/Argonaut/Aeson.purs | 38 +++++++++++++++++++++++++++++++++--- test/Test/Main.purs | 29 +++++++++++++++++++++++---- 3 files changed, 63 insertions(+), 10 deletions(-) diff --git a/bower.json b/bower.json index 47ec231..4c23b4a 100644 --- a/bower.json +++ b/bower.json @@ -25,10 +25,10 @@ "purescript-argonaut-core": "^0.2.0", "purescript-generics": "^0.7.0", "purescript-integers": "^0.2.1", - "purescript-partial" : "^1.1.0" - + "purescript-partial": "^1.1.0" }, "devDependencies": { - "purescript-strongcheck-generics": "^0.3.0" + "purescript-strongcheck-generics": "^0.3.0", + "purescript-assert": "~0.1.1" } } diff --git a/src/Data/Argonaut/Aeson.purs b/src/Data/Argonaut/Aeson.purs index 9f79354..138d657 100644 --- a/src/Data/Argonaut/Aeson.purs +++ b/src/Data/Argonaut/Aeson.purs @@ -64,12 +64,36 @@ gAesonDecodeJson = genericDecodeJson aesonOptions aesonUserEncoding :: Options -> GenericSignature -> GenericSpine -> Maybe Json -aesonUserEncoding opts sig spine = fromArray <$> encodeTuple opts sig spine +aesonUserEncoding opts sig spine = encodeMaybe opts sig spine + <|> encodeEither opts sig spine + <|> fromArray <$> encodeTuple opts sig spine aesonUserDecoding :: Options -> GenericSignature -> Json -> Maybe GenericSpine aesonUserDecoding _ _ _ = Nothing +encodeMaybe :: Options -> GenericSignature -> GenericSpine -> Maybe Json +encodeMaybe opts (SigProd "Data.Maybe.Maybe" sigArr) (SProd "Data.Maybe.Just" [elem]) = + return $ genericUserEncodeJson' opts valSig val + where + valSig = getSigFromUnaryConstructor sigArr "Data.Maybe.Just" + val = elem unit + +encodeMaybe opts (SigProd "Data.Maybe.Maybe" _) (SProd "Data.Maybe.Nothing" _) = + return jsonNull +encodeMaybe _ _ _ = Nothing + +encodeEither :: Options -> GenericSignature -> GenericSpine -> Maybe Json +encodeEither opts (SigProd "Data.Either.Either" sigArr) (SProd eitherConstr [elem]) = + return + $ fromObject $ SM.fromList + $ Tuple strippedConstr (genericUserEncodeJson' opts valSig val) `Cons` Nil + where + strippedConstr = stripModulePath eitherConstr + valSig = getSigFromUnaryConstructor sigArr eitherConstr + val = elem unit +encodeEither _ _ _ = Nothing + encodeTuple :: Options -> GenericSignature -> GenericSpine -> Maybe JArray encodeTuple opts (SigProd "Data.Tuple.Tuple" sigArr) (SProd "Data.Tuple.Tuple" arr) = append @@ -79,10 +103,18 @@ encodeTuple opts (SigProd "Data.Tuple.Tuple" sigArr) (SProd "Data.Tuple.Tuple" a encodeTupleArgs opts signatures spines -- Or just encode arguments where tupleC = Unsafe.head sigArr - signatures = ($ unit) <$> tupleC.sigValues - spines = ($ unit) <$> arr + signatures = map ($ unit) tupleC.sigValues + spines = map ($ unit) arr encodeTuple _ _ _ = Nothing encodeTupleArgs :: Options -> Array GenericSignature -> Array GenericSpine -> Maybe JArray encodeTupleArgs opts sigs arr = return $ zipWith (genericUserEncodeJson' opts) sigs arr + + +getSigFromUnaryConstructor :: Array DataConstructor -> String -> GenericSignature +getSigFromUnaryConstructor arr name = + let + constr = Unsafe.head <<< filter ((== name) <<< _.sigConstructor) $ arr + in + Unsafe.head $ map ($ unit) constr.sigValues diff --git a/test/Test/Main.purs b/test/Test/Main.purs index 8afd034..c97a61a 100644 --- a/test/Test/Main.purs +++ b/test/Test/Main.purs @@ -2,19 +2,21 @@ module Test.Main where import Prelude -import Data.Argonaut.Core +import Data.Argonaut.Core hiding (toNumber) import Data.Argonaut.Options import Data.Argonaut.Aeson import Data.Argonaut.Decode (decodeJson, DecodeJson, genericDecodeJson, genericDecodeJson') import Data.Argonaut.Encode (encodeJson, EncodeJson, genericEncodeJson, genericEncodeJson') import Data.Argonaut.Combinators ((:=), (~>), (?>>=)) import Data.Either +import Data.Int (toNumber) import Data.Tuple import Data.Maybe import Data.Array import Data.Generic import Data.Foldable (foldl) -import Data.List (toList) +import Data.List (toList, List(..)) +import Data.StrMap as SM import Control.Monad.Eff (Eff()) import Control.Monad.Eff.Exception (EXCEPTION()) @@ -22,6 +24,7 @@ import Control.Monad.Eff.Random (RANDOM()) import Control.Monad.Eff.Console import qualified Data.StrMap as M +import Test.Assert (assert', ASSERT) import Test.StrongCheck import Test.StrongCheck.Gen import Test.StrongCheck.Generic @@ -179,6 +182,22 @@ prop_decoded_spine_valid opts genericValue = Right true == (isValidSpine val.signature <$> genericDecodeJson' opts val.signature (genericEncodeJson' opts val.signature val.spine)) where val = runGenericValue genericValue +checkAesonCompat :: Boolean +checkAesonCompat = + let + myTuple = Tuple (Tuple 1 2) "Hello" + myJust = Just "Test" + myNothing = Nothing :: Maybe Int + myLeft = Left "Foo" :: Either String String + myRight = Right "Bar" :: Either Int String + in + gAesonEncodeJson myTuple == fromArray [fromNumber $ toNumber 1, fromNumber $ toNumber 2, fromString "Hello"] + && gAesonEncodeJson myJust == fromString "Test" + && gAesonEncodeJson myNothing == jsonNull + && gAesonEncodeJson myLeft == fromObject (SM.fromList (Tuple "Left" (fromString "Foo") `Cons` Nil)) + && gAesonEncodeJson myRight == fromObject (SM.fromList (Tuple "Right" (fromString "Bar") `Cons` Nil)) + + genericsCheck :: forall e. Options -> Eff ( err :: EXCEPTION , random :: RANDOM , console :: CONSOLE | e) Unit genericsCheck opts= do let vNullary = Nullary2 @@ -242,15 +261,17 @@ eitherCheck = do Left err -> false err -main:: forall e. Eff ( err :: EXCEPTION, random :: RANDOM, console :: CONSOLE | e ) Unit +main:: forall e. Eff ( err :: EXCEPTION, random :: RANDOM, console :: CONSOLE, assert :: ASSERT | e ) Unit main = do eitherCheck encodeDecodeCheck combinatorsCheck + assert' "aesonCompatcheck: " checkAesonCompat log "genericsCheck check for argonautOptions" genericsCheck argonautOptions log "genericsCheck check for aesonOptions" genericsCheck aesonOptions log "genericsCheck check for unwrapUnaryOptions" - let unwrapUnaryOptions = aesonOptions { unwrapUnaryRecords = true } + let unwrapOpts = case aesonOptions of Options a -> a + let unwrapUnaryOptions = Options $ unwrapOpts { unwrapUnaryRecords = true } genericsCheck unwrapUnaryOptions From 7c2da0aa64d03f3fd58e10242a519daba602c791 Mon Sep 17 00:00:00 2001 From: Robert Klotzner Date: Fri, 11 Mar 2016 12:06:20 +0100 Subject: [PATCH 15/16] Aeson compatible decoding. Tests pass. --- src/Data/Argonaut/Aeson.purs | 76 ++++++++++++++++++++++++++++++---- src/Data/Argonaut/Decode.purs | 23 ++++++---- src/Data/Argonaut/Options.purs | 5 ++- test/Test/Main.purs | 27 +++++++----- 4 files changed, 104 insertions(+), 27 deletions(-) diff --git a/src/Data/Argonaut/Aeson.purs b/src/Data/Argonaut/Aeson.purs index 138d657..1a8df6d 100644 --- a/src/Data/Argonaut/Aeson.purs +++ b/src/Data/Argonaut/Aeson.purs @@ -11,19 +11,22 @@ module Data.Argonaut.Aeson import Prelude import Control.Alt ((<|>)) -import Data.Argonaut.Core (Json(), jsonNull, fromBoolean, fromNumber, fromString, fromArray, fromObject, JArray, jsonNull) +import Data.Argonaut.Core (Json(), jsonNull, fromBoolean, fromNumber, fromString, fromArray, fromObject, JArray, jsonNull, isNull, toObject, toArray) import Data.Argonaut.Options import Data.Argonaut.Encode -import Data.Argonaut.Decode +import Data.Argonaut.Decode hiding (decodeMaybe) import Data.Either (Either(), either) import Data.Foldable (foldr) import Data.Generic (Generic, GenericSpine(..), toSpine, GenericSignature(..), DataConstructor(), toSignature) import Data.Int (toNumber) import Data.List (List(..), fromList) +import Data.List as L import Data.Map as M +import Data.Either (Either(..)) import Data.Maybe (Maybe(..), fromMaybe) import Data.String (fromChar) import Data.StrMap as SM +import Data.Traversable (sequence) import Data.Tuple (Tuple(..)) import Type.Proxy (Proxy(..)) import Data.Tuple (uncurry) @@ -68,8 +71,10 @@ aesonUserEncoding opts sig spine = encodeMaybe opts sig spine <|> encodeEither opts sig spine <|> fromArray <$> encodeTuple opts sig spine -aesonUserDecoding :: Options -> GenericSignature -> Json -> Maybe GenericSpine -aesonUserDecoding _ _ _ = Nothing +aesonUserDecoding :: Options -> GenericSignature -> Json -> Maybe (Either String GenericSpine) +aesonUserDecoding opts sig json = decodeMaybe opts sig json + <|> decodeEither opts sig json + <|> decodeTuple opts sig json encodeMaybe :: Options -> GenericSignature -> GenericSpine -> Maybe Json @@ -83,6 +88,16 @@ encodeMaybe opts (SigProd "Data.Maybe.Maybe" _) (SProd "Data.Maybe.Nothing" _) = return jsonNull encodeMaybe _ _ _ = Nothing +decodeMaybe :: Options -> GenericSignature -> Json -> Maybe (Either String GenericSpine) +decodeMaybe opts (SigProd "Data.Maybe.Maybe" sigArr) json = + if isNull json + then return $ Right $ SProd "Data.Maybe.Nothing" [] + else return $ do + let valSig = getSigFromUnaryConstructor sigArr "Data.Maybe.Just" + decoded <- genericUserDecodeJson' opts valSig json + return $ SProd "Data.Maybe.Just" [\u -> decoded ] +decodeMaybe _ _ _ = Nothing + encodeEither :: Options -> GenericSignature -> GenericSpine -> Maybe Json encodeEither opts (SigProd "Data.Either.Either" sigArr) (SProd eitherConstr [elem]) = return @@ -94,6 +109,20 @@ encodeEither opts (SigProd "Data.Either.Either" sigArr) (SProd eitherConstr [ele val = elem unit encodeEither _ _ _ = Nothing +decodeEither :: Options -> GenericSignature -> Json -> Maybe (Either String GenericSpine) +decodeEither opts (SigProd "Data.Either.Either" sigArr) json = return $ do + obj <- mFail "Expeced an object when decoding Either" $ toObject json + fromMaybe (Left "Expected Left or Right record label when decoding Either") + $ decodeArg "Right" obj <|> decodeArg "Left" obj + where + decodeArg name obj = do + argJson <- SM.lookup name obj + let valSig = getSigFromUnaryConstructor sigArr $ "Data.Either." <> name + return $ do + decoded <- genericUserDecodeJson' opts valSig argJson + return $ SProd ("Data.Either." <> name) [\u -> decoded] +decodeEither _ _ _ = Nothing + encodeTuple :: Options -> GenericSignature -> GenericSpine -> Maybe JArray encodeTuple opts (SigProd "Data.Tuple.Tuple" sigArr) (SProd "Data.Tuple.Tuple" arr) = append @@ -102,19 +131,50 @@ encodeTuple opts (SigProd "Data.Tuple.Tuple" sigArr) (SProd "Data.Tuple.Tuple" a <|> encodeTupleArgs opts signatures spines -- Or just encode arguments where - tupleC = Unsafe.head sigArr - signatures = map ($ unit) tupleC.sigValues + signatures = getSigsFromConstructor sigArr "Data.Tuple.Tuple" spines = map ($ unit) arr encodeTuple _ _ _ = Nothing +decodeTuple :: Options -> GenericSignature -> Json -> Maybe (Either String GenericSpine) +decodeTuple opts (SigProd "Data.Tuple.Tuple" sigArr) json = return $ do + jsonVals <- mFail "Expected an array of values when decoding Tuple" $ toArray json + let sigs = getNestedTupleSigs $ getSigsFromConstructor sigArr "Data.Tuple.Tuple" + decoded <- sequence $ L.zipWith (genericUserDecodeJson' opts) sigs (arrToList jsonVals) + makeTuples decoded + where + makeTuple x1 x2 = SProd "Data.Tuple.Tuple" [\_ -> x1, \_ -> x2] + + makeTuples (Cons x1 (Cons x2 xs)) = return $ makeTuples' (makeTuple x1 x2) xs + makeTuples _ = Left "A tuple needs to have at least two elements" + + makeTuples' inner Nil = inner + makeTuples' inner (Cons x1 xs) = makeTuples' (makeTuple inner x1) xs + + +decodeTuple _ _ _ = Nothing encodeTupleArgs :: Options -> Array GenericSignature -> Array GenericSpine -> Maybe JArray encodeTupleArgs opts sigs arr = return $ zipWith (genericUserEncodeJson' opts) sigs arr getSigFromUnaryConstructor :: Array DataConstructor -> String -> GenericSignature -getSigFromUnaryConstructor arr name = +getSigFromUnaryConstructor arr name = Unsafe.head $ getSigsFromConstructor arr name + +getSigsFromConstructor :: Array DataConstructor -> String -> Array GenericSignature +getSigsFromConstructor arr name = let constr = Unsafe.head <<< filter ((== name) <<< _.sigConstructor) $ arr in - Unsafe.head $ map ($ unit) constr.sigValues + map ($ unit) constr.sigValues + +getNestedTupleSigs :: Array GenericSignature -> List GenericSignature +getNestedTupleSigs = L.reverse <<< getNestedTupleSigs' + +-- Get signatures in reverse order: +getNestedTupleSigs' :: Array GenericSignature -> List GenericSignature +getNestedTupleSigs' [val1, val2] = case val1 of + SigProd "Data.Tuple.Tuple" cVals -> val2 `Cons` getNestedTupleSigs' (getSigsFromConstructor cVals "Data.Tuple.Tuple") + _ -> Cons val2 (Cons val1 Nil) + +arrToList :: forall a. Array a -> List a +arrToList = foldr Cons Nil diff --git a/src/Data/Argonaut/Decode.purs b/src/Data/Argonaut/Decode.purs index 8efecc4..61a04dd 100644 --- a/src/Data/Argonaut/Decode.purs +++ b/src/Data/Argonaut/Decode.purs @@ -4,8 +4,10 @@ module Data.Argonaut.Decode , gDecodeJson , genericDecodeJson , genericDecodeJson' + , genericUserDecodeJson' , decodeMaybe , module Data.Argonaut.Options + , mFail ) where import Prelude @@ -21,7 +23,7 @@ import Data.Generic (Generic, GenericSpine(..), GenericSignature(..), DataConstr import Data.Int (fromNumber) import Data.List (List(..), toList) import Data.Map as Map -import Data.Maybe (maybe, Maybe(..)) +import Data.Maybe (maybe, Maybe(..), fromMaybe) import Data.String (charAt, toChar) import Data.StrMap as M import Data.Traversable (traverse, for) @@ -41,7 +43,14 @@ gDecodeJson = genericDecodeJson argonautOptions -- | Decode `Json` representation of a value which has a `Generic` type. genericDecodeJson :: forall a. (Generic a) => Options -> Json -> Either String a genericDecodeJson opts json = maybe (Left "fromSpine failed") Right <<< fromSpine - =<< genericDecodeJson' opts (toSignature (Proxy :: Proxy a)) json + =<< genericUserDecodeJson' opts (toSignature (Proxy :: Proxy a)) json + + +-- | Generically encode to json, using a supplied userEncoding, falling back to genericEncodeJson': +genericUserDecodeJson' :: Options -> GenericSignature -> Json -> Either String GenericSpine +genericUserDecodeJson' opts'@(Options opts) sign json = fromMaybe (genericDecodeJson' opts' sign json) + (opts.userDecoding opts' sign json) + -- | Decode `Json` representation of a `GenericSpine`. genericDecodeJson' :: Options -> GenericSignature -> Json -> Either String GenericSpine @@ -53,12 +62,12 @@ genericDecodeJson' opts signature json = case signature of SigBoolean -> SBoolean <$> mFail "Expected a boolean" (toBoolean json) SigArray thunk -> do jArr <- mFail "Expected an array" $ toArray json - SArray <$> traverse (map const <<< genericDecodeJson' opts (thunk unit)) jArr + SArray <$> traverse (map const <<< genericUserDecodeJson' opts (thunk unit)) jArr SigRecord props -> do jObj <- mFail "Expected an object" $ toObject json SRecord <$> for props \({recLabel: lbl, recValue: val}) -> do pf <- mFail ("'" <> lbl <> "' property missing") (M.lookup lbl jObj) - sp <- genericDecodeJson' opts (val unit) pf + sp <- genericUserDecodeJson' opts (val unit) pf pure { recLabel: lbl, recValue: const sp } SigProd typeConstr constrSigns -> genericDecodeProdJson' opts typeConstr constrSigns json @@ -68,7 +77,7 @@ genericDecodeProdJson' opts'@(Options opts) tname constrSigns json = then do let constr = Unsafe.head constrSigns let unwrapped = Unsafe.head constr.sigValues unit - r <- genericDecodeJson' opts' unwrapped json + r <- genericUserDecodeJson' opts' unwrapped json pure (SProd constr.sigConstructor [const r]) else if opts.allNullaryToStringTag && allConstructorsNullary constrSigns @@ -88,14 +97,14 @@ genericDecodeProdJson' opts'@(Options opts) tname constrSigns json = vals <- if opts.flattenContentsArray && (length foundConstr.sigValues == 1) then pure [jVals] else mFail (decodingErr "Expected array") (toArray jVals) - sps <- zipWithA (\k -> genericDecodeJson' opts' (k unit)) foundConstr.sigValues vals + sps <- zipWithA (\k -> genericUserDecodeJson' opts' (k unit)) foundConstr.sigValues vals pure (SProd foundConstr.sigConstructor (const <$> sps)) decodingErr msg = "When decoding a " ++ tname ++ ": " ++ msg fixConstr = opts.constructorTagModifier sumConf = case opts.sumEncoding of TaggedObject conf -> conf - _ -> unsafeCrashWith "Only TaggedObject encoding is supported - FIX ME!" -- Not yet supported, waiting for purescript 0.8 + _ -> unsafeCrashWith "Only TaggedObject encoding is supported - FIX ME!" tagL = sumConf.tagFieldName contL = sumConf.contentsFieldName findConstrFail tag = mFail (decodingErr ("'" <> tag <> "' isn't a valid constructor")) (findConstr tag) diff --git a/src/Data/Argonaut/Options.purs b/src/Data/Argonaut/Options.purs index a9d79ff..b8c0fe8 100644 --- a/src/Data/Argonaut/Options.purs +++ b/src/Data/Argonaut/Options.purs @@ -7,6 +7,7 @@ import Data.String (lastIndexOf, drop) import Data.Generic (DataConstructor()) import Data.Array (null, length) import Data.Generic (Generic, GenericSpine(..), toSpine, GenericSignature(..), DataConstructor(), toSignature) +import Data.Either (Either(..)) import Data.Maybe (Maybe(..)) @@ -27,7 +28,7 @@ newtype Options = Options { -- newtype necessary to avoid: https://github.com/pu , userEncoding :: Options -> GenericSignature -> GenericSpine -> Maybe Json -- | You can choose to decode some data types differently than the generic default. -- | Just return Nothing, to relay to generic decoding. -, userDecoding :: Options -> GenericSignature -> Json -> Maybe GenericSpine +, userDecoding :: Options -> GenericSignature -> Json -> Maybe (Either String GenericSpine) } data SumEncoding = @@ -62,7 +63,7 @@ argonautSumEncoding = TaggedObject { dummyUserEncoding :: Options -> GenericSignature -> GenericSpine -> Maybe Json dummyUserEncoding _ _ _ = Nothing -dummyUserDecoding :: Options -> GenericSignature -> Json -> Maybe GenericSpine +dummyUserDecoding :: Options -> GenericSignature -> Json -> Maybe (Either String GenericSpine) dummyUserDecoding _ _ _ = Nothing allConstructorsNullary :: Array DataConstructor -> Boolean diff --git a/test/Test/Main.purs b/test/Test/Main.purs index c97a61a..7fbc204 100644 --- a/test/Test/Main.purs +++ b/test/Test/Main.purs @@ -198,17 +198,28 @@ checkAesonCompat = && gAesonEncodeJson myRight == fromObject (SM.fromList (Tuple "Right" (fromString "Bar") `Cons` Nil)) -genericsCheck :: forall e. Options -> Eff ( err :: EXCEPTION , random :: RANDOM , console :: CONSOLE | e) Unit +genericsCheck :: forall e. Options -> Eff ( err :: EXCEPTION , random :: RANDOM , console :: CONSOLE, assert :: ASSERT | e) Unit genericsCheck opts= do let vNullary = Nullary2 let mArgs = MArgs 9 20 "Hello" let ntw1 = NewTypeWrapper1 { test : "hello" } let ntw2 = NewTypeWrapper2 { test : 9 } - log "Check that decodeJson' and encodeJson' form an isomorphism" - logError " Check all nullary:" (valEncodeDecode opts vNullary) - logError " Check multiple args:" (valEncodeDecode opts mArgs) - logError " Check new type wrapper (1) encoding:" (valEncodeDecode opts ntw1) - logError " Check new type wrapper (2) encoding:" (valEncodeDecode opts ntw2) + let mJust = Just "Test" + let mNothing = Nothing :: Maybe Int + let mRight = Right 9 :: Either String Int + let mLeft = Right (Left 2) :: Either String (Either Int Int) + let mTuple = Tuple (Tuple (Tuple 2 3) "haha") "test" + log "Check that decodeJson' and encodeJson' form an isomorphism .." + assert' " Check all nullary:" (valEncodeDecode opts vNullary) + assert' " Check multiple args:" (valEncodeDecode opts mArgs) + assert' " Check new type wrapper (1) encoding:" (valEncodeDecode opts ntw1) + assert' " Check new type wrapper (2) encoding:" (valEncodeDecode opts ntw2) + assert' " Check Just" (valEncodeDecode opts mJust) + assert' " Check Nothing" (valEncodeDecode opts mNothing) + assert' " Check Right" (valEncodeDecode opts mRight) + assert' " Check Left" (valEncodeDecode opts mLeft) + assert' " Check tuple" (valEncodeDecode opts mTuple) + quickCheck (prop_iso_generic opts) log "Check that decodeJson' returns a valid spine" quickCheck (prop_decoded_spine_valid opts) @@ -246,10 +257,6 @@ genericsCheck opts= do valEncodeDecode :: forall a. (Eq a, Generic a) => Options -> a -> Boolean valEncodeDecode opts val = ((Right val) ==) <<< genericDecodeJson opts <<< genericEncodeJson opts $ val - logError message test = log $ message ++ result test - where result false = " ##########FAILED########!" - result true = " ok." - eitherCheck :: forall e. Eff ( err :: EXCEPTION, random :: RANDOM, console :: CONSOLE | e ) Unit eitherCheck = do log "Test EncodeJson/DecodeJson Either instance" From 8cb1c727a7b3ac4c10853e92769e161039f089bd Mon Sep 17 00:00:00 2001 From: Robert Klotzner Date: Tue, 15 Mar 2016 17:51:29 +0100 Subject: [PATCH 16/16] Changed default for unwrapUnaryRecords Despite what the defaultOptions in aeson suggest, it works as if unwrapUnaryRecords was set to true. --- src/Data/Argonaut/Aeson.purs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Data/Argonaut/Aeson.purs b/src/Data/Argonaut/Aeson.purs index 1a8df6d..7b6f658 100644 --- a/src/Data/Argonaut/Aeson.purs +++ b/src/Data/Argonaut/Aeson.purs @@ -42,7 +42,7 @@ aesonOptions = Options { , allNullaryToStringTag : true , sumEncoding : aesonSumEncoding , flattenContentsArray : true -, unwrapUnaryRecords : false +, unwrapUnaryRecords : true , userEncoding : aesonUserEncoding , userDecoding : aesonUserDecoding }