-
Notifications
You must be signed in to change notification settings - Fork 19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Quality of life enhancements for RPC modules #49
Conversation
src/plugin/emit.ml
Outdated
Code.emit t `Begin "module %s = struct" capitalized_name; | ||
Code.emit t `None "let name = \"/%s/%s\"" service_name name; | ||
Code.emit t `None "let service = \"%s\"" (Scope.get_proto_path scope); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code.emit t `None "let service = \"%s\"" (Scope.get_proto_path scope); | |
Code.emit t `None "let service = \"%s\"" service_name; |
53d5f5d
to
ef3872c
Compare
In general, I'm all in favor of improving developer lives, and I'm ok with merging the PR. However, I think there is an alternative better and more scalable approach; Extending on the pattern in the service module ( Consider the following utility functions: open Runtime
open Service
let create_coders (type t) (module M: Message with type t = t) =
let encode t = M.to_proto t |> Runtime'.Writer.contents in
let decode s = Runtime'.Reader.create s |> M.from_proto in
(encode, decode)
let get_service_attributes (type req) (type rep) (module R : Rpc with type Request.t = req and type Response.t = rep) =
let (service, rpc) = match String.split_on_char '/' R.name with
| [_; service; rpc] -> (service, rpc)
| _ -> failwith ("Unable to parse name: " ^ R.name)
in
let request_coders = create_coders (module R.Request) in
let response_coders = create_coders (module R.Response) in
(service, rpc, request_coders, response_coders) Invoking Let me know what you think. As I said, I'm ok of merging the PR, but would like your take on this first. |
I agree the calling code generated could be much better. I tried out the suggestion (putting the code in (* gRPC client code *)
let do_request = H2_lwt_unix.Client.request connection ~error_handler:ignore in
(* code generation *)
let open Ocaml_protoc_plugin in
let open Greeter.Mypackage in
(* Code gen this module! *)
let a =
let module M = struct
let name = Greeter.SayHello.name
module Request = Greeter.SayHello.Request
module Response = Greeter.SayHello.Response
end in
(module M : Service.Rpc with type Request.t = M.Request.t and type Response.t = M.Response.t)
in
let (service, rpc, _, (encode, decode)) = Service.get_service_attributes a in
Client.call ~service ~rpc ~do_request
~handler:
(Client.Rpc.unary (encode req) ~f:(fun response ->
let+ response = response in
match response with
| Some response -> (
decode response |> function
| Ok v -> v
| Error e ->
failwith
(Printf.sprintf "Could not decode request: %s"
(Result.show_error e)))
| None -> Greeter.SayHello.Response.make () (* Having access to this from module M would be useful. *)))
() I slightly prefer a record type to be returned from let decode, encode = Service.make_service_functions Greeter.sayHello in
let encode, decode = Service.create_coders (fst Greeter.sayHello) in The returned types from the second are better but the argument For defining the service type it does clean up things: let server =
let open Ocaml_protoc_plugin in
let open Greeter.Mypackage in
(* Code gen this module! *)
let a =
let module M = struct
let name = Greeter.SayHello.name
module Request = Greeter.SayHello.Request
module Response = Greeter.SayHello.Response
end in
(module M : Service.Rpc with type Request.t = M.Request.t and type Response.t = M.Response.t)
in
let (service, rpc, _, _) = Service.get_service_attributes a in
Server.(
v ()
|> add_service ~name:service
~service:Server.Service.(v ()
|> add_rpc ~name:rpc ~rpc:(Unary say_hello)
|> handle_request)) This can probably be improved further with more experimentation. I am working on improving the API for gRPC with This PR came originally from patches @quernd at Dialohq had for the codegen. It might be helpful for them to have this accepted and then have a breaking change to get a redesigned API. |
My suggestion was actually that the code would reside on the gRPC side of things and not extend the By defining utility functions for creating client and server, you could hide all boilerplate code behind functions which essentially just takes an let create_client (type req) (type rep) (module R : Rpc with type Request.t = req and type Response.t = rep) = ...
let create_server (type req) (type rep) (module R : Rpc with type Request.t = req and type Response.t = rep) = ...
The point is that if all the needed information is available in the auto generated modules, its trivial to create generic functions to extract this information. |
Thanks @tmcgilchrist for opening this PR and bringing me into the discussion!
That's a good suggestion and maybe that's the most flexible way to avoid boilerplate code. The only thing that irks me a bit is this:
It's a minor inconvenience but it goes against the spirit of generating typesafe code to concatenate strings only to split them again. So
The confusing bit here might be that you always need both Request and Response. The client needs an encoder for the request and a decoder for the response, and the server vice versa. That said, I'd also prefer a record, or even a module. But like @andersfugmann says: if all the information is there we can handle it on our side as we please.
It would be helpful, yes. But I'm starting to think that it's better to use this pattern instead, either in ocaml-grpc or (for the time being) in our own code:
And then for alternative serialization libraries we can leverage the same pattern and define a common record type or module signature that the client and server APIs expect. What do you think? |
I fully agree. I am however a bit confused with the names |
It's the terminology used in protobuf files: ocaml-protoc-plugin/examples/echo/echo.proto Lines 23 to 25 in d57a66b
I think anything else wouldn't be more descriptive, but rather more confusing. |
Fair point, but if IIRC the What terms are using for gRPC? Maybe we could use the same terminology. |
Looking at the API for various languages supported for |
My misunderstanding, it would be more useful for
Happy to provide a PR that does just that, if everyone is agreed it is the right way to go. |
This sounds like the best way forward to me 👍 |
I've created #50 which added the extra fields + tests as suggested above. |
@tmcgilchrist Would the changes made in #50 work for your use case? |
I've merged #50. Would you prefer a new opam release with these changes, or should we hold back a bit for you to try out the changes to see if we need to make additional changes to the interfaces? |
I've create a new release 4.5.0 (ocaml/opam-repository#23936) containing the changes. |
Quality of life enhancements for RPC modules
service
,rpc
,encode
,decode
. This cleans up the calling code for ocaml-grpc.For the Greeter gRPC example of:
This change produces this code:
Calling this code can then be configured with the generated types rather than using strings. For example: