{-# OPTIONS_GHC -fno-warn-orphans #-} {-# LANGUAGE CPP #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE LambdaCase #-} {-| Module : Scripting.Lua.Aeson Copyright : © 2017 Albert Krewinkel License : MIT Maintainer : Albert Krewinkel Stability : experimental Portability : portable Glue to hslua for aeson values. This provides a @StackValue@ instance for aeson's @Value@ type. The following conventions are used: - @Null@ values are encoded as the special global @_NULL@. Using @Nil@ would cause problems with null-containing arrays. - Objects are converted to tables in a straight-forward way. - Arrays are converted to lua tables. Array-length is included as the value at index 0. This makes it possible to distinguish between empty arrays and empty objects. -} module Scripting.Lua.Aeson ( module Scripting.Lua , newstate ) where #if MIN_VERSION_base(4,8,0) #else import Control.Applicative ((<$>), (<*>), (*>)) #endif import Data.HashMap.Lazy (HashMap) import Data.Hashable (Hashable) import Data.Scientific (Scientific, toRealFloat, fromFloatDigits) import Data.Text (Text) import Data.Text.Encoding (decodeUtf8, encodeUtf8) import Data.Vector (Vector, fromList, toList) import Scripting.Lua (LuaState, StackValue) import qualified Data.Aeson as Aeson import qualified Data.HashMap.Lazy as HashMap import qualified Data.Vector as Vector import qualified Scripting.Lua as Lua instance StackValue Scientific where push lua n = Lua.pushnumber lua (toRealFloat n) peek lua n = fmap fromFloatDigits <$> (Lua.peek lua n :: IO (Maybe Lua.LuaNumber)) valuetype _ = Lua.TNUMBER instance StackValue Text where push lua t = Lua.push lua (encodeUtf8 t) peek lua i = fmap decodeUtf8 <$> Lua.peek lua i valuetype _ = Lua.TSTRING instance (StackValue a) => StackValue (Vector a) where push lua v = pushvector lua v peek lua i = tovector lua i valuetype _ = Lua.TTABLE instance (Eq a, Hashable a, StackValue a, StackValue b) => StackValue (HashMap a b) where push lua h = pushTextHashMap lua h peek lua i = fmap HashMap.fromList <$> getPairs lua i valuetype _ = Lua.TTABLE -- | Hslua StackValue instance for the Aeson Value data type. instance StackValue Aeson.Value where push lua = \case Aeson.Object o -> Lua.push lua o Aeson.Number n -> Lua.push lua n Aeson.String s -> Lua.push lua s Aeson.Array a -> Lua.push lua a Aeson.Bool b -> Lua.push lua b Aeson.Null -> Lua.getglobal lua "_NULL" peek lua i = do ltype <- Lua.ltype lua i case ltype of Lua.TBOOLEAN -> fmap Aeson.Bool <$> Lua.peek lua i Lua.TNUMBER -> fmap Aeson.Number <$> Lua.peek lua i Lua.TSTRING -> fmap Aeson.String <$> Lua.peek lua i Lua.TTABLE -> do Lua.rawgeti lua i 0 len <- Lua.peek lua (-1) Lua.pop lua 1 case (len :: Maybe Int) of Just _ -> fmap Aeson.Array <$> Lua.peek lua i Nothing -> do objlen <- Lua.objlen lua i if objlen > 0 then fmap Aeson.Array <$> Lua.peek lua i else do isNull <- isLuaNull lua i if isNull then return $ Just Aeson.Null else fmap Aeson.Object <$> Lua.peek lua i Lua.TNIL -> return $ Just Aeson.Null _ -> error $ "Unexpected type: " ++ (show ltype) valuetype = \case Aeson.Object _ -> Lua.TTABLE Aeson.Number _ -> Lua.TNUMBER Aeson.String _ -> Lua.TSTRING Aeson.Array _ -> Lua.TTABLE Aeson.Bool _ -> Lua.TBOOLEAN Aeson.Null -> Lua.TTABLE -- | Create a new lua state suitable for use with aeson values. This behaves -- like @newstate@ in hslua, but initializes the @_NULL@ global. That variable -- is used to encode null values. newstate :: IO LuaState newstate = do lua <- Lua.newstate Lua.createtable lua 0 0 Lua.setglobal lua "_NULL" return lua -- | Check if the value under the given index is lua-equal to @_NULL@. isLuaNull :: LuaState -> Int -> IO Bool isLuaNull lua i = do let i' = if i < 0 then i - 1 else i Lua.getglobal lua "_NULL" res <- Lua.equal lua i' (-1) Lua.pop lua 1 return res -- | Push a vector unto the stack. pushvector :: StackValue a => LuaState -> Vector a -> IO () pushvector lua v = do Lua.pushlist lua . toList $ v Lua.push lua (Vector.length v) Lua.rawseti lua (-2) 0 -- | Try reading the value under the given index as a vector. tovector :: StackValue a => LuaState -> Int -> IO (Maybe (Vector a)) tovector = fmap (fmap (fmap fromList)) . Lua.tolist -- | Try reading the value under the given index as a list of key-value pairs. getPairs :: (StackValue a, StackValue b) => LuaState -> Int -> IO (Maybe [(a, b)]) getPairs lua t = do Lua.pushnil lua pairs <- sequence <$> remainingPairs return pairs where t' = if t < 0 then t - 1 else t remainingPairs = do res <- nextPair case res of Nothing -> return [] Just a -> (a:) <$> remainingPairs nextPair = do hasNext <- Lua.next lua t' if hasNext then do val <- Lua.peek lua (-1) key <- Lua.peek lua (-2) Lua.pop lua 1 -- removes the value, keeps the key return $ Just <$> ((,) <$> key <*> val) else do return Nothing -- | Push a hashmap unto the stack. pushTextHashMap :: (StackValue a, StackValue b) => LuaState -> HashMap a b -> IO () pushTextHashMap lua hm = do let xs = HashMap.toList hm Lua.createtable lua (length xs + 1) 0 let addValue (k, v) = Lua.push lua k *> Lua.push lua v *> Lua.rawset lua (-3) mapM_ addValue xs