packages feed

bond 0.4.0.2 → 0.4.1.0

raw patch · 14 files changed

+374/−37 lines, 14 filesdep +aeson-prettydep ~Diffdep ~aesondep ~processPVP ok

version bump matches the API change (PVP)

Dependencies added: aeson-pretty

Dependency ranges changed: Diff, aeson, process

API changes (from Hackage documentation)

+ Language.Bond.Codegen.Templates: comm_interface_cs :: MappingContext -> String -> [Import] -> [Declaration] -> (String, Text)
+ Language.Bond.Codegen.Templates: comm_proxy_cs :: MappingContext -> String -> [Import] -> [Declaration] -> (String, Text)
+ Language.Bond.Codegen.Templates: comm_service_cs :: MappingContext -> String -> [Import] -> [Declaration] -> (String, Text)
+ Language.Bond.Syntax.JSON: instance Data.Aeson.Types.Class.FromJSON Language.Bond.Syntax.Types.Method
+ Language.Bond.Syntax.JSON: instance Data.Aeson.Types.Class.ToJSON Language.Bond.Syntax.Types.Method
+ Language.Bond.Syntax.SchemaDef: makeSchemaDef :: Type -> SchemaDef
+ Language.Bond.Syntax.Types: Event :: [Attribute] -> String -> Maybe Type -> Method
+ Language.Bond.Syntax.Types: Function :: [Attribute] -> Maybe Type -> String -> Maybe Type -> Method
+ Language.Bond.Syntax.Types: Service :: [Namespace] -> [Attribute] -> String -> [TypeParam] -> [Method] -> Declaration
+ Language.Bond.Syntax.Types: [methodAttributes] :: Method -> [Attribute]
+ Language.Bond.Syntax.Types: [methodInput] :: Method -> Maybe Type
+ Language.Bond.Syntax.Types: [methodName] :: Method -> String
+ Language.Bond.Syntax.Types: [methodResult] :: Method -> Maybe Type
+ Language.Bond.Syntax.Types: [serviceMethods] :: Declaration -> [Method]
+ Language.Bond.Syntax.Types: data Method
+ Language.Bond.Syntax.Types: instance GHC.Classes.Eq Language.Bond.Syntax.Types.Method
+ Language.Bond.Syntax.Types: instance GHC.Show.Show Language.Bond.Syntax.Types.Method

Files

IO.hs view
@@ -17,8 +17,12 @@ import Control.Applicative
 import Prelude
 import Data.Aeson (eitherDecode)
+import Data.Text
 import Control.Monad.Loops (firstM)
 import qualified Data.ByteString.Lazy as BL
+import Text.Parsec
+import Text.ParserCombinators.Parsec.Error
+import Text.Printf
 import Language.Bond.Syntax.Types (Bond(..))
 import Language.Bond.Syntax.JSON()
 import Language.Bond.Parser
@@ -39,7 +43,7 @@     result <- parseBond file input (cwd </> file) readImportFile
     case result of
         Left err -> do
-            putStrLn $ "Error parsing " ++ file ++ ": " ++ show err
+            putStrLn $ msbuildErrorMessage err
             exitFailure
         Right bond -> return bond
   where
@@ -77,7 +81,26 @@ 
 
 parseNamespaceMappings :: [String] -> IO [NamespaceMapping]
-parseNamespaceMappings = mapM $ 
+parseNamespaceMappings = mapM $
     \ s -> case parseNamespaceMapping s of
         Left err -> fail $ show err
         Right m -> return m
+
+msbuildErrorMessage :: ParseError -> String
+msbuildErrorMessage err = printf "%s(%d,%d) : error B0000: %s" name line col message
+    where
+        message = combinedMessage err
+        pos = errorPos err
+        name = sourceName pos
+        line = sourceLine pos
+        col = sourceColumn pos
+
+combinedMessage :: ParseError -> String
+combinedMessage err = id $ unpack $ intercalate (pack ", ") messages
+    where
+        -- showErrorMessages returns a multi-line String starting with a blank
+        -- line. We need to break it up to make a useful one-line message.
+        messages = splitOn (pack "\n") $ strip $ pack $
+            showErrorMessages "or" "unknown parse error"
+                "expecting" "unexpected" "end of input"
+                (errorMessages err)
Main.hs view
@@ -90,7 +90,6 @@         ]
 cppCodegen _ = error "cppCodegen: impossible happened."
 
-
 csCodegen :: Options -> IO()
 csCodegen options@Cs {..} = do
     let fieldMapping = if readonly_properties
@@ -99,7 +98,7 @@                  then PublicFields
                  else Properties
     let typeMapping = if collection_interfaces then csCollectionInterfacesTypeMapping else csTypeMapping
-    let templates = [ types_cs Class fieldMapping ]
+    let templates = [ comm_interface_cs , comm_proxy_cs , comm_service_cs , types_cs Class fieldMapping ]
     concurrentlyFor_ files $ codeGen options typeMapping templates
 csCodegen _ = error "csCodegen: impossible happened."
 
@@ -117,4 +116,3 @@         createDirectoryIfMissing True outputDir
         let content = if (no_banner options) then code else (commonHeader "//" fileName <> code)
         L.writeFile (outputDir </> fileName) content
-
bond.cabal view
@@ -2,7 +2,7 @@ -- Licensed under the MIT license. See LICENSE file in the project root for full license information.
 
 name:               bond
-version:            0.4.0.2
+version:            0.4.1.0
 cabal-version:      >= 1.8
 tested-with:        GHC>=7.4.1
 synopsis:           Bond schema compiler and code generator
@@ -37,7 +37,7 @@ 
 library
   hs-source-dirs:   src
-  build-depends:    aeson >= 0.7.0.6 && < 0.10.0.0,
+  build-depends:    aeson >= 0.7.0.6 && < 0.12.0.0,
                     base >= 4.5 && < 5,
                     bytestring >= 0.10,
                     filepath >= 1.0,
@@ -64,6 +64,7 @@                     Language.Bond.Codegen.Cpp.Types_cpp
                     Language.Bond.Codegen.Cpp.Types_h
                     Language.Bond.Codegen.Cs.Types_cs
+                    Language.Bond.Codegen.Cs.Comm_cs
                     Language.Bond.Codegen.Cpp.ApplyOverloads
                     Language.Bond.Codegen.Cpp.Util
                     Language.Bond.Codegen.Cs.Util
@@ -79,7 +80,8 @@                     Tests.Syntax
   ghc-options:      -threaded -Wall
   build-depends:    bond,
-                    aeson >= 0.7.0.6 && < 0.10.0.0,
+                    aeson >= 0.7.0.6 && < 0.12.0.0,
+                    aeson-pretty == 0.7.2,
                     base >= 4.5 && < 5,
                     bytestring >= 0.10,
                     cmdargs >= 0.10.10,
@@ -90,12 +92,13 @@                     derive,
                     HUnit,
                     QuickCheck,
-                    Diff >= 0.2 && <= 0.3.2,
+                    Diff >= 0.2 && < 0.4,
                     pretty,
                     tasty,
                     tasty-golden,
                     tasty-hunit,
-                    tasty-quickcheck
+                    tasty-quickcheck,
+                    parsec >= 3.1
 
 executable gbc
   main-is:          Main.hs
@@ -103,13 +106,14 @@                     Options
   ghc-options:      -threaded -Wall
   build-depends:    bond,
-                    aeson >= 0.7.0.6 && < 0.10.0.0,
+                    aeson >= 0.7.0.6 && < 0.12.0.0,
                     async >= 2.0.1.0,
                     base >= 4.5 && < 5,
                     bytestring >= 0.10,
                     cmdargs >= 0.10.10,
-                    process < 1.4,
+                    process < 1.5,
                     directory >= 1.1,
                     filepath >= 1.0,
                     monad-loops >= 0.4,
-                    text >= 0.11
+                    text >= 0.11,
+                    parsec >= 3.1
+ src/Language/Bond/Codegen/Cs/Comm_cs.hs view
@@ -0,0 +1,201 @@+-- Copyright (c) Microsoft. All rights reserved.
+-- Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+{-# LANGUAGE QuasiQuotes, OverloadedStrings, RecordWildCards #-}
+
+module Language.Bond.Codegen.Cs.Comm_cs (
+  comm_interface_cs,
+  comm_proxy_cs,
+  comm_service_cs)
+where
+
+import Data.Maybe
+import Data.Monoid
+import Data.List (nub)
+import Prelude
+import qualified Data.Text.Lazy as L
+import Data.Text.Lazy.Builder
+import Text.Shakespeare.Text
+import Language.Bond.Syntax.Types
+import Language.Bond.Util
+import Language.Bond.Codegen.Util
+import Language.Bond.Codegen.TypeMapping
+import qualified Language.Bond.Codegen.Cs.Util as CS
+
+
+getMessageTypeName :: MappingContext -> Maybe Type -> Builder
+getMessageTypeName ctx t = maybe "global::Bond.Void" (getTypeName ctx) t
+
+getMessageProxyInputParam :: MappingContext -> Maybe Type -> Builder
+getMessageProxyInputParam ctx t = maybe "" constructParam t
+  where
+    constructParam x = getTypeName ctx x `mappend` fromText " param"
+
+paramOrBondVoid :: Maybe Type -> String
+paramOrBondVoid t = if isNothing t then "new global::Bond.Void()" else "param"
+
+-- | Codegen template for generating code containing declarations of
+-- of services including interfaces, proxies and services files.
+
+comm_interface_cs :: MappingContext -> String -> [Import] -> [Declaration] -> (String, L.Text)
+comm_interface_cs cs _ _ declarations = ("_interfaces.cs", [lt|
+#{CS.disableCscWarnings}
+#{CS.disableReSharperWarnings}
+namespace #{csNamespace}
+{
+    #{doubleLineSep 1 comm declarations}
+} // #{csNamespace}
+|])
+  where
+    csNamespace = sepBy "." toText $ getNamespace cs
+
+    comm s@Service{..} = [lt|#{CS.typeAttributes cs s}interface I#{declName}#{generics}
+    {
+        #{doubleLineSep 2 methodDeclaration serviceMethods}
+    }
+    |]
+      where
+        generics = angles $ sepBy ", " paramName declParams
+
+        methodDeclaration Function{..} = [lt|global::System.Threading.Tasks.Task<global::Bond.Comm.IMessage<#{getMessageResultTypeName}>> #{methodName}Async(global::Bond.Comm.IMessage<#{getMessageInputTypeName}> param, global::System.Threading.CancellationToken ct);|]
+          where
+            getMessageResultTypeName = getMessageTypeName cs methodResult
+            getMessageInputTypeName = getMessageTypeName cs methodInput
+
+        methodDeclaration Event{..} = [lt|void #{methodName}Async(global::Bond.Comm.IMessage<#{getMessageInputTypeName}> param);|]
+          where
+            getMessageInputTypeName = getMessageTypeName cs methodInput
+
+    comm _ = mempty
+
+comm_proxy_cs :: MappingContext -> String -> [Import] -> [Declaration] -> (String, L.Text)
+comm_proxy_cs cs _ _ declarations = ("_proxies.cs", [lt|
+#{CS.disableCscWarnings}
+#{CS.disableReSharperWarnings}
+namespace #{csNamespace}
+{
+    #{doubleLineSep 1 comm declarations}
+} // #{csNamespace}
+|])
+  where
+    csNamespace = sepBy "." toText $ getNamespace cs
+    idl = MappingContext idlTypeMapping [] [] []  
+
+    comm s@Service{..} = [lt|#{CS.typeAttributes cs s}public class #{declName}Proxy<#{proxyGenerics}TConnection> : I#{declName}#{interfaceGenerics}#{connConstraint}
+    {
+        private readonly TConnection m_connection;
+
+        public #{declName}Proxy(TConnection connection)
+        {
+            m_connection = connection;
+        }
+
+        #{doubleLineSep 2 proxyMethod serviceMethods}
+    }|]
+      where
+        methodCapability Function {} = "global::Bond.Comm.IRequestResponseConnection"
+        methodCapability Event {} = "global::Bond.Comm.IEventConnection"
+
+        getCapabilities :: [Method] -> [String]
+        getCapabilities m = nub $ map methodCapability m 
+        connConstraint = " where TConnection : " ++ sepBy ", " (\p -> p) (getCapabilities serviceMethods)
+
+        interfaceGenerics = angles $ sepBy "," paramName declParams -- of the form "<T1, T2, T3>"
+        proxyGenerics = sepEndBy ", " paramName declParams -- of the form "T1, T2, T3, "
+
+        proxyMethod Function{..} = [lt|public global::System.Threading.Tasks.Task<global::Bond.Comm.IMessage<#{getMessageResultTypeName}>> #{methodName}Async(#{getMessageProxyInputParam cs methodInput})
+        {
+            var message = new global::Bond.Comm.Message<#{getMessageInputTypeName}>(#{paramOrBondVoid methodInput});
+            return #{methodName}Async(message, global::System.Threading.CancellationToken.None);
+        }
+
+        public global::System.Threading.Tasks.Task<global::Bond.Comm.IMessage<#{getMessageResultTypeName}>> #{methodName}Async(global::Bond.Comm.IMessage<#{getMessageInputTypeName}> param, global::System.Threading.CancellationToken ct)
+        {
+            return m_connection.RequestResponseAsync<#{getMessageInputTypeName}, #{getMessageResultTypeName}>(
+                "#{getDeclTypeName idl s}.#{methodName}",
+                param,
+                ct);
+        }|]
+          where
+            getMessageResultTypeName = getMessageTypeName cs methodResult
+            getMessageInputTypeName = getMessageTypeName cs methodInput
+
+        proxyMethod Event{..} = [lt|public void #{methodName}Async(#{getMessageProxyInputParam cs methodInput})
+        {
+            var message = new global::Bond.Comm.Message<#{getMessageInputTypeName}>(#{paramOrBondVoid methodInput});
+            #{methodName}Async(message);
+        }
+
+        public void #{methodName}Async(global::Bond.Comm.IMessage<#{getMessageInputTypeName}> param)
+        {
+            m_connection.FireEventAsync<#{getMessageInputTypeName}>(
+                "#{getDeclTypeName idl s}.#{methodName}",
+                param);
+        }|]
+          where
+            getMessageInputTypeName = getMessageTypeName cs methodInput
+
+    comm _ = mempty
+
+comm_service_cs :: MappingContext -> String -> [Import] -> [Declaration] -> (String, L.Text)
+comm_service_cs cs _ _ declarations = ("_services.cs", [lt|
+#{CS.disableCscWarnings}
+#{CS.disableReSharperWarnings}
+namespace #{csNamespace}
+{
+    #{doubleLineSep 1 comm declarations}
+} // #{csNamespace}
+|])
+  where
+    csNamespace = sepBy "." toText $ getNamespace cs
+    idl = MappingContext idlTypeMapping [] [] []  
+
+    comm s@Service{..} = [lt|#{CS.typeAttributes cs s}public abstract class #{declName}ServiceBase#{generics} : I#{declName}#{generics}, global::Bond.Comm.IService
+    {
+        public global::System.Collections.Generic.IEnumerable<global::Bond.Comm.ServiceMethodInfo> Methods
+        {
+            get
+            {
+                #{newlineSep 4 methodInfo serviceMethods}
+            }
+        }
+
+        #{doubleLineSep 2 methodAbstract serviceMethods}
+
+        #{doubleLineSep 2 methodGlue serviceMethods}
+    }
+    |]
+      where
+        generics = angles $ sepBy ", " paramName declParams
+
+        methodInfo Function{..} = [lt|yield return new global::Bond.Comm.ServiceMethodInfo {MethodName="#{getDeclTypeName idl s}.#{methodName}", Callback = #{methodName}Async_Glue, CallbackType = global::Bond.Comm.ServiceCallbackType.RequestResponse};|]
+        methodInfo Event{..} = [lt|yield return new global::Bond.Comm.ServiceMethodInfo {MethodName="#{getDeclTypeName idl s}.#{methodName}", Callback = #{methodName}Async_Glue, CallbackType = global::Bond.Comm.ServiceCallbackType.Event};|]
+
+        methodAbstract Function{..} = [lt|public abstract global::System.Threading.Tasks.Task<global::Bond.Comm.IMessage<#{getMessageResultTypeName}>> #{methodName}Async(global::Bond.Comm.IMessage<#{getMessageInputTypeName}> param, global::System.Threading.CancellationToken ct);|]
+          where
+            getMessageResultTypeName = getMessageTypeName cs methodResult
+            getMessageInputTypeName = getMessageTypeName cs methodInput
+
+        methodAbstract Event{..} = [lt|public abstract void #{methodName}Async(global::Bond.Comm.IMessage<#{getMessageInputTypeName}> param);|]
+          where
+            getMessageInputTypeName = getMessageTypeName cs methodInput
+
+        methodGlue Function{..} = [lt|private global::System.Threading.Tasks.Task<global::Bond.Comm.IMessage> #{methodName}Async_Glue(global::Bond.Comm.IMessage param, global::Bond.Comm.ReceiveContext context, global::System.Threading.CancellationToken ct)
+        {
+            return global::Bond.Comm.CodegenHelpers.Upcast<global::Bond.Comm.IMessage<#{getMessageResultTypeName}>,
+                                                           global::Bond.Comm.IMessage>(
+                #{methodName}Async(param.Convert<#{getMessageInputTypeName}>(), ct));
+        }|]
+          where
+            getMessageResultTypeName = getMessageTypeName cs methodResult
+            getMessageInputTypeName = getMessageTypeName cs methodInput
+
+        methodGlue Event{..} = [lt|private global::System.Threading.Tasks.Task #{methodName}Async_Glue(global::Bond.Comm.IMessage param, global::Bond.Comm.ReceiveContext context, global::System.Threading.CancellationToken ct)
+        {
+            #{methodName}Async(param.Convert<#{getMessageInputTypeName}>());
+            return global::Bond.Comm.CodegenHelpers.CompletedTask;
+        }|]
+          where
+            getMessageInputTypeName = getMessageTypeName cs methodInput
+
+    comm _ = mempty
src/Language/Bond/Codegen/Cs/Util.hs view
@@ -73,6 +73,10 @@ typeAttributes cs e@Enum {..} =
     optionalTypeAttributes cs e
  <> generatedCodeAttr
+
+-- C# service attributes
+typeAttributes _ Service {..} = generatedCodeAttr
+
 typeAttributes _ _ = error "typeAttributes: impossible happened."
 
 generatedCodeAttr :: Text
src/Language/Bond/Codegen/Templates.hs view
@@ -45,6 +45,9 @@     , FieldMapping(..)
     , StructMapping(..)
     , types_cs
+    , comm_interface_cs
+    , comm_proxy_cs
+    , comm_service_cs
     )
     where
 
@@ -56,6 +59,7 @@ import Language.Bond.Codegen.Cpp.Types_cpp
 import Language.Bond.Codegen.Cpp.Types_h
 import Language.Bond.Codegen.Cs.Types_cs
+import Language.Bond.Codegen.Cs.Comm_cs
 -- redundant imports for haddock
 import Language.Bond.Codegen.TypeMapping
 import Language.Bond.Syntax.Types
src/Language/Bond/Parser.hs view
@@ -81,10 +81,10 @@ import_ :: Parser Import
 import_ = do
     i <- Import <$ keyword "import" <*> unescapedStringLiteral <?> "import statement"
-    input <- getInput
+    src <- getInput
     pos <- getPosition
     processImport i
-    setInput input
+    setInput src
     setPosition pos
     return i
 
@@ -107,6 +107,7 @@         <|> try view
         <|> try enum
         <|> try alias
+        <|> try service
     updateSymbols decl <?> "declaration"
     return decl
 
@@ -140,13 +141,13 @@     doFind = do
         namespaces <- asks currentNamespaces
         Symbols { symbols = symbols } <- getState
-        case find (delcMatching namespaces name) symbols of
+        case find (declMatching namespaces name) symbols of
             Just decl -> return decl
             Nothing -> fail $ "Unknown symbol: " ++ showQualifiedName name
-    delcMatching namespaces [unqualifiedName] decl =
+    declMatching namespaces [unqualifiedName] decl =
         unqualifiedName == declName decl
      && (not $ null $ intersectBy nsMatching namespaces (declNamespaces decl))
-    delcMatching _ qualifiedName' decl =
+    declMatching _ qualifiedName' decl =
         takeName qualifiedName' == declName decl
      && any ((takeNamespace qualifiedName' ==) . nsName) (declNamespaces decl)
     nsMatching ns1 ns2 =
@@ -210,7 +211,7 @@ view :: Parser Declaration
 view = do
     attr <- attributes
-    name <- keyword "struct" *> identifier
+    name <- keyword "struct" *> identifier <?> "struct view definition"
     decl <- keyword "view_of" *> qualifiedName >>= findStruct
     fields <- braces $ semiOrCommaSepEnd1 identifier
     namespaces <- asks currentNamespaces
@@ -308,26 +309,26 @@     <|> keyword "map" *> angles (BT_Map <$> keyType <* comma <*> type_)
     <|> keyword "bonded" *> angles (BT_Bonded <$> userStruct)
   where
-    keyType = try (basicType <|> checkUserType validKeyType) <?> "scalar, string or enum"
-    userStruct = try (checkUserType validBondedType) <?> "user defined struct"
-    checkUserType valid = do
-        t <- userType
-        if valid t then return t else unexpected "type"
-    validKeyType t = case t of
-        BT_TypeParam _ -> True
-        _ -> isScalar t || isString t
-    validBondedType t = case t of
-        BT_TypeParam _ -> True
-        _ -> isStruct t
+    keyType = try (basicType <|> checkUserType isValidKeyType) <?> "scalar, string or enum"
+    userStruct = try (checkUserType isStruct) <?> "user defined struct"
+    isValidKeyType t = isScalar t || isString t
 
 
 -- parser for user defined type (struct, enum, alias or type parameter)
 userType :: Parser Type
 userType = do
+    symbol_ <- userSymbol
+    case symbol_ of
+        Left param -> return $ BT_TypeParam param
+        Right (Service {..}, _) -> fail $ "Unexpected service " ++ declName ++ ". Expected struct, enum or alias."
+        Right (decl, args) -> return $ BT_UserDefined decl args
+
+userSymbol :: Parser (Either TypeParam (Declaration, [Type]))
+userSymbol = do
     name <- qualifiedName
     params <- asks currentParams
     case find (isParam name) params of
-        Just param -> return $ BT_TypeParam param
+        Just param -> return $ Left param
         Nothing -> do
             decl <- findSymbol name
             args <- option [] (angles $ commaSep1 arg)
@@ -338,7 +339,7 @@                     else
                         " is not a generic type"
                 else
-                    return $ BT_UserDefined decl args
+                    return $ Right (decl, args)
           where
             paramsCount Enum{} = 0
             paramsCount decl   = length $ declParams decl
@@ -357,4 +358,44 @@ ftype = keyword "bond_meta::name" *> pure BT_MetaName
     <|> keyword "bond_meta::full_name" *> pure BT_MetaFullName
     <|> type_
+
+-- service definition parser
+service :: Parser Declaration
+service = do
+    attr <- attributes
+    name <- keyword "service" *> identifier <?> "service definition"
+    params <- parameters
+    namespaces <- asks currentNamespaces
+    local (with params) $ Service namespaces attr name params <$> methods <* optional semi
+  where
+    with params e = e { currentParams = params }
+    methods = braces $ semiEnd (try event <|> try function)
+
+function :: Parser Method
+function = Function <$> attributes <*> payload <*> identifier <*> input
+
+event :: Parser Method
+event = Event <$> attributes <* keyword "nothing" <*> identifier <*> input
+
+input :: Parser (Maybe Type)
+input = do
+  pld <- parens $ optional payload
+  case pld of
+      Nothing -> pure Nothing
+      Just m -> return m
+
+payload :: Parser (Maybe Type)
+payload = void_ <|> liftM Just userStruct
+  where
+    void_ = keyword "void" *> pure Nothing
+    userStruct = try (checkUserType isStruct) <?> "user defined struct"
+
+checkUserType :: (Type -> Bool) -> Parser Type
+checkUserType check = do
+    t <- userType
+    if (valid t) then return t else unexpected "type"
+  where
+    valid t = case t of
+        BT_TypeParam _ -> True
+        _ -> check t
 
src/Language/Bond/Syntax/Internal.hs view
@@ -43,6 +43,7 @@     showPretty Enum {..} = "enum " ++ declName
     showPretty Forward {..} = "struct declaration " ++ declName ++ showTypeParams declParams
     showPretty Alias {..} = "alias " ++ declName ++ showTypeParams declParams
+    showPretty Service {..} = "service " ++ declName ++ showTypeParams declParams
 
 metaField :: Field -> Any
 metaField Field {..} = Any $ isMetaName fieldType
src/Language/Bond/Syntax/JSON.hs view
@@ -267,4 +267,4 @@ $(deriveJSON defaultOptions ''Declaration)
 $(deriveJSON defaultOptions ''Import)
 $(deriveJSON defaultOptions ''Language)
-
+$(deriveJSON defaultOptions ''Method)
src/Language/Bond/Syntax/SchemaDef.hs view
@@ -14,7 +14,8 @@ 
 module Language.Bond.Syntax.SchemaDef
     ( -- * Runtime schema (aka SchemaDef) support
-       encodeSchemaDef
+       encodeSchemaDef,
+       makeSchemaDef
     ) where
 
 import Data.Word
src/Language/Bond/Syntax/Types.hs view
@@ -30,6 +30,8 @@     , Type(..)
     , TypeParam(..)
     , Constraint(..)
+      -- ** Comm
+    , Method(..)
       -- ** Metadata
     , Attribute(..)
       -- ** Deprecated
@@ -119,7 +121,23 @@         }
     deriving (Eq, Show)
 
--- | A declaration of a schema type.
+-- | Method of a service
+data Method =
+    Function
+        { methodAttributes :: [Attribute]   -- zero or more attributes
+        , methodResult :: Maybe Type        -- method result
+        , methodName :: String              -- method name
+        , methodInput :: Maybe Type         -- method parameter
+        }
+    |
+    Event
+        { methodAttributes :: [Attribute]   -- zero or more attributes
+        , methodName :: String              -- method name
+        , methodInput :: Maybe Type         -- method parameter
+        }
+    deriving (Eq, Show)
+
+-- | Bond schema declaration
 data Declaration =
     Struct
         { declNamespaces :: [Namespace]     -- namespace(s) in which the struct is declared
@@ -149,6 +167,14 @@         , declParams :: [TypeParam]         -- type parameters for generics
         , aliasType :: Type                 -- aliased type
         }                                   -- ^ <https://microsoft.github.io/bond/manual/compiler.html#type-aliases type alias definition>
+    |
+    Service
+        { declNamespaces :: [Namespace]     -- namespace(s) in which the service is declared
+        , declAttributes :: [Attribute]     -- zero or more attributes
+        , declName :: String                -- service name
+        , declParams :: [TypeParam]         -- type parameters for generic service
+        , serviceMethods :: [Method]        -- zero or more methods
+        }                                   -- ^ <https://microsoft.github.io/bond/manual/compiler.html#service-definition service definition>
     deriving (Eq, Show)
 
 -- | <https://microsoft.github.io/bond/manual/compiler.html#import-statements Import> declaration.
tests/Main.hs view
@@ -23,6 +23,9 @@             , testCase "inheritance" $ compareAST "inheritance"
             , testCase "type aliases" $ compareAST "aliases"
             , testCase "documentation example" $ compareAST "example"
+            , testCase "simple service syntax" $ compareAST "service"
+            , testCase "service attributes" $ compareAST "service_attributes"
+            , testCase "generic service" $ compareAST "generic_service"
             ]
         ]
     , testGroup "SchemaDef"
@@ -101,6 +104,16 @@                 , "--using=time=System.DateTime"
                 ]
                 "nullable_alias"
+            , testGroup "Comm"
+                [ verifyCsCommCodegen
+                    [ "c#"
+                    ]
+                    "service"
+                , verifyCsCommCodegen
+                    [ "c#"
+                    ]
+                    "generic_service"
+                ]
             ]
         ]
     ]
tests/Tests/Codegen.hs view
@@ -9,6 +9,7 @@     , verifyCppCodegen
     , verifyApplyCodegen
     , verifyCsCodegen
+    , verifyCsCommCodegen
     ) where
 
 import System.FilePath
@@ -61,6 +62,22 @@         ]
 
 
+verifyCsCommCodegen :: [String] -> FilePath -> TestTree
+verifyCsCommCodegen args baseName =
+    testGroup baseName $
+        map (verifyFile (processOptions args) baseName csTypeMapping "")
+            [ comm_interface_cs
+            , comm_proxy_cs
+            , comm_service_cs
+            , types_cs Class (fieldMapping (processOptions args))
+            ]
+  where
+    fieldMapping Cs {..} = if readonly_properties
+        then ReadOnlyProperties
+        else if fields
+             then PublicFields
+             else Properties
+
 verifyFiles :: Options -> FilePath -> [TestTree]
 verifyFiles options baseName =
     map (verify (typeMapping options) "") (templates options)
@@ -115,4 +132,3 @@                             (text "test output")
                             (text . BS.unpack)
                             (getContextDiff 3 (BS.lines x) (BS.lines y))
-
tests/Tests/Syntax.hs view
@@ -14,6 +14,7 @@ import Data.Maybe
 import Data.List
 import Data.Aeson (encode, decode)
+import Data.Aeson.Encode.Pretty (Config(..), encodePretty')
 import Data.DeriveTH
 import qualified Data.ByteString as B
 import qualified Data.ByteString.Lazy as BL
@@ -40,6 +41,7 @@ derive makeArbitrary ''Namespace
 derive makeArbitrary ''Type
 derive makeArbitrary ''TypeParam
+derive makeArbitrary ''Method
 
 roundtripAST :: Bond -> Bool
 roundtripAST x = (decode . encode) x == Just x
@@ -106,8 +108,11 @@             BL.fromStrict $
             replace "\\u003c" "<" $
             replace "\\u003e" ">" $
-            BL.toStrict $ encodeSchemaDef $ BT_UserDefined schema []
+            BL.toStrict $ prettyEncode $ makeSchemaDef $ BT_UserDefined schema []
       where
+        prettyEncode = encodePretty' (Config indentSpaces compare)
+          where
+            indentSpaces = 2
         replace s r bs = if B.null t then h else
             B.append h (B.append r $ replace s r (B.drop (B.length s) t))
           where