Skip to content

Commit

Permalink
Swift: Wire initializer refactor (#2561)
Browse files Browse the repository at this point in the history
This removes the member-wise initializer by default.

There's two reasons for this:
* This is believed to cause code bloat
* This will have unexpected behaviors as we better handle proto extensions

Instead of a member wise initializer, this exposes a configuration closure for any optional parameters.
There's still guaranteed type safety as all `required` parameters _must_ have initialized values.

### Note

Memberwise initializers should now be considered deprecated and will be removed in the near future (for example, November 2023)
  • Loading branch information
lickel authored Aug 17, 2023
1 parent 34ad696 commit dae04ab
Show file tree
Hide file tree
Showing 35 changed files with 1,511 additions and 779 deletions.
30 changes: 23 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -551,7 +551,6 @@ Swift is a pragmatic and expressive programming language with rich support for v
Here's how we used Swift to model Protocol Buffers messages:

* Messages are structs that conform to `Equatable`, `Codable` and `Sendable`. All Messages have value semantics.
* Messages have a memberwise initializer to populate fields.
* Fields are generated as properties.
* The nullability of each field's type depends on its label: `required`, `repeated` and `map`
fields get non-nullable types, whereas `optional` fields are of nullable types.
Expand All @@ -576,12 +575,22 @@ public struct Dinosaur {
/**
* URLs with images of this dinosaur.
*/
public var picture_urls: [String]
public var picture_urls: [String] = []
public var length_meters: Double?
public var mass_kilograms: Double?
public var period: Period?
public var unknownFields: Data = .init()
public var unknownFields: Foundation.Data = .init()

public init(configure: (inout Self) -> Void = { _ in }) {
configure(&self)
}

}

#if WIRE_INCLUDE_MEMBERWISE_INITIALIZER
extension Dinosaur {

@_disfavoredOverload
public init(
name: String? = nil,
picture_urls: [String] = [],
Expand All @@ -597,6 +606,7 @@ public struct Dinosaur {
}

}
#endif

#if !WIRE_REMOVE_EQUATABLE
extension Dinosaur : Equatable {
Expand All @@ -614,12 +624,15 @@ extension Dinosaur : Sendable {
#endif

extension Dinosaur : ProtoMessage {

public static func protoMessageTypeURL() -> String {
return "type.googleapis.com/squareup.dinosaurs.Dinosaur"
}

}

extension Dinosaur : Proto2Codable {

public init(from reader: ProtoReader) throws {
var name: String? = nil
var picture_urls: [String] = []
Expand Down Expand Up @@ -655,10 +668,12 @@ extension Dinosaur : Proto2Codable {
try writer.encode(tag: 5, value: self.period)
try writer.writeUnknownFields(unknownFields)
}

}

#if !WIRE_REMOVE_CODABLE
extension Dinosaur : Codable {

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: StringLiteralCodingKeys.self)
self.name = try container.decodeIfPresent(String.self, forKey: "name")
Expand All @@ -681,17 +696,18 @@ extension Dinosaur : Codable {
try container.encodeIfPresent(self.mass_kilograms, forKey: preferCamelCase ? "massKilograms" : "mass_kilograms")
try container.encodeIfPresent(self.period, forKey: "period")
}

}
#endif
```

Creating and accessing proto models is easy:

```swift
let stegosaurus = Dinosaur(
name: "Stegosaurus",
period: .JURASSIC
)
let stegosaurus = Dinosaur {
$0.name = "Stegosaurus"
$0.period = .JURASSIC
}

print("My favorite dinosaur existed in the \(stegosaurus.period) period.")
```
Expand Down
109 changes: 57 additions & 52 deletions wire-runtime-swift/src/test/swift/CodableTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,20 +44,20 @@ extension CodableTests {
}
"""

let expected = SimpleOptional2(
opt_int32: 1,
opt_int64: 2,
opt_uint32: 3,
opt_uint64: 4,
opt_float: 5,
opt_double: 6,
opt_bytes: Foundation.Data(hexEncoded: "0123456"),
opt_string: "foo",
opt_enum: .UNKNOWN,
repeated_int32: [1, 2, 3],
repeated_string: ["foo", "bar", "baz"],
map_int32_string: [1: "foo", 2: "bar"]
)
let expected = SimpleOptional2 {
$0.opt_int32 = 1
$0.opt_int64 = 2
$0.opt_uint32 = 3
$0.opt_uint64 = 4
$0.opt_float = 5
$0.opt_double = 6
$0.opt_bytes = Foundation.Data(hexEncoded: "0123456")
$0.opt_string = "foo"
$0.opt_enum = .UNKNOWN
$0.repeated_int32 = [1, 2, 3]
$0.repeated_string = ["foo", "bar", "baz"]
$0.map_int32_string = [1: "foo", 2: "bar"]
}

try assertDecode(json: json, expected: expected)
}
Expand Down Expand Up @@ -92,11 +92,12 @@ extension CodableTests {
req_double: 6,
req_bytes: Foundation.Data(hexEncoded: "0123456")!,
req_string: "foo",
req_enum: .UNKNOWN,
repeated_int32: [1, 2, 3],
repeated_string: ["foo", "bar", "baz"],
map_int32_string: [1: "foo", 2: "bar"]
)
req_enum: .UNKNOWN
) {
$0.repeated_int32 = [1, 2, 3]
$0.repeated_string = ["foo", "bar", "baz"]
$0.map_int32_string = [1: "foo", 2: "bar"]
}

try assertDecode(json: json, expected: expected)
}
Expand All @@ -107,20 +108,20 @@ extension CodableTests {
extension CodableTests {
func testEncodeOptional() throws {
// Only include one value in maps until https://bugs.swift.org/browse/SR-13414 is fixed.
let proto = SimpleOptional2(
opt_int32: 1,
opt_int64: 2,
opt_uint32: 3,
opt_uint64: 4,
opt_float: 5,
opt_double: 6,
opt_bytes: Foundation.Data(hexEncoded: "0123456"),
opt_string: "foo",
opt_enum: .UNKNOWN,
repeated_int32: [1, 2, 3],
repeated_string: ["foo", "bar", "baz"],
map_int32_string: [1: "foo"]
)
let proto = SimpleOptional2 {
$0.opt_int32 = 1
$0.opt_int64 = 2
$0.opt_uint32 = 3
$0.opt_uint64 = 4
$0.opt_float = 5
$0.opt_double = 6
$0.opt_bytes = Foundation.Data(hexEncoded: "0123456")
$0.opt_string = "foo"
$0.opt_enum = .UNKNOWN
$0.repeated_int32 = [1, 2, 3]
$0.repeated_string = ["foo", "bar", "baz"]
$0.map_int32_string = [1: "foo"]
}

let expected = """
{
Expand Down Expand Up @@ -155,11 +156,12 @@ extension CodableTests {
req_double: 6,
req_bytes: Foundation.Data(hexEncoded: "0123456")!,
req_string: "foo",
req_enum: .UNKNOWN,
repeated_int32: [1, 2, 3],
repeated_string: ["foo", "bar", "baz"],
map_int32_string: [1: "foo"]
)
req_enum: .UNKNOWN
) {
$0.repeated_int32 = [1, 2, 3]
$0.repeated_string = ["foo", "bar", "baz"]
$0.map_int32_string = [1: "foo"]
}

let expected = """
{
Expand Down Expand Up @@ -244,12 +246,12 @@ extension CodableTests {
}
"""

let proto = SimpleOptional2(
opt_double: 6,
opt_enum: .A,
repeated_string: ["B"],
map_int32_string: [1 : "foo"]
)
let proto = SimpleOptional2 {
$0.opt_double = 6
$0.opt_enum = .A
$0.repeated_string = ["B"]
$0.map_int32_string = [1 : "foo"]
}

try assertEncode(proto: proto, expected: json)
}
Expand All @@ -269,13 +271,13 @@ extension CodableTests {
}
"""

let proto = SimpleOptional2(
opt_int64: 5,
opt_double: 6,
opt_enum: .A,
repeated_string: ["B"],
map_int32_string: [1 : "foo"]
)
let proto = SimpleOptional2 {
$0.opt_int64 = 5
$0.opt_double = 6
$0.opt_enum = .A
$0.repeated_string = ["B"]
$0.map_int32_string = [1 : "foo"]
}

try assertDecode(json: json, expected: proto)
}
Expand All @@ -285,7 +287,10 @@ extension CodableTests {

extension CodableTests {
func testHeapRoundtrip() throws {
let proto = SwiftStackOverflow(value3: "hello")
let proto = SwiftStackOverflow {
$0.value3 = "hello"
}

let json = """
{
"value3":"hello"
Expand Down
8 changes: 6 additions & 2 deletions wire-runtime-swift/src/test/swift/DefaultedTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,17 @@ final class DefaultedTests: XCTestCase {
}

func testProjectedValueIsWrappedValue() throws {
let phoneNumber = Person.PhoneNumber(number: "1234567890", type: Person.PhoneType.WORK)
let phoneNumber = Person.PhoneNumber(number: "1234567890") {
$0.type = .WORK
}
XCTAssertEqual(phoneNumber.type, Person.PhoneType.WORK)
XCTAssertEqual(phoneNumber.$type, Person.PhoneType.WORK)
}

func testProjectedValueWhenResettingWrappedValue() throws {
var phoneNumber = Person.PhoneNumber(number: "1234567890", type: Person.PhoneType.WORK)
var phoneNumber = Person.PhoneNumber(number: "1234567890") {
$0.type = .WORK
}
XCTAssertEqual(phoneNumber.type, Person.PhoneType.WORK)
XCTAssertEqual(phoneNumber.$type, Person.PhoneType.WORK)
phoneNumber.type = nil
Expand Down
14 changes: 11 additions & 3 deletions wire-runtime-swift/src/test/swift/ProtoReaderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,10 @@ final class ProtoReaderTests: XCTestCase {
00 // Length 0
""")!
try test(data: data) { reader in
let expected = Parent(name: "Bob", child: .init())
let expected = Parent {
$0.name = "Bob"
$0.child = .init()
}
XCTAssertEqual(try reader.decode(Parent.self), expected)
}
}
Expand Down Expand Up @@ -327,7 +330,10 @@ final class ProtoReaderTests: XCTestCase {
01 // Value 1
""")!
try test(data: data, enumStrategy: .returnNil) { reader in
let message = OneOfs(standalone_enum: OneOfs.NestedEnum.A, choice: OneOfs.Choice.similar_enum_option(OneOfs.NestedEnum.A))
let message = OneOfs {
$0.standalone_enum = .A
$0.choice = .similar_enum_option(.A)
}
XCTAssertEqual(try reader.decode(OneOfs.self), message)
}
}
Expand All @@ -340,7 +346,9 @@ final class ProtoReaderTests: XCTestCase {
02 // Value 2
""")!
try test(data: data, enumStrategy: .returnNil) { reader in
let message = OneOfs(standalone_enum: OneOfs.NestedEnum.A, choice: nil)
let message = OneOfs {
$0.standalone_enum = .A
}
XCTAssertEqual(try reader.decode(OneOfs.self), message)
}
}
Expand Down
49 changes: 28 additions & 21 deletions wire-runtime-swift/src/test/swift/ProtoWriterTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -123,20 +123,20 @@ final class ProtoWriterTests: XCTestCase {

func testEncodeDefaultProto2Values() throws {
let writer = ProtoWriter()
let proto = SimpleOptional2(
opt_int32: 0,
opt_int64: 0,
opt_uint32: 0,
opt_uint64: 0,
opt_float: 0,
opt_double: 0,
opt_bytes: .init(),
opt_string: "",
opt_enum: .UNKNOWN,
repeated_int32: [],
repeated_string: [],
map_int32_string: [:]
)
let proto = SimpleOptional2 {
$0.opt_int32 = 0
$0.opt_int64 = 0
$0.opt_uint32 = 0
$0.opt_uint64 = 0
$0.opt_float = 0
$0.opt_double = 0
$0.opt_bytes = .init()
$0.opt_string = ""
$0.opt_enum = .UNKNOWN
$0.repeated_int32 = []
$0.repeated_string = []
$0.map_int32_string = [:]
}
try writer.encode(tag: 1, value: proto)

// All values are encoded in proto2, including defaults.
Expand Down Expand Up @@ -191,11 +191,12 @@ final class ProtoWriterTests: XCTestCase {
req_double: 0,
req_bytes: .init(),
req_string: "",
req_enum: .UNKNOWN,
repeated_int32: [],
repeated_string: [],
map_int32_string: [:]
)
req_enum: .UNKNOWN
) {
$0.repeated_int32 = []
$0.repeated_string = []
$0.map_int32_string = [:]
}
try writer.encode(tag: 1, value: proto)

// No data should be encoded. Just the top-level message tag with a length of zero.
Expand Down Expand Up @@ -551,7 +552,10 @@ final class ProtoWriterTests: XCTestCase {
func testEncodePackedRepeatedProto2Default() throws {
let writer = ProtoWriter()
let data = Data(json_data: "12")
let person = Person(name: "name", id: 1, email: "email", ids: [1, 2, 3], data: data)
let person = Person(name: "name", id: 1, data: data) {
$0.email = "email"
$0.ids = [1, 2, 3]
}
try writer.encode(tag: 1, value: person)

// Proto2 should used "packed: false" by default.
Expand Down Expand Up @@ -583,7 +587,10 @@ final class ProtoWriterTests: XCTestCase {

func testEncodePackedRepeatedProto3Default() throws {
let writer = ProtoWriter()
let person = Person3(name: "name", id: 1, email: "email", ids: [1, 2, 3])
let person = Person3(name: "name", id: 1) {
$0.email = "email"
$0.ids = [1, 2, 3]
}
try writer.encode(tag: 1, value: person)

// Proto3 should used "packed: true" by default.
Expand Down
Loading

0 comments on commit dae04ab

Please sign in to comment.