packages feed

cassava-th-0.1.0.0: src/Data/Csv/TH.hs

{-# language TemplateHaskell #-}

-- | This module provides @TemplateHaskell@ support for the @cassava@
-- library, so you can avoid deriving 'Generic' and the compilation
-- overhead that brings.
module Data.Csv.TH
  ( deriveToNamedRecord,
    deriveToRecord,
    deriveDefaultOrdered,
    deriveNamedRecord,
    deriveFromNamedRecord,
    deriveFromRecord,
    deriveToAndFromRecord,
    deriveToNamedRecordAndDefaultOrdered,
    deriveToAndFromNamedRecordAndDefaultOrdered,
    CsvOptions,
    csvDefaultOptions,
  )
where

import Data.Traversable
import Control.Monad
import Control.Monad.Fail
import Data.Csv qualified as Csv
import Data.Vector qualified as V
import Language.Haskell.TH

-- | Cassava uses the same name as Aeson, which is used much more frequently.
-- This alias makes it a bit easier to refer to it.
type CsvOptions = Csv.Options

-- | Cassava uses the same name as Aeson, which is used more frequently. To
-- avoid qualified names and lots of clashes, we export an alias here.
csvDefaultOptions :: Csv.Options
csvDefaultOptions = Csv.defaultOptions

-- | A helper for the common case of deriving both 'Csv.ToNamedRecord' and
-- 'Csv.DefaultOrdered', sharing the same options.
deriveToNamedRecordAndDefaultOrdered :: Csv.Options -> Name -> DecsQ
deriveToNamedRecordAndDefaultOrdered opts name =
  deriveToNamedRecord opts name <> deriveDefaultOrdered opts name

-- | A helper for the common case of deriving 'Csv.ToNamedRecord',
-- 'Csv.FromNamedRecord', and 'Csv.DefaultOrdered', sharing the same options.
deriveToAndFromNamedRecordAndDefaultOrdered :: Csv.Options -> Name -> DecsQ
deriveToAndFromNamedRecordAndDefaultOrdered opts name =
  deriveToNamedRecord opts name
    <> deriveDefaultOrdered opts name
    <> deriveFromNamedRecord opts name

-- | Derives a 'Csv.ToNamedRecord' instance for a given type.
deriveToNamedRecord :: Csv.Options -> Name -> DecsQ
deriveToNamedRecord opts typName = do
  info <- reify typName
  con <-
    case info of
      TyConI dec ->
        case dec of
          NewtypeD _cxt _name _tyvars _mkind con _derivs ->
            pure con
          DataD _cxt _name _tyvars _mkind cons _derivs ->
            case cons of
              [con] ->
                pure con
              _ ->
                fail $
                  concat
                    [ "Expected "
                    , show typName
                    , " to be a record with a single constructor."
                    ]
          _ ->
            fail $
              concat
                [ "Expected "
                , show typName
                , " to be a record with a single constructor."
                ]
      _ ->
        fail $
          concat
            [ "Expected"
            , show typName
            , " to be a type name of a single record constructor."
            ]
  (constrPatternMatch, matchedVariables) <-
    case con of
      RecC constrName fields -> do
        namesWithPatterns <- for fields \(fieldName, _, _) -> do
          FieldName fieldName <$> newName (nameBase fieldName)

        pure (ConP constrName [] (map (VarP . fieldPatternVariable) namesWithPatterns), namesWithPatterns)
      _ ->
        fail $
          concat
            [ "Expected "
            , show typName
            , "to be a record with a single constructor."
            ]

  let fieldExpr fieldName = do
        let modifiedFieldName =
              Csv.fieldLabelModifier opts (nameBase $ fieldOriginalName fieldName)

        [e|
          ( $(litE $ StringL modifiedFieldName)
          , Csv.toField $(varE (fieldPatternVariable fieldName))
          )
          |]

  listExpr <-
    ListE <$> for matchedVariables \fieldName ->
      fieldExpr fieldName
  [d|
    instance Csv.ToNamedRecord $(conT typName) where
      toNamedRecord val =
        case val of
          $(pure constrPatternMatch) ->
            Csv.namedRecord $(pure listExpr)
    |]

data FieldName = FieldName
  { fieldOriginalName :: Name
  , fieldPatternVariable :: Name
  }

-- | Derive an instance of the 'Csv.ToRecord' type class. This only works for
-- non-record types.
deriveToRecord :: Name -> DecsQ
deriveToRecord typName = do
  info <- reify typName
  cons <-
    case info of
      TyConI dec ->
        case dec of
          NewtypeD _cxt _name _tyvars _mkind con _derivs ->
            pure [con]
          DataD _cxt _name _tyvars _mkind cons _derivs ->
            pure cons
          _ ->
            fail $
              concat
                [ "Expected "
                , show typName
                , " to be a datatype."
                ]
      _ ->
        fail $
          concat
            [ "Expected"
            , show typName
            , " to be a type name of a datatype."
            ]

  cases <-
    for cons \case
      NormalC constrName bangTypes -> do
        names <- for bangTypes \_ -> newName "p"
        exprBody <-
          AppE (VarE 'V.fromList) . ListE <$> do
            for names \name ->
              [e|Csv.toField $(varE name)|]
        let constrPattern =
              ConP constrName [] (map VarP names)
        pure $ Match constrPattern (NormalB exprBody) []
      RecC constrName varBangTypes -> do
        names <- for varBangTypes \(name, _, _) -> newName (nameBase name)
        exprBody <-
          AppE (VarE 'V.fromList) . ListE <$> do
            for names \name ->
              [e|Csv.toField $(varE name)|]
        let constrPattern =
              ConP constrName [] (map VarP names)
        pure $ Match constrPattern (NormalB exprBody) []
      _ ->
        fail $
          concat
            [ "Expected "
            , show typName
            , " to have regular constructors"
            ]

  [d|
    instance Csv.ToRecord $(conT typName) where
      toRecord =
        $(pure $ LamCaseE cases)
    |]

-- | Derive an instance of 'Csv.DefaultOrdered' for the type.
deriveDefaultOrdered :: CsvOptions -> Name -> DecsQ
deriveDefaultOrdered opts typName = do
  info <- reify typName
  con <-
    case info of
      TyConI dec ->
        case dec of
          NewtypeD _cxt _name _tyvars _mkind con _derivs ->
            pure con
          DataD _cxt _name _tyvars _mkind cons _derivs ->
            case cons of
              [con] ->
                pure con
              _ ->
                fail $
                  concat
                    [ "Expected "
                    , show typName
                    , " to be a record with a single constructor."
                    ]
          _ ->
            fail $
              concat
                [ "Expected "
                , show typName
                , " to be a record with a single constructor."
                ]
      _ ->
        fail $
          concat
            [ "Expected"
            , show typName
            , " to be a type name of a single record constructor."
            ]
  body <-
    case con of
      RecC _constrName varBangTypes -> do
        ListE <$> for varBangTypes \(fieldName, _, _) -> do
          let modifiedFieldName =
                Csv.fieldLabelModifier opts (nameBase fieldName)
          [e|$(litE $ StringL modifiedFieldName)|]
      _ ->
        fail $
          concat
            [ "Expected "
            , show typName
            , "to be a record with a single constructor."
            ]

  [d|
    instance Csv.DefaultOrdered $(conT typName) where
      headerOrder _ = V.fromList $(pure body)
    |]

-- | Derive an instance of both 'Csv.ToNamedRecord' and 'Csv.FromNamedRecord'
-- for a record type.
deriveNamedRecord :: CsvOptions -> Name -> DecsQ
deriveNamedRecord opts typName =
  deriveToNamedRecord opts typName <> deriveFromNamedRecord opts typName

-- | Derive an instance of 'Csv.FromNamedRecord' for record types.
deriveFromNamedRecord :: CsvOptions -> Name -> DecsQ
deriveFromNamedRecord opts typName = do
  info <- reify typName
  con <-
    case info of
      TyConI dec ->
        case dec of
          NewtypeD _cxt _name _tyvars _mkind con _derivs ->
            pure con
          DataD _cxt _name _tyvars _mkind cons _derivs ->
            case cons of
              [con] ->
                pure con
              _ ->
                fail $
                  concat
                    [ "Expected "
                    , show typName
                    , " to be a record with a single constructor."
                    ]
          _ ->
            fail $
              concat
                [ "Expected "
                , show typName
                , " to be a record with a single constructor."
                ]
      _ ->
        fail $
          concat
            [ "Expected"
            , show typName
            , " to be a type name of a single record constructor."
            ]
  (constrName, fieldNames) <-
    case con of
      RecC constrName fields -> do
        namesWithPatterns <- for fields \(fieldName, _, _) -> do
          pure fieldName

        pure (constrName, namesWithPatterns)
      _ ->
        fail $
          concat
            [ "Expected "
            , show typName
            , "to be a record with a single constructor."
            ]

  lamExpr <- do
    arg <- newName "namedRecord"

    let getFieldExpr fieldName = do
          let modifiedFieldName =
                Csv.fieldLabelModifier opts (nameBase fieldName)

          [e|$(varE arg) Csv..: $(litE $ StringL modifiedFieldName)|]
    body <- do
      foldM
        (\acc fieldName -> [e|$(pure acc) <*> $(getFieldExpr fieldName)|])
        (VarE 'pure `AppE` ConE constrName)
        fieldNames
    pure $ LamE [VarP arg] body

  [d|
    instance Csv.FromNamedRecord $(conT typName) where
      parseNamedRecord = $(pure lamExpr)
    |]

-- | Derive 'Csv.FromRecord' for a type. This ignores the record fields and uses
-- the position of the fields to determine how to parse the record.
deriveFromRecord :: Name -> DecsQ
deriveFromRecord typName = do
  info <- reify typName
  con <-
    case info of
      TyConI dec ->
        case dec of
          NewtypeD _cxt _name _tyvars _mkind con _derivs ->
            pure con
          DataD _cxt _name _tyvars _mkind cons _derivs ->
            case cons of
              [con] ->
                pure con
              _ ->
                fail $
                  concat
                    [ "Expected "
                    , show typName
                    , " to be a record with a single constructor."
                    ]
          _ ->
            fail $
              concat
                [ "Expected "
                , show typName
                , " to be a record with a single constructor."
                ]
      _ ->
        fail $
          concat
            [ "Expected"
            , show typName
            , " to be a type name of a single record constructor."
            ]
  (constrName, fieldCount) <-
    case con of
      RecC constrName fields -> do
        pure (constrName, length fields)
      NormalC constrName fields ->
        pure (constrName, length fields)
      _ ->
        fail $
          concat
            [ "Expected "
            , show typName
            , "to be a record with a single constructor."
            ]

  lamExpr <- do
    arg <- newName "record"

    let getFieldExpr idx = do
          [e|$(varE arg) Csv..! idx|]

    body <- do
      foldM
        (\acc fieldIndex -> [e|$(pure acc) <*> $(getFieldExpr fieldIndex)|])
        (VarE 'pure `AppE` ConE constrName)
        [0 .. fieldCount - 1]
    pure $ LamE [VarP arg] body

  [d|
    instance Csv.FromRecord $(conT typName) where
      parseRecord = $(pure lamExpr)
    |]

deriveToAndFromRecord :: Name -> DecsQ
deriveToAndFromRecord name =
  deriveToRecord name <> deriveFromRecord name