From 1a80aec63ba801d479f2f20233245d1d80fccf8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Quenaudon?= Date: Thu, 2 Jul 2026 14:52:40 +0100 Subject: [PATCH] FieldMask: serialisation and code generation for Java/Kotlin/Swift --- .../com/squareup/wire/java/JavaGenerator.java | 3 + .../squareup/wire/java/JavaGeneratorTest.java | 18 +++ .../squareup/wire/kotlin/KotlinGenerator.kt | 4 + .../wire/kotlin/KotlinGeneratorTest.kt | 18 +++ .../src/main/swift/FieldMask.swift | 137 ++++++++++++++++++ .../src/test/proto/field_mask_message.proto | 23 +++ .../src/test/swift/FieldMaskTests.swift | 67 +++++++++ wire-runtime/api/wire-runtime.api | 1 + .../kotlin/com/squareup/wire/ProtoAdapter.kt | 53 +++++++ .../kotlin/com/squareup/wire/FieldMaskTest.kt | 20 +++ .../kotlin/com/squareup/wire/ProtoAdapter.kt | 2 + .../kotlin/com/squareup/wire/ProtoAdapter.kt | 1 + .../com/squareup/wire/schema/CoreLoader.kt | 2 + .../wire/schema/SchemaProtoAdapterFactory.kt | 1 + .../wire/schema/internal/JvmLanguages.kt | 1 + .../google/protobuf/field_mask.proto | 45 ++++++ .../wire/schema/DynamicSerializationTest.kt | 4 + .../com/squareup/wire/swift/SwiftGenerator.kt | 2 + .../squareup/wire/swift/SwiftGeneratorTest.kt | 26 ++++ 19 files changed, 428 insertions(+) create mode 100644 wire-runtime-swift/src/main/swift/FieldMask.swift create mode 100644 wire-runtime-swift/src/test/proto/field_mask_message.proto create mode 100644 wire-runtime-swift/src/test/swift/FieldMaskTests.swift create mode 100644 wire-schema/src/jvmMain/resources/google/protobuf/field_mask.proto diff --git a/wire-java-generator/src/main/java/com/squareup/wire/java/JavaGenerator.java b/wire-java-generator/src/main/java/com/squareup/wire/java/JavaGenerator.java index c87049e75e..f7f8175811 100644 --- a/wire-java-generator/src/main/java/com/squareup/wire/java/JavaGenerator.java +++ b/wire-java-generator/src/main/java/com/squareup/wire/java/JavaGenerator.java @@ -156,6 +156,7 @@ public static boolean builtInType(ProtoType protoType) { .put(ProtoType.DURATION, ClassName.get("java.time", "Duration")) .put(ProtoType.TIMESTAMP, ClassName.get("java.time", "Instant")) .put(ProtoType.EMPTY, ClassName.get("kotlin", "Unit")) + .put(ProtoType.FIELD_MASK, ClassName.get("com.squareup.wire", "FieldMask")) .put( ProtoType.STRUCT_MAP, ParameterizedTypeName.get( @@ -549,6 +550,8 @@ private CodeBlock singleAdapterFor(ProtoType type) { result.add("$T.$L", ADAPTER, "INSTANT"); } else if (type.equals(ProtoType.EMPTY)) { result.add("$T.$L", ADAPTER, "EMPTY"); + } else if (type.equals(ProtoType.FIELD_MASK)) { + result.add("$T.$L", ADAPTER, "FIELD_MASK"); } else if (type.equals(ProtoType.STRUCT_MAP)) { result.add("$T.$L", ADAPTER, "STRUCT_MAP"); } else if (type.equals(ProtoType.STRUCT_VALUE)) { diff --git a/wire-java-generator/src/test/java/com/squareup/wire/java/JavaGeneratorTest.java b/wire-java-generator/src/test/java/com/squareup/wire/java/JavaGeneratorTest.java index 41a96c345c..6655b7b89d 100644 --- a/wire-java-generator/src/test/java/com/squareup/wire/java/JavaGeneratorTest.java +++ b/wire-java-generator/src/test/java/com/squareup/wire/java/JavaGeneratorTest.java @@ -914,6 +914,24 @@ public void generateTypeUsesPackageNameOnFieldAndClassNameClashWithinPackage() t .contains("public final AnotherStatus common_proto_Status;"); } + @Test + public void usesFieldMask() throws Exception { + Schema schema = + new SchemaBuilder() + .add( + Path.get("a.proto"), + "" + + "package common.proto;\n" + + "import \"google/protobuf/field_mask.proto\";\n" + + "message Message {\n" + + " optional google.protobuf.FieldMask mask = 1;\n" + + "}\n") + .build(); + String code = new JavaWithProfilesGenerator(schema).generateJava("common.proto.Message"); + assertThat(code).contains("import com.squareup.wire.FieldMask;"); + assertThat(code).contains("ProtoAdapter.FIELD_MASK"); + } + @Test public void fieldHasScalarName() throws Exception { Schema schema = diff --git a/wire-kotlin-generator/src/main/java/com/squareup/wire/kotlin/KotlinGenerator.kt b/wire-kotlin-generator/src/main/java/com/squareup/wire/kotlin/KotlinGenerator.kt index e6f525e03b..5eea162892 100644 --- a/wire-kotlin-generator/src/main/java/com/squareup/wire/kotlin/KotlinGenerator.kt +++ b/wire-kotlin-generator/src/main/java/com/squareup/wire/kotlin/KotlinGenerator.kt @@ -2463,6 +2463,9 @@ class KotlinGenerator private constructor( this == ProtoType.EMPTY -> { CodeBlock.of("%T${adapterFieldDelimiterName}EMPTY", ProtoAdapter::class) } + this == ProtoType.FIELD_MASK -> { + CodeBlock.of("%T${adapterFieldDelimiterName}FIELD_MASK", ProtoAdapter::class) + } this == ProtoType.STRUCT_MAP -> { CodeBlock.of("%T${adapterFieldDelimiterName}STRUCT_MAP", ProtoAdapter::class) } @@ -3450,6 +3453,7 @@ class KotlinGenerator private constructor( ProtoType.DURATION to ClassName("com.squareup.wire", "Duration"), ProtoType.TIMESTAMP to ClassName("com.squareup.wire", "Instant"), ProtoType.EMPTY to ClassName("kotlin", "Unit"), + ProtoType.FIELD_MASK to ClassName("com.squareup.wire", "FieldMask"), ProtoType.STRUCT_MAP to ClassName("kotlin.collections", "Map") .parameterizedBy(ClassName("kotlin", "String"), STAR).copy(nullable = true), ProtoType.STRUCT_VALUE to ClassName("kotlin", "Any").copy(nullable = true), diff --git a/wire-kotlin-generator/src/test/java/com/squareup/wire/kotlin/KotlinGeneratorTest.kt b/wire-kotlin-generator/src/test/java/com/squareup/wire/kotlin/KotlinGeneratorTest.kt index 9634708b46..133e596778 100644 --- a/wire-kotlin-generator/src/test/java/com/squareup/wire/kotlin/KotlinGeneratorTest.kt +++ b/wire-kotlin-generator/src/test/java/com/squareup/wire/kotlin/KotlinGeneratorTest.kt @@ -1505,6 +1505,24 @@ class KotlinGeneratorTest { assertThat(code).contains("import com.squareup.wire.AnyMessage") } + @Test fun usesFieldMask() { + val schema = buildSchema { + add( + "a.proto".toPath(), + """ + |package common.proto; + |import "google/protobuf/field_mask.proto"; + |message Message { + | optional google.protobuf.FieldMask mask = 1; + |} + """.trimMargin(), + ) + } + val code = KotlinWithProfilesGenerator(schema).generateKotlin("common.proto.Message") + assertThat(code).contains("import com.squareup.wire.FieldMask") + assertThat(code).contains("ProtoAdapter.FIELD_MASK") + } + @Test fun wildCommentsAreEscaped() { val schema = buildSchema { add( diff --git a/wire-runtime-swift/src/main/swift/FieldMask.swift b/wire-runtime-swift/src/main/swift/FieldMask.swift new file mode 100644 index 0000000000..1ec6faa398 --- /dev/null +++ b/wire-runtime-swift/src/main/swift/FieldMask.swift @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2026 Square, Inc. + * + * 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 + * + * https://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 + +/** + * Wire implementation of `google.protobuf.FieldMask`. + * + * The binary representation is a repeated `paths` string field. + */ +public struct FieldMask { + public var paths: [String] + + public init(paths: [String] = []) { + self.paths = paths + } +} + +#if !WIRE_REMOVE_EQUATABLE +extension FieldMask: Equatable { +} +#endif + +#if !WIRE_REMOVE_HASHABLE +extension FieldMask: Hashable { +} +#endif + +extension FieldMask: Sendable { +} + +extension FieldMask: Proto3Codable { + // google.protobuf.FieldMask: + // repeated string paths = 1; + + public init(from reader: ProtoReader) throws { + var paths: [String] = [] + + let token = try reader.beginMessage() + while let tag = try reader.nextTag(token: token) { + switch tag { + case 1: + try reader.decode(into: &paths) + default: + try reader.readUnknownField(tag: tag) + } + } + // Unknown fields intentionally discarded. + _ = try reader.endMessage(token: token) + + self.paths = paths + } + + public func encode(to writer: ProtoWriter) throws { + try writer.encode(tag: 1, value: self.paths) + } +} + +extension FieldMask: ProtoMessage { + public static func protoMessageTypeURL() -> String { + return "type.googleapis.com/google.protobuf.FieldMask" + } +} + +#if !WIRE_REMOVE_CODABLE +extension FieldMask: Codable { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let value = try container.decode(String.self) + self.paths = value.isEmpty + ? [] + : value.split(separator: ",").map { FieldMask.protoName(String($0)) } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(paths.map(FieldMask.jsonName).joined(separator: ",")) + } + + private static func jsonName(_ path: String) -> String { + return path + .split(separator: ".", omittingEmptySubsequences: false) + .map { lowerCamel(String($0)) } + .joined(separator: ".") + } + + private static func protoName(_ path: String) -> String { + return path + .split(separator: ".", omittingEmptySubsequences: false) + .map { snakeCase(String($0)) } + .joined(separator: ".") + } + + private static func lowerCamel(_ value: String) -> String { + var result = "" + var capitalizeNext = false + for scalar in value.unicodeScalars { + if scalar == "_" { + capitalizeNext = true + } else if capitalizeNext { + result.append(String(scalar).uppercased()) + capitalizeNext = false + } else { + result.append(String(scalar)) + } + } + return result + } + + private static func snakeCase(_ value: String) -> String { + var result = "" + for scalar in value.unicodeScalars { + if scalar.value >= 65 && scalar.value <= 90 { + if !result.isEmpty { + result.append("_") + } + result.append(String(scalar).lowercased()) + } else { + result.append(String(scalar)) + } + } + return result + } +} +#endif diff --git a/wire-runtime-swift/src/test/proto/field_mask_message.proto b/wire-runtime-swift/src/test/proto/field_mask_message.proto new file mode 100644 index 0000000000..37e21f0f14 --- /dev/null +++ b/wire-runtime-swift/src/test/proto/field_mask_message.proto @@ -0,0 +1,23 @@ +/* + * Copyright 2026 Square, Inc. + * + * 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. + */ + +syntax = "proto3"; + +import "google/protobuf/field_mask.proto"; + +message MessageContainingFieldMask { + google.protobuf.FieldMask mask = 1; +} diff --git a/wire-runtime-swift/src/test/swift/FieldMaskTests.swift b/wire-runtime-swift/src/test/swift/FieldMaskTests.swift new file mode 100644 index 0000000000..e6381af554 --- /dev/null +++ b/wire-runtime-swift/src/test/swift/FieldMaskTests.swift @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2026 Square, Inc. + * + * 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 + * + * https://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 Wire +import XCTest + +final class FieldMaskTests: XCTestCase { + func testEncodeFieldMask() throws { + let fieldMask = FieldMask(paths: ["user.display_name", "photo"]) + + let data = try ProtoEncoder().encode(fieldMask) + + XCTAssertEqual( + data, + Foundation.Data(hexEncoded: "0a11757365722e646973706c61795f6e616d650a0570686f746f")! + ) + } + + func testDecodeFieldMask() throws { + let data = Foundation.Data(hexEncoded: "0a11757365722e646973706c61795f6e616d650a0570686f746f")! + + let fieldMask = try ProtoDecoder().decode(FieldMask.self, from: data) + + XCTAssertEqual(fieldMask, FieldMask(paths: ["user.display_name", "photo"])) + } + + func testTypeURL() { + XCTAssertEqual( + FieldMask.protoMessageTypeURL(), + "type.googleapis.com/google.protobuf.FieldMask" + ) + } + + func testJSONRoundTrip() throws { + let jsonData = Foundation.Data(#""user.displayName,photo""#.utf8) + + let fieldMask = try JSONDecoder().decode(FieldMask.self, from: jsonData) + let encoded = try JSONEncoder().encode(fieldMask) + + XCTAssertEqual(fieldMask, FieldMask(paths: ["user.display_name", "photo"])) + XCTAssertEqual(String(data: encoded, encoding: .utf8), #""user.displayName,photo""#) + } + + func testGeneratedMessageWithFieldMask() throws { + let message = MessageContainingFieldMask { + $0.mask = FieldMask(paths: ["user.display_name", "photo"]) + } + + let data = try ProtoEncoder().encode(message) + let decoded = try ProtoDecoder().decode(MessageContainingFieldMask.self, from: data) + + XCTAssertEqual(decoded.mask, FieldMask(paths: ["user.display_name", "photo"])) + } +} diff --git a/wire-runtime/api/wire-runtime.api b/wire-runtime/api/wire-runtime.api index 43bd22d908..c8f802850f 100644 --- a/wire-runtime/api/wire-runtime.api +++ b/wire-runtime/api/wire-runtime.api @@ -164,6 +164,7 @@ public abstract class com/squareup/wire/ProtoAdapter { public static final field DOUBLE_VALUE Lcom/squareup/wire/ProtoAdapter; public static final field DURATION Lcom/squareup/wire/ProtoAdapter; public static final field EMPTY Lcom/squareup/wire/ProtoAdapter; + public static final field FIELD_MASK Lcom/squareup/wire/ProtoAdapter; public static final field FIXED32 Lcom/squareup/wire/ProtoAdapter; public static final field FIXED32_ARRAY Lcom/squareup/wire/ProtoAdapter; public static final field FIXED64 Lcom/squareup/wire/ProtoAdapter; diff --git a/wire-runtime/src/commonMain/kotlin/com/squareup/wire/ProtoAdapter.kt b/wire-runtime/src/commonMain/kotlin/com/squareup/wire/ProtoAdapter.kt index 806bb3e8e5..6c99087408 100644 --- a/wire-runtime/src/commonMain/kotlin/com/squareup/wire/ProtoAdapter.kt +++ b/wire-runtime/src/commonMain/kotlin/com/squareup/wire/ProtoAdapter.kt @@ -248,6 +248,8 @@ expect abstract class ProtoAdapter( val EMPTY: ProtoAdapter + val FIELD_MASK: ProtoAdapter + val STRUCT_MAP: ProtoAdapter?> val STRUCT_LIST: ProtoAdapter?> @@ -1331,6 +1333,57 @@ internal fun commonEmpty(): ProtoAdapter = object : ProtoAdapter( override fun redact(value: Unit): Unit = value } +internal fun commonFieldMask(): ProtoAdapter = object : ProtoAdapter( + LENGTH_DELIMITED, + FieldMask::class, + "type.googleapis.com/google.protobuf.FieldMask", + Syntax.PROTO_3, +) { + override fun encodedSize(value: FieldMask): Int { + var result = 0 + for (path in value.paths) { + result += STRING.encodedSizeWithTag(1, path) + } + return result + } + + override fun encode(writer: ProtoWriter, value: FieldMask) { + for (path in value.paths) { + STRING.encodeWithTag(writer, 1, path) + } + } + + override fun encode(writer: ReverseProtoWriter, value: FieldMask) { + for (i in value.paths.size - 1 downTo 0) { + STRING.encodeWithTag(writer, 1, value.paths[i]) + } + } + + override fun decode(reader: ProtoReader): FieldMask { + val paths = mutableListOf() + reader.forEachTag { tag -> + when (tag) { + 1 -> paths += STRING.decode(reader) + else -> reader.readUnknownField(tag) + } + } + return FieldMask(paths) + } + + override fun decode(reader: ProtoReader32): FieldMask { + val paths = mutableListOf() + reader.forEachTag { tag -> + when (tag) { + 1 -> paths += STRING.decode(reader) + else -> reader.readUnknownField(tag) + } + } + return FieldMask(paths) + } + + override fun redact(value: FieldMask): FieldMask = value +} + internal fun commonStructMap(): ProtoAdapter?> = object : ProtoAdapter?>( LENGTH_DELIMITED, Map::class, diff --git a/wire-runtime/src/commonTest/kotlin/com/squareup/wire/FieldMaskTest.kt b/wire-runtime/src/commonTest/kotlin/com/squareup/wire/FieldMaskTest.kt index 3f7474f82e..a80c179125 100644 --- a/wire-runtime/src/commonTest/kotlin/com/squareup/wire/FieldMaskTest.kt +++ b/wire-runtime/src/commonTest/kotlin/com/squareup/wire/FieldMaskTest.kt @@ -19,6 +19,7 @@ import assertk.assertThat import assertk.assertions.containsExactly import assertk.assertions.isEqualTo import kotlin.test.Test +import okio.ByteString.Companion.decodeHex class FieldMaskTest { @Test fun storesPaths() { @@ -45,4 +46,23 @@ class FieldMaskTest { assertThat(fieldMask.copy(paths = listOf("photo"))).isEqualTo(FieldMask(listOf("photo"))) } + + @Test fun protoAdapterEncodesPaths() { + val fieldMask = FieldMask(listOf("user.display_name", "photo")) + + assertThat(ProtoAdapter.FIELD_MASK.encodeByteString(fieldMask)) + .isEqualTo("0a11757365722e646973706c61795f6e616d650a0570686f746f".decodeHex()) + } + + @Test fun protoAdapterDecodesPaths() { + val bytes = "0a11757365722e646973706c61795f6e616d650a0570686f746f".decodeHex() + + assertThat(ProtoAdapter.FIELD_MASK.decode(bytes)) + .isEqualTo(FieldMask(listOf("user.display_name", "photo"))) + } + + @Test fun protoAdapterHasTypeUrl() { + assertThat(ProtoAdapter.FIELD_MASK.typeUrl) + .isEqualTo("type.googleapis.com/google.protobuf.FieldMask") + } } diff --git a/wire-runtime/src/jvmMain/kotlin/com/squareup/wire/ProtoAdapter.kt b/wire-runtime/src/jvmMain/kotlin/com/squareup/wire/ProtoAdapter.kt index 815a641653..462cf521e5 100644 --- a/wire-runtime/src/jvmMain/kotlin/com/squareup/wire/ProtoAdapter.kt +++ b/wire-runtime/src/jvmMain/kotlin/com/squareup/wire/ProtoAdapter.kt @@ -318,6 +318,8 @@ actual abstract class ProtoAdapter actual constructor( @JvmField actual val EMPTY: ProtoAdapter = commonEmpty() + @JvmField actual val FIELD_MASK: ProtoAdapter = commonFieldMask() + @JvmField actual val STRUCT_MAP: ProtoAdapter?> = commonStructMap() @JvmField actual val STRUCT_LIST: ProtoAdapter?> = commonStructList() diff --git a/wire-runtime/src/nonJvmMain/kotlin/com/squareup/wire/ProtoAdapter.kt b/wire-runtime/src/nonJvmMain/kotlin/com/squareup/wire/ProtoAdapter.kt index 404d6f9ab3..ffabce4d65 100644 --- a/wire-runtime/src/nonJvmMain/kotlin/com/squareup/wire/ProtoAdapter.kt +++ b/wire-runtime/src/nonJvmMain/kotlin/com/squareup/wire/ProtoAdapter.kt @@ -182,6 +182,7 @@ actual abstract class ProtoAdapter actual constructor( actual val DURATION: ProtoAdapter = commonDuration() actual val INSTANT: ProtoAdapter = commonInstant() actual val EMPTY: ProtoAdapter = commonEmpty() + actual val FIELD_MASK: ProtoAdapter = commonFieldMask() actual val STRUCT_MAP: ProtoAdapter?> = commonStructMap() actual val STRUCT_LIST: ProtoAdapter?> = commonStructList() actual val STRUCT_NULL: ProtoAdapter = commonStructNull() diff --git a/wire-schema/src/commonMain/kotlin/com/squareup/wire/schema/CoreLoader.kt b/wire-schema/src/commonMain/kotlin/com/squareup/wire/schema/CoreLoader.kt index cb17ec7a58..7879d78911 100644 --- a/wire-schema/src/commonMain/kotlin/com/squareup/wire/schema/CoreLoader.kt +++ b/wire-schema/src/commonMain/kotlin/com/squareup/wire/schema/CoreLoader.kt @@ -33,6 +33,7 @@ fun isWireRuntimeProto(path: String): Boolean = path == ANY_PROTO || path == DESCRIPTOR_PROTO || path == DURATION_PROTO || path == EMPTY_PROTO || + path == FIELD_MASK_PROTO || path == STRUCT_PROTO || path == TIMESTAMP_PROTO || path == WRAPPERS_PROTO || @@ -44,6 +45,7 @@ internal const val WIRE_EXTENSIONS_PROTO = "wire/extensions.proto" private const val ANY_PROTO = "google/protobuf/any.proto" private const val DURATION_PROTO = "google/protobuf/duration.proto" private const val EMPTY_PROTO = "google/protobuf/empty.proto" +private const val FIELD_MASK_PROTO = "google/protobuf/field_mask.proto" private const val STRUCT_PROTO = "google/protobuf/struct.proto" private const val TIMESTAMP_PROTO = "google/protobuf/timestamp.proto" private const val WRAPPERS_PROTO = "google/protobuf/wrappers.proto" diff --git a/wire-schema/src/commonMain/kotlin/com/squareup/wire/schema/SchemaProtoAdapterFactory.kt b/wire-schema/src/commonMain/kotlin/com/squareup/wire/schema/SchemaProtoAdapterFactory.kt index 3c8963a707..03816193c7 100644 --- a/wire-schema/src/commonMain/kotlin/com/squareup/wire/schema/SchemaProtoAdapterFactory.kt +++ b/wire-schema/src/commonMain/kotlin/com/squareup/wire/schema/SchemaProtoAdapterFactory.kt @@ -58,6 +58,7 @@ internal class SchemaProtoAdapterFactory( ProtoType.DURATION to ProtoAdapter.DURATION, ProtoType.TIMESTAMP to ProtoAdapter.INSTANT, ProtoType.EMPTY to ProtoAdapter.EMPTY, + ProtoType.FIELD_MASK to ProtoAdapter.FIELD_MASK, ProtoType.STRUCT_MAP to ProtoAdapter.STRUCT_MAP, ProtoType.STRUCT_VALUE to ProtoAdapter.STRUCT_VALUE, ProtoType.STRUCT_NULL to ProtoAdapter.STRUCT_NULL, diff --git a/wire-schema/src/jvmMain/kotlin/com/squareup/wire/schema/internal/JvmLanguages.kt b/wire-schema/src/jvmMain/kotlin/com/squareup/wire/schema/internal/JvmLanguages.kt index e62e1abc36..a629c0b314 100644 --- a/wire-schema/src/jvmMain/kotlin/com/squareup/wire/schema/internal/JvmLanguages.kt +++ b/wire-schema/src/jvmMain/kotlin/com/squareup/wire/schema/internal/JvmLanguages.kt @@ -64,6 +64,7 @@ fun builtInAdapterString(type: ProtoType, useArray: Boolean = false): String? { ProtoType.DURATION -> ProtoAdapter::class.java.name + "#DURATION" ProtoType.TIMESTAMP -> ProtoAdapter::class.java.name + "#INSTANT" ProtoType.EMPTY -> ProtoAdapter::class.java.name + "#EMPTY" + ProtoType.FIELD_MASK -> ProtoAdapter::class.java.name + "#FIELD_MASK" ProtoType.STRUCT_MAP -> ProtoAdapter::class.java.name + "#STRUCT_MAP" ProtoType.STRUCT_VALUE -> ProtoAdapter::class.java.name + "#STRUCT_VALUE" ProtoType.STRUCT_NULL -> ProtoAdapter::class.java.name + "#STRUCT_NULL" diff --git a/wire-schema/src/jvmMain/resources/google/protobuf/field_mask.proto b/wire-schema/src/jvmMain/resources/google/protobuf/field_mask.proto new file mode 100644 index 0000000000..814d58bf91 --- /dev/null +++ b/wire-schema/src/jvmMain/resources/google/protobuf/field_mask.proto @@ -0,0 +1,45 @@ +// 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 google.protobuf; + +option go_package = "google.golang.org/protobuf/types/known/fieldmaskpb"; +option java_package = "com.google.protobuf"; +option java_outer_classname = "FieldMaskProto"; +option java_multiple_files = true; +option objc_class_prefix = "GPB"; +option csharp_namespace = "Google.Protobuf.WellKnownTypes"; +option cc_enable_arenas = true; + +message FieldMask { + repeated string paths = 1; +} diff --git a/wire-schema/src/jvmTest/kotlin/com/squareup/wire/schema/DynamicSerializationTest.kt b/wire-schema/src/jvmTest/kotlin/com/squareup/wire/schema/DynamicSerializationTest.kt index 5b5ac30a97..ad3993baaa 100644 --- a/wire-schema/src/jvmTest/kotlin/com/squareup/wire/schema/DynamicSerializationTest.kt +++ b/wire-schema/src/jvmTest/kotlin/com/squareup/wire/schema/DynamicSerializationTest.kt @@ -17,6 +17,7 @@ package com.squareup.wire.schema import assertk.assertThat import assertk.assertions.isEqualTo +import com.squareup.wire.FieldMask import com.squareup.wire.buildSchema import com.squareup.wire.durationOfSeconds import com.squareup.wire.ofEpochSecond @@ -36,6 +37,7 @@ class DynamicSerializationTest { |import "google/protobuf/duration.proto"; |import "google/protobuf/timestamp.proto"; |import "google/protobuf/empty.proto"; + |import "google/protobuf/field_mask.proto"; |import "google/protobuf/struct.proto"; |import "google/protobuf/wrappers.proto"; | @@ -56,6 +58,7 @@ class DynamicSerializationTest { | google.protobuf.BoolValue bool_value_field = 14; | google.protobuf.StringValue string_value_field = 15; | google.protobuf.BytesValue bytes_value_field = 16; + | google.protobuf.FieldMask field_mask_field = 17; |} | """.trimMargin(), @@ -79,6 +82,7 @@ class DynamicSerializationTest { "bool_value_field" to true, "string_value_field" to "πάμε", "bytes_value_field" to "πάμε".encodeUtf8(), + "field_mask_field" to FieldMask(listOf("user.display_name", "photo")), ) assertThat(adapter.decode(adapter.encode(expected))).isEqualTo(expected) } diff --git a/wire-swift-generator/src/main/java/com/squareup/wire/swift/SwiftGenerator.kt b/wire-swift-generator/src/main/java/com/squareup/wire/swift/SwiftGenerator.kt index 98c1e399d7..2268b73fd6 100644 --- a/wire-swift-generator/src/main/java/com/squareup/wire/swift/SwiftGenerator.kt +++ b/wire-swift-generator/src/main/java/com/squareup/wire/swift/SwiftGenerator.kt @@ -198,6 +198,7 @@ class SwiftGenerator private constructor( // @ProtoDefaulted support. STRUCT_NULL is intentionally omitted -- it's an // enum, so it falls through to the isEnum check below. if (type == ProtoType.ANY || + type == ProtoType.FIELD_MASK || type == ProtoType.STRUCT_MAP || type == ProtoType.STRUCT_VALUE || type == ProtoType.STRUCT_LIST @@ -1874,6 +1875,7 @@ class SwiftGenerator private constructor( ProtoType.UINT32 to UINT32, ProtoType.UINT64 to UINT64, ProtoType.ANY to DeclaredTypeName.typeName("Wire.AnyMessage"), + ProtoType.FIELD_MASK to DeclaredTypeName.typeName("Wire.FieldMask"), ProtoType.STRUCT_LIST to DeclaredTypeName.typeName("Wire.ListValue"), ProtoType.STRUCT_MAP to DeclaredTypeName.typeName("Wire.StructMessage"), ProtoType.STRUCT_NULL to DeclaredTypeName.typeName("Wire.StructNull"), diff --git a/wire-swift-generator/src/test/java/com/squareup/wire/swift/SwiftGeneratorTest.kt b/wire-swift-generator/src/test/java/com/squareup/wire/swift/SwiftGeneratorTest.kt index 9cde528abe..fa3cbca9fe 100644 --- a/wire-swift-generator/src/test/java/com/squareup/wire/swift/SwiftGeneratorTest.kt +++ b/wire-swift-generator/src/test/java/com/squareup/wire/swift/SwiftGeneratorTest.kt @@ -64,6 +64,32 @@ class SwiftGeneratorTest { assertThat(code).contains("self.parseUnknownField(fieldNumber: 50003)") } + @Test fun usesFieldMask() { + val schema = buildSchema { + add( + "message.proto".toPath(), + """ + |syntax = "proto3"; + | + |package squareup.protos3; + | + |import "google/protobuf/field_mask.proto"; + | + |message Message { + | google.protobuf.FieldMask mask = 1; + |} + """.trimMargin(), + ) + } + + val code = schema.generateSwift("squareup.protos3.Message") + + assertThat(code).contains("import Wire") + assertThat(code).contains("public var mask: FieldMask?") + assertThat(code).contains("mask = try protoReader.decode(FieldMask.self)") + assertThat(code).doesNotContain("@ProtoDefaulted") + } + private fun Schema.generateSwift(typeName: String): String { val swiftGenerator = SwiftGenerator(this) val type = requireNotNull(getType(typeName))