{-# LANGUAGE OverloadedStrings #-}
module Foreign.Lua.Peek
( Peeker
, PeekError (..)
, errorMsg
, force
, formatPeekError
, pushMsg
, toPeeker
, peekBool
, peekIntegral
, peekRealFloat
, peekByteString
, peekLazyByteString
, peekString
, peekText
, peekStringy
, peekKeyValuePairs
, peekList
, peekMap
, peekSet
, optional
) where
import Control.Applicative ((<|>))
import Data.Bifunctor (first, second)
import Data.ByteString (ByteString)
import Data.List.NonEmpty (NonEmpty (..), (<|))
import Data.Map (Map)
import Data.Maybe (fromMaybe)
import Data.Set (Set)
import Data.String (IsString (fromString))
import Data.Text (Text)
import Foreign.Lua.Core as Lua
import Text.Read (readMaybe)
import qualified Data.ByteString.Lazy as BL
import qualified Data.List.NonEmpty as NonEmpty
import qualified Data.Map as Map
import qualified Data.Set as Set
import qualified Data.Text as T
import qualified Foreign.Lua.Utf8 as Utf8
newtype PeekError = PeekError { fromPeekError :: NonEmpty Text }
deriving (Eq, Show)
formatPeekError :: PeekError -> String
formatPeekError (PeekError msgs) = T.unpack $
T.intercalate "\n\t" (NonEmpty.toList msgs)
type Peeker a = StackIndex -> Lua (Either PeekError a)
errorMsg :: Text -> PeekError
errorMsg = PeekError . pure
pushMsg :: Text -> PeekError -> PeekError
pushMsg msg (PeekError lst) = PeekError $ msg <| lst
retrieving :: Text -> Either PeekError a -> Either PeekError a
retrieving msg = first $ pushMsg ("retrieving " <> msg)
force :: Either PeekError a -> Lua a
force = either (throwMessage . formatPeekError) return
toPeeker :: (StackIndex -> Lua a)
-> Peeker a
toPeeker op idx =
(Right <$> op idx) <|> return (Left $ errorMsg "retrieving failed")
typeChecked :: Text
-> (StackIndex -> Lua Bool)
-> Peeker a
-> Peeker a
typeChecked expectedType test peekfn idx = do
v <- test idx
if v
then peekfn idx
else Left <$> mismatchError expectedType idx
reportValueOnFailure :: Text
-> (StackIndex -> Lua (Maybe a))
-> Peeker a
reportValueOnFailure expected peekMb idx = do
res <- peekMb idx
case res of
Just x -> return $ Right x
Nothing -> Left <$> mismatchError expected idx
mismatchError :: Text -> StackIndex -> Lua PeekError
mismatchError expected idx = do
actualType <- ltype idx >>= typename
actualValue <- Utf8.toText <$> tostring' idx <* pop 1
return . errorMsg $
"expected " <> expected <> ", got '" <>
actualValue <> "' (" <> T.pack actualType <> ")"
peekBool :: Peeker Bool
peekBool = fmap Right . toboolean
toByteString :: StackIndex -> Lua (Maybe ByteString)
toByteString idx = do
pushvalue idx
tostring stackTop <* pop 1
peekByteString :: Peeker ByteString
peekByteString = reportValueOnFailure "string" toByteString
peekLazyByteString :: Peeker BL.ByteString
peekLazyByteString = fmap (second BL.fromStrict) . peekByteString
peekString :: Peeker String
peekString = peekStringy
peekStringy :: IsString a => Peeker a
peekStringy = fmap (second $ fromString . Utf8.toString) . peekByteString
peekText :: Peeker T.Text
peekText = fmap (second Utf8.toText) . peekByteString
peekIntegral :: (Integral a, Read a) => Peeker a
peekIntegral idx =
ltype idx >>= \case
TypeNumber -> second fromIntegral <$>
reportValueOnFailure "Integral" tointeger idx
TypeString -> do
str <- Utf8.toString .
fromMaybe (Prelude.error "programming error in peekIntegral")
<$> tostring idx
let msg = "expected Integral, got '" <> T.pack str <> "' (string)"
return $ maybe (Left $ errorMsg msg) Right $ readMaybe str
_ -> Left <$> mismatchError "Integral" idx
peekRealFloat :: (RealFloat a, Read a) => Peeker a
peekRealFloat idx =
ltype idx >>= \case
TypeString -> do
str <- Utf8.toString .
fromMaybe (Prelude.error "programming error in peekRealFloat")
<$> tostring idx
let msg = "expected RealFloat, got '" <> T.pack str <> "' (string)"
return $ maybe (Left $ errorMsg msg) Right $ readMaybe str
_ -> second realToFrac <$>
reportValueOnFailure "RealFloat" tonumber idx
peekList :: Peeker a -> Peeker [a]
peekList peekElement = typeChecked "table" istable $ \idx -> do
let elementsAt [] = return (Right [])
elementsAt (i : is) = do
eitherX <- rawgeti idx i *> peekElement (nthFromTop 1) <* pop 1
case eitherX of
Right x -> second (x:) <$> elementsAt is
Left err -> return . Left $
pushMsg ("in field " <> T.pack (show i)) err
listLength <- fromIntegral <$> rawlen idx
elementsAt [1..listLength]
peekMap :: Ord a => Peeker a -> Peeker b -> Peeker (Map a b)
peekMap keyPeeker valuePeeker =
fmap (retrieving "Map" . second Map.fromList)
. peekKeyValuePairs keyPeeker valuePeeker
peekKeyValuePairs :: Peeker a -> Peeker b -> Peeker [(a, b)]
peekKeyValuePairs keyPeeker valuePeeker =
typeChecked "table" istable $ \idx -> do
idx' <- absindex idx
let remainingPairs = do
res <- nextPair keyPeeker valuePeeker idx'
case res of
Left err -> return $ Left err
Right Nothing -> return $ Right []
Right (Just a) -> second (a:) <$> remainingPairs
pushnil
remainingPairs
nextPair :: Peeker a -> Peeker b -> Peeker (Maybe (a, b))
nextPair keyPeeker valuePeeker idx = retrieving "key-value pair" <$> do
hasNext <- next idx
if not hasNext
then return $ Right Nothing
else do
key <- retrieving "key" <$> keyPeeker (nthFromTop 2)
value <- retrieving "value" <$> valuePeeker (nthFromTop 1)
pop 1
return $ curry Just <$> key <*> value
peekSet :: Ord a => Peeker a -> Peeker (Set a)
peekSet elementPeeker =
fmap (retrieving "Set" .
second (Set.fromList . map fst . filter snd))
. peekKeyValuePairs elementPeeker peekBool
optional :: Peeker a
-> Peeker (Maybe a)
optional peeker idx = do
noValue <- Lua.isnoneornil idx
if noValue
then return $ Right Nothing
else fmap Just <$> peeker idx