packages feed

bolty-0.1.0.0: src/Database/Bolty/Value/Type.hs

-- | Internal module. Not part of the public API.
--
-- Bolt value types: the Haskell representation of Neo4j values returned by queries.
module Database.Bolty.Value.Type
  ( Bolt(..)
    -- * Bolt value extractors
  , asNull, asBool, asInt, asFloat, asBytes, asText
  , asList, asDict, asNode, asRelationship, asPath
    -- * Graph types
  , Node(..)
  , Relationship(..)
  , UnboundRelationship(..)
  , Path(..)
    -- * Temporal types
  , Date(..)
  , Time(..)
  , LocalTime(..)
  , DateTime(..)
  , DateTimeZoneId(..)
  , LocalDateTime(..)
  , Duration(..)
    -- * Spatial types
  , Point2D(..)
  , Point3D(..)
  ) where

import           Data.Int              (Int64)
import           Data.Kind             (Type)
import           GHC.Generics          (Generic)
import qualified Data.ByteString       as BS
import qualified Data.HashMap.Lazy     as H
import qualified Data.Text             as T
import qualified Data.Vector           as V

import           Data.PackStream.Ps
import           Data.PackStream.Result  (Result(..))
import           Data.PackStream.Integer (PSInteger)
import           Database.Bolty.Value.Helpers (sigNode, sigRel, sigURel, sigPath
                                              , sigDate, sigTime, sigLocalTime, sigDateTime
                                              , sigDateTimeZoneId, sigLocalDateTime, sigDuration
                                              , sigPoint2D, sigPoint3D)
import           Numeric                (showHex)


-- | A typed Neo4j value. Every field in a query result record is a 'Bolt'.
type Bolt :: Type
data Bolt
  = BoltNull
  | BoltBoolean !Bool
  | BoltInteger !PSInteger
  | BoltFloat !Double
  | BoltBytes !BS.ByteString
  | BoltString !T.Text
  | BoltList !(V.Vector Bolt)
  | BoltDictionary !(H.HashMap T.Text Bolt)
  | BoltNode Node
  | BoltRelationship Relationship
  | BoltUnboundRelationship UnboundRelationship
  | BoltPath Path
  | BoltDate Date
  | BoltTime Time
  | BoltLocalTime LocalTime
  | BoltDateTime DateTime
  | BoltDateTimeZoneId DateTimeZoneId
  | BoltLocalDateTime LocalDateTime
  | BoltDuration Duration
  | BoltPoint2D Point2D
  | BoltPoint3D Point3D
  deriving stock (Show, Eq, Generic)

instance PackStream Bolt where
  toPs BoltNull                    = PsNull
  toPs (BoltBoolean b)             = PsBoolean b
  toPs (BoltInteger n)             = PsInteger n
  toPs (BoltFloat d)               = PsFloat d
  toPs (BoltBytes b)               = PsBytes b
  toPs (BoltString t)              = PsString t
  toPs (BoltList v)                = PsList (V.map toPs v)
  toPs (BoltDictionary m)          = PsDictionary (H.map toPs m)
  toPs (BoltNode n)                = toPs n
  toPs (BoltRelationship r)        = toPs r
  toPs (BoltUnboundRelationship r) = toPs r
  toPs (BoltPath p)                = toPs p
  toPs (BoltDate d)                = toPs d
  toPs (BoltTime t)                = toPs t
  toPs (BoltLocalTime t)           = toPs t
  toPs (BoltDateTime dt)           = toPs dt
  toPs (BoltDateTimeZoneId dt)     = toPs dt
  toPs (BoltLocalDateTime dt)      = toPs dt
  toPs (BoltDuration d)            = toPs d
  toPs (BoltPoint2D p)             = toPs p
  toPs (BoltPoint3D p)             = toPs p

  fromPs PsNull         = Success BoltNull
  fromPs (PsBoolean b)  = Success (BoltBoolean b)
  fromPs (PsInteger n)  = Success (BoltInteger n)
  fromPs (PsFloat d)    = Success (BoltFloat d)
  fromPs (PsBytes b)    = Success (BoltBytes b)
  fromPs (PsString t)   = Success (BoltString t)
  fromPs (PsList v)     = BoltList <$> traverse fromPs v
  fromPs (PsDictionary m) = BoltDictionary <$> traverse fromPs m
  fromPs ps@(PsStructure t _) = case t of
    _ | t == sigNode    -> BoltNode <$> fromPs ps
      | t == sigRel     -> BoltRelationship <$> fromPs ps
      | t == sigURel    -> BoltUnboundRelationship <$> fromPs ps
      | t == sigPath    -> BoltPath <$> fromPs ps
      | t == sigDate    -> BoltDate <$> fromPs ps
      | t == sigTime    -> BoltTime <$> fromPs ps
      | t == sigLocalTime    -> BoltLocalTime <$> fromPs ps
      | t == sigDateTime     -> BoltDateTime <$> fromPs ps
      | t == sigDateTimeZoneId -> BoltDateTimeZoneId <$> fromPs ps
      | t == sigLocalDateTime  -> BoltLocalDateTime <$> fromPs ps
      | t == sigDuration -> BoltDuration <$> fromPs ps
      | t == sigPoint2D  -> BoltPoint2D <$> fromPs ps
      | t == sigPoint3D  -> BoltPoint3D <$> fromPs ps
      | otherwise -> Error $ "unknown structure tag: 0x" <> T.pack (showHex t "")


-- | A graph node.
type Node :: Type
data Node = Node
  { id         :: !Int64
  -- ^ Legacy numeric ID (use 'element_id' for BOLT 5+).
  , labels     :: !(V.Vector T.Text)
  -- ^ Node labels.
  , properties :: !(H.HashMap T.Text Ps)
  -- ^ Node properties.
  , element_id :: !T.Text
  -- ^ Element ID string (empty on BOLT 4.x).
  }
  deriving stock (Show, Eq, Generic)
instance PackStream Node where
  toPs Node{id, labels, properties, element_id} =
    PsStructure sigNode $ V.fromList [toPs id, toPs labels, toPs properties, toPs element_id]
  -- BOLT 5+: 4 fields (id, labels, properties, element_id)
  fromPs (PsStructure t fs) | t == sigNode, V.length fs == 4 =
    Node <$> fromPs (fs V.! 0) <*> fromPs (fs V.! 1) <*> fromPs (fs V.! 2) <*> fromPs (fs V.! 3)
  -- BOLT 4.x: 3 fields (id, labels, properties) — no element_id
  fromPs (PsStructure t fs) | t == sigNode, V.length fs == 3 =
    Node <$> fromPs (fs V.! 0) <*> fromPs (fs V.! 1) <*> fromPs (fs V.! 2) <*> pure ""
  fromPs other = typeMismatch "Node" other

-- | A graph relationship (edge).
type Relationship :: Type
data Relationship = Relationship
  { id                     :: !Int64
  -- ^ Legacy numeric ID.
  , startNodeId            :: !Int64
  -- ^ Legacy numeric ID of the start node.
  , endNodeId              :: !Int64
  -- ^ Legacy numeric ID of the end node.
  , type_                  :: !T.Text
  -- ^ Relationship type name.
  , properties             :: !(H.HashMap T.Text Ps)
  -- ^ Relationship properties.
  , element_id             :: !T.Text
  -- ^ Element ID string (empty on BOLT 4.x).
  , start_node_element_id  :: !T.Text
  -- ^ Start node element ID (empty on BOLT 4.x).
  , end_node_element_id    :: !T.Text
  -- ^ End node element ID (empty on BOLT 4.x).
  }
  deriving stock (Show, Eq, Generic)
instance PackStream Relationship where
  toPs Relationship{id, startNodeId, endNodeId, type_, properties, element_id, start_node_element_id, end_node_element_id} =
    PsStructure sigRel $ V.fromList
      [toPs id, toPs startNodeId, toPs endNodeId, toPs type_, toPs properties
      , toPs element_id, toPs start_node_element_id, toPs end_node_element_id]
  -- BOLT 5+: 8 fields (id, startNodeId, endNodeId, type_, properties, element_id, start_node_element_id, end_node_element_id)
  fromPs (PsStructure t fs) | t == sigRel, V.length fs == 8 =
    Relationship <$> fromPs (fs V.! 0) <*> fromPs (fs V.! 1) <*> fromPs (fs V.! 2)
                 <*> fromPs (fs V.! 3) <*> fromPs (fs V.! 4) <*> fromPs (fs V.! 5)
                 <*> fromPs (fs V.! 6) <*> fromPs (fs V.! 7)
  -- BOLT 4.x: 5 fields (id, startNodeId, endNodeId, type_, properties) — no element_id fields
  fromPs (PsStructure t fs) | t == sigRel, V.length fs == 5 =
    Relationship <$> fromPs (fs V.! 0) <*> fromPs (fs V.! 1) <*> fromPs (fs V.! 2)
                 <*> fromPs (fs V.! 3) <*> fromPs (fs V.! 4) <*> pure "" <*> pure "" <*> pure ""
  fromPs other = typeMismatch "Relationship" other


-- | A relationship without start\/end node information (used in paths).
type UnboundRelationship :: Type
data UnboundRelationship = UnboundRelationship
  { id         :: !Int64
  , type_      :: !T.Text
  -- ^ Relationship type name.
  , properties :: !(H.HashMap T.Text Ps)
  , element_id :: !T.Text
  -- ^ Element ID string (empty on BOLT 4.x).
  }
  deriving stock (Show, Eq, Generic)
instance PackStream UnboundRelationship where
  toPs UnboundRelationship{id, type_, properties, element_id} =
    PsStructure sigURel $ V.fromList [toPs id, toPs type_, toPs properties, toPs element_id]
  -- BOLT 5+: 4 fields (id, type_, properties, element_id)
  fromPs (PsStructure t fs) | t == sigURel, V.length fs == 4 =
    UnboundRelationship <$> fromPs (fs V.! 0) <*> fromPs (fs V.! 1) <*> fromPs (fs V.! 2) <*> fromPs (fs V.! 3)
  -- BOLT 4.x: 3 fields (id, type_, properties) — no element_id
  fromPs (PsStructure t fs) | t == sigURel, V.length fs == 3 =
    UnboundRelationship <$> fromPs (fs V.! 0) <*> fromPs (fs V.! 1) <*> fromPs (fs V.! 2) <*> pure ""
  fromPs other = typeMismatch "UnboundRelationship" other


-- | A graph path: an alternating sequence of nodes and relationships.
type Path :: Type
data Path = Path
  { nodes   :: !(V.Vector Node)
  -- ^ Unique nodes in the path.
  , rels    :: !(V.Vector UnboundRelationship)
  -- ^ Unique relationships in the path.
  , indices :: !(V.Vector Int64)
  -- ^ Indices into 'nodes' and 'rels' describing the traversal order.
  }
  deriving stock (Show, Eq, Generic)
instance PackStream Path where
  toPs Path{nodes, rels, indices} =
    PsStructure sigPath $ V.fromList [toPs nodes, toPs rels, toPs indices]
  fromPs (PsStructure t fs) | t == sigPath, V.length fs == 3 =
    Path <$> fromPs (fs V.! 0) <*> fromPs (fs V.! 1) <*> fromPs (fs V.! 2)
  fromPs other = typeMismatch "Path" other


-- | A date (days since Unix epoch).
type Date :: Type
data Date = Date
  { days :: !Int64
  }
  deriving stock (Show, Eq, Generic)
instance PackStream Date where
  toPs Date{days} =
    PsStructure sigDate $ V.singleton (toPs days)
  fromPs (PsStructure t fs) | t == sigDate, V.length fs == 1 =
    Date <$> fromPs (fs V.! 0)
  fromPs other = typeMismatch "Date" other


-- | A time with timezone offset.
type Time :: Type
data Time = Time
  { nanoseconds       :: !Int64
  -- ^ Nanoseconds since midnight.
  , tz_offset_seconds :: !Int64
  -- ^ Timezone offset in seconds from UTC.
  }
  deriving stock (Show, Eq, Generic)
instance PackStream Time where
  toPs Time{nanoseconds, tz_offset_seconds} =
    PsStructure sigTime $ V.fromList [toPs nanoseconds, toPs tz_offset_seconds]
  fromPs (PsStructure t fs) | t == sigTime, V.length fs == 2 =
    Time <$> fromPs (fs V.! 0) <*> fromPs (fs V.! 1)
  fromPs other = typeMismatch "Time" other


-- | A local time (no timezone).
type LocalTime :: Type
data LocalTime = LocalTime
  { nanoseconds :: !Int64
  -- ^ Nanoseconds since midnight.
  }
  deriving stock (Show, Eq, Generic)
instance PackStream LocalTime where
  toPs LocalTime{nanoseconds} =
    PsStructure sigLocalTime $ V.singleton (toPs nanoseconds)
  fromPs (PsStructure t fs) | t == sigLocalTime, V.length fs == 1 =
    LocalTime <$> fromPs (fs V.! 0)
  fromPs other = typeMismatch "LocalTime" other


-- | A date-time with timezone offset.
type DateTime :: Type
data DateTime = DateTime
  { seconds           :: !Int64
  -- ^ Seconds since Unix epoch.
  , nanoseconds       :: !Int64
  -- ^ Nanosecond adjustment.
  , tz_offset_seconds :: !Int64
  -- ^ Timezone offset in seconds from UTC.
  }
  deriving stock (Show, Eq, Generic)
instance PackStream DateTime where
  toPs DateTime{seconds, nanoseconds, tz_offset_seconds} =
    PsStructure sigDateTime $ V.fromList [toPs seconds, toPs nanoseconds, toPs tz_offset_seconds]
  fromPs (PsStructure t fs) | t == sigDateTime, V.length fs == 3 =
    DateTime <$> fromPs (fs V.! 0) <*> fromPs (fs V.! 1) <*> fromPs (fs V.! 2)
  fromPs other = typeMismatch "DateTime" other


-- | A date-time with named timezone (e.g. @\"Europe\/Paris\"@).
type DateTimeZoneId :: Type
data DateTimeZoneId = DateTimeZoneId
  { seconds     :: !Int64
  -- ^ Seconds since Unix epoch.
  , nanoseconds :: !Int64
  -- ^ Nanosecond adjustment.
  , tz_id       :: !T.Text
  -- ^ IANA timezone identifier.
  }
  deriving stock (Show, Eq, Generic)
instance PackStream DateTimeZoneId where
  toPs DateTimeZoneId{seconds, nanoseconds, tz_id} =
    PsStructure sigDateTimeZoneId $ V.fromList [toPs seconds, toPs nanoseconds, toPs tz_id]
  fromPs (PsStructure t fs) | t == sigDateTimeZoneId, V.length fs == 3 =
    DateTimeZoneId <$> fromPs (fs V.! 0) <*> fromPs (fs V.! 1) <*> fromPs (fs V.! 2)
  fromPs other = typeMismatch "DateTimeZoneId" other


-- | A local date-time (no timezone).
type LocalDateTime :: Type
data LocalDateTime = LocalDateTime
  { seconds     :: !Int64
  -- ^ Seconds since Unix epoch.
  , nanoseconds :: !Int64
  -- ^ Nanosecond adjustment.
  }
  deriving stock (Show, Eq, Generic)
instance PackStream LocalDateTime where
  toPs LocalDateTime{seconds, nanoseconds} =
    PsStructure sigLocalDateTime $ V.fromList [toPs seconds, toPs nanoseconds]
  fromPs (PsStructure t fs) | t == sigLocalDateTime, V.length fs == 2 =
    LocalDateTime <$> fromPs (fs V.! 0) <*> fromPs (fs V.! 1)
  fromPs other = typeMismatch "LocalDateTime" other


-- | A temporal duration (months, days, seconds, nanoseconds).
type Duration :: Type
data Duration = Duration
  { months      :: !Int64
  , days        :: !Int64
  , seconds     :: !Int64
  , nanoseconds :: !Int64
  }
  deriving stock (Show, Eq, Generic)
instance PackStream Duration where
  toPs Duration{months, days, seconds, nanoseconds} =
    PsStructure sigDuration $ V.fromList [toPs months, toPs days, toPs seconds, toPs nanoseconds]
  fromPs (PsStructure t fs) | t == sigDuration, V.length fs == 4 =
    Duration <$> fromPs (fs V.! 0) <*> fromPs (fs V.! 1) <*> fromPs (fs V.! 2) <*> fromPs (fs V.! 3)
  fromPs other = typeMismatch "Duration" other


-- | A 2D spatial point.
type Point2D :: Type
data Point2D = Point2D
  { srid :: !Int64
  -- ^ Spatial Reference Identifier (e.g. 4326 for WGS-84).
  , x    :: !Double
  , y    :: !Double
  }
  deriving stock (Show, Eq, Generic)
instance PackStream Point2D where
  toPs Point2D{srid, x, y} =
    PsStructure sigPoint2D $ V.fromList [toPs srid, toPs x, toPs y]
  fromPs (PsStructure t fs) | t == sigPoint2D, V.length fs == 3 =
    Point2D <$> fromPs (fs V.! 0) <*> fromPs (fs V.! 1) <*> fromPs (fs V.! 2)
  fromPs other = typeMismatch "Point2D" other


-- | A 3D spatial point.
type Point3D :: Type
data Point3D = Point3D
  { srid :: !Int64
  -- ^ Spatial Reference Identifier (e.g. 4979 for WGS-84-3D).
  , x    :: !Double
  , y    :: !Double
  , z    :: !Double
  }
  deriving stock (Show, Eq, Generic)
instance PackStream Point3D where
  toPs Point3D{srid, x, y, z} =
    PsStructure sigPoint3D $ V.fromList [toPs srid, toPs x, toPs y, toPs z]
  fromPs (PsStructure t fs) | t == sigPoint3D, V.length fs == 4 =
    Point3D <$> fromPs (fs V.! 0) <*> fromPs (fs V.! 1) <*> fromPs (fs V.! 2) <*> fromPs (fs V.! 3)
  fromPs other = typeMismatch "Point3D" other


-- = Bolt value extractors

-- | Extract a null value.
asNull :: Bolt -> Maybe ()
asNull BoltNull = Just ()
asNull _        = Nothing

-- | Extract a boolean value.
asBool :: Bolt -> Maybe Bool
asBool (BoltBoolean b) = Just b
asBool _               = Nothing

-- | Extract an integer value.
asInt :: Bolt -> Maybe PSInteger
asInt (BoltInteger i) = Just i
asInt _               = Nothing

-- | Extract a floating-point value.
asFloat :: Bolt -> Maybe Double
asFloat (BoltFloat d) = Just d
asFloat _             = Nothing

-- | Extract a byte string value.
asBytes :: Bolt -> Maybe BS.ByteString
asBytes (BoltBytes b) = Just b
asBytes _             = Nothing

-- | Extract a text value.
asText :: Bolt -> Maybe T.Text
asText (BoltString t) = Just t
asText _              = Nothing

-- | Extract a list value.
asList :: Bolt -> Maybe (V.Vector Bolt)
asList (BoltList v) = Just v
asList _            = Nothing

-- | Extract a dictionary value.
asDict :: Bolt -> Maybe (H.HashMap T.Text Bolt)
asDict (BoltDictionary m) = Just m
asDict _                  = Nothing

-- | Extract a node value.
asNode :: Bolt -> Maybe Node
asNode (BoltNode n) = Just n
asNode _            = Nothing

-- | Extract a relationship value.
asRelationship :: Bolt -> Maybe Relationship
asRelationship (BoltRelationship r) = Just r
asRelationship _                    = Nothing

-- | Extract a path value.
asPath :: Bolt -> Maybe Path
asPath (BoltPath p) = Just p
asPath _            = Nothing