packages feed

bencoding 0.1.0.0 → 0.2.0.0

raw patch · 6 files changed

+473/−130 lines, 6 filesdep +ghc-primdep ~bytestringdep ~deepseqPVP ok

version bump matches the API change (PVP)

Dependencies added: ghc-prim

Dependency ranges changed: bytestring, deepseq

API changes (from Hackage documentation)

- Data.BEncode: dictAssoc :: [(ByteString, BEncode)] -> BEncode
- Data.BEncode: printPretty :: BEncode -> IO ()
+ Data.BEncode: data Assoc
+ Data.BEncode: instance (Constructor c, GBEncodable f Dict, GBEncodable f [BEncode]) => GBEncodable (M1 C c f) BEncode
+ Data.BEncode: instance (Eq e, Monoid e) => GBEncodable U1 e
+ Data.BEncode: instance (GBEncodable a Dict, GBEncodable b Dict) => GBEncodable (a :*: b) Dict
+ Data.BEncode: instance (GBEncodable a [BEncode], GBEncodable b [BEncode]) => GBEncodable (a :*: b) [BEncode]
+ Data.BEncode: instance (GBEncodable a e, GBEncodable b e) => GBEncodable (a :+: b) e
+ Data.BEncode: instance (Selector s, GBEncodable f BEncode) => GBEncodable (M1 S s f) Dict
+ Data.BEncode: instance BEncodable f => GBEncodable (K1 R f) BEncode
+ Data.BEncode: instance GBEncodable f BEncode => GBEncodable (M1 S s f) [BEncode]
+ Data.BEncode: instance GBEncodable f e => GBEncodable (M1 D d f) e
+ Data.BEncode: instance NFData BEncode
+ Data.BEncode: ppBEncode :: BEncode -> Doc
+ Data.BEncode: type Dict = Map ByteString BEncode
- Data.BEncode: (>--) :: BEncodable a => Map ByteString BEncode -> ByteString -> Result a
+ Data.BEncode: (>--) :: BEncodable a => Dict -> ByteString -> Result a
- Data.BEncode: (>--?) :: BEncodable a => Map ByteString BEncode -> ByteString -> Result (Maybe a)
+ Data.BEncode: (>--?) :: BEncodable a => Dict -> ByteString -> Result (Maybe a)
- Data.BEncode: class BEncodable a
+ Data.BEncode: class BEncodable a where toBEncode = gto . from fromBEncode x = to <$> gfrom x
- Data.BEncode: optKey :: BEncodable a => Map ByteString BEncode -> ByteString -> Result (Maybe a)
+ Data.BEncode: optKey :: BEncodable a => Dict -> ByteString -> Result (Maybe a)
- Data.BEncode: reqKey :: BEncodable a => Map ByteString BEncode -> ByteString -> Result a
+ Data.BEncode: reqKey :: BEncodable a => Dict -> ByteString -> Result a

Files

README.md view
@@ -1,17 +1,18 @@-# Synopsis+### Synopsis  [BEncode][bencode] is [JSON][json-ref]-like format used in bittorrent protocol but might be used anywhere else. -# Description+### Description  This package implements fast seamless encoding/decoding to/from bencode format for many native datatypes. To achive-[more performance][cmp] we use [bytestring builders][bytestring-builder]-and hand optimized [attoparsec][attoparsec] parser so this library is-considered as replacement for BEncode and AttoBencode packages.+[more performance][cmp] we use+[bytestring builders][bytestring-builder] and hand optimized+[attoparsec][attoparsec] parser so this library is considered as+replacement for BEncode and AttoBencode packages. -## Format+#### Format  Bencode is pretty similar to JSON: it has dictionaries(JSON objects), lists(JSON arrays), strings and integers. However bencode has a few@@ -31,27 +32,29 @@ * Bencode is certainly less human readable. * Bencode is rarely used, except bittorrent protocol of course. -# Documentation+### Documentation -For documentation see haddock generated documentation.+For documentation see package [hackage][hackage] page. -# Build Status+### Build Status  [![Build Status][travis-img]][travis-log] -# Authors--This library is written and maintained by Sam T. <pxqr.sta@gmail.com>+### Maintainer <pxqr.sta@gmail.com> -Feel free to report bugs and suggestions via github issue tracker or the mail.+Feel free to report bugs and suggestions via [issue tracker][issues]+or the mail.  -[cmp]: http://htmlpreview.github.com/?https://github.com/pxqr/bencoding/master/bench/comparison.html+[cmp]: http://htmlpreview.github.com/?https://raw.github.com/wiki/cobit/bencoding/comparison.html  [bencode]: https://wiki.theory.org/BitTorrentSpecification#Bencoding [json-ref]: http://www.json.org/ [attoparsec]: http://hackage.haskell.org/package/attoparsec-0.10.4.0 [bytestring-builder]: http://hackage.haskell.org/packages/archive/bytestring/0.10.2.0/doc/html/Data-ByteString-Builder.html -[travis-img]: https://travis-ci.org/pxqr/bencoding.png-[travis-log]: https://travis-ci.org/pxqr/bencoding+[travis-img]: https://travis-ci.org/cobit/bencoding.png+[travis-log]: https://travis-ci.org/cobit/bencoding++[hackage]: http://hackage.haskell.org/package/bencoding+[issues]: https://github.com/cobit/bencoding/issues
bench/Main.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE PackageImports #-}+{-# LANGUAGE DeriveGeneric  #-} module Main (main) where  import Control.DeepSeq@@ -10,11 +11,14 @@ import Criterion.Main import System.Environment +import GHC.Generics+ import "bencode"   Data.BEncode     as A import             Data.AttoBencode as B import             Data.AttoBencode.Parser as B import "bencoding" Data.BEncode     as C + instance NFData A.BEncode where     rnf (A.BInt    i) = rnf i     rnf (A.BString s) = rnf s@@ -27,16 +31,26 @@     rnf (B.BList   l) = rnf l     rnf (B.BDict   d) = rnf d -instance NFData C.BEncode where-    rnf (C.BInteger i) = rnf i-    rnf (C.BString  s) = rnf s-    rnf (C.BList    l) = rnf l-    rnf (C.BDict    d) = rnf d- getRight :: Either String a -> a-getRight (Left e) = error e-getRight (Right x) = x+getRight = either error id +data List a = Cons a (List a) | Nil+              deriving Generic++instance BEncodable a => BEncodable (List a)++instance NFData a => NFData (List a) where+  rnf  Nil        = ()+  rnf (Cons x xs) = rnf (x, xs)++replicate' :: Int -> a -> List a+replicate' c x+    |   c >= 0  = go c+    | otherwise = Nil+  where+    go 0 = Nil+    go n = Cons x $ go (pred n)+ main :: IO () main = do   (path : args) <- getArgs@@ -48,9 +62,12 @@    withArgs args $        defaultMain-       [ bench "decode/bencode"     $ nf A.bRead                            lazyTorrentFile-       , bench "decode/AttoBencode" $ nf (getRight . Atto.parseOnly bValue) torrentFile-       , bench "decode/bencoding"   $ nf (getRight . C.decode)              torrentFile+       [ bench "decode/bencode"     $+           nf A.bRead                            lazyTorrentFile+       , bench "decode/AttoBencode" $+           nf (getRight . Atto.parseOnly bValue) torrentFile+       , bench "decode/bencoding"   $+           nf (getRight . C.decode)              torrentFile         , let Just v = A.bRead lazyTorrentFile in          bench "encode/bencode"     $ nf A.bPack v@@ -59,19 +76,21 @@        , let Right v = C.decode torrentFile in          bench "encode/bencoding"   $ nf C.encode v -       , bench "decode+encode/bencode"     $ nf (A.bPack  . fromJust . A.bRead)-               lazyTorrentFile-       , bench "decode+encode/AttoBencode" $ nf (B.encode . getRight . Atto.parseOnly bValue)-               torrentFile-       , bench "decode+encode/bencoding"   $ nf (C.encode . getRight . C.decode)-               torrentFile+       , bench "decode+encode/bencode"     $+           nf (A.bPack  . fromJust . A.bRead) lazyTorrentFile+       , bench "decode+encode/AttoBencode" $+           nf (B.encode . getRight . Atto.parseOnly bValue) torrentFile+       , bench "decode+encode/bencoding"   $+           nf (C.encode . getRight . C.decode) torrentFile -       , bench "list10000int/bencode/encode" $ nf-           (A.bPack . A.BList . L.map (A.BInt . fromIntegral))-             [0..10000 :: Int]+       , bench "list10000int/bencode/encode" $+           nf (A.bPack . A.BList . L.map (A.BInt . fromIntegral))+              [0..10000 :: Int] -       , bench "list10000int/attobencode/encode" $ nf B.encode [1..20000 :: Int]-       , bench "list10000int/bencoding/encode" $ nf C.encoded [1..20000 :: Int]+       , bench "list10000int/attobencode/encode" $+           nf B.encode [1..20000 :: Int]+       , bench "list10000int/bencoding/encode" $+           nf C.encoded [1..20000 :: Int]          , let d = A.bPack $ A.BList $@@ -88,6 +107,14 @@             (C.decoded :: B.ByteString -> Either String [Int]) d)         , let d = L.replicate 10000 0-         in bench "list10000int/bencoding/encode>>decode" $ nf-            (getRight . C.decoded . BL.toStrict . C.encoded :: [Int] ->  [Int] ) d+         in bench "list10000int/bencoding/encode>>decode" $+            nf  (getRight . C.decoded . BL.toStrict . C.encoded+                 :: [Int] ->  [Int] )+                d++       , let d = replicate' 10000 0+         in bench "list10000int/bencoding/encode>>decode/generic" $+            nf (getRight . C.decoded . BL.toStrict . C.encoded+                :: List Int -> List Int)+               d        ]
bencoding.cabal view
@@ -1,8 +1,8 @@ name:                  bencoding-version:               0.1.0.0+version:               0.2.0.0 synopsis:              A library for encoding and decoding of BEncode data.-homepage:              https://github.com/pxqr/bencoding-bug-reports:           https://github.com/pxqr/bencoding/issues+homepage:              https://github.com/cobit/bencoding+bug-reports:           https://github.com/cobit/bencoding/issues license:               MIT license-file:          LICENSE author:                Sam T.@@ -10,7 +10,8 @@ copyright:             (c) 2013, Sam T. category:              Data build-type:            Simple-cabal-version:         >= 1.8+cabal-version:         >= 1.10+tested-with:           GHC==7.4.1, GHC==7.6.3 description:    A library for encoding and decoding of BEncode data.@@ -18,64 +19,71 @@   [/Release notes/]   .     * /0.1.0.0:/ Initial version.+  .+    * /0.2.0.0:/ Added default decoders/encoders using GHC Generics.  -extra-source-files:    README.md .travis.yml+extra-source-files:    README.md+                       .travis.yml  source-repository head   type:                git-  location:            git://github.com/pxqr/bencoding.git-+  location:            git://github.com/cobit/bencoding.git  library+  default-language:    Haskell2010+  default-extensions:  PatternGuards+  hs-source-dirs:      src   exposed-modules:     Data.BEncode   build-depends:       base       == 4.*+                     , ghc-prim+                     , deepseq    == 1.3.*                      , containers >= 0.4-                     , bytestring >= 0.10.2.0+                     , bytestring >= 0.10.0.2                      , attoparsec >= 0.10                      , text       >= 0.11                      , pretty--  hs-source-dirs:      src-  extensions:          PatternGuards   ghc-options:         -Wall -fno-warn-unused-do-bind - executable pp+  default-language:    Haskell2010+  hs-source-dirs:      pp   main-is:             pp.hs   build-depends:       base        == 4.*                      , bytestring                      , bencoding-  hs-source-dirs:      pp   ghc-options:         -Wall   test-suite properties   type:                exitcode-stdio-1.0-  main-is:             properties.hs+  default-language:    Haskell2010   hs-source-dirs:      tests-+  main-is:             properties.hs   build-depends:       base       == 4.*+                     , ghc-prim+                      , containers >= 0.4-                     , bytestring >= 0.10.2.0+                     , bytestring >= 0.10.0.2                      , attoparsec >= 0.10                       , test-framework                      , test-framework-quickcheck2                      , QuickCheck                      , bencoding-   ghc-options:         -Wall -fno-warn-orphans   benchmark bench-comparison   type:                exitcode-stdio-1.0-  main-is:             Main.hs+  default-language:    Haskell2010   hs-source-dirs:      bench-+  main-is:             Main.hs   build-depends:       base == 4.*+                     , ghc-prim+                      , attoparsec >= 0.10-                     , bytestring >= 0.10.2.0+                     , bytestring >= 0.10.0.2                       , criterion                      , deepseq@@ -83,5 +91,4 @@                      , bencoding                      , bencode     >= 0.5                      , AttoBencode >= 0.2-   ghc-options:         -O2 -Wall -fno-warn-orphans
pp/pp.hs view
@@ -10,5 +10,5 @@   path : _ <- getArgs   content  <- B.readFile path   case decode content of-    Left e -> hPutStrLn stderr e-    Right be -> printPretty be+    Left  e  -> hPutStrLn stderr e+    Right be -> print $ ppBEncode be
src/Data/BEncode.hs view
@@ -1,4 +1,6 @@--- TODO: make int's instances platform independent so we can make library portable.+-- TODO: make int's instances platform independent so we can make+-- library portable.+ -- | --   Copyright   :  (c) Sam T. 2013 --   License     :  MIT@@ -6,8 +8,9 @@ --   Stability   :  stable --   Portability :  non-portable -----   This module provides convinient and fast way to serialize, deserealize---   and construct/destructure Bencoded values with optional fields.+--   This module provides convinient and fast way to serialize,+--   deserealize and construct/destructure Bencoded values with+--   optional fields. -- --   It supports four different types of values: --@@ -19,9 +22,10 @@ -- --     * dictionaries — represented as 'Map'; -----    To serialize any other types we need to make conversion.---    To make conversion more convenient there is type class for it: 'BEncodable'.---    Any textual strings are considered as UTF8 encoded 'Text'.+--    To serialize any other types we need to make conversion.  To+--    make conversion more convenient there is type class for it:+--    'BEncodable'. Any textual strings are considered as UTF8 encoded+--    'Text'. -- --    The complete Augmented BNF syntax for bencoding format is: --@@ -42,41 +46,70 @@ --    This module is considered to be imported qualified. -- {-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE Trustworthy #-}+{-# LANGUAGE Trustworthy       #-}+{-# LANGUAGE CPP               #-}++#if __GLASGOW_HASKELL__ >= 702+{-# LANGUAGE TypeOperators          #-}+{-# LANGUAGE DefaultSignatures      #-}+{-# LANGUAGE FlexibleContexts       #-}+{-# LANGUAGE FlexibleInstances      #-}+{-# LANGUAGE MultiParamTypeClasses  #-}+{-# LANGUAGE ScopedTypeVariables    #-}+#endif+ module Data.BEncode        ( -- * Datatype          BEncode(..)+       , Dict+       , ppBEncode -         -- * Construction && Destructuring-       , BEncodable (..), dictAssoc, Result+         -- * Conversion+       , BEncodable (..)+       , Result +         -- * Serialization+       , encode+       , decode+       , encoded+       , decoded+          -- ** Dictionaries          -- *** Building-       , (-->), (-->?), fromAssocs, fromAscAssocs+       , Assoc+       , (-->)+       , (-->?)+       , fromAssocs+       , fromAscAssocs           -- *** Extraction-       , reqKey, optKey, (>--), (>--?)--         -- * Serialization-       , encode, decode-       , encoded, decoded+       , decodingError+       , reqKey+       , optKey+       , (>--)+       , (>--?)           -- * Predicates-       , isInteger, isString, isList, isDict+       , isInteger+       , isString+       , isList+       , isDict           -- * Extra-       , builder, parser, decodingError, printPretty+       , builder+       , parser        ) where   import Control.Applicative+import Control.DeepSeq import Control.Monad import Data.Int-import Data.Maybe (mapMaybe)-import Data.Monoid ((<>))-import Data.Foldable (foldMap)-import Data.Traversable (traverse)-import Data.Word (Word8, Word16, Word32, Word64, Word)+import Data.Maybe         (mapMaybe)+import Data.Monoid        -- (mempty, (<>))+import Data.Foldable      (foldMap)+import Data.Traversable   (traverse)+import Data.Word          (Word8, Word16, Word32, Word64, Word) import           Data.Map (Map) import qualified Data.Map as M import           Data.Set (Set)@@ -87,24 +120,26 @@ import qualified Data.ByteString as B import qualified Data.ByteString.Char8 as BC import qualified Data.ByteString.Lazy as Lazy+import qualified Data.ByteString.Lazy.Builder as B+import qualified Data.ByteString.Lazy.Builder.ASCII as B import           Data.ByteString.Internal as B (c2w, w2c)-import qualified Data.ByteString.Builder as B-import qualified Data.ByteString.Builder.Prim as BP (int64Dec, primBounded) import           Data.Text (Text) import qualified Data.Text.Encoding as T import           Data.Version import           Text.PrettyPrint hiding ((<>)) import qualified Text.ParserCombinators.ReadP as ReadP -+#if __GLASGOW_HASKELL__ >= 702+import GHC.Generics+#endif +-- | BEncode key-value dictionary. type Dict = Map ByteString BEncode --- | 'BEncode' is straightforward ADT for b-encoded values.---   Please note that since dictionaries are sorted, in most cases we can---   compare BEncoded values without serialization and vice versa.---   Lists is not required to be sorted through.---   Also note that 'BEncode' have JSON-like instance for 'Pretty'.+-- | 'BEncode' is straightforward ADT for b-encoded values. Please+-- note that since dictionaries are sorted, in most cases we can+-- compare BEncoded values without serialization and vice versa.+-- Lists is not required to be sorted through. -- data BEncode = BInteger {-# UNPACK #-} !Int64              | BString  !ByteString@@ -112,17 +147,196 @@              | BDict    Dict                deriving (Show, Read, Eq, Ord) +instance NFData BEncode where+    rnf (BInteger i) = rnf i+    rnf (BString  s) = rnf s+    rnf (BList    l) = rnf l+    rnf (BDict    d) = rnf d++-- | Result used in decoding operations. type Result = Either String +-- | This class is used to define new datatypes that could be easily+-- serialized using bencode format.+--+--   By default 'BEncodable' have a generic implementation; suppose+--   the following datatype:+--+-- > data List a = Cons { _head  :: a+-- >                    , __tail :: (List a) }+-- >             | Nil+-- >               deriving Generic+--+--   If we don't need to obey any particular specification or+--   standard, the default instance could be derived automatically+--   from the 'Generic' instance:+--+-- > instance BEncodable a => BEncodable (List a)+--+--   Example of derived 'toBEncode' result:+--+-- > > toBEncode (Cons 123 $ Cons 1 Nil)+-- > BDict (fromList [("head",BInteger 123),("tail",BList [])])+--+--  Note that '_' prefixes are omitted.+-- class BEncodable a where+  -- | See an example of implementation here 'Assoc'   toBEncode   :: a -> BEncode++#if __GLASGOW_HASKELL__ >= 702+  default toBEncode+    :: Generic a+    => GBEncodable (Rep a) BEncode+    => a -> BEncode++  toBEncode = gto . from+#endif++  -- | See an example of implementation here 'reqKey'.   fromBEncode :: BEncode -> Result a +#if __GLASGOW_HASKELL__ >= 702+  default fromBEncode+    :: Generic a+    => GBEncodable (Rep a) BEncode+    => BEncode -> Result a +  fromBEncode x = to <$> gfrom x+#endif++-- | Typically used to throw an decoding error in fromBEncode; when+-- BEncode value to match expected value. decodingError :: String -> Result a decodingError s = Left ("fromBEncode: unable to decode " ++ s) {-# INLINE decodingError #-} +{--------------------------------------------------------------------+  Generics+--------------------------------------------------------------------}++{- NOTE: SELECTORS FOLDING/UNFOLDING+Both List and Map are monoids:++* if fields are named, we fold record to the map;+* otherwise we collect fields using list;++and then unify them using BDict and BList constrs.+-}++#if __GLASGOW_HASKELL__ >= 702++class GBEncodable f e where+  gto   :: f a -> e+  gfrom :: e -> Result (f a)++instance BEncodable f+      => GBEncodable (K1 R f) BEncode where+  {-# INLINE gto #-}+  gto = toBEncode . unK1++  {-# INLINE gfrom #-}+  gfrom x = K1 <$> fromBEncode x++instance (Eq e, Monoid e)+      => GBEncodable U1 e where+  {-# INLINE gto #-}+  gto U1 = mempty++  {-# INLINE gfrom #-}+  gfrom x+    | x == mempty = pure U1+    |   otherwise = decodingError "U1"++instance (GBEncodable a [BEncode], GBEncodable b [BEncode])+      => GBEncodable (a :*: b) [BEncode] where+  {-# INLINE gto #-}+  gto (a :*: b) = gto a ++ gto b++  {-# INLINE gfrom #-}+  gfrom (x : xs) = (:*:) <$> gfrom [x] <*> gfrom xs+  gfrom []       = decodingError "generic: not enough fields"++instance (GBEncodable a Dict, GBEncodable b Dict)+      => GBEncodable (a :*: b) Dict where+  {-# INLINE gto #-}+  gto (a :*: b) = gto a <> gto b++  {-# INLINE gfrom #-}+  -- Just look at this! >.<+  gfrom dict = (:*:) <$> gfrom dict <*> gfrom dict+++instance (GBEncodable a e, GBEncodable b e)+      =>  GBEncodable (a :+: b) e where+  {-# INLINE gto #-}+  gto (L1 x) = gto x+  gto (R1 x) = gto x++  {-# INLINE gfrom #-}+  gfrom x = case gfrom x of+    Right lv -> return (L1 lv)+    Left  le -> do+      case gfrom x of+        Right rv -> return (R1 rv)+        Left  re -> decodingError $ "generic: both" ++ le ++ " " ++ re++selRename :: String -> String+selRename = dropWhile ('_'==)++gfromM1S :: forall c. Selector c+         => GBEncodable f BEncode+         => Dict -> Result (M1 i c f p)+gfromM1S dict+  | Just va <- M.lookup (BC.pack (selRename name)) dict = M1 <$> gfrom va+  | otherwise = decodingError $ "generic: Selector not found " ++ show name+  where+    name = selName (error "gfromM1S: impossible" :: M1 i c f p)++instance (Selector s, GBEncodable f BEncode)+       => GBEncodable (M1 S s f) Dict where+  {-# INLINE gto #-}+  gto s @ (M1 x) = BC.pack (selRename (selName s)) `M.singleton` gto x++  {-# INLINE gfrom #-}+  gfrom = gfromM1S++-- TODO DList+instance GBEncodable f BEncode+      => GBEncodable (M1 S s f) [BEncode] where+  {-# INLINE gto #-}+  gto (M1 x) = [gto x]++  gfrom [x] = M1 <$> gfrom x+  gfrom _   = decodingError "generic: empty selector"+  {-# INLINE gfrom #-}++instance (Constructor c, GBEncodable f Dict, GBEncodable f [BEncode])+       => GBEncodable (M1 C c f) BEncode where+  {-# INLINE gto #-}+  gto con @ (M1 x)+      | conIsRecord con = BDict (gto x)+      |    otherwise    = BList (gto x)++  {-# INLINE gfrom #-}+  gfrom (BDict a) = M1 <$> gfrom a+  gfrom (BList a) = M1 <$> gfrom a+  gfrom _         = decodingError "generic: Constr"++instance GBEncodable f e+      => GBEncodable (M1 D d f) e where+  {-# INLINE gto #-}+  gto (M1 x) = gto x++  {-# INLINE gfrom #-}+  gfrom x = M1 <$> gfrom x++#endif++{--------------------------------------------------------------------+  Basic instances+--------------------------------------------------------------------}+ instance BEncodable BEncode where   {-# SPECIALIZE instance BEncodable BEncode #-}   toBEncode = id@@ -280,84 +494,128 @@   fromBEncode _ = decodingError "Data.Version"   {-# INLINE fromBEncode #-} -dictAssoc :: [(ByteString, BEncode)] -> BEncode-dictAssoc = BDict . M.fromList-{-# INLINE dictAssoc #-}- {--------------------------------------------------------------------   Building dictionaries --------------------------------------------------------------------} -data Assoc = Required ByteString BEncode-           | Optional ByteString (Maybe BEncode)+-- | /Assoc/ used to easily build dictionaries with required and+-- optional keys. Suppose we have we following datatype we want to+-- serialize:+--+--  > data FileInfo = FileInfo+--  >   { fileLength :: Integer+--  >   , fileMD5sum :: Maybe ByteString+--  >   , filePath   :: [ByteString]+--  >   , fileTags   :: Maybe [Text]+--  >   } deriving (Show, Read, Eq)+--+-- We need to make /instance BEncodable FileInfo/, though we don't+-- want to check the both /maybes/ manually. The more declarative and+-- convenient way to define the 'toBEncode' method is to use+-- dictionary builders:+--+--  > instance BEncodable FileInfo where+--  >   toBEncode FileInfo {..} = fromAssocs+--  >     [ "length" -->  fileLength+--  >     , "md5sum" -->? fileMD5sum+--  >     , "path"   -->  filePath+--  >     , "tags"   -->? fileTags+--  >     ]+--  >     ...+--+newtype Assoc = Assoc { unAssoc :: Maybe (ByteString, BEncode) } +-- | Make required key value pair. (-->) :: BEncodable a => ByteString -> a -> Assoc-key --> val = Required key (toBEncode val)+key --> val = Assoc $ Just $ (key, toBEncode val) {-# INLINE (-->) #-} +-- | Like (-->) but if the value is not present then the key do not+-- appear in resulting bencoded dictionary.+-- (-->?) :: BEncodable a => ByteString -> Maybe a -> Assoc-key -->? mval = Optional key (toBEncode <$> mval)+key -->? mval = Assoc $ ((,) key . toBEncode) <$> mval {-# INLINE (-->?) #-} -mkAssocs :: [Assoc] -> [(ByteString, BEncode)]-mkAssocs = mapMaybe unpackAssoc-  where-    unpackAssoc (Required n v)        = Just (n, v)-    unpackAssoc (Optional n (Just v)) = Just (n, v)-    unpackAssoc (Optional _ Nothing)  = Nothing-+-- | Build BEncode dictionary using key -> value description. fromAssocs :: [Assoc] -> BEncode-fromAssocs = BDict . M.fromList . mkAssocs+fromAssocs = BDict . M.fromList . mapMaybe unAssoc {-# INLINE fromAssocs #-} --- | A faster version of 'fromAssocs'.---   Should be used only when keys are sorted by ascending.+-- | A faster version of 'fromAssocs'. Should be used only when keys+-- in builder list are sorted by ascending. fromAscAssocs :: [Assoc] -> BEncode-fromAscAssocs = BDict . M.fromList . mkAssocs+fromAscAssocs = BDict . M.fromAscList . mapMaybe unAssoc {-# INLINE fromAscAssocs #-}  {---------------------------------------------------------------------  Extraction+  Dictionary extraction --------------------------------------------------------------------} -reqKey :: BEncodable a => Map ByteString BEncode -> ByteString -> Result a+-- | Dictionary extractor are similar to dictionary builders, but play+-- the opposite role: they are used to define 'fromBEncode' method in+-- declarative style. Using the same /FileInfo/ datatype 'fromBEncode'+-- looks like:+--+--  > instance BEncodable FileInfo where+--  >   ...+--  >   fromBEncode (BDict d) =+--  >     FileInfo <$> d >--  "length"+--  >              <*> d >--? "md5sum"+--  >              <*> d >--  "path"+--  >              <*> d >--? "tags"+--  >   fromBEncode _ = decodingError "FileInfo"+--+--  The /reqKey/ is used to extract required key — if lookup is failed+--  then whole destructuring fail.+reqKey :: BEncodable a => Dict -> ByteString -> Result a reqKey d key   | Just b <- M.lookup key d = fromBEncode b-  |        otherwise         = Left ("required field `" ++ BC.unpack key ++ "' not found")+  |        otherwise         = Left msg+  where+    msg = "required field `" ++ BC.unpack key ++ "' not found" -optKey :: BEncodable a => Map ByteString BEncode -> ByteString -> Result (Maybe a)+-- | Used to extract optional key — if lookup is failed returns+-- 'Nothing'.+optKey :: BEncodable a => Dict -> ByteString -> Result (Maybe a) optKey d key   | Just b <- M.lookup key d   , Right r <- fromBEncode b = return (Just r)   | otherwise                = return Nothing -(>--) :: BEncodable a => Map ByteString BEncode -> ByteString -> Result a+-- | Infix version of the 'reqKey'.+(>--) :: BEncodable a => Dict -> ByteString -> Result a (>--) = reqKey {-# INLINE (>--) #-} -(>--?) :: BEncodable a => Map ByteString BEncode -> ByteString -> Result (Maybe a)+-- | Infix version of the 'optKey'.+(>--?) :: BEncodable a => Dict -> ByteString -> Result (Maybe a) (>--?) = optKey {-# INLINE (>--?) #-}  {---------------------------------------------------------------------  Predicated+  Predicates --------------------------------------------------------------------} +-- | Test if bencoded value is an integer. isInteger :: BEncode -> Bool isInteger (BInteger _) = True isInteger _            = False {-# INLINE isInteger #-} +-- | Test if bencoded value is a string, both raw and utf8 encoded. isString :: BEncode -> Bool isString (BString _) = True isString _           = False {-# INLINE isString #-} +-- | Test if bencoded value is a list. isList :: BEncode -> Bool isList (BList _) = True isList _         = False {-# INLINE isList #-} +-- | Test if bencoded value is a dictionary. isDict :: BEncode -> Bool isDict (BList _) = True isDict _         = False@@ -367,15 +625,21 @@   Encoding --------------------------------------------------------------------} +-- | Convert bencoded value to raw bytestring according to the+-- specification. encode :: BEncode -> Lazy.ByteString encode = B.toLazyByteString . builder +-- | Try to convert raw bytestring to bencoded value according to+-- specification. decode :: ByteString -> Result BEncode decode = P.parseOnly parser +-- | The same as 'decode' but returns any bencodable value. decoded :: BEncodable a => ByteString -> Result a decoded = decode >=> fromBEncode +-- | The same as 'encode' but takes any bencodable value. encoded :: BEncodable a => a -> Lazy.ByteString encoded = encode . toBEncode @@ -383,11 +647,12 @@   Internals --------------------------------------------------------------------} +-- | BEncode format encoder according to specification. builder :: BEncode -> B.Builder builder = go     where       go (BInteger i) = B.word8 (c2w 'i') <>-                        BP.primBounded BP.int64Dec i <> -- TODO FIXME+                        B.int64Dec i <>                         B.word8 (c2w 'e')       go (BString  s) = buildString s       go (BList    l) = B.word8 (c2w 'l') <>@@ -404,7 +669,8 @@                       B.byteString s       {-# INLINE buildString #-} --- | TODO try to replace peekChar with something else+-- TODO try to replace peekChar with something else+-- | BEncode format parser according to specification. parser :: Parser BEncode parser = valueP   where@@ -420,8 +686,9 @@               'l' -> P.anyChar *> ((BList    <$> listBody) <* P.anyChar)               'd' -> do                      P.anyChar-                     (BDict . M.fromDistinctAscList <$> many ((,) <$> stringP <*> valueP))-                        <* P.anyChar+                     (BDict . M.fromDistinctAscList <$>+                          many ((,) <$> stringP <*> valueP))+                       <* P.anyChar               t   -> fail ("bencode unknown tag: " ++ [t])      listBody = do@@ -451,17 +718,17 @@   Pretty Printing --------------------------------------------------------------------} -printPretty :: BEncode -> IO ()-printPretty = print . ppBEncode- ppBS :: ByteString -> Doc ppBS = text . map w2c . B.unpack +-- | Convert to easily readable JSON-like document. Typically used for+-- debugging purposes. ppBEncode :: BEncode -> Doc-ppBEncode (BInteger i) = int (fromIntegral i)+ppBEncode (BInteger i) = int $ fromIntegral i ppBEncode (BString  s) = ppBS s-ppBEncode (BList    l) = brackets $ hsep (punctuate comma (map ppBEncode l))-ppBEncode (BDict    d) = braces $ vcat (punctuate comma (map ppKV (M.toAscList d)))+ppBEncode (BList    l) = brackets $ hsep $ punctuate comma $ map ppBEncode l+ppBEncode (BDict    d)+    = braces $ vcat $ punctuate comma $ map ppKV $ M.toAscList d   where     ppKV (k, v) = ppBS k <+> colon <+> ppBEncode v 
tests/properties.hs view
@@ -1,3 +1,5 @@+{-# LANGUAGE DeriveGeneric #-}+{-# OPTIONS  -fno-warn-unused-binds #-} module Main (main) where  import Control.Applicative@@ -6,9 +8,11 @@ import Test.Framework (defaultMain) import Test.Framework.Providers.QuickCheck2 (testProperty) import Test.QuickCheck+import GHC.Generics  import Data.BEncode + instance Arbitrary B.ByteString where     arbitrary = fmap B.pack arbitrary @@ -19,12 +23,47 @@                 , (5,  BList    <$> (arbitrary `suchThat` ((10 >) . length)))                 ] + prop_EncDec :: BEncode -> Bool prop_EncDec x = case decode (L.toStrict (encode x)) of                   Left _   -> False                   Right x' -> x == x' +data List a = Cons a (List a) | Nil+              deriving (Show, Eq, Generic)++instance BEncodable a => BEncodable (List a)++instance Arbitrary a => Arbitrary (List a) where+  arbitrary = frequency+    [ (90, pure Nil)+    , (10, Cons <$> arbitrary <*> arbitrary)+    ]++data FileInfo = FileInfo+  { fiLength :: !Integer+  , fiPath   :: [B.ByteString]+  , fiMD5Sum :: B.ByteString+  } deriving (Show, Eq, Generic)++instance BEncodable FileInfo++instance Arbitrary FileInfo where+  arbitrary = FileInfo <$> arbitrary <*> arbitrary <*> arbitrary++data T a = T++prop_bencodable :: Eq a => BEncodable a => T a -> a -> Bool+prop_bencodable _ x = decoded (L.toStrict (encoded x)) == Right x++-- All tests are (encode >>> decode = id) main :: IO () main = defaultMain-       [ testProperty "encode <-> decode" prop_EncDec+       [ testProperty "BEncode" prop_EncDec++       , testProperty "generic recordless" $+            prop_bencodable (T :: T (List Int))++       , testProperty "generic records" $+            prop_bencodable (T :: T FileInfo)        ]