packages feed

bolty-0.1.0.0: src/Database/Bolty/Connection/Version.hs

-- | Internal module. Not part of the public API.
module Database.Bolty.Connection.Version
  ( boltVersionsToSpec
  , Version(..)
  ) where

import           Data.Bits           (shiftL, (.|.))
import           Data.Kind           (Type)
import           Data.List           (foldl', sort)
import           Data.Maybe          (catMaybes)
import           Data.Word           (Word8, Word32)
import qualified Data.ByteString     as BS
import qualified Data.Text           as T
import qualified Validation          as V
import           TextShow            (showt)


-- | A BOLT protocol version (major.minor).
type Version :: Type
data Version = Version
  { minor :: Word8
  , major :: Word8
  }
  deriving stock (Eq)

instance Ord Version where
  compare Version{minor = minorA, major = majorA} Version{minor = minorB, major = majorB} =
    let compare_major = compare majorA majorB
    in if compare_major == EQ then
         compare minorA minorB
       else
         compare_major


validateVersion :: Version -> Maybe T.Text
validateVersion Version{minor, major}
  | major < 4                = Just "Bolt protocol versions below 4.4 are not supported"
  | major == 4 && minor < 4  = Just "Bolt protocol version 4.4 is the minimum supported minor; 4.0-4.3 are not supported"
  | major == 4 && minor > 4  = Just "Bolt protocol version 4 does not have any minor versions higher than 4 (4.4 is the highest minor)"
  | major == 5 && minor > 4  = Just "The driver supports up to Bolt 5.4"
  | major > 5                = Just "The latest supported Bolt major version is 5, please open a github issue for support for later protocols"
  | otherwise                = Nothing

-- | Validate and compact a list of BOLT versions into a 4-element handshake spec.
boltVersionsToSpec :: [Version] -> V.Validation [T.Text] [Word32]
boltVersionsToSpec [] = V.Failure ["Specify at least one protocol version"]
boltVersionsToSpec versions =
  let errors :: [(Version, T.Text)]
      errors = catMaybes $ map (\x -> fmap (\e -> (x, e)) $ validateVersion x) versions
      version_error_to_string :: (Version, T.Text) -> T.Text
      version_error_to_string (Version{minor, major}, err) = showt (BS.singleton major) <> "." <> showt (BS.singleton minor) <> ": " <> err
  in if not (null errors) then
        V.Failure $ map version_error_to_string errors
      else
        let ws = compactVersions versions
        in if length ws > 4 then
             V.Failure ["Could not make a version specification, try specifying less versions"]
           else
             V.Success $ ws <> replicate (4 - length ws) (0 :: Word32)


word8ToWord32 :: Word8 -> Word8 -> Word8 -> Word8 -> Word32
word8ToWord32 a b c d
    = (fromIntegral a `shiftL` 24)
  .|. (fromIntegral b `shiftL` 16)
  .|. (fromIntegral c `shiftL` 8)
  .|. fromIntegral d

compactVersions :: [Version] -> [Word32]
compactVersions versions =
  let (trailing, acc) = foldl' f (Nothing, []) $ reverse $ sort versions
  in acc <> maybe [] (\p -> [versionAndBelowToWord p]) trailing
  where versionAndBelowToWord :: (Version, Word8) -> Word32
        versionAndBelowToWord (Version{minor, major}, versions_below) = word8ToWord32 0 versions_below minor major

        f :: (Maybe (Version, Word8), [Word32]) -> Version -> (Maybe (Version, Word8), [Word32])
        f (previous, acc) current@Version{minor = minorCurrent, major = majorCurrent} =
            let supportsVersionCompression = (majorCurrent == 4 && minorCurrent >= 3) || majorCurrent > 4
            in  if not supportsVersionCompression then
                  case previous of
                    Nothing -> (Nothing, acc <> [versionAndBelowToWord (current, 0)])
                    Just p -> (Nothing, acc <> [versionAndBelowToWord p, versionAndBelowToWord (current, 0)])
                else
                  case previous of
                    Just (p@(Version{minor = minorLast, major = majorLast}), versions_below) ->
                      if majorLast == majorCurrent && minorCurrent == minorLast - versions_below - 1 then
                        (Just (p, versions_below + 1), acc)
                      else
                        (Just (current, 0), acc <> [versionAndBelowToWord (p, versions_below)])
                    Nothing -> (Just (current, 0), acc)