Skip to content

Conversation

@stefanadranca
Copy link
Collaborator

@stefanadranca stefanadranca commented Dec 20, 2023

Motivation:
In the generated client code for a service described in a Source IDL file we want to have:

  • a protocol declaring the methods for a client, with the serializer and deserializer as parameters.
  • an extension for the protocol defining the methods of the client, without the serializer and deserializer as parameters,
    that are calling the already declared methods, providing the default Protobuf serializer and deserializer.
  • a struct representing the generated client that has a GRPCClient as a property and conforms to the Client Protocol.

Modifications:

  • Implemented the ClientCodeTranslator struct that contains all the methods for creating the StructuredSwiftRepresentation for the client code.
  • Added a new struct in the StructuredSwiftRepresentation that represents closure signatures, and a new ParameterDescription property that is set only when the parameter is a closure signature.
  • Added and modified render functions for the StructuredSwiftRepresentation additions.
  • Implemented snippet tests.

Result:
Client code can now be generated for a CodeGenerationRequest object.

Comment on lines 520 to 521
genericType: String? = nil,
conformances: [String] = []
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We need these to be able to represent generic functions

Copy link
Collaborator

Choose a reason for hiding this comment

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

This isn't quite right: the "conformance" here applies to the generic type, not the function. Also functions can have multiple generic types.

It looks like the best way to represent this would be by modifying the FunctionSignatureDescription to add: a generics: [ExistingTypeDescription] property and an optional var whereClause: WhereClause?.

That way we can express the Sendable requirement as a where clause:

func foo<R>() -> R where R: Sendable { 
 // ...
}

///
/// The parameter doesn't have a type, but is a closure.
/// For example: `(String) async throws -> Int`.
var closureDescription: ClosureSignatureDescription? = nil
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I have decided to have a different variable that signals if the parameter is a closure (and also its description) because the ExistingTypeDescription rendering doesn't allow more complex types.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think this is the right decision, ExistingTypeDescription should be able to describe a closure so should probably just have a closure case which holds a ClosureSignatureDescription

@stefanadranca stefanadranca marked this pull request as ready for review December 20, 2023 16:58
)
let expectedSwift =
"""
protocol namespaceA_ServiceAClientProtocol: Sendable {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Docs are missing

let expectedSwift =
"""
protocol namespaceA_ServiceAClientProtocol: Sendable {
func methodA<R: Sendable>(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Docs are missing

Comment on lines 75 to 79
request,
namespaceA.ServiceA.Methods.methodA.descriptor,
serializer,
deserializer,
body
Copy link
Collaborator

Choose a reason for hiding this comment

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

The labels are missing here

)
}
}
struct namespaceA_ServiceAClient: namespaceA.ServiceA.ClientProtocol {
Copy link
Collaborator

Choose a reason for hiding this comment

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

docs

init(client: GRPCCore.GRPCClient) {
self.client = client
}
func methodA<R: Sendable>(
Copy link
Collaborator

Choose a reason for hiding this comment

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

docs

func methodA<R: Sendable>(
request: ClientRequest.Single<namespaceA.ServiceA.Methods.methodA.Input>,
_ body: @Sendable @escaping (ClientResponse.Single<namespaceA.ServiceA.Methods.methodA.Output>) async throws -> R
) async rethrows -> R {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This should just be throws (the client API changed slightly so we can't use rethrows anymore)

Comment on lines 520 to 521
genericType: String? = nil,
conformances: [String] = []
Copy link
Collaborator

Choose a reason for hiding this comment

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

This isn't quite right: the "conformance" here applies to the generic type, not the function. Also functions can have multiple generic types.

It looks like the best way to represent this would be by modifying the FunctionSignatureDescription to add: a generics: [ExistingTypeDescription] property and an optional var whereClause: WhereClause?.

That way we can express the Sendable requirement as a where clause:

func foo<R>() -> R where R: Sendable { 
 // ...
}

Comment on lines 15 to 16
*/
/// Creates a representation for the client code that will be generated based on the ``CodeGenerationRequest`` object
Copy link
Collaborator

Choose a reason for hiding this comment

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

Leave a blank line between these

Comment on lines 54 to 63
/// ) async rethrows -> R {
/// try await self.client.clientStreaming(
/// request,
/// namespaceA.ServiceA.Methods.methodA.descriptor,
/// serializer,
/// deserializer,
/// body
/// )
/// }
/// }
Copy link
Collaborator

Choose a reason for hiding this comment

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

The indentation looks off here

for service: CodeGenerationRequest.ServiceDescriptor,
in codeGenerationRequest: CodeGenerationRequest
) -> Declaration {
let methods = service.methods.compactMap {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This should just be a map as makeClientProtocolMethod doesn't return an optional

for service: CodeGenerationRequest.ServiceDescriptor,
in codeGenerationRequest: CodeGenerationRequest
) -> Declaration {
let methods = service.methods.compactMap {
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: use map here instead

for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor,
in service: CodeGenerationRequest.ServiceDescriptor,
from codeGenerationRequest: CodeGenerationRequest,
serializerDeserializer: Bool
Copy link
Collaborator

Choose a reason for hiding this comment

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

don't love this naming, serializerAndDeserializer / generateSerializerAndDeserializer are clearer

Comment on lines 153 to 161
if !serializerDeserializer {
let body = self.makeSerializerDeserializerCall(
for: method,
in: service,
from: codeGenerationRequest
)
return .function(signature: functionSignature, body: body)
}
return .function(signature: functionSignature)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Bit of a subjective style thing, but in my opinion if the condition being checked is an if-else then we should spell out both branches (i.e. if serializerDeserializer { x } else { y }). If the condition is being used to determine whether we can continue then it's okay to just have an if/guard (e.g. if !inputIsValid { return }).

Comment on lines 221 to 223
method.isInputStreaming
? ExistingTypeDescription.member(["ClientRequest", "Stream"])
: ExistingTypeDescription.member(["ClientRequest", "Single"])
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: minimise branching

let requestType = method.isInputStreaming ? "Stream" : "Single"
let clientRequestType = ExistingTypeDescription.member(["ClientRequest", requestType])

Comment on lines 333 to 342
if method.isInputStreaming && method.isOutputStreaming {
return "bidirectionalStreaming"
}
if method.isInputStreaming && !method.isOutputStreaming {
return "clientStreaming"
}
if !method.isInputStreaming && method.isOutputStreaming {
return "serverStreaming"
}
return "unary"
Copy link
Collaborator

Choose a reason for hiding this comment

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

There's a neater way of doing this which ensures you cover all possibilities:

switch (method.isInputStreaming, method.isOutputStreaming) {
case (true, true):
  return "bidi..."
case (true, false):
  return "client..."
case (false, true):
  return "server..."
case (false, false):
  return "unary"
}

)
}

private func getGRPCMethodName(
Copy link
Collaborator

Choose a reason for hiding this comment

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

"grpc method name" is misleading here, it sounds like the name of the RPC, clientMethod is a bit clearer. Also we should just pass in two parameters here: input and output streaming

}
writer.writeLine(")")

do {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This do looks unnecessary, there's no catch


do {
let keywords = signature.keywords
if !keywords.isEmpty {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This looks unnecessary too, we'll catch the is empty catch in the loop over keywords

writer.writeLine(renderedFunctionKind(signature.kind) + "(")
let generics = signature.generics
let genericsString =
"\(generics.map{renderedExistingTypeDescription($0)}.joined(separator: ", "))"
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
"\(generics.map{renderedExistingTypeDescription($0)}.joined(separator: ", "))"
"\(generics.map {renderedExistingTypeDescription($0) }.joined(separator: ", "))"

///
/// The parameter doesn't have a type, but is a closure.
/// For example: `(String) async throws -> Int`.
var closureDescription: ClosureSignatureDescription? = nil
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think this is the right decision, ExistingTypeDescription should be able to describe a closure so should probably just have a closure case which holds a ClosureSignatureDescription

Comment on lines 24 to 25
let serverCodeTranslator = ServerCodeTranslator()
let clientCodeTranslator = ClientCodeTranslator()
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should only create these if we use them, i.e. in the if server/if client blocks

@glbrntt glbrntt merged commit 53855b1 into grpc:main Jan 12, 2024
glbrntt pushed a commit to glbrntt/grpc-swift that referenced this pull request Feb 5, 2024
Motivation:

In the generated client code for a service described in a Source IDL file we want to have:
- a protocol declaring the methods for a client, with the serializer and deserializer as parameters.
- an extension for the protocol defining the methods of the client, without the serializer and deserializer as parameters,
that are calling the already declared methods, providing the default Protobuf serializer and deserializer.
- a struct representing the generated client that has a GRPCClient as a property and conforms to the Client Protocol.

Modifications:

- Implemented the ClientCodeTranslator struct that contains all the methods for creating the `StructuredSwiftRepresentation` for the client code.
- Added a new struct in the `StructuredSwiftRepresentation` that represents closure signatures, and a new `ParameterDescription` property that is set only when the parameter is a closure signature.
- Added and modified render functions for the `StructuredSwiftRepresentation` additions.
- Implemented snippet tests.

Result:

Client code can now be generated for a CodeGenerationRequest object.
@glbrntt glbrntt added the version/v2 Relates to v2 label Feb 8, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

version/v2 Relates to v2

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants