packages feed

bolty-0.1.0.0: src/Database/Bolty/Notification.hs

-- | Server notification types (warnings, deprecations, performance hints).
module Database.Bolty.Notification
  ( Notification(..)
  , Severity(..)
  , Position(..)
  , parseNotifications
  ) where

import           Prelude

import           Data.Int                      (Int64)
import           Data.Kind                     (Type)
import qualified Data.HashMap.Lazy             as H
import qualified Data.Text                     as T
import qualified Data.Vector                   as V

import           Data.PackStream.Ps            (Ps(..))
import           Data.PackStream.Integer       (fromPSInteger)


-- | A notification emitted by the server during query execution.
-- Notifications include warnings about performance (cartesian products,
-- missing indexes), deprecation notices, and hints.
type Notification :: Type
data Notification = Notification
  { nCode        :: !T.Text
  -- ^ e.g. @"Neo.ClientNotification.Statement.CartesianProduct"@
  , nTitle       :: !T.Text
  -- ^ Human-readable title.
  , nDescription :: !T.Text
  -- ^ Detailed description of the issue.
  , nSeverity    :: !Severity
  -- ^ WARNING or INFORMATION.
  , nCategory    :: !T.Text
  -- ^ e.g. @"PERFORMANCE"@, @"DEPRECATION"@, @"HINT"@, @"UNRECOGNIZED"@, @"UNSUPPORTED"@, @"GENERIC"@, @"SECURITY"@, @"TOPOLOGY"@, @"SCHEMA"@.
  , nPosition    :: !(Maybe Position)
  -- ^ Position in the Cypher query, if available.
  }
  deriving stock (Show, Eq)


-- | Severity level of a notification.
type Severity :: Type
data Severity = SevWarning | SevInformation | SevUnknown !T.Text
  deriving stock (Show, Eq)


-- | Position in a Cypher query string.
type Position :: Type
data Position = Position
  { offset :: !Int64
  , line   :: !Int64
  , col    :: !Int64
  }
  deriving stock (Show, Eq)


-- | Parse the raw @notifications@ field from PULL SUCCESS metadata.
-- Returns an empty vector if the field is 'Nothing' or not a list.
parseNotifications :: Maybe Ps -> V.Vector Notification
parseNotifications Nothing = V.empty
parseNotifications (Just (PsList items)) = V.mapMaybe parseOne items
parseNotifications (Just _) = V.empty


-- | Parse a single notification dictionary.
parseOne :: Ps -> Maybe Notification
parseOne (PsDictionary m) = do
  code <- lookupText "code" m
  title <- lookupText "title" m
  desc <- lookupText "description" m
  let severity = parseSeverity $ H.lookup "severity" m
  let category = case lookupText "category" m of
                   Just c  -> c
                   Nothing -> ""
  let position = parsePosition $ H.lookup "position" m
  Just Notification
    { nCode        = code
    , nTitle       = title
    , nDescription = desc
    , nSeverity    = severity
    , nCategory    = category
    , nPosition    = position
    }
parseOne _ = Nothing


lookupText :: T.Text -> H.HashMap T.Text Ps -> Maybe T.Text
lookupText key m = case H.lookup key m of
  Just (PsString t) -> Just t
  _                 -> Nothing


parseSeverity :: Maybe Ps -> Severity
parseSeverity (Just (PsString "WARNING"))     = SevWarning
parseSeverity (Just (PsString "INFORMATION")) = SevInformation
parseSeverity (Just (PsString other))         = SevUnknown other
parseSeverity _                               = SevUnknown ""


parsePosition :: Maybe Ps -> Maybe Position
parsePosition (Just (PsDictionary m)) = do
  off  <- lookupInt64 "offset" m
  ln   <- lookupInt64 "line" m
  c    <- lookupInt64 "column" m
  Just Position{offset = off, line = ln, col = c}
parsePosition _ = Nothing


lookupInt64 :: T.Text -> H.HashMap T.Text Ps -> Maybe Int64
lookupInt64 key m = case H.lookup key m of
  Just (PsInteger n) -> fromPSInteger n
  _                  -> Nothing