Skip to content

Add option to generate proto3 optionals as swift optionals #1793

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions Documentation/PLUGIN.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,20 @@ mappings will be preceded by a visibility modifier corresponding to the visibili
**Important:** It is strongly encouraged to use `internal` imports instead of `@_implementationOnly` imports.
Hence `UseAccessLevelOnImports` and `ImplementationOnlyImports` options exclude each other.

##### Generation Option: `Proto3OptionalAsSwiftOptional` - generates proto3 optionals as swift optionals

By default the code generator does not generate swift optionals for proto3 optionals, this option changes that by generating swift optionals **only** for proto3 optionals

```
$ protoc --swift_opt=Proto3OptionalAsSwiftOptional=[value] --swift_out=. foo/bar/*.proto mumble/*.proto
```

The possible values for `Proto3OptionalAsSwiftOptional` are:

* `false`: Default option, no swift optionals are generated.
* `true`: Swift optionals are generated for proto3 optionals

**Important:** This goes against the design principles of proto3, so should be used with caution. A detailed explanation can be found [here](./INTERNALS.md#field-storage).

### Building your project

Expand Down
8 changes: 7 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,12 @@ test-plugin: build ${PROTOC_GEN_SWIFT}
--tfiws_opt=UseAccessLevelOnImports=true \
--tfiws_out=_test/CompileTests/InternalImportsByDefault \
`(find Protos/CompileTests/InternalImportsByDefault -type f -name "*.proto")`
@mkdir -p _test/CompileTests/Proto3OptionalAsSwiftOptional
${GENERATE_SRCS} \
-I Protos/CompileTests/Proto3OptionalAsSwiftOptional \
--tfiws_opt=Proto3OptionalAsSwiftOptional=true \
--tfiws_out=_test/CompileTests/Proto3OptionalAsSwiftOptional \
`(find Protos/CompileTests/Proto3OptionalAsSwiftOptional -type f -name "*.proto")`
diff -ru _test Reference

# Test the SPM plugin.
Expand Down Expand Up @@ -536,7 +542,7 @@ regenerate-compiletests-multimodule-protos: build ${PROTOC_GEN_SWIFT}
# We use the plugin for the InternalImportsByDefault test, so we don't actually need to regenerate
# anything. However, to keep the protos centralised in a single place (the Protos directory),
# this simply copies those files to the InternalImportsByDefault package in case they change.
copy-compiletests-internalimportsbydefault-protos:
copy-compiletests-internalimportsbydefault-protos:
@cp Protos/CompileTests/InternalImportsByDefault/* CompileTests/InternalImportsByDefault/Sources/InternalImportsByDefault/Protos

# Helper to check if there is a protobuf checkout as expected.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Simple file used to test that we can use both optional and non-optional fields
// in proto3, and that the optional field is represented as a Swift Optional.
// This is a simple test case that does not use any imports, so it should not
// generate any imports in the Swift file, and should not require any special
// handling in the compiler plugin.
//
syntax = "proto3";

package swift_proto_testing.proto3;

message SimpleOptional {
string non_optional = 1;
optional string optional = 2;

message NestedMessage {
string nested_non_optional = 1;
optional string nested_optional = 2;
}

NestedMessage nested_message = 3;
optional NestedMessage optional_nested_message = 4;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Protocol Buffers - Google's data interchange format
// Copyright 2008 Google Inc. All rights reserved.
// https://developers.google.com/protocol-buffers/
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

syntax = "proto3";

package swift_proto_testing;

message TestProto3Optional {
message NestedMessage {
// The field name "b" fails to compile in proto1 because it conflicts with
// a local variable named "b" in one of the generated methods. Doh.
// This file needs to compile in proto1 to test backwards-compatibility.
optional int32 bb = 1;
}

enum NestedEnum {
UNSPECIFIED = 0;
FOO = 1;
BAR = 2;
BAZ = 3;
NEG = -1; // Intentionally negative.
}

// Singular
optional int32 optional_int32 = 1;
optional int64 optional_int64 = 2;
optional uint32 optional_uint32 = 3;
optional uint64 optional_uint64 = 4;
optional sint32 optional_sint32 = 5;
optional sint64 optional_sint64 = 6;
optional fixed32 optional_fixed32 = 7;
optional fixed64 optional_fixed64 = 8;
optional sfixed32 optional_sfixed32 = 9;
optional sfixed64 optional_sfixed64 = 10;
optional float optional_float = 11;
optional double optional_double = 12;
optional bool optional_bool = 13;
optional string optional_string = 14;
optional bytes optional_bytes = 15;
optional string optional_cord = 16 [ctype = CORD];

optional NestedMessage optional_nested_message = 18;
optional NestedMessage lazy_nested_message = 19 [lazy = true];
optional NestedEnum optional_nested_enum = 21;

// Add some non-optional fields to verify we can mix them.
int32 singular_int32 = 22;
int64 singular_int64 = 23;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
// DO NOT EDIT.
// swift-format-ignore-file
// swiftlint:disable all
//
// Generated by the Swift generator plugin for the protocol buffer compiler.
// Source: proto3_simple_optional.proto
//
// For information on using the generated types, please see the documentation:
// https://github.com/apple/swift-protobuf/

/// Simple file used to test that we can use both optional and non-optional fields
/// in proto3, and that the optional field is represented as a Swift Optional.
/// This is a simple test case that does not use any imports, so it should not
/// generate any imports in the Swift file, and should not require any special
/// handling in the compiler plugin.

import SwiftProtobuf

// If the compiler emits an error on this type, it is because this file
// was generated by a version of the `protoc` Swift plug-in that is
// incompatible with the version of SwiftProtobuf to which you are linking.
// Please ensure that you are building against the same version of the API
// that was used to generate this file.
fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck {
struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {}
typealias Version = _2
}

struct SwiftProtoTesting_Proto3_SimpleOptional: Sendable {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.

var nonOptional: String = String()

var optional: String? = nil
/// Returns true if `optional` has been explicitly set.
var hasOptional: Bool {return self.optional != nil}
/// Clears the value of `optional`. Subsequent reads from it will return its default value.
mutating func clearOptional() {self.optional = nil}

var nestedMessage: SwiftProtoTesting_Proto3_SimpleOptional.NestedMessage {
get {return _nestedMessage ?? SwiftProtoTesting_Proto3_SimpleOptional.NestedMessage()}
set {_nestedMessage = newValue}
}
/// Returns true if `nestedMessage` has been explicitly set.
var hasNestedMessage: Bool {return self._nestedMessage != nil}
/// Clears the value of `nestedMessage`. Subsequent reads from it will return its default value.
mutating func clearNestedMessage() {self._nestedMessage = nil}

var optionalNestedMessage: SwiftProtoTesting_Proto3_SimpleOptional.NestedMessage? = nil
/// Returns true if `optionalNestedMessage` has been explicitly set.
var hasOptionalNestedMessage: Bool {return self.optionalNestedMessage != nil}
/// Clears the value of `optionalNestedMessage`. Subsequent reads from it will return its default value.
mutating func clearOptionalNestedMessage() {self.optionalNestedMessage = nil}

var unknownFields = SwiftProtobuf.UnknownStorage()

struct NestedMessage: Sendable {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.

var nestedNonOptional: String = String()

var nestedOptional: String? = nil
/// Returns true if `nestedOptional` has been explicitly set.
var hasNestedOptional: Bool {return self.nestedOptional != nil}
/// Clears the value of `nestedOptional`. Subsequent reads from it will return its default value.
mutating func clearNestedOptional() {self.nestedOptional = nil}

var unknownFields = SwiftProtobuf.UnknownStorage()

init() {}
}

init() {}

fileprivate var _nestedMessage: SwiftProtoTesting_Proto3_SimpleOptional.NestedMessage? = nil
}

// MARK: - Code below here is support for the SwiftProtobuf runtime.

fileprivate let _protobuf_package = "swift_proto_testing.proto3"

extension SwiftProtoTesting_Proto3_SimpleOptional: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = _protobuf_package + ".SimpleOptional"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .standard(proto: "non_optional"),
2: .same(proto: "optional"),
3: .standard(proto: "nested_message"),
4: .standard(proto: "optional_nested_message"),
]

mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber {
case 1: try { try decoder.decodeSingularStringField(value: &self.nonOptional) }()
case 2: try { try decoder.decodeSingularStringField(value: &self.optional) }()
case 3: try { try decoder.decodeSingularMessageField(value: &self._nestedMessage) }()
case 4: try { try decoder.decodeSingularMessageField(value: &self.optionalNestedMessage) }()
default: break
}
}
}

func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every if/case branch local when no optimizations
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
// https://github.com/apple/swift-protobuf/issues/1182
if !self.nonOptional.isEmpty {
try visitor.visitSingularStringField(value: self.nonOptional, fieldNumber: 1)
}
try { if let v = self.optional {
try visitor.visitSingularStringField(value: v, fieldNumber: 2)
} }()
try { if let v = self._nestedMessage {
try visitor.visitSingularMessageField(value: v, fieldNumber: 3)
} }()
try { if let v = self.optionalNestedMessage {
try visitor.visitSingularMessageField(value: v, fieldNumber: 4)
} }()
try unknownFields.traverse(visitor: &visitor)
}

static func ==(lhs: SwiftProtoTesting_Proto3_SimpleOptional, rhs: SwiftProtoTesting_Proto3_SimpleOptional) -> Bool {
if lhs.nonOptional != rhs.nonOptional {return false}
if lhs.optional != rhs.optional {return false}
if lhs._nestedMessage != rhs._nestedMessage {return false}
if lhs.optionalNestedMessage != rhs.optionalNestedMessage {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}

extension SwiftProtoTesting_Proto3_SimpleOptional.NestedMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = SwiftProtoTesting_Proto3_SimpleOptional.protoMessageName + ".NestedMessage"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .standard(proto: "nested_non_optional"),
2: .standard(proto: "nested_optional"),
]

mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber {
case 1: try { try decoder.decodeSingularStringField(value: &self.nestedNonOptional) }()
case 2: try { try decoder.decodeSingularStringField(value: &self.nestedOptional) }()
default: break
}
}
}

func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every if/case branch local when no optimizations
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
// https://github.com/apple/swift-protobuf/issues/1182
if !self.nestedNonOptional.isEmpty {
try visitor.visitSingularStringField(value: self.nestedNonOptional, fieldNumber: 1)
}
try { if let v = self.nestedOptional {
try visitor.visitSingularStringField(value: v, fieldNumber: 2)
} }()
try unknownFields.traverse(visitor: &visitor)
}

static func ==(lhs: SwiftProtoTesting_Proto3_SimpleOptional.NestedMessage, rhs: SwiftProtoTesting_Proto3_SimpleOptional.NestedMessage) -> Bool {
if lhs.nestedNonOptional != rhs.nestedNonOptional {return false}
if lhs.nestedOptional != rhs.nestedOptional {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}
Loading