diff --git a/.gitignore b/.gitignore index 316009b..7224891 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ dist cabal-dev +.cabal-sandbox/ +cabal.sandbox.config diff --git a/Data/Bson.hs b/Data/Bson.hs index 2a8e5a9..99b38d9 100644 --- a/Data/Bson.hs +++ b/Data/Bson.hs @@ -4,55 +4,62 @@ -- Use the GHC language extension /OverloadedStrings/ to automatically convert -- String literals to Text -{-# LANGUAGE OverloadedStrings, TypeSynonymInstances, FlexibleInstances #-} -{-# LANGUAGE DeriveDataTypeable, RankNTypes, OverlappingInstances #-} -{-# LANGUAGE IncoherentInstances, ScopedTypeVariables #-} -{-# LANGUAGE ForeignFunctionInterface, BangPatterns #-} +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE ForeignFunctionInterface #-} +{-# LANGUAGE IncoherentInstances #-} +{-# LANGUAGE OverlappingInstances #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeSynonymInstances #-} module Data.Bson ( - -- * Document - Document, (!?), look, lookup, valueAt, at, include, exclude, merge, - -- * Field - Field(..), (=:), (=?), - Label, - -- * Value - Value(..), Val(..), fval, cast, typed, typeOfVal, - -- * Special Bson value types - Binary(..), Function(..), UUID(..), MD5(..), UserDefined(..), - Regex(..), Javascript(..), Symbol(..), MongoStamp(..), MinMaxKey(..), - -- ** ObjectId - ObjectId(..), timestamp, genObjectId + -- * Document + Document, (!?), look, lookup, valueAt, at, include, exclude, merge, + -- * Field + Field(..), (=:), (=?), + Label, + -- * Value + Value(..), Val(..), fval, cast, typed, typeOfVal, + -- * Special Bson value types + Binary(..), Function(..), UUID(..), MD5(..), UserDefined(..), + Regex(..), Javascript(..), Symbol(..), MongoStamp(..), MinMaxKey(..), + -- ** ObjectId + ObjectId(..), timestamp, genObjectId ) where -import Prelude hiding (lookup) -import Control.Applicative ((<$>), (<*>)) -import Control.Monad (foldM) -import Data.Bits (shift, (.|.)) -import Data.Int (Int32, Int64) -import Data.IORef (IORef, newIORef, atomicModifyIORef) -import Data.List (find, findIndex) -import Data.Maybe (maybeToList, mapMaybe) -import Data.Time.Clock (UTCTime) -import Data.Time.Clock.POSIX (POSIXTime, posixSecondsToUTCTime, - utcTimeToPOSIXSeconds, getPOSIXTime) -import Data.Time.Format () -- for Show and Read instances of UTCTime -import Data.Typeable hiding (cast) -import Data.Word (Word8, Word16, Word32, Word64) -import Numeric (readHex, showHex) -import System.IO.Unsafe (unsafePerformIO) -import Text.Read (Read(..)) - -import qualified Data.ByteString as S -import qualified Data.ByteString.Char8 as SC -import qualified Text.ParserCombinators.ReadP as R +import Control.Applicative ((<$>), (<*>)) +import Control.Monad (foldM) +import Data.Bits (shift, (.|.)) +import Data.Int (Int32, Int64) +import Data.IORef (IORef, atomicModifyIORef, + newIORef) +import Data.List (find, findIndex) +import Data.Maybe (mapMaybe, maybeToList) +import Data.Time.Clock (UTCTime) +import Data.Time.Clock.POSIX (POSIXTime, getPOSIXTime, + posixSecondsToUTCTime, + utcTimeToPOSIXSeconds) +import Data.Time.Format () +import Data.Typeable hiding (cast) +import Data.Word (Word16, Word32, Word64, Word8) +import Numeric (readHex, showHex) +import Prelude hiding (lookup) +import System.IO.Unsafe (unsafePerformIO) +import Text.Read (Read (..)) + +import qualified Data.ByteString as S +import qualified Data.ByteString.Char8 as SC +import qualified Text.ParserCombinators.ReadP as R import qualified Text.ParserCombinators.ReadPrec as R (lift, readS_to_Prec) -import Control.Monad.Identity (runIdentity) -import Network.BSD (getHostName) -import Data.Text (Text) +import Control.Monad.Identity (runIdentity) +import Data.Text (Text) +import Network.BSD (getHostName) -import qualified Data.Text as T -import qualified Crypto.Hash.MD5 as MD5 +import qualified Crypto.Hash.MD5 as MD5 +import qualified Data.Text as T getProcessID :: IO Int -- ^ Get the current process id. @@ -68,8 +75,8 @@ roundTo mult n = fromIntegral (round (n / mult)) * mult showHexLen :: (Show n, Integral n) => Int -> n -> ShowS -- ^ showHex of n padded with leading zeros if necessary to fill d digits showHexLen d n = showString (replicate (d - sigDigits n) '0') . showHex n where - sigDigits 0 = 1 - sigDigits n' = truncate (logBase 16 $ fromIntegral n') + 1 + sigDigits 0 = 1 + sigDigits n' = truncate (logBase 16 $ fromIntegral n') + 1 -- * Document @@ -85,7 +92,7 @@ doc !? label = foldM (flip lookup) doc (init chunks) >>= lookup (last chunks) look :: (Monad m) => Label -> Document -> m Value -- ^ Value of field in document, or fail (Nothing) if field not found look k doc = maybe notFound (return . value) (find ((k ==) . label) doc) where - notFound = fail $ "expected " ++ show k ++ " in " ++ show doc + notFound = fail $ "expected " ++ show k ++ " in " ++ show doc lookup :: (Val v, Monad m) => Label -> Document -> m v -- ^ Lookup value of field in document and cast to expected type. Fail (Nothing) if field not found or value not of expected type. @@ -98,7 +105,7 @@ valueAt k = runIdentity . look k at :: forall v. (Val v) => Label -> Document -> v -- ^ Typed value of field in document. Error if missing or wrong type. at k doc = maybe err id (lookup k doc) where - err = error $ "expected (" ++ show k ++ " :: " ++ show (typeOf (undefined :: v)) ++ ") in " ++ show doc + err = error $ "expected (" ++ show k ++ " :: " ++ show (typeOf (undefined :: v)) ++ ") in " ++ show doc include :: [Label] -> Document -> Document -- ^ Only include fields of document in label list @@ -111,9 +118,9 @@ exclude keys doc = filter (\(k := _) -> notElem k keys) doc merge :: Document -> Document -> Document -- ^ Merge documents with preference given to first one when both have the same label. I.e. for every (k := v) in first argument, if k exists in second argument then replace its value with v, otherwise add (k := v) to second argument. merge es doc = foldl f doc es where - f doc (k := v) = case findIndex ((k ==) . label) doc of - Nothing -> doc ++ [k := v] - Just i -> let (x, _ : y) = splitAt i doc in x ++ [k := v] ++ y + f doc (k := v) = case findIndex ((k ==) . label) doc of + Nothing -> doc ++ [k := v] + Just i -> let (x, _ : y) = splitAt i doc in x ++ [k := v] ++ y -- * Field @@ -131,7 +138,7 @@ k =: v = k := val v k =? ma = maybeToList (fmap (k =:) ma) instance Show Field where - showsPrec d (k := v) = showParen (d > 0) $ showString (' ' : T.unpack k) . showString ": " . showsPrec 1 v + showsPrec d (k := v) = showParen (d > 0) $ showString (' ' : T.unpack k) . showString ": " . showsPrec 1 v type Label = Text -- ^ The name of a BSON field @@ -140,61 +147,61 @@ type Label = Text -- | A BSON value is one of the following types of values data Value = - Float Double | - String Text | - Doc Document | - Array [Value] | - Bin Binary | - Fun Function | - Uuid UUID | - Md5 MD5 | - UserDef UserDefined | - ObjId ObjectId | - Bool Bool | - UTC UTCTime | - Null | - RegEx Regex | - JavaScr Javascript | - Sym Symbol | - Int32 Int32 | - Int64 Int64 | - Stamp MongoStamp | - MinMax MinMaxKey - deriving (Typeable, Eq, Ord) + Float Double | + String Text | + Doc Document | + Array [Value] | + Bin Binary | + Fun Function | + Uuid UUID | + Md5 MD5 | + UserDef UserDefined | + ObjId ObjectId | + Bool Bool | + UTC UTCTime | + Null | + RegEx Regex | + JavaScr Javascript | + Sym Symbol | + Int32 Int32 | + Int64 Int64 | + Stamp MongoStamp | + MinMax MinMaxKey + deriving (Typeable, Eq, Ord) instance Show Value where - showsPrec d v = fval (showsPrec d) v + showsPrec d v = fval (showsPrec d) v fval :: (forall a . (Val a) => a -> b) -> Value -> b -- ^ Apply generic function to typed value fval f v = case v of - Float x -> f x - String x -> f x - Doc x -> f x - Array x -> f x - Bin x -> f x - Fun x -> f x - Uuid x -> f x - Md5 x -> f x - UserDef x -> f x - ObjId x -> f x - Bool x -> f x - UTC x -> f x - Null -> f (Nothing :: Maybe Value) - RegEx x -> f x - JavaScr x -> f x - Sym x -> f x - Int32 x -> f x - Int64 x -> f x - Stamp x -> f x - MinMax x -> f x + Float x -> f x + String x -> f x + Doc x -> f x + Array x -> f x + Bin x -> f x + Fun x -> f x + Uuid x -> f x + Md5 x -> f x + UserDef x -> f x + ObjId x -> f x + Bool x -> f x + UTC x -> f x + Null -> f (Nothing :: Maybe Value) + RegEx x -> f x + JavaScr x -> f x + Sym x -> f x + Int32 x -> f x + Int64 x -> f x + Stamp x -> f x + MinMax x -> f x -- * Value conversion cast :: forall m a. (Val a, Monad m) => Value -> m a -- ^ Convert Value to expected type, or fail (Nothing) if not of that type cast v = maybe notType return (cast' v) where - notType = fail $ "expected " ++ show (typeOf (undefined :: a)) ++ ": " ++ show v + notType = fail $ "expected " ++ show (typeOf (undefined :: a)) ++ ": " ++ show v typed :: (Val a) => Value -> a -- ^ Convert Value to expected type. Error if not that type. @@ -208,167 +215,167 @@ typeOfVal = fval typeOf -- | Haskell types of this class correspond to BSON value types class (Typeable a, Show a, Eq a) => Val a where - val :: a -> Value - cast' :: Value -> Maybe a + val :: a -> Value + cast' :: Value -> Maybe a instance Val Double where - val = Float - cast' (Float x) = Just x - cast' (Int32 x) = Just (fromIntegral x) - cast' (Int64 x) = Just (fromIntegral x) - cast' _ = Nothing + val = Float + cast' (Float x) = Just x + cast' (Int32 x) = Just (fromIntegral x) + cast' (Int64 x) = Just (fromIntegral x) + cast' _ = Nothing instance Val Float where - val = Float . realToFrac - cast' (Float x) = Just (realToFrac x) - cast' (Int32 x) = Just (fromIntegral x) - cast' (Int64 x) = Just (fromIntegral x) - cast' _ = Nothing + val = Float . realToFrac + cast' (Float x) = Just (realToFrac x) + cast' (Int32 x) = Just (fromIntegral x) + cast' (Int64 x) = Just (fromIntegral x) + cast' _ = Nothing instance Val Text where - val = String - cast' (String x) = Just x - cast' (Sym (Symbol x)) = Just x - cast' _ = Nothing + val = String + cast' (String x) = Just x + cast' (Sym (Symbol x)) = Just x + cast' _ = Nothing instance Val String where - val = String . T.pack - cast' (String x) = Just $ T.unpack x - cast' (Sym (Symbol x)) = Just $ T.unpack x - cast' _ = Nothing + val = String . T.pack + cast' (String x) = Just $ T.unpack x + cast' (Sym (Symbol x)) = Just $ T.unpack x + cast' _ = Nothing instance Val Document where - val = Doc - cast' (Doc x) = Just x - cast' _ = Nothing + val = Doc + cast' (Doc x) = Just x + cast' _ = Nothing instance Val [Value] where - val = Array - cast' (Array x) = Just x - cast' _ = Nothing + val = Array + cast' (Array x) = Just x + cast' _ = Nothing instance (Val a) => Val [a] where - val = Array . map val - cast' (Array x) = mapM cast x - cast' _ = Nothing + val = Array . map val + cast' (Array x) = mapM cast x + cast' _ = Nothing instance Val Binary where - val = Bin - cast' (Bin x) = Just x - cast' _ = Nothing + val = Bin + cast' (Bin x) = Just x + cast' _ = Nothing instance Val Function where - val = Fun - cast' (Fun x) = Just x - cast' _ = Nothing + val = Fun + cast' (Fun x) = Just x + cast' _ = Nothing instance Val UUID where - val = Uuid - cast' (Uuid x) = Just x - cast' _ = Nothing + val = Uuid + cast' (Uuid x) = Just x + cast' _ = Nothing instance Val MD5 where - val = Md5 - cast' (Md5 x) = Just x - cast' _ = Nothing + val = Md5 + cast' (Md5 x) = Just x + cast' _ = Nothing instance Val UserDefined where - val = UserDef - cast' (UserDef x) = Just x - cast' _ = Nothing + val = UserDef + cast' (UserDef x) = Just x + cast' _ = Nothing instance Val ObjectId where - val = ObjId - cast' (ObjId x) = Just x - cast' _ = Nothing + val = ObjId + cast' (ObjId x) = Just x + cast' _ = Nothing instance Val Bool where - val = Bool - cast' (Bool x) = Just x - cast' _ = Nothing + val = Bool + cast' (Bool x) = Just x + cast' _ = Nothing instance Val UTCTime where - val = UTC - cast' (UTC x) = Just x - cast' _ = Nothing + val = UTC + cast' (UTC x) = Just x + cast' _ = Nothing instance Val POSIXTime where - val = UTC . posixSecondsToUTCTime . roundTo (1/1000) - cast' (UTC x) = Just (utcTimeToPOSIXSeconds x) - cast' _ = Nothing + val = UTC . posixSecondsToUTCTime . roundTo (1/1000) + cast' (UTC x) = Just (utcTimeToPOSIXSeconds x) + cast' _ = Nothing instance Val (Maybe Value) where - val Nothing = Null - val (Just v) = v - cast' Null = Just Nothing - cast' v = Just (Just v) + val Nothing = Null + val (Just v) = v + cast' Null = Just Nothing + cast' v = Just (Just v) instance (Val a) => Val (Maybe a) where - val Nothing = Null - val (Just a) = val a - cast' Null = Just Nothing - cast' v = fmap Just (cast' v) + val Nothing = Null + val (Just a) = val a + cast' Null = Just Nothing + cast' v = fmap Just (cast' v) instance Val Regex where - val = RegEx - cast' (RegEx x) = Just x - cast' _ = Nothing + val = RegEx + cast' (RegEx x) = Just x + cast' _ = Nothing instance Val Javascript where - val = JavaScr - cast' (JavaScr x) = Just x - cast' _ = Nothing + val = JavaScr + cast' (JavaScr x) = Just x + cast' _ = Nothing instance Val Symbol where - val = Sym - cast' (Sym x) = Just x - cast' (String x) = Just (Symbol x) - cast' _ = Nothing + val = Sym + cast' (Sym x) = Just x + cast' (String x) = Just (Symbol x) + cast' _ = Nothing instance Val Int32 where - val = Int32 - cast' (Int32 x) = Just x - cast' (Int64 x) = fitInt x - cast' (Float x) = Just (round x) - cast' _ = Nothing + val = Int32 + cast' (Int32 x) = Just x + cast' (Int64 x) = fitInt x + cast' (Float x) = Just (round x) + cast' _ = Nothing instance Val Int64 where - val = Int64 - cast' (Int64 x) = Just x - cast' (Int32 x) = Just (fromIntegral x) - cast' (Float x) = Just (round x) - cast' _ = Nothing + val = Int64 + cast' (Int64 x) = Just x + cast' (Int32 x) = Just (fromIntegral x) + cast' (Float x) = Just (round x) + cast' _ = Nothing instance Val Int where - val n = maybe (Int64 $ fromIntegral n) Int32 (fitInt n) - cast' (Int32 x) = Just (fromIntegral x) - cast' (Int64 x) = Just (fromEnum x) - cast' (Float x) = Just (round x) - cast' _ = Nothing + val n = maybe (Int64 $ fromIntegral n) Int32 (fitInt n) + cast' (Int32 x) = Just (fromIntegral x) + cast' (Int64 x) = Just (fromEnum x) + cast' (Float x) = Just (round x) + cast' _ = Nothing instance Val Integer where - val n = maybe (maybe err Int64 $ fitInt n) Int32 (fitInt n) where - err = error $ show n ++ " is too large for Bson Int Value" - cast' (Int32 x) = Just (fromIntegral x) - cast' (Int64 x) = Just (fromIntegral x) - cast' (Float x) = Just (round x) - cast' _ = Nothing + val n = maybe (maybe err Int64 $ fitInt n) Int32 (fitInt n) where + err = error $ show n ++ " is too large for Bson Int Value" + cast' (Int32 x) = Just (fromIntegral x) + cast' (Int64 x) = Just (fromIntegral x) + cast' (Float x) = Just (round x) + cast' _ = Nothing instance Val MongoStamp where - val = Stamp - cast' (Stamp x) = Just x - cast' _ = Nothing + val = Stamp + cast' (Stamp x) = Just x + cast' _ = Nothing instance Val MinMaxKey where - val = MinMax - cast' (MinMax x) = Just x - cast' _ = Nothing + val = MinMax + cast' (MinMax x) = Just x + cast' _ = Nothing fitInt :: forall n m. (Integral n, Integral m, Bounded m) => n -> Maybe m -- ^ If number fits in type m then cast to m, otherwise Nothing fitInt n = if fromIntegral (minBound :: m) <= n && n <= fromIntegral (maxBound :: m) - then Just (fromIntegral n) - else Nothing + then Just (fromIntegral n) + else Nothing -- * Haskell types corresponding to special Bson value types @@ -412,13 +419,13 @@ data ObjectId = Oid Word32 Word64 deriving (Typeable, Eq, Ord) -- ^ A BSON ObjectID is a 12-byte value consisting of a 4-byte timestamp (seconds since epoch), a 3-byte machine id, a 2-byte process id, and a 3-byte counter. Note that the timestamp and counter fields must be stored big endian unlike the rest of BSON. This is because they are compared byte-by-byte and we want to ensure a mostly increasing order. instance Show ObjectId where - showsPrec _ (Oid x y) = showHexLen 8 x . showHexLen 16 y + showsPrec _ (Oid x y) = showHexLen 8 x . showHexLen 16 y instance Read ObjectId where - readPrec = do - [(x, "")] <- readHex <$> R.lift (R.count 8 R.get) - y <- R.readS_to_Prec $ const readHex - return (Oid x y) + readPrec = do + [(x, "")] <- readHex <$> R.lift (R.count 8 R.get) + y <- R.readS_to_Prec $ const readHex + return (Oid x y) timestamp :: ObjectId -> UTCTime -- ^ Time when objectId was created @@ -427,19 +434,19 @@ timestamp (Oid time _) = posixSecondsToUTCTime (fromIntegral time) genObjectId :: IO ObjectId -- ^ Create a fresh ObjectId genObjectId = do - time <- truncate <$> getPOSIXTime - pid <- fromIntegral <$> getProcessID - inc <- nextCount - return $ Oid time (composite machineId pid inc) + time <- truncate <$> getPOSIXTime + pid <- fromIntegral <$> getProcessID + inc <- nextCount + return $ Oid time (composite machineId pid inc) where - machineId :: Word24 - machineId = unsafePerformIO (makeWord24 . S.unpack . S.take 3 . MD5.hash . SC.pack <$> getHostName) - {-# NOINLINE machineId #-} - counter :: IORef Word24 - counter = unsafePerformIO (newIORef 0) - {-# NOINLINE counter #-} - nextCount :: IO Word24 - nextCount = atomicModifyIORef counter $ \n -> (wrap24 (n + 1), n) + machineId :: Word24 + machineId = unsafePerformIO (makeWord24 . S.unpack . S.take 3 . MD5.hash . SC.pack <$> getHostName) + {-# NOINLINE machineId #-} + counter :: IORef Word24 + counter = unsafePerformIO (newIORef 0) + {-# NOINLINE counter #-} + nextCount :: IO Word24 + nextCount = atomicModifyIORef counter $ \n -> (wrap24 (n + 1), n) composite :: Word24 -> Word16 -> Word24 -> Word64 composite mid pid inc = fromIntegral mid `shift` 40 .|. fromIntegral pid `shift` 24 .|. fromIntegral inc diff --git a/Data/Bson/Binary.hs b/Data/Bson/Binary.hs index 3e56447..952dce1 100644 --- a/Data/Bson/Binary.hs +++ b/Data/Bson/Binary.hs @@ -1,103 +1,111 @@ -- | Standard binary encoding of BSON documents, version 1.0. See bsonspec.org module Data.Bson.Binary ( - putDocument, getDocument, - putDouble, getDouble, - putInt32, getInt32, - putInt64, getInt64, - putCString, getCString + putDocument, getDocument, + putDouble, getDouble, + putInt32, getInt32, + putInt64, getInt64, + putCString, getCString ) where -import Prelude hiding (length, concat) -import Control.Applicative ((<$>), (<*>)) -import Control.Monad (when) -import Data.Binary.Get (Get, runGet, getWord8, getWord32be, getWord64be, - getWord32le, getWord64le, getLazyByteStringNul, - getLazyByteString, getByteString, lookAhead) -import Data.Binary.Put (Put, runPut, putWord8, putWord32le, putWord64le, - putWord32be, putWord64be, putLazyByteString, - putByteString) -import Data.Binary.IEEE754 (getFloat64le, putFloat64le) -import Data.ByteString (ByteString) -import Data.Int (Int32, Int64) -import Data.Time.Clock (UTCTime) -import Data.Time.Clock.POSIX (posixSecondsToUTCTime, utcTimeToPOSIXSeconds) -import Data.Word (Word8) - -import qualified Data.ByteString.Char8 as SC +import Control.Applicative ((<$>), (<*>)) +import Control.Monad (when) +import Data.Binary.Get (Get, getByteString, + getLazyByteString, + getLazyByteStringNul, getWord32be, + getWord32le, getWord64be, + getWord64le, getWord8, lookAhead, + runGet) +import Data.Binary.IEEE754 (getFloat64le, putFloat64le) +import Data.Binary.Put (Put, putByteString, + putLazyByteString, putWord32be, + putWord32le, putWord64be, + putWord64le, putWord8, runPut) +import Data.ByteString (ByteString) +import Data.Int (Int32, Int64) +import Data.Time.Clock (UTCTime) +import Data.Time.Clock.POSIX (posixSecondsToUTCTime, + utcTimeToPOSIXSeconds) +import Data.Word (Word8) +import Prelude hiding (concat, length) + +import qualified Data.ByteString.Char8 as SC import qualified Data.ByteString.Lazy.Char8 as LC -import Data.Text (Text) -import qualified Data.Text as T -import qualified Data.Text.Encoding as TE +import Data.Text (Text) +import qualified Data.Text as T +import qualified Data.Text.Encoding as TE -import Data.Bson (Document, Value(..), ObjectId(..), MongoStamp(..), Symbol(..), - Javascript(..), UserDefined(..), Regex(..), MinMaxKey(..), - Binary(..), UUID(..), Field(..), MD5(..), Function(..)) +import Data.Bson (Binary (..), Document, Field (..), + Function (..), Javascript (..), + MD5 (..), MinMaxKey (..), + MongoStamp (..), ObjectId (..), + Regex (..), Symbol (..), UUID (..), + UserDefined (..), Value (..)) putField :: Field -> Put -- ^ Write binary representation of element putField (k := v) = case v of - Float x -> putTL 0x01 >> putDouble x - String x -> putTL 0x02 >> putString x - Doc x -> putTL 0x03 >> putDocument x - Array x -> putTL 0x04 >> putArray x - Bin (Binary x) -> putTL 0x05 >> putBinary 0x00 x - Fun (Function x) -> putTL 0x05 >> putBinary 0x01 x - Uuid (UUID x) -> putTL 0x05 >> putBinary 0x04 x - Md5 (MD5 x) -> putTL 0x05 >> putBinary 0x05 x - UserDef (UserDefined x) -> putTL 0x05 >> putBinary 0x80 x - ObjId x -> putTL 0x07 >> putObjectId x - Bool x -> putTL 0x08 >> putBool x - UTC x -> putTL 0x09 >> putUTC x - Null -> putTL 0x0A - RegEx x -> putTL 0x0B >> putRegex x - JavaScr (Javascript env code) -> if null env - then putTL 0x0D >> putString code - else putTL 0x0F >> putClosure code env - Sym x -> putTL 0x0E >> putSymbol x - Int32 x -> putTL 0x10 >> putInt32 x - Int64 x -> putTL 0x12 >> putInt64 x - Stamp x -> putTL 0x11 >> putMongoStamp x - MinMax x -> case x of - MinKey -> putTL 0xFF - MaxKey -> putTL 0x7F + Float x -> putTL 0x01 >> putDouble x + String x -> putTL 0x02 >> putString x + Doc x -> putTL 0x03 >> putDocument x + Array x -> putTL 0x04 >> putArray x + Bin (Binary x) -> putTL 0x05 >> putBinary 0x00 x + Fun (Function x) -> putTL 0x05 >> putBinary 0x01 x + Uuid (UUID x) -> putTL 0x05 >> putBinary 0x04 x + Md5 (MD5 x) -> putTL 0x05 >> putBinary 0x05 x + UserDef (UserDefined x) -> putTL 0x05 >> putBinary 0x80 x + ObjId x -> putTL 0x07 >> putObjectId x + Bool x -> putTL 0x08 >> putBool x + UTC x -> putTL 0x09 >> putUTC x + Null -> putTL 0x0A + RegEx x -> putTL 0x0B >> putRegex x + JavaScr (Javascript env code) -> if null env + then putTL 0x0D >> putString code + else putTL 0x0F >> putClosure code env + Sym x -> putTL 0x0E >> putSymbol x + Int32 x -> putTL 0x10 >> putInt32 x + Int64 x -> putTL 0x12 >> putInt64 x + Stamp x -> putTL 0x11 >> putMongoStamp x + MinMax x -> case x of + MinKey -> putTL 0xFF + MaxKey -> putTL 0x7F where - putTL t = putTag t >> putLabel k + putTL t = putTag t >> putLabel k getField :: Get Field -- ^ Read binary representation of Element getField = do - t <- getTag - k <- getLabel - v <- case t of - 0x01 -> Float <$> getDouble - 0x02 -> String <$> getString - 0x03 -> Doc <$> getDocument - 0x04 -> Array <$> getArray - 0x05 -> getBinary >>= \(s, b) -> case s of - 0x00 -> return $ Bin (Binary b) - 0x01 -> return $ Fun (Function b) - 0x03 -> return $ Uuid (UUID b) - 0x04 -> return $ Uuid (UUID b) - 0x05 -> return $ Md5 (MD5 b) - 0x80 -> return $ UserDef (UserDefined b) - _ -> fail $ "unknown Bson binary subtype " ++ show s - 0x07 -> ObjId <$> getObjectId - 0x08 -> Bool <$> getBool - 0x09 -> UTC <$> getUTC - 0x0A -> return Null - 0x0B -> RegEx <$> getRegex - 0x0D -> JavaScr . Javascript [] <$> getString - 0x0F -> JavaScr . uncurry (flip Javascript) <$> getClosure - 0x0E -> Sym <$> getSymbol - 0x10 -> Int32 <$> getInt32 - 0x12 -> Int64 <$> getInt64 - 0x11 -> Stamp <$> getMongoStamp - 0xFF -> return (MinMax MinKey) - 0x7F -> return (MinMax MaxKey) - _ -> fail $ "unknown Bson value type " ++ show t - return (k := v) + t <- getTag + k <- getLabel + v <- case t of + 0x01 -> Float <$> getDouble + 0x02 -> String <$> getString + 0x03 -> Doc <$> getDocument + 0x04 -> Array <$> getArray + 0x05 -> getBinary >>= \(s, b) -> case s of + 0x00 -> return $ Bin (Binary b) + 0x01 -> return $ Fun (Function b) + 0x03 -> return $ Uuid (UUID b) + 0x04 -> return $ Uuid (UUID b) + 0x05 -> return $ Md5 (MD5 b) + 0x80 -> return $ UserDef (UserDefined b) + _ -> fail $ "unknown Bson binary subtype " ++ show s + 0x07 -> ObjId <$> getObjectId + 0x08 -> Bool <$> getBool + 0x09 -> UTC <$> getUTC + 0x0A -> return Null + 0x0B -> RegEx <$> getRegex + 0x0D -> JavaScr . Javascript [] <$> getString + 0x0F -> JavaScr . uncurry (flip Javascript) <$> getClosure + 0x0E -> Sym <$> getSymbol + 0x10 -> Int32 <$> getInt32 + 0x12 -> Int64 <$> getInt64 + 0x11 -> Stamp <$> getMongoStamp + 0xFF -> return (MinMax MinKey) + 0x7F -> return (MinMax MaxKey) + _ -> fail $ "unknown Bson value type " ++ show t + return (k := v) putTag = putWord8 getTag = getWord8 @@ -122,44 +130,44 @@ getInt64 = fromIntegral <$> getWord64le putCString :: Text -> Put putCString x = do - putByteString $ TE.encodeUtf8 x - putWord8 0 + putByteString $ TE.encodeUtf8 x + putWord8 0 getCString :: Get Text getCString = TE.decodeUtf8 . SC.concat . LC.toChunks <$> getLazyByteStringNul putString :: Text -> Put putString x = let b = TE.encodeUtf8 x in do - putInt32 $ toEnum (SC.length b + 1) - putByteString b - putWord8 0 + putInt32 $ toEnum (SC.length b + 1) + putByteString b + putWord8 0 getString :: Get Text getString = do - len <- subtract 1 <$> getInt32 - b <- getByteString (fromIntegral len) - getWord8 - return $ TE.decodeUtf8 b + len <- subtract 1 <$> getInt32 + b <- getByteString (fromIntegral len) + getWord8 + return $ TE.decodeUtf8 b putDocument :: Document -> Put putDocument es = let b = runPut (mapM_ putField es) in do - putInt32 $ (toEnum . fromEnum) (LC.length b + 5) -- include length and null terminator - putLazyByteString b - putWord8 0 + putInt32 $ (toEnum . fromEnum) (LC.length b + 5) -- include length and null terminator + putLazyByteString b + putWord8 0 getDocument :: Get Document getDocument = do - len <- subtract 4 <$> getInt32 - b <- getLazyByteString (fromIntegral len) - return (runGet getFields b) + len <- subtract 4 <$> getInt32 + b <- getLazyByteString (fromIntegral len) + return (runGet getFields b) where - getFields = lookAhead getWord8 >>= \done -> if done == 0 - then return [] - else (:) <$> getField <*> getFields + getFields = lookAhead getWord8 >>= \done -> if done == 0 + then return [] + else (:) <$> getField <*> getFields putArray :: [Value] -> Put putArray vs = putDocument (zipWith f [0..] vs) - where f i v = (T.pack $! show i) := v + where f i v = (T.pack $! show i) := v getArray :: Get [Value] getArray = map value <$> getDocument @@ -168,33 +176,33 @@ type Subtype = Word8 putBinary :: Subtype -> ByteString -> Put putBinary t x = let len = toEnum (SC.length x) in do - putInt32 len - putTag t - putByteString x + putInt32 len + putTag t + putByteString x getBinary :: Get (Subtype, ByteString) getBinary = do - len <- getInt32 - t <- getTag - x <- getByteString (fromIntegral len) - return (t, x) + len <- getInt32 + t <- getTag + x <- getByteString (fromIntegral len) + return (t, x) {-putBinary :: Subtype -> ByteString -> Put -- When Binary subtype (0x02) insert extra length field before bytes putBinary t x = let len = toEnum (length x) in do - putInt32 $ len + if t == 0x02 then 4 else 0 - putTag t - when (t == 0x02) (putInt32 len) - putByteString x-} + putInt32 $ len + if t == 0x02 then 4 else 0 + putTag t + when (t == 0x02) (putInt32 len) + putByteString x-} {-getBinary :: Get (Subtype, ByteString) -- When Binary subtype (0x02) there is an extra length field before bytes getBinary = do - len <- getInt32 - t <- getTag - len' <- if t == 0x02 then getInt32 else return len - x <- getByteString (fromIntegral len') - return (t, x)-} + len <- getInt32 + t <- getTag + len' <- if t == 0x02 then getInt32 else return len + x <- getByteString (fromIntegral len') + return (t, x)-} putRegex (Regex x y) = putCString x >> putCString y getRegex = Regex <$> getCString <*> getCString @@ -221,15 +229,15 @@ getUTC = posixSecondsToUTCTime . (/ 1000) . fromIntegral <$> getInt64 putClosure :: Text -> Document -> Put putClosure x y = let b = runPut (putString x >> putDocument y) in do - putInt32 $ (toEnum . fromEnum) (LC.length b + 4) -- including this length field - putLazyByteString b + putInt32 $ (toEnum . fromEnum) (LC.length b + 4) -- including this length field + putLazyByteString b getClosure :: Get (Text, Document) getClosure = do - getInt32 - x <- getString - y <- getDocument - return (x, y) + getInt32 + x <- getString + y <- getDocument + return (x, y) {- Authors: Tony Hannan diff --git a/bson.cabal b/bson.cabal index 077ded7..2e6984e 100644 --- a/bson.cabal +++ b/bson.cabal @@ -9,7 +9,7 @@ Description: A BSON Document is an untyped (dynamically type-checked) record. and special types (Binary, Javascript, ObjectId, RegEx, and a few others). - A BSON Document is serialized to a standard binary encoding + A BSON Document is serialized to a standard binary encoding defined at . This implements version 1 of that spec. Category: Data diff --git a/tests/Data/Bson/Binary/Tests.hs b/tests/Data/Bson/Binary/Tests.hs index 71b5ee0..47fac8b 100644 --- a/tests/Data/Bson/Binary/Tests.hs +++ b/tests/Data/Bson/Binary/Tests.hs @@ -2,7 +2,7 @@ module Data.Bson.Binary.Tests ( tests ) where -import Test.Framework (Test, testGroup) +import Test.Framework (Test, testGroup) tests :: Test -tests = testGroup "Data.Bson.Binary.Tests" [] \ No newline at end of file +tests = testGroup "Data.Bson.Binary.Tests" [] diff --git a/tests/Data/Bson/Tests.hs b/tests/Data/Bson/Tests.hs index 991e873..3481df4 100644 --- a/tests/Data/Bson/Tests.hs +++ b/tests/Data/Bson/Tests.hs @@ -4,24 +4,33 @@ module Data.Bson.Tests ( tests ) where -import Control.Applicative ((<$>), (<*>)) -import Data.Int (Int32, Int64) -import Data.Time.Calendar (Day(ModifiedJulianDay)) -import Data.Time.Clock.POSIX (POSIXTime) -import Data.Time.Clock (UTCTime(..), addUTCTime) -import qualified Data.ByteString as S - -import Data.Text (Text) -import Test.Framework (Test, testGroup) -import Test.Framework.Providers.QuickCheck2 (testProperty) -import Test.QuickCheck (Arbitrary(..), elements, oneof) -import qualified Data.Text as T - -import Data.Bson (Val(cast', val), ObjectId(..), MinMaxKey(..), MongoStamp(..), - Symbol(..), Javascript(..), Regex(..), UserDefined(..), - MD5(..), UUID(..), Function(..), Binary(..), Field((:=)), - Document, Value(..)) -import qualified Data.Bson as Bson +import Control.Applicative ((<$>), (<*>)) +import qualified Data.ByteString as S +import Data.Int (Int32, Int64) +import Data.Time.Calendar (Day (ModifiedJulianDay)) +import Data.Time.Clock (UTCTime (..), addUTCTime) +import Data.Time.Clock.POSIX (POSIXTime) + +import Data.Text (Text) +import qualified Data.Text as T +import Test.Framework (Test, testGroup) +import Test.Framework.Providers.QuickCheck2 (testProperty) +import Test.QuickCheck (Arbitrary (..), elements, + oneof) + +import Data.Bson (Binary (..), Document, + Field ((:=)), + Function (..), + Javascript (..), + MD5 (..), MinMaxKey (..), + MongoStamp (..), + ObjectId (..), + Regex (..), Symbol (..), + UUID (..), + UserDefined (..), + Val (cast', val), + Value (..)) +import qualified Data.Bson as Bson instance Arbitrary S.ByteString where arbitrary = S.pack <$> arbitrary diff --git a/tests/Tests.hs b/tests/Tests.hs index e1f1109..a9bd0bf 100644 --- a/tests/Tests.hs +++ b/tests/Tests.hs @@ -1,9 +1,9 @@ module Main where -import Test.Framework (defaultMain) +import Test.Framework (defaultMain) -import qualified Data.Bson.Tests import qualified Data.Bson.Binary.Tests +import qualified Data.Bson.Tests main :: IO () main = defaultMain