I’m building a gRPC library and trying to figure out how to route strings from the request to an actual method dispatch. Here’s an example of what I’m working with:
abstract class CatalogService
include Protobuf::Service
# rpc GetProduct (ProductRequest) returns (ProductResponse) {}
rpc GetProduct, ProductRequest, ProductResponse
# rpc SearchProducts (ProductSearchRequest) returns (ProductsResponse) {}
rpc SearchProducts, ProductSearchRequest, ProductsResponse
end
Given this code, I need some way of getting the string ”GetProduct”
to call the get_product
method. I’m thinking the result code might need something along the lines of this:
def handle(method, request_body)
case method
when “GetProduct”
get_product(ProductRequest.from_protobuf(request_body)
when “SearchProducts”
search_products(ProductSearchRequest.from_protobuf(request_body)
# ...
end
end
But I haven’t figured out a way to do that with individual calls to the rpc
macro. Do I need to combine my calls to rpc
into a single macro invocation?
1 Like
It’s possible. Here’s one way:
module Protobuf::Service
macro included
RPCS = [] of Nil
end
macro rpc(name, input, output)
{% RPCS << [name.id, input.id, output.id] %}
end
def handle(method, request_body)
{% begin %}
case method
{% for rpc in @type.constant("RPCS") %}
when {{rpc[0].stringify}}
{{rpc[0].underscore}}({{rpc[1]}}.from_protobuf(request_body))
{% end %}
end
{% end %}
end
end
class CatalogService
include Protobuf::Service
# rpc GetProduct (ProductRequest) returns (ProductResponse) {}
rpc GetProduct, ProductRequest, ProductResponse
# rpc SearchProducts (ProductSearchRequest) returns (ProductsResponse) {}
rpc SearchProducts, ProductSearchRequest, ProductsResponse
end
service = CatalogService.new
service.handle("GetProduct", "some_body")
The idea is to define an RCPS
constant that in each included type that, at compile time, accumulates stuff on each rpc
macro call. We always have to type the array but we use [] of Nil
as a way to say “we aren’t really going to use this at runtime, just at compile time”. It’s a bit hacky, maybe, maybe not.
Then we define a handle
method that traverses that RCPS
array that we accumulated. We can’t use RPCS
directly because constants are looked up on the class that defines the method, not subtypes, but we can use @type.constant("RPCS")
to get it on the actual type. Another way would be to also define handle
in the included
hook but then we have to double escape.
1 Like
My take on routing is that the mapping of string to methods should be done either as a convention or in a way the compiler checks if the string will be resolved to a method.
In https://github.com/bcardiff/crystal-routing I dropped a way to do that in a basic way. For both http or plain strings scenarios.
2 Likes
Ah, the macro constant. Excellent idea! I was trying all kinds of things last night:
- Defining
handle
inside a macro finished
- This didn’t seem to work at all, but I suppose that kinda makes sense since I guess all the methods are supposed to have been defined already?
- This is the first time I’ve ever used this hook so I don’t quite understand the mechanics
- Annotations (which might actually not be a bad idea, but not the idea)
- A bunch of other things I can’t quite remember right now
- Crying
Oh snap, I think this might be an easy way to get what I want, based on where the code is now. I keep forgetting about previous_def
.
Sure enough, this worked:
module Protobuf
module Service
# ...
macro included
def handle(method_name : String, request_body : IO)
raise InvalidMethodName.new("Unknown RPC method {{@type.id}}/#{method_name}")
end
macro rpc(name, receives, returns)
abstract def \{{name.stringify.underscore.id}}(request : \{{receives}}) : \{{returns}}
def handle(method_name : String, request_body : IO)
if method_name == \{{name.stringify}}
\{{name.stringify.underscore.id}}(\{{receives}}.from_protobuf(request_body))
else
previous_def(method_name, request_body)
end
end
end
end
end
end
The nested macro was primarily to mention @type
in the exception. I’m not sure that’ll be necessary, though, so I might be able to clean it up a bit.
1 Like