dbus-core-0.8.3: src/introspection.anansi
:# Copyright (C) 2009-2010 John Millikin <jmillikin@gmail.com>
:#
:# This program is free software: you can redistribute it and/or modify
:# it under the terms of the GNU General Public License as published by
:# the Free Software Foundation, either version 3 of the License, or
:# any later version.
:#
:# This program is distributed in the hope that it will be useful,
:# but WITHOUT ANY WARRANTY; without even the implied warranty of
:# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
:# GNU General Public License for more details.
:#
:# You should have received a copy of the GNU General Public License
:# along with this program. If not, see <http://www.gnu.org/licenses/>.
\section{Introspection}
D-Bus objects may be ``introspected'' to determine which methods, signals,
etc they support. Intospection data is sent over the bus in {\sc xml}, in
a mostly standardised but undocumented format.
An XML introspection document looks like this:
\begin{verbatim}
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node name="/org/example/example">
<interface name="org.example.ExampleInterface">
<method name="Echo">
<arg name="text" type="s" direction="in"/>
<arg type="s" direction="out"/>
</method>
<signal name="Echoed">
<arg type="s"/>
</signal>
<property name="EchoCount" type="u" access="read"/>
</interface>
<node name="child_a"/>
<node name="child/b"/>
</node>
\end{verbatim}
:f DBus/Introspection.hs
|copyright|
|text extensions|
module DBus.Introspection
( Object (..)
, Interface (..)
, Method (..)
, Signal (..)
, Parameter (..)
, Property (..)
, PropertyAccess (..)
, toXML
, fromXML
) where
|text imports|
|introspection imports|
import qualified DBus.Types as T
:
HaXml is used to do the heavy lifting of XML parsing because HXT cannot
be combined with Parsec 3.
:d introspection imports
import qualified Text.XML.HaXml as H
:
\subsection{Data types}
:f DBus/Introspection.hs
data Object = Object T.ObjectPath [Interface] [Object]
deriving (Show, Eq)
data Interface = Interface T.InterfaceName [Method] [Signal] [Property]
deriving (Show, Eq)
data Method = Method T.MemberName [Parameter] [Parameter]
deriving (Show, Eq)
data Signal = Signal T.MemberName [Parameter]
deriving (Show, Eq)
data Parameter = Parameter Text T.Signature
deriving (Show, Eq)
data Property = Property Text T.Signature [PropertyAccess]
deriving (Show, Eq)
data PropertyAccess = Read | Write
deriving (Show, Eq)
:
\subsection{Parsing XML}
The root {\tt node} is special, in that it's the only {\tt node} which is
not required to have a {\tt name} attribute. If the root has no {\tt name},
its path will default to the path of the introspected object.
If parsing fails, {\tt fromXML} will return {\tt Nothing}. Aside from the
elements directly accessed by the parser, no effort is made to check the
document's validity because there is no DTD as of yet.
:d introspection imports
import Text.XML.HaXml.Parse (xmlParse')
import DBus.Util (eitherToMaybe)
:
:f DBus/Introspection.hs
fromXML :: T.ObjectPath -> Text -> Maybe Object
fromXML path text = do
doc <- eitherToMaybe . xmlParse' "" . TL.unpack $ text
let (H.Document _ _ root _) = doc
parseRoot path root
:
Even though the root object's {\tt name} is optional, if present, it must
still be a valid object path.
:f DBus/Introspection.hs
parseRoot :: T.ObjectPath -> H.Element a -> Maybe Object
parseRoot defaultPath e = do
path <- case getAttr "name" e of
Nothing -> Just defaultPath
Just x -> T.mkObjectPath x
parseObject' path e
:
Child {\tt nodes} have ``relative'' paths -- that is, their {\tt name}
attribute is not a valid object path, but should be valid when appended to
the root object's path.
:f DBus/Introspection.hs
parseChild :: T.ObjectPath -> H.Element a -> Maybe Object
parseChild parentPath e = do
let parentPath' = case T.strObjectPath parentPath of
"/" -> "/"
x -> TL.append x "/"
pathSegment <- getAttr "name" e
path <- T.mkObjectPath $ TL.append parentPath' pathSegment
parseObject' path e
:
Other than the name, both root and non-root {\tt nodes} have identical
contents. They may contain interface definitions, and child {\tt node}s.
:f DBus/Introspection.hs
parseObject' :: T.ObjectPath -> H.Element a -> Maybe Object
parseObject' path e@(H.Elem "node" _ _) = do
interfaces <- children parseInterface (H.tag "interface") e
children' <- children (parseChild path) (H.tag "node") e
return $ Object path interfaces children'
parseObject' _ _ = Nothing
:
Interfaces may contain methods, signals, and properties.
:f DBus/Introspection.hs
parseInterface :: H.Element a -> Maybe Interface
parseInterface e = do
name <- T.mkInterfaceName =<< getAttr "name" e
methods <- children parseMethod (H.tag "method") e
signals <- children parseSignal (H.tag "signal") e
properties <- children parseProperty (H.tag "property") e
return $ Interface name methods signals properties
:
Methods contain a list of parameters, which default to ``in'' parameters
if no direction is specified.
:f DBus/Introspection.hs
parseMethod :: H.Element a -> Maybe Method
parseMethod e = do
name <- T.mkMemberName =<< getAttr "name" e
paramsIn <- children parseParameter (isParam ["in", ""]) e
paramsOut <- children parseParameter (isParam ["out"]) e
return $ Method name paramsIn paramsOut
:
Signals are similar to methods, except they have no ``in'' parameters.
:f DBus/Introspection.hs
parseSignal :: H.Element a -> Maybe Signal
parseSignal e = do
name <- T.mkMemberName =<< getAttr "name" e
params <- children parseParameter (isParam ["out", ""]) e
return $ Signal name params
:
A parameter has a free-form name, and a single valid type.
:f DBus/Introspection.hs
parseParameter :: H.Element a -> Maybe Parameter
parseParameter e = do
let name = getAttr' "name" e
sig <- parseType e
return $ Parameter name sig
:
:f DBus/Introspection.hs
parseType :: H.Element a -> Maybe T.Signature
parseType e = do
sig <- T.mkSignature =<< getAttr "type" e
case T.signatureTypes sig of
[_] -> Just sig
_ -> Nothing
:
Properties are used by the {\tt org.freedesktop.DBus.Properties} interface.
Each property may be read, written, or both, and has an associated type.
:f DBus/Introspection.hs
parseProperty :: H.Element a -> Maybe Property
parseProperty e = do
let name = getAttr' "name" e
sig <- parseType e
access <- case getAttr' "access" e of
"" -> Just []
"read" -> Just [Read]
"write" -> Just [Write]
"readwrite" -> Just [Read, Write]
_ -> Nothing
return $ Property name sig access
:
HaXml doesn't seem to have any way to retrieve the ``real'' value of an
attribute, so {\tt attrValue} implements this.
:d introspection imports
import Data.Char (chr)
:
:f DBus/Introspection.hs
attrValue :: H.AttValue -> Maybe Text
attrValue attr = fmap (TL.pack . concat) $ mapM unescape parts where
(H.AttValue parts) = attr
unescape (Left x) = Just x
unescape (Right (H.RefEntity x)) = lookup x namedRefs
unescape (Right (H.RefChar x)) = Just [chr x]
namedRefs =
[ ("lt", "<")
, ("gt", ">")
, ("amp", "&")
, ("apos", "'")
, ("quot", "\"")
]
:
Some helper functions for dealing with HaXml filters
:d introspection imports
import Data.Maybe (fromMaybe)
:
:f DBus/Introspection.hs
getAttr :: String -> H.Element a -> Maybe Text
getAttr name (H.Elem _ attrs _) = lookup name attrs >>= attrValue
getAttr' :: String -> H.Element a -> Text
getAttr' = (fromMaybe "" .) . getAttr
isParam :: [Text] -> H.CFilter a
isParam dirs content = do
arg@(H.CElem e _) <- H.tag "arg" content
let direction = getAttr' "direction" e
[arg | direction `elem` dirs]
children :: Monad m => (H.Element a -> m b) -> H.CFilter a -> H.Element a -> m [b]
children f filt (H.Elem _ _ contents) =
mapM f [x | (H.CElem x _) <- concatMap filt contents]
:
\subsection{Generating XML}
:f DBus/Introspection.hs
dtdPublicID, dtdSystemID :: String
dtdPublicID = "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
dtdSystemID = "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd"
:
HaXml punts to the {\tt pretty} package for serialising XML.
:d introspection imports
import Text.XML.HaXml.Pretty (document)
import Text.PrettyPrint.HughesPJ (render)
:
Generating XML can fail; if a child object's path is not a sub-path of the
parent, {\tt toXML} will return {\tt Nothing}.
:f DBus/Introspection.hs
toXML :: Object -> Maybe Text
toXML obj = fmap (TL.pack . render . document) doc where
prolog = H.Prolog Nothing [] (Just doctype) []
doctype = H.DTD "node" (Just (H.PUBLIC
(H.PubidLiteral dtdPublicID)
(H.SystemLiteral dtdSystemID))) []
doc = do
root <- xmlRoot obj
return $ H.Document prolog H.emptyST root []
:
When writing objects to {\tt node}s, the root object must have an absolute
path, and children must have paths relative to their parent.
:f DBus/Introspection.hs
xmlRoot :: Object -> Maybe (H.Element a)
xmlRoot obj@(Object path _ _) = do
(H.CElem root _) <- xmlObject' (T.strObjectPath path) obj
return root
:
:f DBus/Introspection.hs
xmlObject :: T.ObjectPath -> Object -> Maybe (H.Content a)
xmlObject parentPath obj@(Object path _ _) = do
let path' = T.strObjectPath path
parent' = T.strObjectPath parentPath
relpath <- if TL.isPrefixOf parent' path'
then Just $ if parent' == "/"
then TL.drop 1 path'
else TL.drop (TL.length parent' + 1) path'
else Nothing
xmlObject' relpath obj
:
:f DBus/Introspection.hs
xmlObject' :: Text -> Object -> Maybe (H.Content a)
xmlObject' path (Object fullPath interfaces children') = do
children'' <- mapM (xmlObject fullPath) children'
return $ mkElement "node"
[mkAttr "name" $ TL.unpack path]
$ concat
[ map xmlInterface interfaces
, children''
]
:
:f DBus/Introspection.hs
xmlInterface :: Interface -> H.Content a
xmlInterface (Interface name methods signals properties) =
mkElement "interface"
[mkAttr "name" . TL.unpack . T.strInterfaceName $ name]
$ concat
[ map xmlMethod methods
, map xmlSignal signals
, map xmlProperty properties
]
:
:f DBus/Introspection.hs
xmlMethod :: Method -> H.Content a
xmlMethod (Method name inParams outParams) = mkElement "method"
[mkAttr "name" . TL.unpack . T.strMemberName $ name]
$ concat
[ map (xmlParameter "in") inParams
, map (xmlParameter "out") outParams
]
:
:f DBus/Introspection.hs
xmlSignal :: Signal -> H.Content a
xmlSignal (Signal name params) = mkElement "signal"
[mkAttr "name" . TL.unpack . T.strMemberName $ name]
$ map (xmlParameter "out") params
:
:f DBus/Introspection.hs
xmlParameter :: String -> Parameter -> H.Content a
xmlParameter direction (Parameter name sig) = mkElement "arg"
[ mkAttr "name" . TL.unpack $ name
, mkAttr "type" . TL.unpack . T.strSignature $ sig
, mkAttr "direction" direction
] []
:
:f DBus/Introspection.hs
xmlProperty :: Property -> H.Content a
xmlProperty (Property name sig access) = mkElement "property"
[ mkAttr "name" . TL.unpack $ name
, mkAttr "type" . TL.unpack . T.strSignature $ sig
, mkAttr "access" $ xmlAccess access
] []
:
:f DBus/Introspection.hs
xmlAccess :: [PropertyAccess] -> String
xmlAccess access = readS ++ writeS where
readS = if elem Read access then "read" else ""
writeS = if elem Write access then "write" else ""
:
:f DBus/Introspection.hs
mkElement :: String -> [H.Attribute] -> [H.Content a] -> H.Content a
mkElement name attrs contents = H.CElem (H.Elem name attrs contents) undefined
mkAttr :: String -> String -> H.Attribute
mkAttr name value = (name, H.AttValue [Left escaped]) where
raw = H.CString True value ()
escaped = H.verbatim $ H.xmlEscapeContent H.stdXmlEscaper [raw]
:
\subsection{Test support}
:f Tests.hs
subObject :: ObjectPath -> Gen I.Object
subObject parentPath = sized $ \n -> resize (min n 4) $ do
let nonRoot = do
x <- arbitrary
case strObjectPath x of
"/" -> nonRoot
x' -> return x'
thisPath <- nonRoot
let path' = case strObjectPath parentPath of
"/" -> thisPath
x -> TL.append x thisPath
let path = mkObjectPath_ path'
ifaces <- arbitrary
children <- halfSized . listOf . subObject $ path
return $ I.Object path ifaces children
instance Arbitrary I.Object where
arbitrary = arbitrary >>= subObject
instance Arbitrary I.Interface where
arbitrary = do
name <- arbitrary
methods <- arbitrary
signals <- arbitrary
properties <- arbitrary
return $ I.Interface name methods signals properties
instance Arbitrary I.Method where
arbitrary = do
name <- arbitrary
inParams <- arbitrary
outParams <- arbitrary
return $ I.Method name inParams outParams
instance Arbitrary I.Signal where
arbitrary = do
name <- arbitrary
params <- arbitrary
return $ I.Signal name params
singleType :: Gen Signature
singleType = do
t <- arbitrary
case mkSignature $ typeCode t of
Just x -> return x
Nothing -> singleType
instance Arbitrary I.Parameter where
arbitrary = do
name <- listOf $ arbitrary `suchThat` isPrint
sig <- singleType
return $ I.Parameter (TL.pack name) sig
instance Arbitrary I.Property where
arbitrary = do
name <- listOf $ arbitrary `suchThat` isPrint
sig <- singleType
access <- elements
[[], [I.Read], [I.Write],
[I.Read, I.Write]]
return $ I.Property (TL.pack name) sig access
:
:d test cases
, F.testGroup "Introspection"
[ testProperty "Generate -> Parse"
$ \x@(I.Object path _ _) -> let
xml = I.toXML x
Just xml' = xml
parsed = I.fromXML path xml'
in isJust xml ==> I.fromXML path xml' == Just x
]
: