{-# LANGUAGE OverloadedStrings #-} {-| Module : Foreign.Lua.Peek Copyright : © 2020 Albert Krewinkel License : MIT Maintainer : Albert Krewinkel Stability : beta Portability : Portable Functions which unmarshal and retrieve Haskell values from Lua's stack. -} module Foreign.Lua.Peek ( Peeker , PeekError (..) , errorMsg , force , formatPeekError , pushMsg , toPeeker -- * Primitives , peekBool , peekIntegral , peekRealFloat -- * Strings , peekByteString , peekLazyByteString , peekString , peekText , peekStringy -- * Collections , peekKeyValuePairs , peekList , peekMap , peekSet -- * Combinators , 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 -- | List of errors which occurred while retrieving a value from -- the stack. newtype PeekError = PeekError { fromPeekError :: NonEmpty Text } deriving (Eq, Show) formatPeekError :: PeekError -> String formatPeekError (PeekError msgs) = T.unpack $ T.intercalate "\n\t" (NonEmpty.toList msgs) -- | Function to retrieve a value from Lua's stack. type Peeker a = StackIndex -> Lua (Either PeekError a) -- | Create a peek error from an error message. errorMsg :: Text -> PeekError errorMsg = PeekError . pure -- | Add a message to the peek traceback stack. pushMsg :: Text -> PeekError -> PeekError pushMsg msg (PeekError lst) = PeekError $ msg <| lst -- | Add context information to the peek traceback stack. retrieving :: Text -> Either PeekError a -> Either PeekError a retrieving msg = first $ pushMsg ("retrieving " <> msg) -- | Force creation of a result, throwing an exception if that's -- not possible. force :: Either PeekError a -> Lua a force = either (throwMessage . formatPeekError) return -- | Convert an old peek funtion to a 'Peeker'. toPeeker :: (StackIndex -> Lua a) -> Peeker a toPeeker op idx = (Right <$> op idx) <|> return (Left $ errorMsg "retrieving failed") -- | Use @test@ to check whether the value at stack index @n@ has -- the correct type and use @peekfn@ to convert it to a Haskell -- value if possible. A successfully received value is wrapped -- using the 'Right' constructor, while a type mismatch results -- in @Left PeekError@ with the given error message. typeChecked :: Text -- ^ expected type -> (StackIndex -> Lua Bool) -- ^ pre-condition checker -> Peeker a -> Peeker a typeChecked expectedType test peekfn idx = do v <- test idx if v then peekfn idx else Left <$> mismatchError expectedType idx -- | Report the expected and actual type of the value under the given index if -- conversion failed. 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 -- | Return a Result error containing a message about the assertion failure. 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 <> ")" -- | Retrieves a 'Bool' as a Lua boolean. peekBool :: Peeker Bool peekBool = fmap Right . toboolean -- -- Strings -- -- | Like @'tostring', but ensures that the value at the given index is -- not silently converted to a string, as would happen with numbers. toByteString :: StackIndex -> Lua (Maybe ByteString) toByteString idx = do -- copy value, as tostring converts numbers to strings *in-place*. pushvalue idx tostring stackTop <* pop 1 -- | Retrieves a 'ByteString' as a raw string. peekByteString :: Peeker ByteString peekByteString = reportValueOnFailure "string" toByteString -- | Retrieves a lazy 'BL.ByteString' as a raw string. peekLazyByteString :: Peeker BL.ByteString peekLazyByteString = fmap (second BL.fromStrict) . peekByteString -- | Retrieves a 'String' from an UTF-8 encoded Lua string. peekString :: Peeker String peekString = peekStringy -- | Retrieves a String-like value from an UTF-8 encoded Lua string. -- -- This should not be used to peek 'ByteString' values or other values -- for which construction via 'fromString' can result in loss of -- information. peekStringy :: IsString a => Peeker a peekStringy = fmap (second $ fromString . Utf8.toString) . peekByteString -- | Retrieves a 'T.Text' value as an UTF-8 encoded string. peekText :: Peeker T.Text peekText = fmap (second Utf8.toText) . peekByteString -- -- Numbers -- -- | Retrieves an 'Integral' value from the Lua stack. 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 -- | Retrieve a 'RealFloat' (e.g., 'Float' or 'Double') from the stack. 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 -- | Reads a numerically indexed table @t@ into a list, where the 'length' of -- the list is equal to @#t@. The operation will fail if a numerical field @n@ -- with @1 ≤ n < #t@ is missing. 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] -- | Retrieves a key-value Lua table as 'Map'. peekMap :: Ord a => Peeker a -> Peeker b -> Peeker (Map a b) peekMap keyPeeker valuePeeker = fmap (retrieving "Map" . second Map.fromList) . peekKeyValuePairs keyPeeker valuePeeker -- | Read a table into a list of pairs. 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 -- | Get the next key-value pair from a table. Assumes the last -- key to be on the top of the stack and the table at the given -- index @idx@. The next key, if it exists, is left at the top of -- the stack. 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 -- remove value, leave the key return $ curry Just <$> key <*> value -- | Retrieves a 'Set' from an idiomatic Lua representation. A -- set in Lua is idiomatically represented as a table with the -- elements as keys. Elements with falsy values are omitted. peekSet :: Ord a => Peeker a -> Peeker (Set a) peekSet elementPeeker = fmap (retrieving "Set" . second (Set.fromList . map fst . filter snd)) . peekKeyValuePairs elementPeeker peekBool -- -- Combinators -- -- | Makes a result optional. Returns 'Nothing' if the Lua value -- is @nil@; otherwise applies the peeker and returns its result. optional :: Peeker a -- ^ peeker -> Peeker (Maybe a) optional peeker idx = do noValue <- Lua.isnoneornil idx if noValue then return $ Right Nothing else fmap Just <$> peeker idx