sq-0.0.1: lib/Sq/Output.hs
{-# LANGUAGE StrictData #-}
module Sq.Output
( Output
, ErrOutput (..)
, decode
, runOutput
, output
) where
import Control.Applicative
import Control.Exception.Safe qualified as Ex
import Control.Monad
import Control.Monad.Trans.Resource qualified as R hiding (runResourceT)
import Data.String
import Database.SQLite3 qualified as S
import Sq.Decoders
import Sq.Names
--------------------------------------------------------------------------------
-- | How to decode an output row from a single 'Sq.Statement'.
--
-- * Construct with 'decode', 'IsString'.
--
-- * Nest with 'output'.
--
-- * Compose with 'Monoid', 'Functor', 'Applicative', 'Alternative', 'Monad',
-- 'MonadPlus', 'MonadFail' and 'Ex.MonadThrow' tools.
data Output o
= Output_Pure o
| Output_Fail Ex.SomeException
| Output_Decode BindingName (Decode (Output o))
data ErrOutput
= -- | Error from v'Decode'.
ErrOutput_ColumnValue BindingName ErrDecode
| -- | Missing column name in the raw 'SQL'.
ErrOutput_ColumnMissing BindingName
| -- | Error from 'Ex.MonadThrow'.
ErrOutput_Fail Ex.SomeException
deriving stock (Show)
deriving anyclass (Ex.Exception)
-- | Decode the column with the given 'Name'.
--
-- @
-- 'Sq.readStatement'
-- 'mempty'
-- ('decode' \"foo\" 'decodeDefault')
-- \"SELECT foo FROM t\"
-- :: ('DecodeDefault' x)
-- => 'Sq.Statement' 'Sq.Read' () x
-- @
--
-- Note that by design, this library doesn't support positional 'Output'
-- parameters. You must always pick a 'Name'. In the raw SQL, you can use @AS@
-- to rename your output columns as necessary.
--
-- @
-- 'Sq.readStatement'
-- 'mempty'
-- ('decode' \"abc\" 'decodeDefault')
-- \"SELECT foo AS abc FROM t\"
-- :: ('DecodeDefault' x)
-- => 'Sq.Statement' 'Sq.Read' () x
-- @
--
-- Multiple 'Outputs's can be composed with 'Monoid', 'Functor', 'Applicative',
-- 'Alternative', 'Monad', 'MonadPlus', 'MonadFail' and 'Ex.MonadThrow' tools.
--
-- @
-- 'Sq.readStatement'
-- 'mempty'
-- (do foo <- 'decode' \"foo\" 'decodeDefault'
-- 'when' (foo > 10) do
-- 'fail' \"Oh no!"
-- bar <- 'decode' \"bar\" 'decodeDefault'
-- 'pure' (foo, bar))
-- \"SELECT foo, bar FROM t\"
-- :: ('DecodeDefault' y)
-- => 'Sq.Statement' 'Sq.Read' () ('Int', y)
-- @
--
-- Pro-tip: Consider using the 'IsString' instance for 'Output',
-- where for example @\"foo\"@ means @'decode' \"foo\" 'decodeDefault'@:
--
-- @
-- 'Sq.readStatement'
-- ('liftA2' (,) \"foo\" \"bar\")
-- 'mempty'
-- \"SELECT foo, bar FROM t\"
-- :: ('DecodeDefault' x, 'DecodeDefault' y)
-- => 'Sq.Statement' 'Sq.Read' () (x, y)
-- @
decode :: Name -> Decode o -> Output o
decode n vda = Output_Decode (bindingName n) (Output_Pure <$> vda)
{-# INLINE decode #-}
-- | Add a prefix 'Name' to column names in the given 'Output',
-- separated by @\__@
--
-- This is useful for making reusable 'Output's. For example,
-- consider the following.
--
-- @
-- data Point = Point { x :: 'Int', y :: 'Int' }
--
-- pointOutput :: 'Output' Point
-- pointOutput = Point '<$>' \"x\" '<*>' \"y\"
-- @
--
-- After using 'output':
--
-- @
-- 'Sq.readStatement'
-- 'mempty'
-- ('liftA2' ('output' \"p1\" pointInput)
-- ('output' \"p2\" pointInput))
-- ['Sq.sql'|
-- SELECT ax AS p1\__x, ay AS p1\__y,
-- bx AS p2\__x, by AS p2\__y
-- FROM vectors|]
-- :: 'Sq.Statement' 'Sq.Read' () (Point, Point)
-- @
output :: Name -> Output o -> Output o
output n = \case
Output_Decode bn d ->
Output_Decode (bindingName n <> bn) (output n <$> d)
o -> o
-- | TODO cache names after lookup. Important for Alternative.
runOutput
:: (Monad m)
=> (BindingName -> m (Maybe S.SQLData))
-> Output o
-> m (Either ErrOutput o)
runOutput f = \case
Output_Decode bn (Decode vda) -> do
f bn >>= \case
Just s -> case vda s of
Right d -> runOutput f d
Left e -> pure $ Left $ ErrOutput_ColumnValue bn e
Nothing -> pure $ Left $ ErrOutput_ColumnMissing bn
Output_Pure a -> pure $ Right a
Output_Fail e -> pure $ Left $ ErrOutput_Fail e
instance Functor Output where
fmap = liftA
{-# INLINE fmap #-}
instance Applicative Output where
pure = Output_Pure
{-# INLINE pure #-}
liftA2 = liftM2
{-# INLINE liftA2 #-}
instance Alternative Output where
empty = fail "empty"
{-# INLINE empty #-}
l <|> r = case l of
Output_Decode n vda ->
Output_Decode n (fmap (<|> r) vda)
Output_Pure _ -> l
Output_Fail _ -> r
instance MonadPlus Output where
mzero = fail "mzero"
{-# INLINE mzero #-}
mplus = (<|>)
{-# INLINE mplus #-}
instance Monad Output where
l >>= k = case l of
Output_Decode n vda ->
Output_Decode n (fmap (>>= k) vda)
Output_Pure a -> k a
Output_Fail e -> Output_Fail e
instance Ex.MonadThrow Output where
throwM = Output_Fail . Ex.toException
instance MonadFail Output where
fail = Ex.throwString
instance (Semigroup o) => Semigroup (Output o) where
(<>) = liftA2 (<>)
{-# INLINE (<>) #-}
instance (Monoid o) => Monoid (Output o) where
mempty = pure mempty
{-# INLINE mempty #-}
instance (DecodeDefault i) => IsString (Output i) where
fromString s = decode (fromString s) decodeDefault
{-# INLINE fromString #-}