packages feed

pg-entity-0.0.5.1: src/Database/PostgreSQL/Entity/Internal/QQ.hs

{-# LANGUAGE TemplateHaskell #-}

{-|
 Module     : Database.PostgreSQL.Entity.Internal.QQ
 Copyright  : © Koz Ross, 2021
 License    : MIT
 Maintainer : koz.ross@retro-freedom.nz
 Stability  : Experimental

 A quasi-quoter for 'Field's, supporting optional types.

 There is little reason to import this module directly; instead, import
 'Database.PostgreSQL.Entity', which re-exports the 'field' quasiquoter.
-}
module Database.PostgreSQL.Entity.Internal.QQ (field) where

import Data.Text (Text, pack)
import Database.PostgreSQL.Entity.Internal.Unsafe (Field (Field))
import Language.Haskell.TH (Dec, Exp, Pat, Q, Type)
import Language.Haskell.TH.Quote (QuasiQuoter (QuasiQuoter))
import Language.Haskell.TH.Syntax (lift)
import Text.Parsec (Parsec, anyChar, manyTill, parse, space, spaces, string, try, (<|>))

{-| A quasi-quoter for safely constructing 'Field's.

 == Example:

 > instance Entity BlogPost where
 >   tableName  = "blogposts"
 >   primaryKey = [field| blog_post_id |]
 >   fields = [ [field| blog_post_id |]
 >            , [field| author_id |]
 >            , [field| uuid_list :: uuid[] |] -- ← This is where we specify an optional PostgreSQL type annotation
 >            , [field| title |]
 >            , [field| content |]
 >            , [field| created_at |]
 >            ]

 @since 0.1.0.0
-}
field :: QuasiQuoter
field = QuasiQuoter fieldExp errorFieldPat errorFieldType errorFieldDec

-- Helpers

fieldExp :: String -> Q Exp
fieldExp input = case parse fieldParser "Expression" input of
  Left err -> fail . show $ err
  Right (name, Nothing) -> [e|Field $(lift name) Nothing|]
  Right (name, Just typ) -> [e|Field $(lift name) (Just $(lift typ))|]

errorFieldPat :: String -> Q Pat
errorFieldPat _ = fail "Cannot use 'field' in a pattern context."

fieldParser :: Parsec String () (Text, Maybe Text)
fieldParser = do
  spaces
  res <- try withType <|> noType
  spaces
  pure res
  where
    withType :: Parsec String () (Text, Maybe Text)
    withType = do
      name <- manyTill anyChar (try space)
      case name of
        [] -> fail "Cannot have an empty field name."
        _ -> do
          spaces
          _ <- string "::"
          spaces
          typ <- manyTill anyChar (try space)
          case typ of
            [] -> fail "Cannot have an empty type."
            _ -> pure (pack name, Just . pack $ typ)
    noType :: Parsec String () (Text, Maybe Text)
    noType = do
      name <- manyTill anyChar (try space)
      case name of
        [] -> fail "Cannot have an empty field name."
        _ -> pure (pack name, Nothing)

errorFieldType :: String -> Q Type
errorFieldType _ = fail "Cannot use 'field' in a type context."

errorFieldDec :: String -> Q [Dec]
errorFieldDec _ = fail "Cannot use 'field' in a declaration context."