tagged-binary-0.2.0.0: Data/Binary/Tagged/Internal.hs
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE OverloadedStrings #-}
-- |
-- Module : Data.Binary.Tagged.Internal
-- Copyright : (c) Justin Le 2014
-- License : MIT
--
-- Maintainer : justin@jle.im
-- Stability : unstable
-- Portability : portable
--
-- Internals for the library, exported in case you should need it.
-- Usually, the parts you would need should be re-exported in
-- 'Data.Binary.Tagged'.
--
module Data.Binary.Tagged.Internal (
-- * Data types
Tagged -- abstract, instances: Show, Eq, Binary, Typeable, Generic
, TagFingerprint -- abstract, instances: Show, Eq, Ord, Binary, Typeable, Generic, Default
-- * Tagging and extracting data
, tag -- :: Typeable a => a -> Tagged a
, getTagged -- :: Typeable a => Tagged a -> Maybe a
, tagMatched -- :: Typeable a => Tagged a -> Bool
-- * 'TagFingerprint' utilities
, typeFingerprint -- :: Typeable a => a -> TagFingerprint
, tagFingerprint -- :: Tagged a -> TagFingerprint
, bsFingerprint -- :: ByteString -> Maybe TagFingerprint
, emptyTagFP -- :: TagFingerprint
) where
import Control.Applicative ((<$>),(<*>))
import Control.Monad (guard, forM_)
import Data.Binary
import Data.Binary.Get
import Data.ByteString.Lazy.Char8 as LC
import Data.Digest.Pure.MD5
import Data.Maybe (isJust)
import Data.Typeable.Internal
import GHC.Generics
-- | A data type tupling together data with a 'TagFingerprint',
-- representing data tagged with its type.
--
-- It's best to interface directly with data using 'encodeTagged',
-- 'decodeTagged', etc, using 'tag' to tag data and 'extractTagged' to
-- extract data from valid tagged data. This type is exported mostly when
-- you want to specifically decode a 'ByteString' into tagged data, and
-- manually extract it yourself. If you are writing a framework, it is
-- preferred to handle this for the end user.
data Tagged a = Tagged !TagFingerprint !a
deriving (Show, Eq, Generic, Typeable)
instance Binary a => Binary (Tagged a) where
put (Tagged fp x) = do
put TagLead
put fp
put x
get = do
_ <- get :: Get TagLead
Tagged <$> get <*> get
-- | A data type representing a fingerprint for a 'Typeable' type.
-- Ideally, this would be 'Data.Typeable.Internal''s own 'Fingerprint'
-- types; however, for some reason, the fingerprints for the same data type
-- from the same modules differ between different GHC backends. So for
-- now, it is just a 'ByteString' representation of the name of the type.
-- This is literally a bad idea, and so two types with the same name but
-- from different modules will share a non-unique 'TagFingerprint'.
-- Hopefully in the future when I find out a way to fix this or the GHC
-- backend maintainers find a way to provide consistent type fingerprints,
-- this will be fixed.
--
-- This type is mostly used for the ability to categorized Tagged items
-- by their type.
--
-- 'emptyTagFP' gives a 'TagFingerprint' that will most likely never be
-- matched by any actual tag from a real type, so can be used as a test if
-- needed. This replaces functionality that used to come from the
-- 'Default' instance.
newtype TagFingerprint = TagFP MD5Digest
deriving (Show, Typeable, Generic, Eq, Ord)
instance Binary TagFingerprint
-- | 'TagFingerprint' that is meant to never be matched by any actual
-- normal type's 'TagFingerprint'.
emptyTagFP :: TagFingerprint
emptyTagFP = TagFP (md5 "")
-- | Put at the start of a 'Tagged' to signify that it is a 'Tagged'.
data TagLead = TagLead
instance Binary TagLead where
put _ = mapM_ put leadingBytes
get = do
forM_ leadingBytes $ \b ->
guard . (== b) =<< get
return TagLead
leadingBytes :: [Word8]
leadingBytes = [0xfe,0xfe]
-- | Wrap data inside a 'Tagged' tuple.
tag :: Typeable a => a -> Tagged a
tag x = Tagged (typeFingerprint x) x
-- | Compute the 'Fingerprint' representing a type. It is non-strict on
-- its parameter, so passing in undefined should work if you want to just
-- get the 'Fingerprint' of a specific type without having data of that
-- type on hand:
--
-- > typeFingerprint (undefined :: Int)
--
typeFingerprint :: Typeable a => a -> TagFingerprint
typeFingerprint = TagFP . md5 . LC.pack . show . typeOf
-- | Extract data out of a 'Tagged', but only the type of the data matches
-- the type represented by the fingerprint. It is polymorphic on its
-- output and meant to be used when decoding a 'Tagged' item with a desired
-- type.
getTagged :: Typeable a => Tagged a -> Maybe a
getTagged (Tagged tfp x) | tfp == xfp = Just x
| otherwise = Nothing
where
xfp = typeFingerprint x
-- | Check if the type inside the 'Tagged' matches the fingerprint.
tagMatched :: Typeable a => Tagged a -> Bool
tagMatched = isJust . getTagged
-- | Extract the 'Fingerprint' out of a 'Tagged'. Mostly used so that you
-- can categorize and associate Tagged items; to check if a 'Tagged' is
-- of a desired typed, 'getTagged' and 'tagMatched' might be more useful.
tagFingerprint :: Tagged a -> TagFingerprint
tagFingerprint (Tagged fp _) = fp
-- | With a 'ByteString', expecting tagged data, returns the 'Fingerprint'
-- that the data is tagged with. Returns @Nothing@ if the data is not
-- decodable as tagged data. Might accidentally decode untagged data
-- though!
bsFingerprint :: ByteString -> Maybe TagFingerprint
bsFingerprint bs = case getRes of
Left _ -> Nothing
Right (_,_,fp) -> Just fp
where
getRes = flip runGetOrFail bs $ do
_ <- get :: Get TagLead
get