{-# LANGUAGE TemplateHaskell, ExistentialQuantification #-} ---------------------------------------------------------------------------- -- | -- Module : Data.Binary.Bitmap.TH -- Copyright : (c) Ilya Portnov 2014 -- License : BSD3-style (see LICENSE) -- -- Maintainer : portnov84@rambler.ru -- Stability : unstable -- Portability : not tested -- -- This module contains QuasiQuoter for declarative description of -- ISO 8583-based message formats. -- ---------------------------------------------------------------------------- module Data.Binary.ISO8583.TH (-- * Usage -- $usage FieldType (..), FieldValue (..), Field (..), pData, string2data, binaryQ, binary ) where import Control.Monad import Text.Parsec import qualified Text.Parsec.Token as P import Text.Parsec.Language (haskellDef) import Text.Parsec.String import qualified Data.ByteString as B import qualified Data.ByteString.Lazy as L import Data.ByteString (ByteString) import qualified Data.Map as M import Data.Generics import Language.Haskell.TH.Syntax import Language.Haskell.TH.Quote import Language.Haskell.TH.Lift import Data.Binary import Data.Binary.Get import Data.Binary.Put import Data.Binary.ISO8583 -- $usage -- -- Typical usage is: -- -- > [binary| -- > Message -- > 2 pan embedded 2 -- > 4 amount int 12 -- > 11 stan int 6 -- > 43 termAddress TermAddress 222 -- > |] -- -- > data TermAddress = TermAddress ... -- -- > instance Binary TermAddress where ... -- -- Quasi-quote format is: -- -- - First line - name of data type to generate -- -- - Each of other lines describes one field, in following format: -- {Field number} {Field name} {data type} {type parameter} -- -- Internally supported data types are: -- -- - int - integer field. Parameter defines size of field. -- -- - str - fixed-length string field. Parameter defines size of field. -- -- - embedded - embedded-length string field. Parameter defines number of bytes used to store field length (2 for LLVAR, 3 for LLLVAR and so on). -- -- Any other data type, instance of Binary class, may be used as well. -- -- Quasi-quoter generates data type definition, and also following functions: -- -- - get[Message] :: Int -> Maybe (Get FieldValue) -- -- - put[Message] :: Message -> [(Int, Maybe Put)] -- -- - construct[Message] :: M.Map Int FieldValue -> Message -- -- Concrete ISO 8583-based formats usually use some kind of message header, or -- use bitmap only as small part of overall format. So, it's usually no point of -- generating instance Binary Message - it's better to write instance for your -- format by using functions mentioned above. -- -- > instance Binary Message where -- > get = do -- > -- parse some kind of header here, then: -- > m <- getBitmap getMessage -- > return $ constructMessage m -- > -- > put msg = do -- > -- write some kind of header here, then: -- > putBitmap' (putMessage msg) -- -- | Supported field types data FieldType = TInt Int -- ^ Integer field of given size | TString Int -- ^ Fixed-length string field | TEmbeddedLen Int -- ^ Variable-length string field with embedded length (LLVAR and so on) | TOther String Int -- ^ User-defined field - any data type with instance Binary. NB: second parameter is not used currently. deriving (Eq, Show) deriveLift ''FieldType -- | Field format description data Field = Field { fNumber :: Int -- ^ Field number , fName :: String -- ^ Field name , fType :: FieldType -- ^ Field type description } deriving (Eq, Show) -- | Supported field values data FieldValue = FInt Integer -- ^ Integer field | FString ByteString -- ^ String fields | forall t. (Binary t, Show t, Typeable t) => FOther t -- ^ User-defined field instance Eq FieldValue where (FInt x) == (FInt y) = x == y (FString x) == (FString y) = x == y (FOther x) == (FOther y) = runPut (put x) == runPut (put y) _ == _ = False instance Show FieldValue where show (FInt x) = show x show (FString x) = show x show (FOther x) = show $ runPut (put x) unpackFInt :: FieldValue -> Integer unpackFInt (FInt x) = x unpackFInt x = error $ "Internal error: not an integer: " ++ show x unpackFString :: FieldValue -> ByteString unpackFString (FString str) = str unpackFString x = error $ "Internal error: not a string: " ++ show x toStrict :: L.ByteString -> B.ByteString toStrict s = B.concat $ L.toChunks s fromStrict :: B.ByteString -> L.ByteString fromStrict s = L.fromChunks [s] unpackOther :: FieldValue -> ByteString unpackOther (FOther x) = toStrict $ runPut (put x) unpackOther _ = error $ "Internal error: not a FOther" lexer = P.makeTokenParser haskellDef identifier = P.identifier lexer number = P.natural lexer pField :: Parser Field pField = do skipMany (space <|> newline) n <- number skipMany space name <- identifier skipMany space t <- identifier skipMany space param <- number ft <- case t of "int" -> return $ TInt (fromIntegral param) "str" -> return $ TString (fromIntegral param) "embedded" -> return $ TEmbeddedLen (fromIntegral param) _ -> return $ TOther t (fromIntegral param) return $ Field (fromIntegral n) name ft pFields :: Parser [Field] pFields = do fs <- pField `sepEndBy1` many newline eof return fs -- | Parse data format definition pData :: Parser (String, [Field]) pData = do skipMany (space <|> newline) name <- identifier skipMany newline fs <- pFields return (name, fs) -- | Generate `data Message = Message {...' generateData :: String -- ^ Data type name -> [Field] -- ^ Fields description -> Q [Dec] generateData nameStr fields = do let name = mkName nameStr fields' <- mapM convertField fields let constructor = RecC name fields' return [ DataD [] name [] [constructor] [] ] where convertField (Field _ fname ftype) = do t <- convertType ftype return (mkName fname, NotStrict, t) convertType (TInt _) = [t| Maybe Integer |] convertType (TString _) = [t| Maybe B.ByteString |] convertType (TEmbeddedLen _) = [t| Maybe B.ByteString |] convertType (TOther n _) = do let name = mkName n return $ ConT (mkName "Maybe") `AppT` ConT name -- | Generate `getMessage 2 = Just $ ...' mkGetter :: String -> [Field] -> Q [Dec] mkGetter nameStr fields = do let name = mkName nameStr clauses <- forM fields $ \(Field f _ ft) -> mkGetClause f ft unsupportedClause <- mkUnsupportedClause return [FunD name (clauses ++ [unsupportedClause])] where mkGetClause :: Int -> FieldType -> Q Clause mkGetClause n ft = do let pats = [LitP (IntegerL (fromIntegral n))] let int x = return $ LitE $ IntegerL (fromIntegral x) body <- case ft of TInt sz -> [| Just $ FInt `fmap` asciiNumberF $(int n) $(int sz) |] TString sz -> [| Just $ FString `fmap` getByteString $(int sz) |] TEmbeddedLen sz -> [| Just $ FString `fmap` embeddedLen $(int n) $(int sz) |] TOther name _ -> [| Just $ FOther `fmap` (get :: Get $(return $ ConT $ mkName name) ) |] return $ Clause pats (NormalB body) [] mkUnsupportedClause :: Q Clause mkUnsupportedClause = do body <- [| Nothing |] let pats = [ WildP ] return $ Clause pats (NormalB body) [] -- | Generate `putMessage' mkPutter :: String -> [Field] -> Q [Dec] mkPutter nameStr fields = do msg <- newName "msg" let name = mkName nameStr pat = VarP msg body <- ListE `fmap` mapM (mkBody msg) fields let clause = Clause [pat] (NormalB body) [] return [FunD name [clause]] where mkBody msg (Field f fname ftype) = do let getter = return $ VarE $ mkName fname let msgvar = return $ VarE msg let wrapper = case ftype of TInt _ -> return $ ConE $ mkName "FInt" TString _ -> return $ ConE $ mkName "FString" TEmbeddedLen _ -> return $ ConE $ mkName "FString" TOther name _ -> return $ ConE $ mkName "FOther" let putter = [| putField $(lift ftype) ( $(wrapper) `fmap` $(getter) $(msgvar) ) |] [| ( $(lift f) , $(putter) ) |] putField :: FieldType -> Maybe FieldValue -> Maybe Put putField _ Nothing = Nothing putField (TInt sz) (Just (FInt n)) = Just $ putAsciiNumber sz n putField (TString sz) (Just (FString s)) = Just $ putByteStringPad sz s putField (TEmbeddedLen sz) (Just (FString s)) = Just $ putEmbeddedLen sz s putField (TOther _ _) (Just (FOther x)) = Just $ put x putField t (Just v) = fail $ "Internal error: field value " ++ show v ++ " does not match type " ++ show t mkConstructor :: String -> String -> [Field] -> Q [Dec] mkConstructor prefix nameStr fields = do msg <- newName "msg" let name = mkName nameStr pat = VarP msg body <- RecConE name `fmap` mapM (mkBody msg) fields let clause = Clause [pat] (NormalB body) [] return [FunD (mkName $ prefix ++ nameStr) [clause]] where mkBody msg (Field f fname ftype) = do let msgvar = return $ VarE msg let unpack = case ftype of TInt _ -> [| unpackFInt |] TString _ -> [| unpackFString |] TEmbeddedLen _ -> [| unpackFString |] TOther _ _ -> [| runGet get . fromStrict . unpackOther |] rhs <- [| $(unpack) `fmap` M.lookup $(lift f) $(msgvar) |] return (mkName fname, rhs) -- | Generate only data type definition string2data :: String -> Q [Dec] string2data str = do case parse pData "" str of Right (name, fields) -> generateData name fields Left err -> fail $ show err binaryQ :: String -> Q [Dec] binaryQ str = do case parse pData "" str of Right (name, fields) -> do dtype <- generateData name fields getter <- mkGetter ("get" ++ name) fields putter <- mkPutter ("put" ++ name) fields cons <- mkConstructor "construct" name fields return $ dtype ++ getter ++ putter ++ cons Left err -> fail $ show err dataOnly :: QuasiQuoter dataOnly = QuasiQuoter {quoteDec = string2data, quoteExp=undefined, quotePat=undefined, quoteType=undefined} -- | Main function here. binary :: QuasiQuoter binary = QuasiQuoter {quoteDec = binaryQ, quoteExp=undefined, quotePat=undefined, quoteType=undefined}