scion-0.1: server/Scion/Server/Protocol.hs
{-# LANGUAGE ExistentialQuantification, TypeSynonymInstances, PatternGuards #-}
-- |
-- Module : Scion.Server.Protocol
-- Copyright : (c) Thomas Schilling 2008
-- License : BSD-style
--
-- Maintainer : nominolo@gmail.com
-- Stability : experimental
-- Portability : portable
--
-- Server message types and methods for serialising and deserialising them to
-- strings.
--
-- TODO: Document protocol + message format.
--
module Scion.Server.Protocol where
import Prelude hiding ( span )
import Scion.Types
import Scion.Types.Notes
import Data.Char ( isDigit, isSpace )
import Numeric ( showInt )
import Text.ParserCombinators.ReadP
import qualified Data.Map as M
import qualified Data.MultiSet as MS
------------------------------------------------------------------------------
-- Bump this before every release whenever the wire-protocol is changed
-- (extension-only is fine)
--
-- Don't forget to also adjust the client code.
scionVersion :: Int
scionVersion = 1
------------------------------------------------------------------------------
class Sexp a where toSexp :: a -> ShowS
instance Sexp String where toSexp s = showString (show s)
instance Sexp Int where toSexp i = showInt i
instance Sexp Integer where toSexp i = showInt i
instance Sexp () where toSexp _ = showString "nil"
instance Sexp Bool where
toSexp True = showChar 't'
toSexp False = showString "nil"
instance (Sexp a, Sexp b) => Sexp (a, b) where
toSexp (x, y) = parens (toSexp x <+> toSexp y)
instance Sexp a => Sexp (Maybe a) where
toSexp Nothing = showString "nil"
toSexp (Just x) = toSexp x
newtype Lst a = Lst [a]
instance Sexp a => Sexp (Lst a) where
toSexp (Lst xs) = parens (go xs)
where go [] = id
go [y] = toSexp y
go (y:ys) = toSexp y <+> go ys
newtype Keyword = K String deriving (Eq, Ord, Show)
instance Sexp Keyword where
toSexp (K s) = showChar ':' . showString s
-- if you need to cheat
newtype ExactSexp = ExactSexp ShowS
instance Sexp ExactSexp where
toSexp (ExactSexp s) = s
instance (Sexp a, Sexp b) => Sexp (M.Map a b) where
toSexp m = parens (go (M.assocs m))
where go ((k,v):r) = toSexp k <+> toSexp v <+> go r
go [] = id
instance Sexp Component where
toSexp Library = showString ":library"
toSexp (Executable n) =
parens (showString ":executable" <+> showString (show n))
toSexp (File f) =
parens (showString ":file" <+> showString (show f))
instance Sexp a => Sexp (MS.MultiSet a) where
toSexp ms = toSexp (Lst (MS.toList ms))
instance Sexp NoteKind where
toSexp k = showString $ case k of
ErrorNote -> ":error"
WarningNote -> ":warning"
InfoNote -> ":info"
OtherNote -> ":other"
instance Sexp Note where
toSexp n =
parens (toSexp (noteKind n) <+>
toSexp (noteLoc n) <+>
putString (noteMessage n) <+>
putString "")
instance Sexp Location where
toSexp loc
| isValidLoc loc,
(f, sl, sc, el, ec) <- viewLoc loc =
parens (showString ":loc" <+> toSexp f <+>
showInt sl <+> showInt sc <+>
showInt el <+> showInt ec)
| otherwise =
parens (showString ":no-loc" <+> showString (show (noLocText loc)))
instance Sexp LocSource where
toSexp (FileSrc f) = showString (show f)
toSexp (OtherSrc s) = showString s
------------------------------------------------------------------------------
data Request
= Rex (ScionM String) Int -- Remote EXecute
| RQuit
instance Show Request where
show (Rex _ i) = "Rex <cmd> " ++ show i
show RQuit = "RQuit"
data Response
= RReturn String Int
| RReaderError String String
deriving Show
data Command = Command {
getCommand :: ReadP (ScionM String)
}
------------------------------------------------------------------------------
-- * Parsing Requests
parseRequest :: [Command] -> String -> Maybe Request
parseRequest cmds msg =
let rs = readP_to_S (messageParser cmds) msg in
case [ r | (r, "") <- rs ] of
[m] -> Just m
[] -> Nothing
_ -> error "Ambiguous grammar for message. This is a bug."
-- | At the moment messages are in a very simple Lisp-style format. This
-- should also be easy to parse (and generate) for non-lisp clients.
messageParser :: [Command] -> ReadP Request
messageParser cmds = do
r <- inParens $ choice
[ string ":quit" >> return RQuit
, do string ":emacs-rex"
sp
c <- inParens (choice (map getCommand cmds))
sp
i <- getInt
return (Rex c i)
]
skipSpaces
return r
inParens :: ReadP a -> ReadP a
inParens = between (char '(') (char ')')
getString :: ReadP String
getString = decodeEscapes `fmap` (char '"' >> munchmunch False)
where
munchmunch had_backspace = do
c <- get
if c == '"' && not had_backspace
then return []
else do
(c:) `fmap` munchmunch (c == '\\')
getInt :: ReadP Int
getInt = munch1 isDigit >>= return . read
decodeEscapes :: String -> String
decodeEscapes = id -- XXX
-- | One or more spaces.
sp :: ReadP ()
sp = skipMany1 (satisfy isSpace)
------------------------------------------------------------------------------
-- * Writing Responses
showResponse :: Response -> String
showResponse r = shows' r "\n"
where
shows' (RReturn f i) =
parens (showString ":return" <+>
parens (showString ":ok" <+> showString f)
<+> showInt i)
shows' (RReaderError s t) =
parens (showString ":reader-error" <+>
showString (show s) <+>
showString (show t))
parens :: ShowS -> ShowS
parens p = showChar '(' . p . showChar ')'
putString :: String -> ShowS
putString s = showString (show s)
infixr 1 <+>
(<+>) :: ShowS -> ShowS -> ShowS
l <+> r = l . showChar ' ' . r