packages feed

hedn-0.3.0.3: lib/Data/EDN/QQ.hs

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-}

-- XXX: Lift instances. Don't want to pollute main module with TH shenanigans.
{-# OPTIONS_GHC -fno-warn-orphans #-}

module Data.EDN.QQ
  ( edn
  , ednList
  , ednMap
  , ednVec
  , ednSet
  , fromEDN
  ) where

import Data.Data (Data)
#if MIN_VERSION_base(4,13,0)
#else
import Data.Semigroup ((<>))
#endif
import Data.Text (Text)
import Data.Typeable (cast)
import Language.Haskell.TH.Quote (QuasiQuoter(..))
import Language.Haskell.TH.Syntax (Exp(..), Lift(..), Q)

import qualified Data.Text as Text
import qualified Data.Vector as Vector
import qualified Language.Haskell.TH.Syntax as TH

import Data.EDN (FromEDN, decodeText, parseText)
import Data.EDN.AST.Types (Tagged(..), Value(..))

-- | Quasiquoter for 'Data.EDN.TaggedValue'.
--
-- @
-- Tagged "foo" "bar" Nil === [edn| #foo/bar nil |]
-- @
edn :: QuasiQuoter
edn = ednQQ $ \str -> do
  src <- fmap TH.loc_filename TH.qLocation

  case parseText src (Text.pack str) of
    Right val ->
      lift val
    Left err ->
      error err

-- | Quasiquoter for untagged 'Value' wrapped in a List.
--
-- @
-- [ednList| #foo/bar nil |]
-- ===
-- List [ Tagged "foo" "bar" Nil ]
-- @
ednList :: QuasiQuoter
ednList = ednQQ $ \str -> do
  src <- fmap TH.loc_filename TH.qLocation
  let doc = "(" <> Text.pack str <> ")"

  case parseText src doc of
    Right (NoTag tv) ->
      lift tv
    Right Tagged{} ->
      error "unexpected tagged value"
    Left err ->
      error err

-- | Quasiquoter for untagged 'Value' wrapped in a Vec.
--
-- @
-- [ednVec| #foo/bar nil |]
-- ===
-- Vec [ Tagged "foo" "bar" Nil ]
-- @
ednVec :: QuasiQuoter
ednVec = ednQQ $ \str -> do
  src <- fmap TH.loc_filename TH.qLocation
  let doc = "[" <> Text.pack str <> "]"

  case parseText src doc of
    Right (NoTag tv) ->
      lift tv
    Right Tagged{} ->
      error "unexpected tagged value"
    Left err ->
      error err

-- | Quasiquoter for untagged 'Value' wrapped in a Set.
--
-- @
-- [ednList| #foo/bar nil |]
-- ===
-- List [ Tagged "foo" "bar" Nil ]
-- @
ednSet :: QuasiQuoter
ednSet = ednQQ $ \str -> do
  src <- fmap TH.loc_filename TH.qLocation
  let doc = "#{" <> Text.pack str <> "}"

  case parseText src doc of
    Right (NoTag tv) ->
      lift tv
    Right Tagged{} ->
      error "unexpected tagged value"
    Left err ->
      error err

-- | Quasiquoter for untagged 'Value' wrapped in a Map.
--
-- @
-- [ednMap| :key value |]
-- ===
-- Map [ (NoTag (Keyword "key"), NoTag (Symbol "" "value")) ]
-- @
ednMap :: QuasiQuoter
ednMap = ednQQ $ \str -> do
  src <- fmap TH.loc_filename TH.qLocation
  let doc = "{" <> Text.pack str <> "}"

  case parseText src doc of
    Right (NoTag tv) ->
      lift tv
    Right Tagged{} ->
      error "unexpected tagged value"
    Left err ->
      error err

-- | Specializable QuasiQuoter for compile-time decoding.
--
-- > ednPerson = fromEDN @Person
--
-- And in another module (a TH restriction):
--
-- > theFred = [ednPerson| #myapp/Person { :first "Fred" } |]
fromEDN :: forall a. (Lift a, FromEDN a) => QuasiQuoter
fromEDN = ednQQ $ \str -> do
  src <- fmap TH.loc_filename TH.qLocation
  case decodeText src (Text.pack str) of
    Left err ->
      error err
    Right (val :: a) ->
      lift val

ednQQ :: (String -> Q Exp) -> QuasiQuoter
ednQQ qexp = QuasiQuoter
  { quoteExp  = qexp
  , quotePat  = error "EDN unavailable in patterns"
  , quoteType = error "EDN unavailable in types"
  , quoteDec  = error "EDN unavailable in declarations"
  }

-- XXX: Workaround for Text.pack not present in the same module with Text constructors.
-- See https://stackoverflow.com/a/38182444
#if MIN_VERSION_base(4,15,0)
liftData' :: (Data a, TH.Quote m) => a -> m Exp
#else
liftData' :: Data a => a -> Q Exp
#endif
liftData' = TH.dataToExpQ $ fmap liftText . cast

#if MIN_VERSION_base(4,15,0)
liftText :: TH.Quote m => Text.Text -> m Exp
#else
liftText :: Text.Text -> Q Exp
#endif
liftText txt = AppE (VarE 'Text.pack) <$> lift (Text.unpack txt)

#if MIN_VERSION_base(4,15,0)
liftVector :: (Lift a, TH.Quote m) => Vector.Vector a -> m Exp
#else
liftVector :: Lift a => Vector.Vector a -> Q Exp
#endif
liftVector vec =
  AppE (VarE 'Vector.fromList) <$> lift (Vector.toList vec)

-- XXX: Workaround for undefined toConstr in Data instance for Vector.
instance Data a => Lift (Tagged Text a) where
  lift = \case
    NoTag val -> do
      val' <- liftData' val
      pure $ ConE 'NoTag `AppE` val'
    Tagged tagNS tag val -> do
      tagNS' <- liftText tagNS
      tag' <- liftText tag
      val' <- liftData' val
      pure $ ConE 'Tagged `AppE` tagNS' `AppE` tag' `AppE` val'

#if MIN_VERSION_template_haskell(2,17,0)
  liftTyped = TH.unsafeCodeCoerce . lift
#elif MIN_VERSION_template_haskell(2,16,0)
  liftTyped = TH.unsafeTExpCoerce . lift
#endif

instance Lift Value where
  lift = \case
    Vec items ->
      AppE (ConE 'Vec) <$> liftVector items
    val ->
      liftData' val

#if MIN_VERSION_template_haskell(2,17,0)
  liftTyped = TH.unsafeCodeCoerce . lift
#elif MIN_VERSION_template_haskell(2,16,0)
  liftTyped = TH.unsafeTExpCoerce . lift
#endif