# packstream
Haskell implementation of the [PackStream](https://neo4j.com/docs/bolt/current/packstream/) binary serialization format, used as the wire format by Neo4j's BOLT protocol.
PackStream is similar to [MessagePack](https://msgpack.org/) but adds a `Structure` type for encoding typed graph objects (nodes, relationships, temporal values, spatial points, etc.).
> **Note:** Most users should depend on [bolty](https://github.com/philippedev101/bolty) instead, which provides a full Neo4j driver. This package is useful if you need to work with the PackStream wire format directly.
## Types
PackStream defines 9 value types, represented by the `Ps` algebraic data type:
| PackStream type | Haskell constructor | Description |
|---|---|---|
| Null | `PsNull` | Missing or empty value |
| Boolean | `PsBoolean !Bool` | True or false |
| Integer | `PsInteger !PSInteger` | Signed integer (up to 64-bit) |
| Float | `PsFloat !Double` | 64-bit IEEE 754 float |
| Bytes | `PsBytes !ByteString` | Raw byte array |
| String | `PsString !Text` | UTF-8 text |
| List | `PsList !(Vector Ps)` | Ordered collection |
| Dictionary | `PsDictionary !(HashMap Text Ps)` | Key-value map (text keys) |
| Structure | `PsStructure !Tag !(Vector Ps)` | Tagged composite (tag byte + positional fields) |
Integers use a variable-width encoding: values in `[-16, 127]` are encoded in a single byte (no tag), with `INT_8`, `INT_16`, `INT_32`, and `INT_64` for larger values.
## The `PackStream` type class
Convert between Haskell types and `Ps` values:
```haskell
class PackStream a where
toPs :: a -> Ps -- encode to Ps AST
toBinary :: a -> Put -- encode directly to wire format (optional, defaults to putPs . toPs)
fromPs :: Ps -> Result a -- decode from Ps AST
```
Built-in instances exist for `Bool`, `Int`, `Int64`, `Word8`, `Word16`, `Word32`, `Double`, `Text`, `ByteString`, `Vector`, `HashMap Text`, `Maybe`, tuples (up to 9), and more.
## Quick start
### Encoding and decoding `Ps` values
```haskell
import Data.PackStream (pack, unpack)
import Data.PackStream.Ps (Ps(..))
import qualified Data.HashMap.Lazy as H
-- Encode a dictionary to binary
let ps = PsDictionary $ H.fromList
[ ("name", PsString "Alice")
, ("age", PsInteger 30)
]
let bytes = pack ps -- :: Lazy ByteString
-- Decode binary back to a Ps value
case unpack bytes of
Right val -> print (val :: Ps)
Left err -> putStrLn $ "Decode error: " <> show err
```
### Custom types
```haskell
import Data.PackStream.Ps (PackStream(..), Ps(..), (.:), withDictionary)
import Data.PackStream.Result (Result(..))
import qualified Data.HashMap.Lazy as H
data Person = Person { name :: Text, age :: Int64 }
instance PackStream Person where
toPs Person{name, age} = PsDictionary $ H.fromList
[ ("name", toPs name)
, ("age", toPs age)
]
fromPs = withDictionary "Person" $ \m -> do
name <- m .: "name" -- uses (.:) operator for key lookup + decode
age <- m .: "age"
pure $ Person name age
```
### Structures
Structures are the key differentiator from MessagePack. They carry a tag byte identifying the type and positional fields:
```haskell
import Data.PackStream.Ps (Ps(..), PackStream(..))
import qualified Data.Vector as V
-- A Date structure (tag 0x44) with one field: days since Unix epoch
let date = PsStructure 0x44 (V.singleton (PsInteger 19737))
-- Define a custom structure type
data MyDate = MyDate { days :: Int64 }
instance PackStream MyDate where
toPs (MyDate d) = PsStructure 0x44 (V.singleton (toPs d))
fromPs (PsStructure 0x44 fs) | V.length fs == 1 = MyDate <$> fromPs (fs V.! 0)
fromPs other = typeMismatch "MyDate" other
```
Neo4j uses structures for graph types (Node `0x4E`, Relationship `0x52`, Path `0x50`), temporal types (Date `0x44`, Time `0x54`, DateTime `0x49`, Duration `0x45`), and spatial types (Point2D `0x58`, Point3D `0x59`).
## Module structure
**Public API:**
- `Data.PackStream` — top-level `pack`/`unpack` + re-exports
- `Data.PackStream.Ps` — core `Ps` type, `PackStream` class, operators
- `Data.PackStream.Result` — `Result` type (`Success`/`Error`)
- `Data.PackStream.Tags` — wire format tag constants
- `Data.PackStream.Timestamp` — helpers for epoch-based temporal conversions
**Internal modules** (exposed but not part of the stable API):
- `Data.PackStream.Get` / `Data.PackStream.Get.Internal` — binary decoding primitives
- `Data.PackStream.Put` — binary encoding primitives
- `Data.PackStream.Integer` — variable-width integer encoding
- `Data.PackStream.Generic` — GHC.Generics-based deriving (experimental)
- `Data.PackStream.Assoc` — ordered association lists
- `Compat.Binary` / `Compat.Prelude` — compatibility wrappers
## Supported GHC versions
9.6.7, 9.8.4, 9.10.3, 9.12.3
## License
Apache-2.0