roshask-0.2: src/executable/Parse.hs
{-# LANGUAGE OverloadedStrings, TupleSections #-}
-- | Parser components for the ROS message description language (@msg@
-- files). See http://wiki.ros.org/msg for reference.
module Parse (parseMsg, parseSrv, simpleFieldAssoc) where
import Prelude hiding (takeWhile)
import Control.Applicative
import Control.Arrow ((&&&))
import Data.Attoparsec.ByteString.Char8
import Data.ByteString (ByteString)
import Data.ByteString.Char8 (pack, unpack)
import qualified Data.ByteString.Char8 as B
import Data.Char (toLower, digitToInt)
import Data.Either (partitionEithers)
import Data.List (foldl')
import System.FilePath (dropExtension, takeFileName, splitDirectories)
import Types
simpleFieldTypes :: [MsgType]
simpleFieldTypes = [ RBool, RInt8, RUInt8, RInt16, RUInt16, RInt32, RUInt32,
RInt64, RUInt64, RFloat32, RFloat64, RString,
RTime, RDuration, RByte, RChar ]
simpleFieldAssoc :: [(MsgType, ByteString)]
simpleFieldAssoc = map (id &&& B.pack . map toLower . tail . show)
simpleFieldTypes
eatLine :: Parser ()
eatLine = manyTill anyChar (eitherP endOfLine endOfInput) *> skipSpace
parseName :: Parser ByteString
parseName = skipSpace *> identifier <* eatLine <* try comment
identifier :: Parser ByteString
identifier = B.cons <$> letter_ascii <*> takeWhile validChar
where validChar c = any ($ c) [isDigit, isAlpha_ascii, (== '_'), (== '/')]
parseInt :: Parser Int
parseInt = foldl' (\s x -> s*10 + digitToInt x) 0 <$> many1 digit
comment :: Parser [()]
comment = many $ skipSpace *> try (char '#' *> eatLine)
simpleParser :: (MsgType, ByteString) -> Parser (ByteString, MsgType)
simpleParser (t,b) = (, t) <$> (string b *> space *> parseName)
fixedArrayParser :: (MsgType, ByteString) -> Parser (ByteString, MsgType)
fixedArrayParser (t,b) = (\len name -> (name, RFixedArray len t)) <$>
(string b *> char '[' *> parseInt <* char ']') <*>
(space *> parseName)
varArrayParser :: (MsgType, ByteString) -> Parser (ByteString, MsgType)
varArrayParser (t,b) = (, RVarArray t) <$>
(string b *> string "[]" *> space *> parseName)
userTypeParser :: Parser (ByteString, MsgType)
userTypeParser = choice [userSimple, userVarArray, userFixedArray]
userSimple :: Parser (ByteString, MsgType)
userSimple = (\t name -> (name, RUserType t)) <$>
identifier <*> (space *> parseName)
userVarArray :: Parser (ByteString, MsgType)
userVarArray = (\t name -> (name, RVarArray (RUserType t))) <$>
identifier <*> (string "[]" *> space *> parseName)
userFixedArray :: Parser (ByteString, MsgType)
userFixedArray = (\t n name -> (name, RFixedArray n (RUserType t))) <$>
identifier <*>
(char '[' *> parseInt <* char ']') <*>
(space *> parseName)
-- Parse constants defined in the message
constParser :: ByteString -> MsgType ->
Parser (ByteString, MsgType, ByteString)
constParser s x = (,x,) <$>
(string s *> space *> identifier) <*>
(skipSpace *> char '=' *> skipSpace *> restOfLine <* skipSpace)
where restOfLine :: Parser ByteString
restOfLine = pack <$> manyTill anyChar (eitherP endOfLine endOfInput)
constParsers :: [Parser (ByteString, MsgType, ByteString)]
constParsers = map (uncurry constParser . swap) simpleFieldAssoc
where swap (x,y) = (y,x)
-- String constants are parsed somewhat differently from numeric
-- constants. For numerical constants, we drop comments and trailing
-- spaces. For strings, we take the whole line (so comments aren't
-- stripped).
sanitizeConstants :: (a, MsgType, ByteString) -> (a, MsgType, ByteString)
sanitizeConstants c@(_, RString, _) = c
sanitizeConstants (name, t, val) =
(name, t, B.takeWhile (\c -> c /= '#' && not (isSpace c)) val)
-- Parsers fields and constants.
fieldParsers :: [Parser (Either (ByteString, MsgType)
(ByteString, MsgType, ByteString))]
fieldParsers = map (comment *>) $
map (Right . sanitizeConstants <$>) constParsers ++
map (Left <$>) (builtIns ++ [userTypeParser])
where builtIns = concatMap (`map` simpleFieldAssoc)
[simpleParser, fixedArrayParser, varArrayParser]
mkParser :: MsgName -> String -> ByteString -> Parser Msg
mkParser sname lname txt = aux . partitionEithers <$> many (choice fieldParsers)
where aux (fs, cs) = Msg sname lname txt
(map buildField fs)
(map buildConst cs)
buildField :: (ByteString, MsgType) -> MsgField
buildField (name,typ) = MsgField (sanitize name) typ name
buildConst :: (ByteString, MsgType, ByteString) -> MsgConst
buildConst (name,typ,val) = MsgConst (sanitize name) typ val name
{-
testMsg :: ByteString
testMsg = "# Foo bar\n\n# \nHeader header # a header\nuint32 aNum # a number \n # It's not important\ngeometry_msgs/PoseStamped[] poses\nbyte DEBUG=1 #debug level\n"
test :: Result Msg
test = feed (parse (comment *> (mkParser "" "" testMsg)) testMsg) ""
-}
-- Ensure that field and constant names are valid Haskell identifiers
-- and do not coincide with Haskell reserved words.
sanitize :: ByteString -> ByteString
sanitize "data" = "_data"
sanitize "type" = "_type"
sanitize "class" = "_class"
sanitize "module" = "_module"
sanitize x = B.cons (toLower (B.head x)) (B.tail x)
pkgName :: FilePath -> String
pkgName f = let parts = splitDirectories f
[pkg,_,_msgFile] = drop (length parts - 3) parts
in pkg
parseMsg :: FilePath -> IO (Either String Msg)
parseMsg fname = do msgFile <- B.readFile fname
let tName = msgName . dropExtension . takeFileName $ fname
packageName = pkgName fname
return $ parseMsgWithName tName packageName msgFile
parseMsgWithName :: MsgName -> String -> ByteString -> Either String Msg
parseMsgWithName name packageName msgFile =
case feed (parse parser msgFile) "" of
Done leftOver msg
| B.null leftOver -> Right msg
| otherwise -> Left $ "Couldn't parse " ++
unpack leftOver
Fail _ _ctxt err -> Left err
Partial _ -> Left "Incomplete msg definition"
where
parser = comment *> mkParser name packageName msgFile
-- | Parse a service file by splitting the file into a request and a response
-- | and parsing each part separately.
parseSrv :: FilePath -> IO (Either String Srv)
parseSrv fname = do
srvFile <- B.readFile fname
let (request, response) = splitService srvFile
packageName = pkgName fname
rawServiceName = dropExtension . takeFileName $ fname
return $ do
rqst <- parseMsgWithName (requestMsgName rawServiceName) packageName request
resp <- parseMsgWithName (responseMsgName rawServiceName) packageName response
return Srv{srvRequest = rqst
, srvResponse = resp
, srvName = msgName rawServiceName
, srvPackage = packageName
, srvSource = srvFile}
splitService :: ByteString -> (ByteString, ByteString)
splitService service = (request, response) where
-- divider does not include newlines to allow it match even
-- if there is no request or response message
divider = "---"
(request, dividerAndResponse) = B.breakSubstring divider service
--Add 1 to the length of the divider to remove newline
response = B.drop (1 + B.length divider) dividerAndResponse