{-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} {- | Options used to derive FromJSON/ToJSON instance. These options generally comply to our style regarding names. Of course sometimes they don't fit one's needs, so treat them as just sensible defaults. -} module Data.Aeson.Options ( -- * Custom options defaultOptions , leaveTagOptions , defaultOptionsPS , stripTypeOptions -- * Generic functions , genericParseJSONStripType , genericToJSONStripType ) where import Data.Aeson.Types (Parser) import Data.Char (isLower, isPunctuation, isUpper, toLower) import Data.List (findIndex, isPrefixOf) import GHC.Generics (Generic, Rep) import Type.Reflection (Typeable, typeRep) import qualified Data.Aeson as A headToLower :: String -> String headToLower [] = error "Can not use headToLower on empty String" headToLower (x:xs) = toLower x : xs stripFieldPrefix :: String -> String stripFieldPrefix = dropWhile (not . isUpper) dropPunctuation :: String -> String dropPunctuation = filter (not . isPunctuation) stripConstructorPrefix :: String -> String stripConstructorPrefix t = maybe t (flip drop t . decrementSafe) $ findIndex isLower t where decrementSafe 0 = 0 decrementSafe i = i - 1 -- | These options do the following transformations: -- 1. Names of field -- records are assumed to be camelCased, `camel` part is removed, -- `Cased` part is converted to `cased`. So `camelCased` becomes -- `cased`. Also all punctuation symbols are dropped before doing it. -- 2. Constructors are assumed to start with some capitalized prefix -- (which finished right before the last capital letter). This prefix -- is dropped and then the first letter is lowercased. defaultOptions :: A.Options defaultOptions = A.defaultOptions { A.fieldLabelModifier = headToLower . stripFieldPrefix . dropPunctuation , A.constructorTagModifier = headToLower . stripConstructorPrefix , A.sumEncoding = A.ObjectWithSingleField } -- | These options are the same as `defaultOptions`, but they don't -- modify constructor tags. leaveTagOptions :: A.Options leaveTagOptions = defaultOptions { A.constructorTagModifier = id } -- | Options used for communication with PureScript by default. defaultOptionsPS :: A.Options defaultOptionsPS = A.defaultOptions { A.constructorTagModifier = headToLower . stripConstructorPrefix } {- | Allows to create 'A.FromJSON' instance that strips the data type name prefix from every field. Doesn't change name of the fields that doesn't start with the type name. >>> data Foo = Foo { fooBar :: String, fooQuux :: Int } deriving (Generic, Show) >>> instance FromJSON Foo where parseJSON = genericParseJSONStripType >>> decode @Foo "{ \"bar\": \"test\", \"quux\": 42 }" Just (Foo {fooBar = "test", fooQuux = 42}) -} genericParseJSONStripType :: forall a . (Typeable a, Generic a, A.GFromJSON A.Zero (Rep a)) => A.Value -> Parser a genericParseJSONStripType = A.genericParseJSON (stripTypeOptions @a) {- | Allows to create 'A.ToJSON' instance that strips the data type name prefix from every field. Doesn't change name of the fields that doesn't start with the type name. >>> data Foo = Foo { fooBar :: String, fooQuux :: Int } deriving (Generic, Show) >>> instance ToJSON Foo where toJSON = genericToJSONStripType >>> encode $ Foo { fooBar = "test", fooQuux = 42 } "{\"quux\":42,\"bar\":\"test\"}" -} genericToJSONStripType :: forall a . (Typeable a, Generic a, A.GToJSON A.Zero (Rep a)) => a -> A.Value genericToJSONStripType = A.genericToJSON (stripTypeOptions @a) {- | Options to strip type name from the field names. See 'genericParseJSONStripType' and 'genericToJSONStripType' for examples. -} stripTypeOptions :: forall a . Typeable a => A.Options stripTypeOptions = A.defaultOptions { A.fieldLabelModifier = stripTypeNamePrefix } where typeName :: String typeName = headToLower $ show $ typeRep @a stripTypeNamePrefix :: String -> String stripTypeNamePrefix fieldName = if typeName `isPrefixOf` fieldName then headToLower $ drop (length typeName) fieldName else fieldName