From c7f0375d1c916b6808f4096001225a0b6f77e806 Mon Sep 17 00:00:00 2001 From: Edsko de Vries Date: Fri, 18 Oct 2024 13:42:25 +0200 Subject: [PATCH] Quick start tutorial Also improve some of the docs in the main library --- .github/workflows/haskell-ci.yml | 14 +++- cabal.haskell-ci | 2 +- cabal.project | 4 +- grapesy/src/Network/GRPC/Client/Call.hs | 6 +- grapesy/src/Network/GRPC/Client/Connection.hs | 5 ++ grapesy/src/Network/GRPC/Common/Protobuf.hs | 1 + grapesy/src/Network/GRPC/Server.hs | 7 ++ grapesy/src/Network/GRPC/Server/StreamType.hs | 29 +++++++- tutorials/quickstart/LICENSE | 31 +++++++++ tutorials/quickstart/LICENSE.proto | 18 +++++ tutorials/quickstart/Setup.hs | 3 + tutorials/quickstart/app/Client.hs | 18 +++++ tutorials/quickstart/app/Server.hs | 29 ++++++++ tutorials/quickstart/proto/helloworld.proto | 17 +++++ tutorials/quickstart/quickstart.cabal | 68 +++++++++++++++++++ .../quickstart/src/Proto/API/Helloworld.hs | 15 ++++ 16 files changed, 259 insertions(+), 8 deletions(-) create mode 100644 tutorials/quickstart/LICENSE create mode 100644 tutorials/quickstart/LICENSE.proto create mode 100644 tutorials/quickstart/Setup.hs create mode 100644 tutorials/quickstart/app/Client.hs create mode 100644 tutorials/quickstart/app/Server.hs create mode 100644 tutorials/quickstart/proto/helloworld.proto create mode 100644 tutorials/quickstart/quickstart.cabal create mode 100644 tutorials/quickstart/src/Proto/API/Helloworld.hs diff --git a/.github/workflows/haskell-ci.yml b/.github/workflows/haskell-ci.yml index 725c4253..f4d6ca74 100644 --- a/.github/workflows/haskell-ci.yml +++ b/.github/workflows/haskell-ci.yml @@ -74,7 +74,7 @@ jobs: "$HOME/.ghcup/bin/ghcup" install ghc "$HCVER" || (cat "$HOME"/.ghcup/logs/*.* && false) "$HOME/.ghcup/bin/ghcup" install cabal 3.12.1.0 || (cat "$HOME"/.ghcup/logs/*.* && false) apt-get update - apt-get install -y libsnappy-dev + apt-get install -y libsnappy-dev protobuf-compiler env: HCKIND: ${{ matrix.compilerKind }} HCNAME: ${{ matrix.compiler }} @@ -156,6 +156,7 @@ jobs: run: | touch cabal.project echo "packages: $GITHUB_WORKSPACE/source/./grapesy" >> cabal.project + echo "packages: $GITHUB_WORKSPACE/source/./tutorials/quickstart" >> cabal.project cat cabal.project - name: sdist run: | @@ -169,22 +170,29 @@ jobs: run: | PKGDIR_grapesy="$(find "$GITHUB_WORKSPACE/unpacked" -maxdepth 1 -type d -regex '.*/grapesy-[0-9.]*')" echo "PKGDIR_grapesy=${PKGDIR_grapesy}" >> "$GITHUB_ENV" + PKGDIR_quickstart="$(find "$GITHUB_WORKSPACE/unpacked" -maxdepth 1 -type d -regex '.*/quickstart-[0-9.]*')" + echo "PKGDIR_quickstart=${PKGDIR_quickstart}" >> "$GITHUB_ENV" rm -f cabal.project cabal.project.local touch cabal.project touch cabal.project.local echo "packages: ${PKGDIR_grapesy}" >> cabal.project + echo "packages: ${PKGDIR_quickstart}" >> cabal.project echo "package grapesy" >> cabal.project echo " ghc-options: -Werror=missing-methods" >> cabal.project + echo "package quickstart" >> cabal.project + echo " ghc-options: -Werror=missing-methods" >> cabal.project cat >> cabal.project <> cabal.project.local + $HCPKG list --simple-output --names-only | perl -ne 'for (split /\s+/) { print "constraints: any.$_ installed\n" unless /^(grapesy|quickstart)$/; }' >> cabal.project.local cat cabal.project cat cabal.project.local - name: dump install plan @@ -214,6 +222,8 @@ jobs: run: | cd ${PKGDIR_grapesy} || false ${CABAL} -vnormal check + cd ${PKGDIR_quickstart} || false + ${CABAL} -vnormal check - name: haddock run: | $CABAL v2-haddock --disable-documentation --haddock-all $ARG_COMPILER --with-haddock $HADDOCK $ARG_TESTS $ARG_BENCH all diff --git a/cabal.haskell-ci b/cabal.haskell-ci index 11a37fb0..943e5682 100644 --- a/cabal.haskell-ci +++ b/cabal.haskell-ci @@ -1,3 +1,3 @@ branches: main copy-fields: all -apt: libsnappy-dev +apt: libsnappy-dev protobuf-compiler diff --git a/cabal.project b/cabal.project index 4df67aef..97171219 100644 --- a/cabal.project +++ b/cabal.project @@ -1,4 +1,4 @@ -packages: ./grapesy +packages: ./grapesy, ./tutorials/quickstart package grapesy tests: True @@ -10,4 +10,6 @@ package grapesy -- allow-newer: proto-lens:base +allow-newer: proto-lens-protoc:base allow-newer: proto-lens-runtime:base +allow-newer: proto-lens-setup:base diff --git a/grapesy/src/Network/GRPC/Client/Call.hs b/grapesy/src/Network/GRPC/Client/Call.hs index f3ef6b9b..0ea82500 100644 --- a/grapesy/src/Network/GRPC/Client/Call.hs +++ b/grapesy/src/Network/GRPC/Client/Call.hs @@ -76,6 +76,10 @@ data Call rpc = SupportsClientRpc rpc => Call { -- | Scoped RPC call -- +-- This is the low-level API for making RPC calls, providing full flexibility. +-- You may wish to consider using the infrastructure from +-- "Network.GRPC.Client.StreamType.IO" instead. +-- -- Typical usage: -- -- > withRPC conn def (Proxy @ListFeatures) $ \call -> do @@ -99,7 +103,7 @@ data Call rpc = SupportsClientRpc rpc => Call { -- considered closed and the cancellation exception is not raised. Under normal -- circumstances (with well-behaved server handlers) this should not arise. -- (The gRPC specification itself is not very specific about this case; see --- discussion at https://stackoverflow.com/questions/55511528/should-grpc-server-side-half-closing-implicitly-terminate-the-client.) +-- discussion at .) -- -- If there are still /inbound/ messages upon leaving the scope of 'withRPC' no -- exception is raised (but the call is nonetheless still closed, and the server diff --git a/grapesy/src/Network/GRPC/Client/Connection.hs b/grapesy/src/Network/GRPC/Client/Connection.hs index c94d131a..5e7e10e0 100644 --- a/grapesy/src/Network/GRPC/Client/Connection.hs +++ b/grapesy/src/Network/GRPC/Client/Connection.hs @@ -63,6 +63,8 @@ import Network.GRPC.Util.TLS qualified as Util.TLS -- | Open connection to server -- +-- See 'withConnection'. +-- -- Before we can send RPC requests, we have to connect to a specific server -- first. Once we have opened a connection to that server, we can send as many -- RPC requests over that one connection as we wish. 'Connection' abstracts over @@ -263,6 +265,9 @@ data Server = -- | Open connection to the server -- +-- See 'Network.GRPC.Client.withRPC' for making individual RPCs on the new +-- connection. +-- -- The connection to the server is set up asynchronously; the first call to -- 'withRPC' will block until the connection has been established. -- diff --git a/grapesy/src/Network/GRPC/Common/Protobuf.hs b/grapesy/src/Network/GRPC/Common/Protobuf.hs index 66f5d750..3dfed48e 100644 --- a/grapesy/src/Network/GRPC/Common/Protobuf.hs +++ b/grapesy/src/Network/GRPC/Common/Protobuf.hs @@ -11,6 +11,7 @@ module Network.GRPC.Common.Protobuf ( , (.~) , (^.) -- ** "Data.ProtoLens" + , StreamingType(..) , HasField(..) , FieldDefault(..) , defMessage diff --git a/grapesy/src/Network/GRPC/Server.hs b/grapesy/src/Network/GRPC/Server.hs index 585ef694..7da41844 100644 --- a/grapesy/src/Network/GRPC/Server.hs +++ b/grapesy/src/Network/GRPC/Server.hs @@ -70,6 +70,13 @@ import Network.GRPC.Util.HTTP2.Stream (ClientDisconnected(..)) -- The server can be run using the standard infrastructure offered by the -- @http2@ package, but "Network.GRPC.Server.Run" provides some convenience -- functions. +-- +-- If you are using Protobuf (or if you have another way to compute a list of +-- methods at the type level), you may wish to use the infrastructure from +-- "Network.GRPC.Server.StreamType" (in particular, +-- 'Network.GRPC.Server.StreamType.fromMethods' or +-- 'Network.GRPC.Server.StreamType.fromServices') to construct the set of +-- handlers. mkGrpcServer :: ServerParams -> [SomeRpcHandler IO] -> IO HTTP2.Server mkGrpcServer params@ServerParams{serverTopLevel} handlers = do ctxt <- newServerContext params diff --git a/grapesy/src/Network/GRPC/Server/StreamType.hs b/grapesy/src/Network/GRPC/Server/StreamType.hs index 9f9c505d..5379aae9 100644 --- a/grapesy/src/Network/GRPC/Server/StreamType.hs +++ b/grapesy/src/Network/GRPC/Server/StreamType.hs @@ -209,15 +209,20 @@ fromNextElem _ = NextElem.toStreamElem def -- -- This will reveal types such as this: -- --- > _getFeature :: NonStreamingHandler IO (Protobuf RouteGuide "getFeature") +-- > _getFeature :: ServerHandler' NonStreaming IO (Protobuf RouteGuide "getFeature") -- --- Finally, if we then refine the skeleton to +-- which we can simplify to +-- +-- > _getFeature :: ServerHandler IO (Protobuf RouteGuide "getFeature") +-- +-- (the non-primed version of 'ServerHandler' infers the streaming type from +-- the RPC, when possible). Finally, if we then refine the skeleton to -- -- > Method (mkNonStreaming $ _getFeature) -- -- ghc will tell us -- --- > _getFeature :: Point -> IO Feature +-- > _getFeature :: Proto Point -> IO (Proto Feature) data Methods (m :: Type -> Type) (rpcs :: [k]) where -- | All methods of the service handled NoMoreMethods :: Methods m '[] @@ -298,6 +303,15 @@ fromMethod = SServerStreaming -> someRpcHandler . fromStreamingHandler SBiDiStreaming -> someRpcHandler . fromStreamingHandler +-- | List handlers for all methods of a given service. +-- +-- This can be used to verify at the type level that all methods of the given +-- service are handled. A typical definition of the 'Methods' might have a type +-- such as this: +-- +-- > methods :: Methods IO (ProtobufMethodsOf RouteGuide) +-- +-- See also 'fromServices' if you are defining more than one service. fromMethods :: forall m rpcs. MonadIO m => Methods m rpcs -> [SomeRpcHandler m] @@ -309,6 +323,15 @@ fromMethods = go go (RawMethod m ms) = someRpcHandler m : go ms go (UnsupportedMethod ms) = go ms +-- | List handlers for all methods of all services. +-- +-- This can be used to verify at the type level that all methods of all services +-- are handled. A typical definition of the 'Services' might have a type such as +-- this: +-- +-- > services :: Services IO (ProtobufServices '[Greeter, RouteGuide]) +-- +-- See also 'fromMethods' if you are only defining one service. fromServices :: forall m servs. MonadIO m => Services m servs -> [SomeRpcHandler m] diff --git a/tutorials/quickstart/LICENSE b/tutorials/quickstart/LICENSE new file mode 100644 index 00000000..54362a91 --- /dev/null +++ b/tutorials/quickstart/LICENSE @@ -0,0 +1,31 @@ +Copyright (c) 2023-2024, Well-Typed LLP and Anduril Industries Inc. + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Well-Typed LLP, the name of Anduril + Industries Inc., nor the names of other contributors may be + used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/tutorials/quickstart/LICENSE.proto b/tutorials/quickstart/LICENSE.proto new file mode 100644 index 00000000..6c72d38d --- /dev/null +++ b/tutorials/quickstart/LICENSE.proto @@ -0,0 +1,18 @@ +The protobuf file is a modified version of `examples/protos/helloworld.proto` +from the official gRPC repository at https://github.com/grpc/grpc. + +Its license is reproduced below: + +// Copyright 2015 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. \ No newline at end of file diff --git a/tutorials/quickstart/Setup.hs b/tutorials/quickstart/Setup.hs new file mode 100644 index 00000000..bf45b62e --- /dev/null +++ b/tutorials/quickstart/Setup.hs @@ -0,0 +1,3 @@ +import Data.ProtoLens.Setup + +main = defaultMainGeneratingProtos "proto" \ No newline at end of file diff --git a/tutorials/quickstart/app/Client.hs b/tutorials/quickstart/app/Client.hs new file mode 100644 index 00000000..51d0852e --- /dev/null +++ b/tutorials/quickstart/app/Client.hs @@ -0,0 +1,18 @@ +module Client (main) where + +import Network.GRPC.Client +import Network.GRPC.Client.StreamType.IO +import Network.GRPC.Common +import Network.GRPC.Common.Protobuf + +import Proto.API.Helloworld + +main :: IO () +main = + withConnection def server $ \conn -> do + let req = defMessage & #name .~ "you" + resp <- nonStreaming conn (rpc @(Protobuf Greeter "sayHello")) req + print resp + where + server :: Server + server = ServerInsecure $ Address "127.0.0.1" defaultInsecurePort Nothing diff --git a/tutorials/quickstart/app/Server.hs b/tutorials/quickstart/app/Server.hs new file mode 100644 index 00000000..8fde68be --- /dev/null +++ b/tutorials/quickstart/app/Server.hs @@ -0,0 +1,29 @@ +module Server (main) where + +import Network.GRPC.Common +import Network.GRPC.Common.Protobuf +import Network.GRPC.Server.Protobuf +import Network.GRPC.Server.Run +import Network.GRPC.Server.StreamType + +import Proto.API.Helloworld + +methods :: Methods IO (ProtobufMethodsOf Greeter) +methods = + Method (mkNonStreaming sayHello) + $ NoMoreMethods + +sayHello :: Proto HelloRequest -> IO (Proto HelloReply) +sayHello req = do + let resp = defMessage & #message .~ "Hello, " <> req ^. #name + return resp + +main :: IO () +main = + runServerWithHandlers def config $ fromMethods methods + where + config :: ServerConfig + config = ServerConfig { + serverInsecure = Just (InsecureConfig Nothing defaultInsecurePort) + , serverSecure = Nothing + } diff --git a/tutorials/quickstart/proto/helloworld.proto b/tutorials/quickstart/proto/helloworld.proto new file mode 100644 index 00000000..4f2dd1f2 --- /dev/null +++ b/tutorials/quickstart/proto/helloworld.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +// The greeting service definition. +service Greeter { + // Sends a greeting + rpc SayHello (HelloRequest) returns (HelloReply) {} +} + +// The request message containing the user's name. +message HelloRequest { + string name = 1; +} + +// The response message containing the greetings +message HelloReply { + string message = 1; +} diff --git a/tutorials/quickstart/quickstart.cabal b/tutorials/quickstart/quickstart.cabal new file mode 100644 index 00000000..984009d1 --- /dev/null +++ b/tutorials/quickstart/quickstart.cabal @@ -0,0 +1,68 @@ +cabal-version: 3.0 +name: quickstart +version: 0.1.0 +license: BSD-3-Clause +license-file: LICENSE +author: Edsko de Vries +maintainer: edsko@well-typed.com +build-type: Custom +extra-source-files: proto/helloworld.proto +tested-with: GHC==8.10.7 + , GHC==9.2.8 + , GHC==9.4.8 + , GHC==9.6.4 + , GHC==9.8.2 + , GHC==9.10.1 + +custom-setup + setup-depends: + base + , Cabal + , proto-lens-setup + +common lang + build-depends: base >= 4.14 && < 5 + default-language: Haskell2010 + ghc-options: -Wall + + default-extensions: + DataKinds + OverloadedLabels + OverloadedStrings + TypeApplications + TypeFamilies + +library + import: lang + hs-source-dirs: src + build-tool-depends: proto-lens-protoc:proto-lens-protoc + + build-depends: + , grapesy + , proto-lens-runtime + exposed-modules: + Proto.API.Helloworld + other-modules: + Proto.Helloworld + autogen-modules: + Proto.Helloworld + +executable greeter_server + import: lang + main-is: Server.hs + hs-source-dirs: app + ghc-options: -main-is Server + + build-depends: + , grapesy + , quickstart + +executable greeter_client + import: lang + main-is: Client.hs + hs-source-dirs: app + ghc-options: -main-is Client + + build-depends: + , grapesy + , quickstart \ No newline at end of file diff --git a/tutorials/quickstart/src/Proto/API/Helloworld.hs b/tutorials/quickstart/src/Proto/API/Helloworld.hs new file mode 100644 index 00000000..d6a98386 --- /dev/null +++ b/tutorials/quickstart/src/Proto/API/Helloworld.hs @@ -0,0 +1,15 @@ +module Proto.API.Helloworld ( + module Proto.Helloworld + ) where + +import Proto.Helloworld +import Network.GRPC.Common.Protobuf +import Network.GRPC.Common + +{------------------------------------------------------------------------------- + Metadata +-------------------------------------------------------------------------------} + +type instance RequestMetadata (Protobuf Greeter meth) = NoMetadata +type instance ResponseInitialMetadata (Protobuf Greeter meth) = NoMetadata +type instance ResponseTrailingMetadata (Protobuf Greeter meth) = NoMetadata