domaindriven-0.5.0: src/DomainDriven/Internal/NamedJsonFields.hs
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE UndecidableInstances #-}
module DomainDriven.Internal.NamedJsonFields where
import Control.Applicative
import Control.Monad.State
import Data.Aeson
import qualified Data.Aeson.Key as Key
import qualified Data.Aeson.KeyMap as KM
import Data.Aeson.Types
import Data.Generics.Product
import Data.Kind (Type)
import Data.Maybe (fromMaybe)
import qualified Data.OpenApi as O
import Data.OpenApi.Declare
import Data.Proxy
import Data.Text (Text)
import qualified Data.Text as T
import Data.Typeable
import DomainDriven.Internal.HasFieldName
import GHC.Generics
import Lens.Micro hiding (to)
import Prelude
import qualified Lens.Micro as Lens
packed :: Getting r String Text
packed = Lens.to T.pack
newtype NamedJsonFields a = NamedJsonFields a
instance (GNamedToJSON (Rep a), Generic a) => ToJSON (NamedJsonFields a) where
toJSON (NamedJsonFields a) = gNamedToJson defaultNamedJsonOptions a
instance (GNamedFromJSON (Rep a), Generic a) => FromJSON (NamedJsonFields a) where
parseJSON = fmap NamedJsonFields . gNamedParseJson defaultNamedJsonOptions
instance (Typeable a, GNamedToSchema (Rep a)) => O.ToSchema (NamedJsonFields a) where
declareNamedSchema _ = gNamedDeclareNamedSchema defaultNamedJsonOptions (Proxy @a)
-- evalStateT (gDeclareNamedSchema defaultNamedJsonOptions $ Proxy @(Rep a)) []
gNamedToJson :: (GNamedToJSON (Rep a), Generic a) => NamedJsonOptions -> a -> Value
gNamedToJson opts a = Object . KM.fromList $ evalState (gToTupleList opts $ from a) []
gNamedParseJson
:: (GNamedFromJSON (Rep a), Generic a) => NamedJsonOptions -> Value -> Parser a
gNamedParseJson opts v = to <$> evalStateT (gNamedFromJSON opts v) []
gNamedDeclareNamedSchema
:: forall a
. (GNamedToSchema (Rep a))
=> NamedJsonOptions
-> Proxy a
-> Declare (O.Definitions O.Schema) O.NamedSchema
gNamedDeclareNamedSchema opts _ =
evalStateT (gDeclareNamedSchema opts (Proxy @(Rep a))) []
-----------------------------------------Options------------------------------------------
data NamedJsonOptions = NamedJsonOptions
{ constructorTagModifier :: String -> String
, tagFieldName :: String
, skipTagField :: Bool
, datatypeNameModifier :: String -> String
}
deriving (Generic)
defaultNamedJsonOptions :: NamedJsonOptions
defaultNamedJsonOptions =
NamedJsonOptions
{ constructorTagModifier = id
, tagFieldName = "tag"
, skipTagField = False
, datatypeNameModifier = id
}
-----------------------------------------O.ToSchema-----------------------------------------
data Proxy3 a b c = Proxy3
class GNamedToSchema (f :: Type -> Type) where
gDeclareNamedSchema
:: NamedJsonOptions
-> Proxy f
-> StateT [UsedName] (Declare (O.Definitions O.Schema)) O.NamedSchema
-- Grab the name of the datatype
instance (Datatype d, GNamedToSchema f) => GNamedToSchema (D1 d f) where
gDeclareNamedSchema opts _ = do
let dtName :: String
dtName = opts ^. field @"datatypeNameModifier" $ datatypeName (Proxy3 @d @f)
O.NamedSchema _ rest <- gDeclareNamedSchema opts (Proxy @f)
pure $ O.NamedSchema (Just $ T.pack dtName) rest
-- Grab the name of the constructor to use for the `tag` field content.
instance (GNamedToSchema f, Constructor c) => GNamedToSchema (C1 c f) where
gDeclareNamedSchema opts _ = do
O.NamedSchema _ s <- gDeclareNamedSchema opts (Proxy @f)
if opts ^. field @"skipTagField"
then pure $ O.NamedSchema Nothing s
else do
let tagName :: Key
tagName = Key.fromText $ opts ^. field @"tagFieldName" . packed
state $ \ss -> ((), tagName : ss)
let constructorName :: Text
constructorName =
T.pack
. (opts ^. field @"constructorTagModifier")
. conName
$ Proxy3 @c @f
tagFieldSchema :: O.Schema
tagFieldSchema =
mempty
& O.properties
<>~ [
( Key.toText tagName
, O.Inline $
mempty
& O.type_
?~ O.OpenApiString
& O.enum_
?~ [String constructorName]
)
]
& O.required
<>~ [Key.toText tagName]
pure $ O.NamedSchema Nothing $ tagFieldSchema <> s
-- Grab the name of the field, but not not set it as O.required
instance
{-# OVERLAPPING #-}
(O.ToSchema f, HasFieldName f)
=> GNamedToSchema (S1 s (Rec0 (Maybe f)))
where
gDeclareNamedSchema _opts _ = do
let fName = Key.fromText $ fieldName @(Maybe f)
usedNames <- state (\used -> (used, fName : used))
_ <- lift $ O.declareSchemaRef $ Proxy @f
pure
. O.NamedSchema Nothing
$ mempty
& O.properties
<>~ [
( Key.toText $ actualFieldName usedNames fName
, O.Inline $ O.toSchema $ Proxy @f
)
]
-- Grab the name of the field and set it as O.required
instance
{-# OVERLAPPABLE #-}
(O.ToSchema f, HasFieldName f)
=> GNamedToSchema (S1 s (Rec0 f))
where
gDeclareNamedSchema _opts _ = do
let fName = Key.fromText $ fieldName @f
usedNames <- state (\used -> (used, fName : used))
_ <- lift $ O.declareSchemaRef $ Proxy @f
pure
. O.NamedSchema Nothing
$ mempty
& O.properties
<>~ [
( Key.toText $ actualFieldName usedNames fName
, O.Inline $ O.toSchema $ Proxy @f
)
]
& O.required
<>~ [Key.toText $ actualFieldName usedNames fName]
instance GNamedToSchema U1 where
gDeclareNamedSchema _opts _ = pure $ O.NamedSchema Nothing mempty
instance (GNamedToSchema f, GNamedToSchema g) => GNamedToSchema (f :*: g) where
gDeclareNamedSchema opts _ = do
O.NamedSchema _ a <- gDeclareNamedSchema opts (Proxy @f)
O.NamedSchema _ b <- gDeclareNamedSchema opts (Proxy @g)
pure (O.NamedSchema Nothing $ a <> b)
instance (GNamedToSchema f, GNamedToSchema g) => GNamedToSchema (f :+: g) where
gDeclareNamedSchema opts _ = do
-- Sum types do not share fields, thus we do not need to adjust the names
O.NamedSchema _ a <- lift $ evalStateT (gDeclareNamedSchema opts (Proxy @f)) []
O.NamedSchema _ b <- lift $ evalStateT (gDeclareNamedSchema opts (Proxy @g)) []
let unwrapOneOf :: O.Schema -> [O.Referenced O.Schema]
unwrapOneOf x = fromMaybe [O.Inline x] $ x ^. O.oneOf
pure $
O.NamedSchema Nothing $
mempty
& O.oneOf
?~ unwrapOneOf a <> unwrapOneOf b
------------------------------------------ToJSON------------------------------------------
type UsedName = Key
class GNamedToJSON a where
gToTupleList :: NamedJsonOptions -> a x -> State [UsedName] [(Key, Value)]
instance (GNamedToJSON f) => GNamedToJSON (M1 D d f) where
gToTupleList opts = gToTupleList opts . unM1
instance (GNamedToJSON f, Constructor c) => GNamedToJSON (M1 C c f) where
gToTupleList opts a = do
tag <-
if opts ^. field @"skipTagField"
then pure []
else do
usedNames <- get
put $ usedNames <> [Key.fromText $ opts ^. field @"tagFieldName" . packed]
pure
[
( Key.fromText $ opts ^. field @"tagFieldName" . packed
, String
. T.pack
. (opts ^. field @"constructorTagModifier")
$ conName a
)
]
rest <- gToTupleList opts $ unM1 a
pure $ tag <> rest
instance (ToJSON t, HasFieldName t) => GNamedToJSON (M1 S c (Rec0 t)) where
gToTupleList _opts a = do
usedNames <- get
let fName = Key.fromText $ fieldName @t
put $ usedNames <> [fName]
pure [(actualFieldName usedNames fName, toJSON . unK1 $ unM1 a)]
instance GNamedToJSON U1 where
gToTupleList _opts U1 = pure []
instance (GNamedToJSON a, GNamedToJSON b) => GNamedToJSON (a :*: b) where
gToTupleList opts (a :*: b) = do
p1 <- gToTupleList opts a
p2 <- gToTupleList opts b
pure $ p1 <> p2
instance (GNamedToJSON a, GNamedToJSON b) => GNamedToJSON (a :+: b) where
gToTupleList opts = \case
L1 a -> gToTupleList opts a
R1 a -> gToTupleList opts a
actualFieldName :: [UsedName] -> Key -> Key
actualFieldName usedNames fName =
fName <> case length (filter (== fName) usedNames) of
0 -> Key.fromText ""
i -> Key.fromText $ "_" <> T.pack (show (i + 1))
-----------------------------------------FromJSON-----------------------------------------
lookupKey :: Key -> Value -> StateT [UsedName] Parser Value
lookupKey k = \case
Object o -> do
usedNames <- get
put $ usedNames <> [k]
maybe (fail $ "No key " <> show k) pure $ KM.lookup k o
_ -> fail $ "Expected " <> show k <> " to be an object."
class GNamedFromJSON a where
gNamedFromJSON :: NamedJsonOptions -> Value -> StateT [UsedName] Parser (a x)
instance GNamedFromJSON p => GNamedFromJSON (M1 D f p) where
gNamedFromJSON opts v = M1 <$> gNamedFromJSON opts v
instance (Constructor f, GNamedFromJSON p) => GNamedFromJSON (M1 C f p) where
gNamedFromJSON opts v =
if opts ^. field @"skipTagField"
then M1 <$> gNamedFromJSON opts v
else do
tag <- lookupKey (Key.fromText $ opts ^. field @"tagFieldName" . packed) v
c <- M1 <$> gNamedFromJSON opts v
let constructorName =
T.pack . (opts ^. field @"constructorTagModifier") $ conName c
case tag of
String t | t == constructorName -> pure c
_ -> fail "Unknown tag"
instance GNamedFromJSON U1 where
gNamedFromJSON _opts _ = pure U1
instance (GNamedFromJSON a, GNamedFromJSON b) => GNamedFromJSON (a :+: b) where
gNamedFromJSON opts vals =
L1 <$> gNamedFromJSON opts vals <|> R1 <$> gNamedFromJSON opts vals
instance (GNamedFromJSON a, GNamedFromJSON b) => GNamedFromJSON (a :*: b) where
gNamedFromJSON opts vals = do
p1 <- gNamedFromJSON opts vals
p2 <- gNamedFromJSON opts vals
pure $ p1 :*: p2
instance (FromJSON t, HasFieldName t) => GNamedFromJSON (M1 S c (Rec0 t)) where
gNamedFromJSON _opts vals = do
usedNames <- get
let fName = Key.fromText $ fieldName @t
v <- lookupKey (actualFieldName usedNames fName) vals
put $ usedNames <> [fName]
lift $ M1 . K1 <$> (parseJSON @t) v