packages feed

generic-aeson 0.1.1.1 → 0.2.0.0

raw patch · 5 files changed

+162/−71 lines, 5 filesdep +unordered-containersPVP ok

version bump matches the API change (PVP)

Dependencies added: unordered-containers

API changes (from Hackage documentation)

+ Generics.Generic.Aeson: Settings :: Maybe String -> Settings
+ Generics.Generic.Aeson: data Settings
+ Generics.Generic.Aeson: defaultSettings :: Settings
+ Generics.Generic.Aeson: gparseJsonWithSettings :: (Generic a, GfromJson (Rep a), ConNames (Rep a), GIsEnum (Rep a)) => Settings -> Value -> Parser a
+ Generics.Generic.Aeson: gtoJsonWithSettings :: (Generic a, GtoJson (Rep a), ConNames (Rep a), GIsEnum (Rep a)) => Settings -> a -> Value
+ Generics.Generic.Aeson: stripPrefix :: Settings -> Maybe String
+ Generics.Generic.Aeson.Util: Settings :: Maybe String -> Settings
+ Generics.Generic.Aeson.Util: data Settings
+ Generics.Generic.Aeson.Util: defaultSettings :: Settings
+ Generics.Generic.Aeson.Util: instance Show Settings
+ Generics.Generic.Aeson.Util: stripPrefix :: Settings -> Maybe String
- Generics.Generic.Aeson: formatLabel :: Text -> Text
+ Generics.Generic.Aeson: formatLabel :: Settings -> Text -> Text
- Generics.Generic.Aeson: gparseJSONf :: GfromJson f => Bool -> Bool -> Bool -> StateT [Value] Parser (f a)
+ Generics.Generic.Aeson: gparseJSONf :: GfromJson f => Settings -> Bool -> Bool -> Bool -> StateT [Value] Parser (f a)
- Generics.Generic.Aeson: gtoJSONf :: GtoJson f => Bool -> Bool -> f a -> Either [Value] [(Text, Value)]
+ Generics.Generic.Aeson: gtoJSONf :: GtoJson f => Settings -> Bool -> Bool -> f a -> Either [Value] [(Text, Value)]
- Generics.Generic.Aeson.Util: conNameT :: Constructor c => t c f a -> Text
+ Generics.Generic.Aeson.Util: conNameT :: Constructor c => Settings -> t c f a -> Text
- Generics.Generic.Aeson.Util: formatLabel :: Text -> Text
+ Generics.Generic.Aeson.Util: formatLabel :: Settings -> Text -> Text
- Generics.Generic.Aeson.Util: selNameT :: Selector s => t s f a -> Text
+ Generics.Generic.Aeson.Util: selNameT :: Selector s => Settings -> t s f a -> Maybe Text

Files

CHANGELOG.md view
@@ -1,5 +1,17 @@ # Changelog +## 0.2.0.0++* Add `gtoJsonWithSettings` and `gparseJsonWithSettings` to customize+  the generated JSON, currently only to strip specified prefixes from+  record fields.+* Format Change: The behavior of Maybes was inconsistent and buggy,+  now we always map `Just` directly to the value, and `Nothing` to+  null if on the top level or in an unnamed field and remove the+  property if it's in a named field.+* Changed the type of `selNameT` to return a `Maybe Text` which will+  be `Nothing` instead of `""` (unnamed fields)+ #### 0.1.1.1  * Fix regression in implementation of `multipleConstructors` introduced in 0.1.1
README.md view
@@ -5,7 +5,7 @@ The structure of the generated JSON is meant to be close to idiomatic JSON. This means: -* Enumerations are converted to JSON strings.+* Enumerations (data types containing constructors without fields) are converted to JSON strings.  * Record fields become JSON keys. @@ -15,6 +15,28 @@  * Multiple constructors are represented by keys. -* 'Maybe' values are either an absent key, or the value.+* `Maybe` values are either an absent key, or the value. +* Leading and trailing underscores are removed from constructor names and record fields+ See `tests/Main.hs` for more examples.+++## How does generic-aeson compare to the TH/Generics already present in aeson?++generic-aeson contains more special cases for creating more concise+and idiomatic json. If you're working with the JSON representation+directly generic-aeson should feel more natural.++## Will the generated format ever change?++Changing the format would incur a breaking change to every API that+uses generic-aeson so we must keep it intact.++If we find a bug where the fix changes the format we need to create a+new package or version the generation code.++## Schemas++[json-schema](http://hackage.haskell.org/package/json-schema) has+generic derivation of schemas that match the generic-aeson format.
generic-aeson.cabal view
@@ -1,5 +1,5 @@ name:                generic-aeson-version:             0.1.1.1+version:             0.2.0.0 synopsis:            Derivation of Aeson instances using GHC generics. description:         Derivation of Aeson instances using GHC generics. author:              Silk@@ -34,4 +34,5 @@     , mtl >= 2.0 && < 2.3     , tagged >= 0.2 && < 0.8     , text >= 0.11 && < 1.2+    , unordered-containers == 0.2.*     , vector == 0.10.*
src/Generics/Generic/Aeson.hs view
@@ -33,6 +33,10 @@   , GtoJson (..)   , GfromJson (..)   , formatLabel+  , Settings (..)+  , defaultSettings+  , gtoJsonWithSettings+  , gparseJsonWithSettings   ) where  import Control.Applicative@@ -44,8 +48,9 @@ import Data.Text (Text) import GHC.Generics import Generics.Deriving.ConNames-import qualified Data.Text   as T-import qualified Data.Vector as V+import qualified Data.HashMap.Strict as H+import qualified Data.Text           as T+import qualified Data.Vector         as V  import Generics.Generic.Aeson.Util @@ -59,7 +64,7 @@   -- constructors). A functor is then converted to either a list   -- of values (for non-labeled fields) or a list of String/value   -- pairs (for labeled fields).-  gtoJSONf :: Bool -> Bool -> f a -> Either [Value] [(Text, Value)]+  gtoJSONf :: Settings -> Bool -> Bool -> f a -> Either [Value] [(Text, Value)]  -- | Class for parsing the functors from "GHC.Generics" from JSON. -- You generally don't need to give any custom instances. Just add@@ -73,58 +78,71 @@   -- data type is an enumeration (only empty constructors). The third   -- is a function for parsing the recursive positions. A JSON value   -- is then parsed to either a functor, or a failure.-  gparseJSONf :: Bool -> Bool -> Bool -> StateT [Value] Parser (f a)+  gparseJSONf :: Settings -> Bool -> Bool -> Bool -> StateT [Value] Parser (f a)  -- Void: Used for data types without constructors -- instance GJSON V1  -- Unit: Used for constructors without arguments instance GtoJson U1 where-  gtoJSONf _ _ U1 = Right []+  gtoJSONf _ _ _ U1 = Right [] instance GfromJson U1 where-  gparseJSONf _ _ _ = return U1+  gparseJSONf _ _ _ _ = return U1  -- | Convert any datatype with a 'Generic' instance to a JSON 'Value'.-gtoJson :: forall a. (Generic a, GtoJson (Rep a), ConNames (Rep a), GIsEnum (Rep a)) -        => a -> Value-gtoJson x =-  case gtoJSONf (multipleConstructors $ conNames x) (isEnum (Proxy :: Proxy a)) (from x) of+gtoJson+  :: forall a. (Generic a, GtoJson (Rep a), ConNames (Rep a), GIsEnum (Rep a))+  => a -> Value+gtoJson = gtoJsonWithSettings defaultSettings++gtoJsonWithSettings+  :: forall a. (Generic a, GtoJson (Rep a), ConNames (Rep a), GIsEnum (Rep a))+   => Settings -> a -> Value+gtoJsonWithSettings settings x =+  case gtoJSONf settings (multipleConstructors $ conNames x) (isEnum (Proxy :: Proxy a)) (from x) of     Left  [v] -> v     Left  _   -> error "The impossible happened: multiple returned values in gtoJSON."     Right _   -> error "The impossible happened: labeled values returned in gtoJSON."  -- | Parse any datatype with a 'Generic' instance from a JSON 'Value'.-gparseJson :: forall a. (Generic a, GfromJson (Rep a), ConNames (Rep a), GIsEnum (Rep a))-           => Value -> Parser a gparseJson+  :: forall a. (Generic a, GfromJson (Rep a), ConNames (Rep a), GIsEnum (Rep a))+  => Value -> Parser a+gparseJson = gparseJsonWithSettings defaultSettings++gparseJsonWithSettings+  :: forall a. (Generic a, GfromJson (Rep a), ConNames (Rep a), GIsEnum (Rep a))+  => Settings -> Value -> Parser a+gparseJsonWithSettings set   = fmap to-  . evalStateT (gparseJSONf (multipleConstructors $ conNames (undefined :: a)) False (isEnum (Proxy :: Proxy a)))+  . evalStateT (gparseJSONf set (multipleConstructors $ conNames (undefined :: a)) False (isEnum (Proxy :: Proxy a)))   . return  -- Structure type for constant values. instance (ToJSON c) => GtoJson (K1 a c) where-  gtoJSONf _ _ (K1 a) = Left [toJSON a]+  gtoJSONf _ _ _ (K1 a) = Left [toJSON a] instance (FromJSON c) => GfromJson (K1 a c) where-  gparseJSONf _ _ _   = lift . fmap K1 . parseJSON =<< pop+  gparseJSONf _ _ _ _   = lift . fmap K1 . parseJSON =<< pop  instance (GtoJson f, GtoJson g) => GtoJson (f :+: g) where-  gtoJSONf mc enm (L1 x) = gtoJSONf mc enm x-  gtoJSONf mc enm (R1 x) = gtoJSONf mc enm x+  gtoJSONf set mc enm (L1 x) = gtoJSONf set mc enm x+  gtoJSONf set mc enm (R1 x) = gtoJSONf set mc enm x instance (GfromJson f, GfromJson g) => GfromJson (f :+: g) where-  gparseJSONf mc smf enm  =  L1 <$> gparseJSONf mc smf enm-                        <|> R1 <$> gparseJSONf mc smf enm+  gparseJSONf set mc smf enm+    =  L1 <$> gparseJSONf set mc smf enm+   <|> R1 <$> gparseJSONf set mc smf enm  instance (GtoJson f, GtoJson g) => GtoJson (f :*: g) where-  gtoJSONf mc enm (x :*: y) =-    case (gtoJSONf mc enm x, gtoJSONf mc enm y) of+  gtoJSONf set mc enm (x :*: y) =+    case (gtoJSONf set mc enm x, gtoJSONf set mc enm y) of       (Left  xvs, Left  yvs) -> Left  (xvs ++ yvs)       (Right xvs, Right yvs) -> Right (xvs ++ yvs)       _                      -> error "The impossible happened: product of mixed label and non-label fields in GJSON instance for (:*:)." instance (GfromJson f, GfromJson g) => GfromJson (f :*: g) where-  gparseJSONf mc smf enm =+  gparseJSONf set mc smf enm =     do unless smf selFields-       (:*:) <$> gparseJSONf mc True enm <*> gparseJSONf mc True enm+       (:*:) <$> gparseJSONf set mc True enm <*> gparseJSONf set mc True enm     where       selFields =         do v <- pop@@ -133,25 +151,15 @@              Array vs   -> put (V.toList vs)              _          -> fail "Expected object or array in gparseJSONf for (:*:)." -instance (Selector c, ToJSON a) => GtoJson (M1 S c (K1 i (Maybe a))) where-  gtoJSONf _  _ (M1 (K1 Nothing )) = Right []-  gtoJSONf _  _ (M1 (K1 (Just x))) = Right [(selNameT (undefined :: M1 S c f p), toJSON x)]-instance (Selector c, FromJSON a) => GfromJson (M1 S c (K1 i (Maybe a))) where-  gparseJSONf mc smf enm =-    do (M1 (K1 x)) <- gparseJSONf mc smf enm :: StateT [Value] Parser (M1 S c (K1 i a) p)-       return (M1 (K1 (Just x)))-    <|>-    return (M1 (K1 Nothing))- instance GtoJson f => GtoJson (M1 D c f) where-  gtoJSONf a b (M1 x) = gtoJSONf a b x+  gtoJSONf set a b (M1 x) = gtoJSONf set a b x instance GfromJson f => GfromJson (M1 D c f) where-  gparseJSONf a b x = M1 <$> gparseJSONf a b x+  gparseJSONf set a b x = M1 <$> gparseJSONf set a b x  instance (Constructor c, GtoJson f) => GtoJson (M1 C c f) where-  gtoJSONf _  True  (M1 _) = Left [toJSON $ conNameT (undefined :: M1 C c f p)]-  gtoJSONf mc False (M1 x) =-    case gtoJSONf mc False x of+  gtoJSONf set _  True  (M1 _) = Left [toJSON $ conNameT set (undefined :: M1 C c f p)]+  gtoJSONf set mc False (M1 x) =+    case gtoJSONf set mc False x of       -- Single field constructors are not wrapped in an array.       Left  [v] -> Left [wrap v]       Left  vs  -> Left [wrap . Array $ V.fromList vs]@@ -160,46 +168,73 @@       wrap = if mc              then toObject                 . return-                . (conNameT (undefined :: M1 C c f p), )+                . (conNameT set (undefined :: M1 C c f p), )              else id instance (Constructor c, GfromJson f) => GfromJson (M1 C c f) where-  gparseJSONf mc smf True =+  gparseJSONf set mc smf True =     do str    <- pop        conStr <- lift (parseJSON str)-       let expectedConStr = conNameT (undefined :: M1 C c f p)+       let expectedConStr = conNameT set (undefined :: M1 C c f p)        unless (conStr == expectedConStr) $          fail $ "Error parsing enumeration: expected " ++ T.unpack expectedConStr ++ ", found " ++ T.unpack conStr ++ "."-       M1 <$> gparseJSONf mc smf True-  gparseJSONf mc smf False =-    do when mc (selProp "C" propName)-       M1 <$> gparseJSONf mc smf False+       M1 <$> gparseJSONf set mc smf True+  gparseJSONf set mc smf False =+    do+       when mc (selProp "C" propName)+       M1 <$> gparseJSONf set mc smf False     where-      propName = conNameT (undefined :: M1 C c f p)+      propName = case conNameT set (undefined :: M1 C c f p) of+        "" -> Nothing+        n  -> Just n  instance (Selector c, GtoJson f) => GtoJson (M1 S c f) where-  gtoJSONf mc enm (M1 x) =-    case gtoJSONf mc enm x of-      Left  [v] -> case selNameT (undefined :: M1 S c f p) of-        "" -> Left [v]-        n  -> Right [(n, v)]+  gtoJSONf set mc enm (M1 x) =+    case gtoJSONf set mc enm x of+      Left  [v] -> case selNameT set (undefined :: M1 S c f p) of+        Nothing -> Left [v]+        Just n  -> Right [(n, v)]       Left  _   -> error "The impossible happened: multiple returned values inside label in GJSON instance for S."       Right _   -> error "The impossible happened: label inside a label in GJSON instance for S." instance (Selector c, GfromJson f) => GfromJson (M1 S c f) where-  gparseJSONf mc smf enm =+  gparseJSONf set mc smf enm =     do selProp "S" propName-       M1 <$> gparseJSONf mc smf enm+       M1 <$> gparseJSONf set mc smf enm     where-      propName = selNameT (undefined :: M1 S c f p)+      propName = selNameT set (undefined :: M1 S c f p) -selProp :: Text -> Text -> StateT [Value] Parser ()+instance (Selector c, ToJSON a) => GtoJson (M1 S c (K1 i (Maybe a))) where+  gtoJSONf set   _  _   (M1 (K1 n@Nothing)) = case selNameT set (undefined :: M1 S c f p) of+    Nothing -> Left [toJSON n]+    Just _  -> Right []+  gtoJSONf set mc enm (M1 (K1 (Just x))) = gtoJSONf set mc enm (M1 (K1 x) :: (M1 S c (K1 i a)) p)+instance (Selector c, FromJSON a) => GfromJson (M1 S c (K1 i (Maybe a))) where+  gparseJSONf set mc smf enm =+    do (M1 (K1 x)) <- parser+       return (M1 (K1 (Just x)))+    <|>+    do case selNameT set (undefined :: M1 S c (K1 i a) p) of+         Nothing ->+           do o <- pop+              M1 . K1 <$> lift (parseJSON o)+         Just n  ->+           do o <- pop+              case o of+                Object h | H.member n h -> error impossible <$> parser+                         | otherwise    -> return $ M1 (K1 Nothing)+                _ -> lift $ typeMismatch "Object" (Array V.empty)+    where+      parser = (gparseJSONf set mc smf enm :: StateT [Value] Parser (M1 S c (K1 i a) p))+      impossible = "The impossible happened: parser succeeded after failing in GfromJson S Maybe"++selProp :: Text -> Maybe Text -> StateT [Value] Parser () selProp cname propName =   case propName of-    "" -> do o <- pop-             modify (o:)-    _  -> do o <- pop-             v <- lift (withObject ("Expected property " ++ show propName ++ " in object in gparseJSONf for " ++ show cname ++ ".")-                                   (.: propName) o)-             modify (v:)+    Nothing -> do o <- pop+                  modify (o:)+    Just p  -> do o <- pop+                  v <- lift (withObject ("Expected property " ++ show propName ++ " in object in gparseJSONf for " ++ show cname ++ ".")+                                        (.: p) o)+                  modify (v:)  pop :: MonadState [Value] m => m Value pop =
src/Generics/Generic/Aeson/Util.hs view
@@ -11,8 +11,11 @@   , conNameT   , selNameT   , module Generics.Generic.IsEnum+  , Settings (..)+  , defaultSettings   ) where +import Control.Monad ((<=<)) import Data.Char (toLower) import Data.Maybe (fromMaybe) import Data.Text (Text)@@ -21,17 +24,28 @@  import Generics.Generic.IsEnum -conNameT :: forall c (t :: * -> (* -> *) -> * -> *) (f :: * -> *) a. Constructor c => t c f a -> Text-conNameT x = formatLabel . T.pack . conName $ x+conNameT :: forall c (t :: * -> (* -> *) -> * -> *) (f :: * -> *) a. Constructor c => Settings -> t c f a -> Text+conNameT set x = formatLabel set . T.pack . conName $ x -selNameT :: forall s (t :: * -> (* -> *) -> * -> *) (f :: * -> *) a. Selector s => t s f a -> Text-selNameT x = formatLabel . T.pack . selName $ x+selNameT :: forall s (t :: * -> (* -> *) -> * -> *) (f :: * -> *) a. Selector s => Settings -> t s f a -> Maybe Text+selNameT set x = case formatLabel set . T.pack . selName $ x of+  "" -> Nothing+  n  -> Just n  -- | Lowercases the first letter and strips leading and trailing underscores.-formatLabel :: Text -> Text-formatLabel = firstLetterToLower-            . stripLeadingAndTrailingUnderscore+formatLabel :: Settings -> Text -> Text+formatLabel set+  = firstLetterToLower+  . stripLeadingAndTrailingUnderscore+  . stripPref set +stripPref :: Settings -> Text -> Text+stripPref set s = (maybe id (\p t -> fromMaybe t . (disallowEmpty <=< T.stripPrefix (T.pack p)) $ t) . stripPrefix) set s+  where+    disallowEmpty x+      | T.null  x = Just s+      | otherwise = Just x+ stripLeadingAndTrailingUnderscore :: Text -> Text stripLeadingAndTrailingUnderscore = stripLeadingUnderscore                                   . stripTrailingUnderscore@@ -50,3 +64,10 @@  multipleConstructors :: [a] -> Bool multipleConstructors = (> 1) . length++-- Use String over Text so OverloadedStrings isn't necessary+data Settings = Settings { stripPrefix :: Maybe String }+  deriving Show++defaultSettings :: Settings+defaultSettings = Settings { stripPrefix = Nothing }