{-# LANGUAGE BangPatterns #-} {-# LANGUAGE MagicHash #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NumericUnderscores #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-} module Main (main) where import Data.Bytes (Bytes) import Data.Primitive (ByteArray, MutableByteArray) import Data.Word (Word8) import Data.Int (Int8, Int16, Int64) import Flatbuffers.Parser (Parser,TableParser,UnionParser) import Data.Text (Text) import Control.Monad (when) import Numeric (showHex) import Data.Primitive (SmallArray, PrimArray) import Data.Int (Int32) import Data.Word (Word16) import qualified Flatbuffers.Parser as P import qualified Data.Bytes.Parser.Latin as Latin import qualified Data.Primitive as PM import qualified Data.Bytes as Bytes import qualified Data.Bytes.Parser as Parser -- This example is from flatcc: -- namespace Eclectic; -- enum Fruit : byte { Banana = -1, Orange = 42 } -- table FooBar { -- meal : Fruit = Banana; -- density : long (deprecated); -- say : string; -- height : short; -- } -- file_identifier "NOOB"; -- root_type FooBar; main :: IO () main = do ex001 <- loadAndDecode "examples/001.txt" case P.run tableParserFooBar ex001 of Left err -> fail ("Test 001: " ++ show err) Right x -> do when (x.meal /= 42) (fail "Test 001: Wrong meal") when (x.density /= 0) $ fail $ "Test 001: Wrong density. Got: " ++ show x.density ++ " (0x" ++ showHex x.density "" ++ ")" when (x.height /= (-8000)) (fail "Test 001: Wrong height") when (x.say /= "hello") (fail "Test 001: Wrong say") putStrLn "Test 001 passed" ex002 <- loadAndDecode "examples/002.txt" case P.run footerParser ex002 of Left err -> fail ("Test 002: " ++ show err) Right x -> do when (length (x.schema.fields) /= 3) (fail "Test 002: Wrong number of fields") putStrLn "Test 002 passed" putStrLn "All tests passed" data FooBar = FooBar { meal :: !Int8 , density :: !Int64 , say :: !Text , height :: !Int16 } tableParserFooBar :: TableParser FooBar tableParserFooBar = FooBar <$> P.int8 <*> P.int64 <*> P.string <*> P.int16 loadAndDecode :: String -> IO ByteArray loadAndDecode path = do contents <- Bytes.readFile path case decodeInput contents of Nothing -> fail ("Could not decode " ++ path ++ " as hex input") Just x -> pure x decodeInput :: Bytes -> Maybe ByteArray decodeInput = decodeSpacedHex . Bytes.intercalate (Bytes.singleton 0x20) . fmap (Bytes.takeWhile (/= 0x23)) . Bytes.split 0x0A . Bytes.dropWhileEnd (==0x20) . Bytes.dropWhile (==0x20) -- | Decode a byte sequence that looks like this: -- -- > cd 0a bf ea 09 ... -- -- There must be one or more space between each two-character representation -- of an octet. decodeSpacedHex :: Bytes -> Maybe ByteArray decodeSpacedHex !b = Parser.parseBytesMaybe ( do let len = Bytes.length b dst <- Parser.effect (PM.newByteArray (len + 1)) Parser.effect (PM.setByteArray dst 0 len (0 :: Word8)) Latin.skipChar ' ' parserSpacedHex dst 0 ) b parserSpacedHex :: MutableByteArray s -> Int -> Parser.Parser () s ByteArray parserSpacedHex !dst !ix = do w <- Latin.hexFixedWord8 () Parser.effect (PM.writeByteArray dst ix w) Parser.isEndOfInput >>= \case False -> do Latin.skipChar1 () ' ' Parser.isEndOfInput >>= \case True -> Parser.effect $ do PM.shrinkMutableByteArray dst (ix + 1) PM.unsafeFreezeByteArray dst False -> parserSpacedHex dst (ix + 1) True -> Parser.effect $ do PM.shrinkMutableByteArray dst (ix + 1) PM.unsafeFreezeByteArray dst data Footer = Footer { schema :: !Schema , dictionaries :: !(PrimArray Block) , recordBatches :: !(PrimArray Block) } instance PM.Prim Block where sizeOf# _ = 24# alignment# _ = 8# -- We ignore custom_metadata for the Footer footerParser :: TableParser Footer footerParser = Footer <$ P.word16Eq 4 <*> P.table schemaParser <*> P.structs <*> P.structs schemaParser :: TableParser Schema schemaParser = Schema <$ P.word16Eq 0 <*> P.array fieldParser fieldParser :: TableParser Field fieldParser = Field <$> P.string <* P.ignore <*> P.union typeParser typeParser :: UnionParser Type typeParser = P.constructUnion2 (pure Null) (Int <$> (TableInt <$> P.int32 <*> P.boolean)) data Schema = Schema { fields :: !(SmallArray Field) } data Field = Field { name :: !Text , type_ :: Type } data Type = None | Null | Int TableInt data TableInt = TableInt { bitWidth :: !Int32 , isSigned :: !Bool } data Block = Block { offset :: !Int64 , metaDataLength :: !Int32 -- we pad this when we encode it , bodyLength :: !Int64 }