Skip to content
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

Support dynamic messages with Initializer options #630

Closed
wants to merge 10 commits into from
Closed

Conversation

emcfarlane
Copy link
Contributor

@emcfarlane emcfarlane commented Nov 7, 2023

To support dynamic clients and servers we need to initialize messages on Receive. This adds two new options WithRequestInitializer and WithResponseInitializer that provide InitializerFuncs that help construct request messages before use. Proto based schemas can use the new Schema field on Spec to introspect the method types. Other IDLs can set their own schema using the WithSchema option.

Dynamic Message Initializer

As an example to support dynamicpb.Message we will inspect the schema and set the message descriptor. An intializer func can be run on the client or handller. This func is provided to the examples below as an option:

// dynamicMessageInitialzier inits a dynamicpb.Message descriptor to the correct
// input or output type for the method.
func dynamicMessageInitializer(spec Spec, msg any) error {
	dynamic, ok := msg.(*dynamicpb.Message)
	if !ok {
		return nil
	}
	desc, ok := spec.Schema.(protoreflect.MethodDescriptor)
	if !ok {
		return fmt.Errorf("unexpected schema type %T for %T message", spec.Schema, dynamic)
	}
	// Client initializer is run on response, Server on request.
	if spec.IsClient {
		*dynamic = *dynamicpb.NewMessage(desc.Output())
	} else {
		*dynamic = *dynamicpb.NewMessage(desc.Input())
	}
	return nil
}

Dynamic Clients

Construct a client using dynamicpb.Message. Ensure the URL includes the suffix of the handler method to invoke and to include WithSchema and WithResponseInitializer options:

desc, _ = protoregistry.GlobalFiles.FindDescriptorByName("connect.ping.v1.PingService.Ping")
methodDesc := desc.(protoreflect.MethodDescriptor)
// Create a client with [dynamicpb.Message] as the request and response message types.
client := connect.NewClient[dynamicpb.Message, dynamicpb.Message](
	http.DefaultClient,
	serverURL + "/connect.ping.v1.PingService/Ping",
	connect.WithSchema(methodDesc),
	connect.WithResponseInitializer(dynamicMessageInitializer),
	connect.WithIdempotency(connect.IdempotencyNoSideEffects),
)
// Build the message using the input descriptor.
msg := dynamicpb.NewMessage(methodDesc.Input())
msg.Set(
	methodDesc.Input().Fields().ByName("number"),
	protoreflect.ValueOfInt64(42),
)
res, _ := client.CallUnary(context.Background(), connect.NewRequest(msg))
// Get values from the message using the output descriptor.
got := res.Msg.Get(
	methodDesc.Output().Fields().ByName("number")
).Int()

Dynamic Handlers

Create the server func with a signature that includes the dynamicpb.Message. Then construct the handler specifying the procedure URL and ensure to include WithSchema and WithRequestInitializer options:

// Create a handler func with [dynamicpb.Message] as the request and response message types.
dynamicPing := func(_ context.Context, req *connect.Request[dynamicpb.Message]) (*connect.Response[dynamicpb.Message], error) {
	got := req.Msg.Get(methodDesc.Input().Fields().ByName("number")).Int()
	msg := dynamicpb.NewMessage(methodDesc.Output())
	msg.Set(
		methodDesc.Output().Fields().ByName("number"),
		protoreflect.ValueOfInt64(got),
	)
	return connect.NewResponse(msg), nil
}
mux := http.NewServeMux()
mux.Handle("/connect.ping.v1.PingService/Ping",
	connect.NewUnaryHandler(
		"/connect.ping.v1.PingService/Ping",
		dynamicPing,
		connect.WithSchema(methodDesc),
		connect.WithRequestInitializer(dynamicMessageInitializer),
		connect.WithIdempotency(connect.IdempotencyNoSideEffects),
	),
)

Fixes #523

New field Schema of type any on Spec objects. For proto based schemas
the type will be of protoreflect.MethodDescriptor. This allows for easy
introspection to interceptors.
An Initializer helps to construct dynamic messages on Receive. This lets
clients and servers use dynamic messages. A default initializer
for dynamicpb.Message is provided. Other IDLs can provide custom
Initializers using the WithInitializer option.
client.go Show resolved Hide resolved
protocol.go Outdated Show resolved Hide resolved
connect.go Outdated Show resolved Hide resolved
option.go Outdated Show resolved Hide resolved
Base automatically changed from ed/schema to main November 9, 2023 13:31
Copy link
Member

@jhump jhump left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI, I've merged the schema PR, so this needs a rebase

@emcfarlane emcfarlane marked this pull request as ready for review November 9, 2023 15:42
option.go Show resolved Hide resolved
Copy link
Member

@akshayjshah akshayjshah left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haven't looked at the implementation in detail, but the exported API makes sense to me.

@emcfarlane
Copy link
Contributor Author

Closing, reopening as a fork PR: #640

@emcfarlane emcfarlane closed this Nov 20, 2023
@jhump jhump deleted the ed/dynamic branch February 2, 2024 16:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support for dynamic or reflected types for client
3 participants