packages feed

registry-aeson-0.2.3.2: src/Data/Registry/Aeson/TH/Decoder.hs

{-# OPTIONS_GHC -Wno-type-defaults #-}

module Data.Registry.Aeson.TH.Decoder where

import Control.Monad.Fail
import Data.List (nub)
import Data.Registry.Aeson.TH.ThOptions
import Data.Registry.Aeson.TH.TH
import Language.Haskell.TH
import Language.Haskell.TH.Syntax
import Protolude as P hiding (Type)

{-
  This module uses TemplateHaskell to extract enough type information to be able to
  build a Decoder based on configuration options
-}

-- | Make an Encoder for a given data type
--   Usage: $(makeDecoder ''MyDataType <: otherEncoders)
makeDecoder :: Name -> ExpQ
makeDecoder = makeDecoderWith defaultThOptions

-- | Make an Encoder for a given data type, where all types names are qualified with their module full name
--    -- MyDataType is defined in X.Y.Z
--    import X.Y.Z qualified
--    $(makeDecoderQualified ''MyDataType <: otherEncoders)
makeDecoderQualified :: Name -> ExpQ
makeDecoderQualified = makeDecoderWith (ThOptions qualified)

-- | Make an Encoder for a given data type, where all types names are qualified with their module name
--    -- MyDataType is defined in X.Y.Z
--    import X.Y.Z qualified as Z
--    $(makeDecoderQualifiedLast ''MyDataType <: otherEncoders)
makeDecoderQualifiedLast :: Name -> ExpQ
makeDecoderQualifiedLast = makeDecoderWith (ThOptions qualifyWithLastName)

-- | Make a Decoder for a given data type and pass options to specify how names must be qualified
--   Usage: $(makeDecoderWith options ''MyDataType <: otherDecoders)
makeDecoderWith :: ThOptions -> Name -> ExpQ
makeDecoderWith thOptions typeName = appE (varE $ mkName "fun") $ do
  info <- reify typeName
  case info of
    TyConI (NewtypeD _context _name _typeVars _kind constructor _deriving) ->
      makeConstructorsDecoder thOptions typeName [constructor]
    TyConI (DataD _context _name _typeVars _kind constructors _deriving) -> do
      case constructors of
        [] -> do
          qReport True "can not make an Decoder for an empty data type"
          fail "decoders creation failed"
        _ -> makeConstructorsDecoder thOptions typeName constructors
    other -> do
      qReport True ("can only create decoders for an ADT, got: " <> P.show other)
      fail "decoders creation failed"

-- | Make a decoder for a given data type by extracting just enough metadata about the data type in order to be able
--   to parse a Value
--
--   For example for the data type:
--
--   data T = T1 {f1::Int, f2::Int} | T2 Int Int
--
--   we add this function to the registry:
--
--   \opts d1 d2 d3 -> Decoder $ \v ->
--     decodeFromDefinitions opts v $ \case
--       ToConstructor "T1" [v1, v2]-> T1 <$> d1 v1 <*> d2 v2 ...
--       ToConstructor "T2" [v1, v2]-> T2 <$> d1 v1 <*> d3 v2 ...
--       other -> Left ("cannot decode " <> valueToText v)
--
--   The \case function is the only one which needs to be generated in order to match the exact shape of the
--   constructors to instantiate
makeConstructorsDecoder :: ThOptions -> Name -> [Con] -> ExpQ
makeConstructorsDecoder thOptions typeName cs = do
  ts <- nub . join <$> for cs typesOf
  let decoderParameters = sigP (varP (mkName "os")) (conT $ mkName "Options") : sigP (varP (mkName "cd")) (conT $ mkName "ConstructorsDecoder") : ((\(t, n) -> sigP (varP (mkName $ "d" <> P.show n)) (appT (conT $ mkName "Decoder") (pure t))) <$> zip ts [0 ..])
  -- makeToConstructors os [Constructor "T1" ["f1", "f2"], Constructor "T2" []] v
  let paramP = varP (mkName "v")
  constructorDefs <- for cs $ \c -> do
    cName <- makeName thOptions <$> nameOf c
    fields <- fmap (litE . StringL . P.show . makeName thOptions) <$> fieldsOf c
    fieldTypes <- fmap (litE . StringL . P.show . getSimpleTypeName thOptions) <$> typesOf c
    varE (mkName "makeConstructorDef") `appE` (litE . StringL $ P.show cName) `appE` listE fields `appE` listE fieldTypes
  let matchClauses = makeMatchClause thOptions typeName ts <$> cs
  let matchFunction = lamCaseE (matchClauses <> [makeErrorClause typeName])
  let resolveFunction = varE (mkName "decodeFromDefinitions") `appE` varE (mkName "os") `appE` varE (mkName "cd") `appE` listE (pure <$> constructorDefs) `appE` varE (mkName "v") `appE` matchFunction
  lamE decoderParameters (appE (conE (mkName "Decoder")) (lamE [paramP] resolveFunction))

-- | Decode the nth constructor of a data type
--    ToConstructor "T1" [v1, v2]-> T1 <$> d1 v1 <*> d2 v2 ...
makeMatchClause :: ThOptions -> Name -> [Type] -> Con -> MatchQ
makeMatchClause thOptions typeName allTypes c = do
  ts <- typesOf c
  constructorTypes <- fmap (\(_,n,k) -> (n, k)) <$> indexConstructorTypes allTypes ts
  cName <- makeName thOptions <$> nameOf c
  let fieldsP = listP $ (\(n, _) -> varP $ mkName ("v" <> P.show n)) <$> constructorTypes
  match
    (conP (mkName "ToConstructor") [litP (StringL . P.show $ cName), fieldsP])
    (normalB (applyDecoder thOptions typeName cName constructorTypes))
    []

-- | Return an error the json value cannot be decoded with a constructor name and some values
makeErrorClause :: Name -> MatchQ
makeErrorClause typeName = do
  let errorMessage =
        (varE (mkName "<>") `appE` litE (StringL ("cannot use this constructor to create an instance of type '" <> P.show typeName <> "': ")))
          `appE` (varE (mkName "show") `appE` varE (mkName "_1"))
  match (varP $ mkName "_1") (normalB (appE (conE $ mkName "Left") errorMessage)) []

-- ConstructorName <$> decodeFieldValue d1 o1 <*> decodeFieldValue d2 o2 ...
applyDecoder :: ThOptions -> Name -> Name -> [(Int, Int)] -> ExpQ
applyDecoder _thOptions _typeName cName [] = appE (varE $ mkName "pure") (conE cName)
applyDecoder thOptions typeName cName (nk : nks) = do
  let cons = appE (varE $ mkName "pure") (conE cName)
  foldr (\i r -> appE (appE (varE (mkName "ap")) r) $ decodeAt i) (appE (appE (varE (mkName "ap")) cons) $ decodeAt nk) (reverse nks)
  where
    decodeAt (n, k) =
      varE (mkName "decodeFieldValue")
        `appE` varE (mkName ("d" <> P.show k))
        `appE` (litE . StringL . P.show . makeName thOptions $ typeName)
        `appE` (litE . StringL . P.show . makeName thOptions $ cName)
        `appE` varE (mkName ("v" <> P.show n))