-
Notifications
You must be signed in to change notification settings - Fork 422
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Motivation: A number of test in this package and others rely on ad-hoc services using Codable. This is less overhead than using protobuf as you it's not always available. It also means the messages are defined in Swift so they're easy to change without needing to regenerate. However, the service glue code is hand rolled. We can avoid this by having a little adapter sit on top of the code gen lib. Modifications: - Add a grpc-dev-tool package to dev. We can use this as a place to add tooling and other helpers without worrying about worsening the experience for end users (because of additional dependencies, more public API and so on). - For now this has a single executable for generating code from a JSON config file. The schema for the services is limited, but that's fine, it's not a general purpose tool. Result: - We have a tool which can generate grpc code from a JSON definition which uses Codable message types.
- Loading branch information
Showing
8 changed files
with
483 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
.DS_Store | ||
/.build | ||
/Packages | ||
xcuserdata/ | ||
DerivedData/ | ||
.swiftpm/configuration/registries.json | ||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata | ||
.netrc |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
// swift-tools-version:6.0 | ||
/* | ||
* Copyright 2025, gRPC Authors All rights reserved. | ||
* | ||
* 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. | ||
*/ | ||
|
||
import PackageDescription | ||
|
||
let package = Package( | ||
name: "grpc-dev-tool", | ||
platforms: [.macOS(.v15)], | ||
dependencies: [ | ||
.package(path: "../.."), | ||
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"), | ||
], | ||
targets: [ | ||
.executableTarget( | ||
name: "grpc-dev-tool", | ||
dependencies: [ | ||
.product(name: "GRPCCodeGen", package: "grpc-swift"), | ||
.product(name: "ArgumentParser", package: "swift-argument-parser"), | ||
] | ||
) | ||
] | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
/* | ||
* Copyright 2025, gRPC Authors All rights reserved. | ||
* | ||
* 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. | ||
*/ | ||
|
||
import ArgumentParser | ||
|
||
@main | ||
struct GRPCDevTool: AsyncParsableCommand { | ||
static let configuration = CommandConfiguration( | ||
commandName: "grpc-dev-tool", | ||
subcommands: [GenerateJSON.self] | ||
) | ||
} |
75 changes: 75 additions & 0 deletions
75
...rpc-dev-tool/Sources/grpc-dev-tool/Subcommands/GenerateJSON/GRPCCodeGen+Conversions.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
/* | ||
* Copyright 2025, gRPC Authors All rights reserved. | ||
* | ||
* 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. | ||
*/ | ||
|
||
import GRPCCodeGen | ||
|
||
/// Creates a `ServiceDescriptor` from a JSON `ServiceSchema`. | ||
extension ServiceDescriptor { | ||
init(_ service: ServiceSchema) { | ||
self.init( | ||
documentation: "", | ||
name: .init( | ||
identifyingName: service.name, | ||
typeName: service.name, | ||
propertyName: service.name | ||
), | ||
methods: service.methods.map { | ||
MethodDescriptor($0) | ||
} | ||
) | ||
} | ||
} | ||
|
||
extension MethodDescriptor { | ||
/// Creates a `MethodDescriptor` from a JSON `ServiceSchema.Method`. | ||
init(_ method: ServiceSchema.Method) { | ||
self.init( | ||
documentation: "", | ||
name: .init( | ||
identifyingName: method.name, | ||
typeName: method.name, | ||
functionName: method.name | ||
), | ||
isInputStreaming: method.kind.streamsInput, | ||
isOutputStreaming: method.kind.streamsOutput, | ||
inputType: method.input, | ||
outputType: method.output | ||
) | ||
} | ||
} | ||
|
||
extension CodeGenerator.Config.AccessLevel { | ||
init(_ level: GeneratorConfig.AccessLevel) { | ||
switch level { | ||
case .internal: | ||
self = .internal | ||
case .package: | ||
self = .package | ||
} | ||
} | ||
} | ||
|
||
extension CodeGenerator.Config { | ||
init(_ config: GeneratorConfig) { | ||
self.init( | ||
accessLevel: CodeGenerator.Config.AccessLevel(config.accessLevel), | ||
accessLevelOnImports: config.accessLevelOnImports, | ||
client: config.generateClient, | ||
server: config.generateServer, | ||
indentation: 2 | ||
) | ||
} | ||
} |
83 changes: 83 additions & 0 deletions
83
...c-dev-tool/Sources/grpc-dev-tool/Subcommands/GenerateJSON/GRPCDevUtils+GenerateJSON.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
/* | ||
* Copyright 2025, gRPC Authors All rights reserved. | ||
* | ||
* 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. | ||
*/ | ||
|
||
import ArgumentParser | ||
import Foundation | ||
|
||
struct GenerateJSON: ParsableCommand { | ||
static let configuration = CommandConfiguration( | ||
commandName: "generate-json", | ||
subcommands: [Generate.self, DumpConfig.self], | ||
defaultSubcommand: Generate.self | ||
) | ||
} | ||
|
||
extension GenerateJSON { | ||
struct Generate: ParsableCommand { | ||
@Argument(help: "The path to a JSON input file.") | ||
var input: String | ||
|
||
func run() throws { | ||
// Decode the input file. | ||
let url = URL(filePath: self.input) | ||
let data = try Data(contentsOf: url) | ||
let json = JSONDecoder() | ||
let config = try json.decode(JSONCodeGeneratorRequest.self, from: data) | ||
|
||
// Generate the output and dump it to stdout. | ||
let generator = JSONCodeGenerator() | ||
let sourceFile = try generator.generate(request: config) | ||
print(sourceFile.contents) | ||
} | ||
} | ||
} | ||
|
||
extension GenerateJSON { | ||
struct DumpConfig: ParsableCommand { | ||
func run() throws { | ||
// Create a request for the code generator using all four RPC kinds. | ||
var request = JSONCodeGeneratorRequest( | ||
service: ServiceSchema(name: "Echo", methods: []), | ||
config: .defaults | ||
) | ||
|
||
let methodNames = ["get", "collect", "expand", "update"] | ||
let methodKinds: [ServiceSchema.Method.Kind] = [ | ||
.unary, | ||
.clientStreaming, | ||
.serverStreaming, | ||
.bidiStreaming, | ||
] | ||
|
||
for (name, kind) in zip(methodNames, methodKinds) { | ||
let method = ServiceSchema.Method( | ||
name: name, | ||
input: "EchoRequest", | ||
output: "EchoResponse", | ||
kind: kind | ||
) | ||
request.service.methods.append(method) | ||
} | ||
|
||
// Encoding the config to JSON and dump it to stdout. | ||
let encoder = JSONEncoder() | ||
encoder.outputFormatting = [.prettyPrinted] | ||
let data = try encoder.encode(request) | ||
let json = String(decoding: data, as: UTF8.self) | ||
print(json) | ||
} | ||
} | ||
} |
119 changes: 119 additions & 0 deletions
119
dev/grpc-dev-tool/Sources/grpc-dev-tool/Subcommands/GenerateJSON/JSONCodeGenerator.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
/* | ||
* Copyright 2025, gRPC Authors All rights reserved. | ||
* | ||
* 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. | ||
*/ | ||
|
||
import Foundation | ||
import GRPCCodeGen | ||
|
||
struct JSONCodeGenerator { | ||
private static let currentYear: Int = { | ||
let now = Date() | ||
let year = Calendar.current.component(.year, from: Date()) | ||
return year | ||
}() | ||
|
||
private static let header = """ | ||
/* | ||
* Copyright \(Self.currentYear), gRPC Authors All rights reserved. | ||
* | ||
* 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. | ||
*/ | ||
""" | ||
|
||
private static let jsonSerializers: String = """ | ||
fileprivate struct JSONSerializer<Message: Codable>: MessageSerializer { | ||
fileprivate func serialize<Bytes: GRPCContiguousBytes>( | ||
_ message: Message | ||
) throws -> Bytes { | ||
do { | ||
let jsonEncoder = JSONEncoder() | ||
let data = try jsonEncoder.encode(message) | ||
return Bytes(data) | ||
} catch { | ||
throw RPCError( | ||
code: .internalError, | ||
message: "Can't serialize message to JSON.", | ||
cause: error | ||
) | ||
} | ||
} | ||
} | ||
fileprivate struct JSONDeserializer<Message: Codable>: MessageDeserializer { | ||
fileprivate func deserialize<Bytes: GRPCContiguousBytes>( | ||
_ serializedMessageBytes: Bytes | ||
) throws -> Message { | ||
do { | ||
let jsonDecoder = JSONDecoder() | ||
let data = serializedMessageBytes.withUnsafeBytes { Data($0) } | ||
return try jsonDecoder.decode(Message.self, from: data) | ||
} catch { | ||
throw RPCError( | ||
code: .internalError, | ||
message: "Can't deserialize message from JSON.", | ||
cause: error | ||
) | ||
} | ||
} | ||
} | ||
""" | ||
|
||
func generate(request: JSONCodeGeneratorRequest) throws -> SourceFile { | ||
let generator = CodeGenerator(config: CodeGenerator.Config(request.config)) | ||
|
||
let codeGenRequest = CodeGenerationRequest( | ||
fileName: request.service.name + ".swift", | ||
leadingTrivia: Self.header, | ||
dependencies: [ | ||
Dependency( | ||
item: Dependency.Item(kind: .struct, name: "Data"), | ||
module: "Foundation", | ||
accessLevel: .internal | ||
), | ||
Dependency( | ||
item: Dependency.Item(kind: .class, name: "JSONEncoder"), | ||
module: "Foundation", | ||
accessLevel: .internal | ||
), | ||
Dependency( | ||
item: Dependency.Item(kind: .class, name: "JSONDecoder"), | ||
module: "Foundation", | ||
accessLevel: .internal | ||
), | ||
], | ||
services: [ServiceDescriptor(request.service)], | ||
makeSerializerCodeSnippet: { type in "JSONSerializer<\(type)>()" }, | ||
makeDeserializerCodeSnippet: { type in "JSONDeserializer<\(type)>()" } | ||
) | ||
|
||
var sourceFile = try generator.generate(codeGenRequest) | ||
|
||
// Insert a fileprivate serializer/deserializer for JSON at the bottom of each file. | ||
sourceFile.contents += "\n\n" | ||
sourceFile.contents += Self.jsonSerializers | ||
|
||
return sourceFile | ||
} | ||
} |
Oops, something went wrong.