Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
137 changes: 137 additions & 0 deletions wire-runtime-swift/src/main/swift/FieldMask.swift
Original file line number Diff line number Diff line change
@@ -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
23 changes: 23 additions & 0 deletions wire-runtime-swift/src/test/proto/field_mask_message.proto
Original file line number Diff line number Diff line change
@@ -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;
}
67 changes: 67 additions & 0 deletions wire-runtime-swift/src/test/swift/FieldMaskTests.swift
Original file line number Diff line number Diff line change
@@ -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"]))
}
}
1 change: 1 addition & 0 deletions wire-runtime/api/wire-runtime.api
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,8 @@ expect abstract class ProtoAdapter<E>(

val EMPTY: ProtoAdapter<Unit>

val FIELD_MASK: ProtoAdapter<FieldMask>

val STRUCT_MAP: ProtoAdapter<Map<String, *>?>

val STRUCT_LIST: ProtoAdapter<List<*>?>
Expand Down Expand Up @@ -1331,6 +1333,57 @@ internal fun commonEmpty(): ProtoAdapter<Unit> = object : ProtoAdapter<Unit>(
override fun redact(value: Unit): Unit = value
}

internal fun commonFieldMask(): ProtoAdapter<FieldMask> = object : ProtoAdapter<FieldMask>(
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<String>()
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<String>()
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<Map<String, *>?> = object : ProtoAdapter<Map<String, *>?>(
LENGTH_DELIMITED,
Map::class,
Expand Down
Loading
Loading