packages feed

hslua-aeson-2.0.0: src/HsLua/Aeson.hs

{-|
Module      :  HsLua.Aeson
Copyright   :  © 2017–2021 Albert Krewinkel
License     :  MIT
Maintainer  :  Albert Krewinkel <tarleb@zeitkraut.de>

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 a special value (stored in the registry field
  @HSLUA_AESON_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.

- JSON numbers are converted to Lua numbers (usually doubles), which can cause
  a loss of precision.
-}
module HsLua.Aeson
  ( peekValue
  , pushValue
  , peekVector
  , pushVector
  , pushNull
  , peekScientific
  , pushScientific
  , peekKeyMap
  , pushKeyMap
  ) where

import Control.Monad ((<$!>), when)
import Data.Aeson.Key (Key, toText, fromText)
import Data.Aeson.KeyMap
import Data.Scientific (Scientific, toRealFloat, fromFloatDigits)
import Data.String (IsString (fromString))
import Data.Vector (Vector)
import HsLua.Core as Lua
import HsLua.Marshalling as Lua

import qualified Data.Aeson as Aeson
import qualified Data.Aeson.KeyMap as KeyMap
import qualified Data.Vector as Vector
import qualified HsLua.Core.Unsafe as Unsafe

-- Scientific
pushScientific :: Pusher e Scientific
pushScientific = pushRealFloat @Double . toRealFloat

peekScientific :: Peeker e Scientific
peekScientific idx = fromFloatDigits <$!> peekRealFloat @Double idx

-- | Hslua StackValue instance for the Aeson Value data type.
pushValue :: LuaError e => Pusher e Aeson.Value
pushValue = \case
  Aeson.Object o -> pushKeyMap pushValue o
  Aeson.Number n -> checkstack 1 >>= \case
    True -> pushScientific n
    False -> failLua "stack overflow"
  Aeson.String s -> checkstack 1 >>= \case
    True -> pushText s
    False -> failLua "stack overflow"
  Aeson.Array a  -> pushVector pushValue a
  Aeson.Bool b   -> checkstack 1 >>= \case
    True -> pushBool b
    False -> failLua "stack overflow"
  Aeson.Null     -> pushNull

peekValue :: LuaError e => Peeker e Aeson.Value
peekValue idx = liftLua (ltype idx) >>= \case
  TypeBoolean -> Aeson.Bool  <$!> peekBool idx
  TypeNumber -> Aeson.Number <$!> peekScientific idx
  TypeString -> Aeson.String <$!> peekText idx
  TypeTable -> liftLua (checkstack 1) >>= \case
    False -> failPeek "stack overflow"
    True -> do
      isInt <- liftLua $ rawgeti idx 0 *> isinteger top <* pop 1
      if isInt
        then Aeson.Array <$!> peekVector peekValue idx
        else do
          rawlen' <- liftLua $ rawlen idx
          if rawlen' > 0
            then Aeson.Array <$!> peekVector peekValue idx
            else do
              isNull' <- liftLua $ isNull idx
              if isNull'
                then return Aeson.Null
                else Aeson.Object <$!> peekKeyMap peekValue idx
  TypeNil -> return Aeson.Null
  luaType -> fail ("Unexpected type: " ++ show luaType)

-- | Registry key containing the representation for JSON null values.
nullRegistryField :: Name
nullRegistryField = "HSLUA_AESON_NULL"

-- | Push the value which represents JSON null values to the stack (a specific
-- empty table by default). Internally, this uses the contents of the
-- @HSLUA_AESON_NULL@ registry field; modifying this field is possible, but it
-- must always be non-nil.
pushNull :: LuaError e => LuaE e ()
pushNull = checkstack 3 >>= \case
  False -> failLua "stack overflow while pushing null"
  True -> do
    pushName nullRegistryField
    rawget registryindex
    uninitialized <- isnil top
    when uninitialized $ do
      pop 1 -- remove nil
      newtable
      pushvalue top
      setfield registryindex nullRegistryField

-- | Check if the value under the given index represents a @null@ value.
isNull :: LuaError e => StackIndex -> LuaE e Bool
isNull idx = do
  idx' <- absindex idx
  pushNull
  rawequal idx' top <* pop 1

-- | Push a vector onto the stack.
pushVector :: LuaError e
           => Pusher e a
           -> Pusher e (Vector a)
pushVector pushItem !v = do
  checkstack 3 >>= \case
    False -> failLua "stack overflow"
    True -> do
      pushList pushItem $ Vector.toList v
      pushIntegral (Vector.length v)
      rawseti (nth 2) 0

-- | Try reading the value under the given index as a vector.
peekVector :: LuaError e
           => Peeker e a
           -> Peeker e (Vector a)
peekVector peekItem = fmap (retrieving "list") .
  typeChecked "table" istable $ \idx -> do
  let elementsAt [] = return []
      elementsAt (i : is) = do
        liftLua (checkstack 2) >>= \case
          False -> failPeek "Lua stack overflow"
          True  -> do
            x  <- retrieving ("index " <> showInt i) $ do
              liftLua (rawgeti idx i)
              peekItem top `lastly` pop 1
            xs <- elementsAt is
            return (x:xs)
      showInt (Lua.Integer x) = fromString $ show x
  listLength <- liftLua (rawlen idx)
  list <- elementsAt [1..fromIntegral listLength]
  return $! Vector.fromList list

-- | Pushes a 'KeyMap' onto the stack.
pushKeyMap :: LuaError e
           => Pusher e a
           -> Pusher e (KeyMap a)
pushKeyMap pushVal x =
  checkstack 3 >>= \case
    True -> pushKeyValuePairs pushKey pushVal $ KeyMap.toList x
    False -> failLua "stack overflow"

-- | Retrieves a 'KeyMap' from a Lua table.
peekKeyMap :: Peeker e a
           -> Peeker e (KeyMap a)
peekKeyMap peekVal =
  typeChecked "table" istable $ \idx -> cleanup $ do
  liftLua (checkstack 1) >>= \case
    False -> failPeek "Lua stack overflow"
    True -> do
      idx' <- liftLua $ absindex idx
      let remainingPairs = nextPair peekVal idx' >>= \case
            Nothing -> return []
            Just a  -> (a:) <$!> remainingPairs
      liftLua pushnil
      KeyMap.fromList <$!> remainingPairs

-- | Pushes a JSON key to the stack.
pushKey :: Pusher e Key
pushKey = pushText . toText

-- | Retrieves a JSON key from the stack.
peekKey :: Peeker e Key
peekKey = fmap fromText . peekText

-- | 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.
--
-- The key must be either nil or must exist in the table, or this
-- function will crash with an unrecoverable error.
nextPair :: Peeker e b -> Peeker e (Maybe (Key, b))
nextPair peekVal idx = retrieving "key-value pair" $ do
  liftLua (checkstack 1) >>= \case
    False -> failPeek "Lua stack overflow"
    True -> do
      hasNext <- liftLua $ Unsafe.next idx
      if not hasNext
        then return Nothing
        else do
        key   <- retrieving "key"   $! peekKey (nth 2)
        value <- retrieving "value" $! peekVal (nth 1)
        return (Just (key, value))
          `lastly` pop 1  -- remove value, leave the key