packages feed

postgresql-types-0.1: src/library/PostgresqlTypes/Macaddr8.hs

module PostgresqlTypes.Macaddr8
  ( Macaddr8 (..),

    -- * Accessors
    toByte1,
    toByte2,
    toByte3,
    toByte4,
    toByte5,
    toByte6,
    toByte7,
    toByte8,

    -- * Constructors
    fromBytes,
  )
where

import qualified Data.Attoparsec.Text as Attoparsec
import PostgresqlTypes.Algebra
import PostgresqlTypes.Prelude
import PostgresqlTypes.Via
import qualified PtrPeeker
import qualified PtrPoker.Write as Write
import qualified TextBuilder

-- | PostgreSQL @macaddr8@ type. 8-byte MAC (Media Access Control) address in EUI-64 format.
--
-- The format is eight groups of two hexadecimal digits, separated by colons.
-- Example: @01:23:45:67:89:ab:cd:ef@
--
-- [PostgreSQL docs](https://www.postgresql.org/docs/18/datatype-net-types.html#DATATYPE-MACADDR8).
data Macaddr8
  = Macaddr8
      -- | First byte
      Word8
      -- | Second byte
      Word8
      -- | Third byte
      Word8
      -- | Fourth byte
      Word8
      -- | Fifth byte
      Word8
      -- | Sixth byte
      Word8
      -- | Seventh byte
      Word8
      -- | Eighth byte
      Word8
  deriving stock (Eq, Ord)
  deriving (Show, Read, IsString) via (ViaIsScalar Macaddr8)

instance Arbitrary Macaddr8 where
  arbitrary = do
    bytes <- replicateM 8 arbitrary
    -- Ensure not all bytes are zero as that might be invalid
    if all (== 0) bytes
      then pure (Macaddr8 0 0 0 0 0 0 0 1) -- Use a valid non-zero MAC
      else case bytes of
        [a, b, c, d, e, f, g, h] -> pure (Macaddr8 a b c d e f g h)
        _ -> error "impossible case"
  shrink (Macaddr8 a b c d e f g h) =
    [ Macaddr8 a' b' c' d' e' f' g' h'
    | (a', b', c', d', e', f', g', h') <- shrink (a, b, c, d, e, f, g, h),
      not (a' == 0 && b' == 0 && c' == 0 && d' == 0 && e' == 0 && f' == 0 && g' == 0 && h' == 0)
    ]

instance Hashable Macaddr8 where
  hashWithSalt salt (Macaddr8 a b c d e f g h) =
    salt
      `hashWithSalt` a
      `hashWithSalt` b
      `hashWithSalt` c
      `hashWithSalt` d
      `hashWithSalt` e
      `hashWithSalt` f
      `hashWithSalt` g
      `hashWithSalt` h

instance IsScalar Macaddr8 where
  schemaName = Tagged Nothing
  typeName = Tagged "macaddr8"
  baseOid = Tagged (Just 774)
  arrayOid = Tagged (Just 775)
  typeParams = Tagged []
  binaryEncoder (Macaddr8 a b c d e f g h) =
    mconcat
      [ Write.word8 a,
        Write.word8 b,
        Write.word8 c,
        Write.word8 d,
        Write.word8 e,
        Write.word8 f,
        Write.word8 g,
        Write.word8 h
      ]
  binaryDecoder =
    PtrPeeker.fixed
      ( Right
          <$> ( Macaddr8
                  <$> PtrPeeker.unsignedInt1
                  <*> PtrPeeker.unsignedInt1
                  <*> PtrPeeker.unsignedInt1
                  <*> PtrPeeker.unsignedInt1
                  <*> PtrPeeker.unsignedInt1
                  <*> PtrPeeker.unsignedInt1
                  <*> PtrPeeker.unsignedInt1
                  <*> PtrPeeker.unsignedInt1
              )
      )
  textualEncoder (Macaddr8 a b c d e f g h) =
    TextBuilder.intercalate ":" $
      [ formatByte a,
        formatByte b,
        formatByte c,
        formatByte d,
        formatByte e,
        formatByte f,
        formatByte g,
        formatByte h
      ]
    where
      formatByte :: Word8 -> TextBuilder.TextBuilder
      formatByte x = TextBuilder.string (printf "%02x" x)
  textualDecoder = do
    a <- hexByte
    _ <- Attoparsec.char ':'
    b <- hexByte
    _ <- Attoparsec.char ':'
    c <- hexByte
    _ <- Attoparsec.char ':'
    d <- hexByte
    _ <- Attoparsec.char ':'
    e <- hexByte
    _ <- Attoparsec.char ':'
    f <- hexByte
    _ <- Attoparsec.char ':'
    g <- hexByte
    _ <- Attoparsec.char ':'
    h <- hexByte
    pure (Macaddr8 a b c d e f g h)
    where
      hexByte = do
        h1 <- hexDigit
        h2 <- hexDigit
        pure (h1 * 16 + h2)
      hexDigit =
        (\c -> fromIntegral (ord c - ord '0'))
          <$> Attoparsec.satisfy (\c -> c >= '0' && c <= '9')
          <|> (\c -> fromIntegral (ord c - ord 'a' + 10))
            <$> Attoparsec.satisfy (\c -> c >= 'a' && c <= 'f')
          <|> (\c -> fromIntegral (ord c - ord 'A' + 10))
            <$> Attoparsec.satisfy (\c -> c >= 'A' && c <= 'F')

-- * Accessors

-- | Extract the first byte of the MAC address.
toByte1 :: Macaddr8 -> Word8
toByte1 (Macaddr8 a _ _ _ _ _ _ _) = a

-- | Extract the second byte of the MAC address.
toByte2 :: Macaddr8 -> Word8
toByte2 (Macaddr8 _ b _ _ _ _ _ _) = b

-- | Extract the third byte of the MAC address.
toByte3 :: Macaddr8 -> Word8
toByte3 (Macaddr8 _ _ c _ _ _ _ _) = c

-- | Extract the fourth byte of the MAC address.
toByte4 :: Macaddr8 -> Word8
toByte4 (Macaddr8 _ _ _ d _ _ _ _) = d

-- | Extract the fifth byte of the MAC address.
toByte5 :: Macaddr8 -> Word8
toByte5 (Macaddr8 _ _ _ _ e _ _ _) = e

-- | Extract the sixth byte of the MAC address.
toByte6 :: Macaddr8 -> Word8
toByte6 (Macaddr8 _ _ _ _ _ f _ _) = f

-- | Extract the seventh byte of the MAC address.
toByte7 :: Macaddr8 -> Word8
toByte7 (Macaddr8 _ _ _ _ _ _ g _) = g

-- | Extract the eighth byte of the MAC address.
toByte8 :: Macaddr8 -> Word8
toByte8 (Macaddr8 _ _ _ _ _ _ _ h) = h

-- * Constructors

-- | Construct a PostgreSQL 'Macaddr8' from 8 bytes.
fromBytes :: Word8 -> Word8 -> Word8 -> Word8 -> Word8 -> Word8 -> Word8 -> Word8 -> Macaddr8
fromBytes = Macaddr8