benc-0.1.1.0: src/Data/Bencode/Encode.hs
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
-- |
-- Conversions from Haskell values to Bencoded @ByteString@s.
--
module Data.Bencode.Encode
(
-- * Quick start
-- $quick
-- * Encoding
Encoding
, toBuilder
-- * String encoders
, string
, text
-- * Integer encoders
, integer
, int
, int64
, int32
, int16
, int8
, word
, word64
, word32
, word16
, word8
-- * List encoders
, list
-- * Dictionary encoders
, dict
, field
, dict'
, FieldEncodings
-- * Miscellaneous
, value
-- * Recipes #recipes#
-- $recipes
) where
import Data.Int
import Data.Monoid (Endo(..))
import Data.Word
import qualified Data.ByteString as B
import qualified Data.ByteString.Builder as BB
import qualified Data.Map as M
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Data.Vector as V
import Data.Bencode.Type (Value(..))
-- | An encoded Bencode value.
newtype Encoding = Encoding { unEncoding :: BB.Builder }
-- | Get a ByteString 'BB.Builder' representation for an encoded Bencode value.
toBuilder :: Encoding -> BB.Builder
toBuilder = unEncoding
-- | Encode a bytestring as a Bencode string.
string :: B.ByteString -> Encoding
string s = Encoding $ BB.intDec (B.length s) <> BB.char7 ':' <> BB.byteString s
-- | Encode an integer as a Bencode integer.
integer :: Integer -> Encoding
integer = integer_ BB.integerDec
-- | Encode a @Vector@ as a Bencode list, using the given encoder for elements.
list :: (a -> Encoding) -> V.Vector a -> Encoding
list enc vs =
Encoding $ BB.char7 'l' <> foldMap (unEncoding . enc) vs <> BB.char7 'e'
{-# INLINE list #-}
-- | Encode a @Map@ as a Bencode dictionary, using the given encoder for values.
dict :: (a -> Encoding) -> M.Map B.ByteString a -> Encoding
dict enc kvs = Encoding $ BB.char7 'd' <> f kvs <> BB.char7 'e'
where
f = M.foldMapWithKey (\k v -> unEncoding (string k) <> unEncoding (enc v))
{-# INLINE dict #-}
-- | Encode @Text@ as a Bencode string. As per the Bencode specification, all
-- text must be encoded as UTF-8 strings.
text :: T.Text -> Encoding
text = string . T.encodeUtf8
{-# INLINE text #-}
-- TODO: Check if Text's encodeUtf8Builder is more efficient. But we would
-- also need to know the UTF-8 len, which is only viable for text >= 2.0.
-- | Encode an @Int@ as a Bencode integer.
int :: Int -> Encoding
int = integer_ BB.intDec
-- | Encode a @Word@ as a Bencode integer.
word :: Word -> Encoding
word = integer_ BB.wordDec
-- | A key-value encoding for a Bencode dictionary. Convert to an @Encoding@
-- with 'dict''.
field :: B.ByteString -> (a -> Encoding) -> a -> FieldEncodings
field k enc v = FE (Endo ((k, enc v):))
{-# INLINE field #-}
-- | Encode Bencode key-value pairs as a Bencode dictionary.
--
-- __WARNING__: If there are duplicate keys in the @FieldEncodings@, an
-- arbitrary key-value pair among them will be encoded and the rest discarded.
dict' :: FieldEncodings -> Encoding
dict' = dict id . M.fromList . ($ []) . appEndo . unFE
{-# INLINE dict' #-}
-- | Key-value encodings for a Bencode dictionary. See 'field' and 'dict''.
newtype FieldEncodings = FE { unFE :: Endo [(B.ByteString, Encoding)] }
deriving (Semigroup, Monoid)
-- FieldEncodings is not just a type alias because there are multiple ways to
-- do this, and in case the implementation changes it will not be a breaking
-- change.
-- | Encode a @Value@.
value :: Value -> Encoding
value v = case v of
String s -> string s
Integer i -> integer i
List vs -> list value vs
Dict vs -> dict value vs
-- | Encode an @Int64@ as a Bencode integer.
--
-- @since 0.1.1.0
int64 :: Int64 -> Encoding
int64 = integer_ BB.int64Dec
-- | Encode an @Int32@ as a Bencode integer.
--
-- @since 0.1.1.0
int32 :: Int32 -> Encoding
int32 = integer_ BB.int32Dec
-- | Encode an @Int16@ as a Bencode integer.
--
-- @since 0.1.1.0
int16 :: Int16 -> Encoding
int16 = integer_ BB.int16Dec
-- | Encode an @Int8@ as a Bencode integer.
--
-- @since 0.1.1.0
int8 :: Int8 -> Encoding
int8 = integer_ BB.int8Dec
-- | Encode a @Word64@ as a Bencode integer.
--
-- @since 0.1.1.0
word64 :: Word64 -> Encoding
word64 = integer_ BB.word64Dec
-- | Encode a @Word32@ as a Bencode integer.
--
-- @since 0.1.1.0
word32 :: Word32 -> Encoding
word32 = integer_ BB.word32Dec
-- | Encode a @Word16@ as a Bencode integer.
--
-- @since 0.1.1.0
word16 :: Word16 -> Encoding
word16 = integer_ BB.word16Dec
-- | Encode a @Word8@ as a Bencode integer.
--
-- @since 0.1.1.0
word8 :: Word8 -> Encoding
word8 = integer_ BB.word8Dec
integer_ :: (a -> BB.Builder) -> a -> Encoding
integer_ f = \x -> Encoding $ BB.char7 'i' <> f x <> BB.char7 'e'
{-# INLINE integer_ #-}
------------------------------
-- Documentation
------------------------------
-- $quick
-- Encoding is done using encoders. An encoder is simply a function from a
-- Haskell type to 'Encoding'. This module defines encoders that can be
-- composed to build encoders for arbitrary types.
--
-- @
-- data File = File
-- { hash :: ByteString
-- , size :: Integer
-- , tags :: Vector Text
-- } deriving Show
-- @
--
-- It is reasonable to encode a @File@ as a Bencode dictionary with the field
-- names as keys, and appropriate types for the values.
--
-- @
-- {-# LANGUAGE OverloadedStrings #-}
-- import qualified Data.Bencode.Encode as E
--
-- encodeFile :: File -> E.'Encoding'
-- encodeFile (File hash size tags) = E.'dict'' $
-- E.'field' "hash" E.'string' hash
-- <> E.'field' "size" E.'integer' size
-- <> E.'field' "tags" (E.'list' E.'text') tags
-- @
--
-- Applying 'toBuilder' to an 'Encoding' gives a @ByteString@
-- 'Data.ByteString.Builder', which can then be converted to a lazy
-- @ByteString@, written to a file, or used otherwise.
--
-- @
-- import qualified Data.ByteString.Builder (toLazyByteString)
-- import qualified Data.Vector as V
-- @
--
-- >>> toLazyByteString $ encodeFile $ File "xxxx" 1024 (V.fromList ["work", "backup"])
-- "d4:hash4:xxxx4:sizei1024e4:tagsl4:work6:backupee"
--
-- In this module, encodings are total conversions from Haskell values to
-- @ByteString@s. If some data should fail to encode, it should be handled
-- separately.
--
-- For more examples, see the [Recipes](#g:recipes) section at the end of this
-- page.
-- $recipes
-- Recipes for some common and uncommon usages.
--
-- The following preface is assumed.
--
-- @
-- {-# LANGUAGE OverloadedStrings #-}
-- import Data.ByteString.Builder (toLazyByteString)
-- import Data.Text (Text)
-- import qualified Data.Vector as V
-- import qualified Data.Bencode.Encode as E
--
-- toLBS = toLazyByteString . E.toBuilder
-- @
--
-- === Encode an optional field
--
-- @
-- data File = File { name :: Text, size :: Maybe Int }
--
-- encodeFile :: File -> E.'Encoding'
-- encodeFile (File name size) = E.'dict'' $
-- E.'field' "name" E.'text' name
-- <> 'foldMap' (E.'field' "size" E.'int') size
-- @
--
-- >>> toLBS $ encodeFile $ File "hello.txt" (Just 16)
-- "d4:name9:hello.txt4:sizei16ee"
-- >>> toLBS $ encodeFile $ File "hello.txt" Nothing
-- "d4:name9:hello.txte"
--
-- === Encode an enum
--
-- @
-- data Color = Red | Green | Blue
--
-- encodeColor :: Color -> E.'Encoding'
-- encodeColor = E.'text' . toText
-- where
-- toText Red = "red"
-- toText Green = "green"
-- toText Blue = "blue"
-- @
--
-- >>> toLBS $ encodeColor Green
-- "5:green"
--
-- === Encode fields differently based on the value
--
-- @
-- data Response = Response { id_ :: Int, result :: Either Text ByteString }
--
-- encodeResponse :: Response -> E.'Encoding'
-- encodeResponse (Response id_ result) = E.'dict'' $
-- E.'field' "id" E.'int' id_
-- <> either err ok result
-- where
-- err reason =
-- E.'field' "status" E.'text' "failure"
-- <> E.'field' "reason" E.'text' reason
-- ok data_ =
-- E.'field' "status" E.'text' "success"
-- <> E.'field' "data" E.'string' data_
-- @
--
-- >>> toLBS $ encodeResponse $ Response 42 (Left "unauthorized")
-- "d2:idi42e6:reason12:unauthorized6:status7:failuree"
-- >>> toLBS $ encodeResponse $ Response 42 (Right "0000")
-- "d4:data4:00002:idi42e6:status7:successe"
--
-- === Encode as nested dicts
--
-- @
-- data File = File { name :: Text, size :: Int }
--
-- encodeFile :: File -> E.'Encoding'
-- encodeFile (File name size) = E.'dict'' $
-- E.'field' "name" E.'text' name
-- <> E.'field' "metadata" id (E.'dict'' $
-- E.'field' "info" id (E.'dict'' $
-- E.'field' "size" E.'int' size))
-- @
--
-- >>> toLBS $ encodeFile $ File "hello.txt" 32
-- "d8:metadatad4:infod4:sizei32eee4:name9:hello.txte"
--
-- === Encode as a heterogeneous list
--
-- @
-- data File = File { name :: Text, size :: Int }
--
-- encodeFile :: File -> E.'Encoding'
-- encodeFile (File name size) =\
-- E.'list' id $ V.fromList [E.'text' name, E.'int' size]
-- @
--
-- >>> toLBS $ encodeFile $ File "hello.txt" 32
-- "l9:hello.txti32ee"
--