Skip to content

Commit 3a4c97d

Browse files
authored
Merge pull request #1505 from pouyayarandi/add-proto-reflection
Add FieldMask utilities to Message types
2 parents faf05e8 + e3d01ea commit 3a4c97d

File tree

7 files changed

+1759
-9
lines changed

7 files changed

+1759
-9
lines changed

Sources/SwiftProtobuf/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,15 @@ add_library(SwiftProtobuf
5050
MathUtils.swift
5151
Message+AnyAdditions.swift
5252
Message+BinaryAdditions.swift
53+
Message+FieldMask.swift
5354
Message+JSONAdditions.swift
5455
Message+JSONArrayAdditions.swift
5556
Message+TextFormatAdditions.swift
5657
Message.swift
5758
MessageExtension.swift
5859
NameMap.swift
60+
PathDecoder.swift
61+
PathVisitor.swift
5962
ProtobufAPIVersionCheck.swift
6063
ProtobufMap.swift
6164
ProtoNameProviding.swift

Sources/SwiftProtobuf/Google_Protobuf_FieldMask+Extensions.swift

Lines changed: 187 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,6 @@
1313
///
1414
// -----------------------------------------------------------------------------
1515

16-
// TODO: We should have utilities to apply a fieldmask to an arbitrary
17-
// message, intersect two fieldmasks, etc.
18-
// Google's C++ implementation does this by having utilities
19-
// to build a tree of field paths that can be easily intersected,
20-
// unioned, traversed to apply to submessages, etc.
2116

2217
// True if the string only contains printable (non-control)
2318
// ASCII characters. Note: This follows the ASCII standard;
@@ -184,3 +179,190 @@ extension Google_Protobuf_FieldMask: _CustomJSONCodable {
184179
return "\"" + jsonPaths.joined(separator: ",") + "\""
185180
}
186181
}
182+
183+
extension Google_Protobuf_FieldMask {
184+
185+
/// Initiates a field mask with all fields of the message type.
186+
///
187+
/// - Parameter messageType: Message type to get all paths from.
188+
public init<M: Message & _ProtoNameProviding>(
189+
allFieldsOf messageType: M.Type
190+
) {
191+
self = .with { mask in
192+
mask.paths = M.allProtoNames
193+
}
194+
}
195+
196+
/// Initiates a field mask from some particular field numbers of a message
197+
///
198+
/// - Parameters:
199+
/// - messageType: Message type to get all paths from.
200+
/// - fieldNumbers: Field numbers of paths to be included.
201+
/// - Returns: Field mask that include paths of corresponding field numbers.
202+
/// - Throws: `FieldMaskError.invalidFieldNumber` if the field number
203+
/// is not on the message
204+
public init<M: Message & _ProtoNameProviding>(
205+
fieldNumbers: [Int],
206+
of messageType: M.Type
207+
) throws {
208+
var paths: [String] = []
209+
for number in fieldNumbers {
210+
guard let name = M.protoName(for: number) else {
211+
throw FieldMaskError.invalidFieldNumber
212+
}
213+
paths.append(name)
214+
}
215+
self = .with { mask in
216+
mask.paths = paths
217+
}
218+
}
219+
}
220+
221+
extension Google_Protobuf_FieldMask {
222+
223+
/// Adds a path to FieldMask after checking whether the given path is valid.
224+
/// This method check-fails if the path is not a valid path for Message type.
225+
///
226+
/// - Parameters:
227+
/// - path: Path to be added to FieldMask.
228+
/// - messageType: Message type to check validity.
229+
public mutating func addPath<M: Message>(
230+
_ path: String,
231+
of messageType: M.Type
232+
) throws {
233+
guard M.isPathValid(path) else {
234+
throw FieldMaskError.invalidPath
235+
}
236+
paths.append(path)
237+
}
238+
239+
/// Converts a FieldMask to the canonical form. It will:
240+
/// 1. Remove paths that are covered by another path. For example,
241+
/// "foo.bar" is covered by "foo" and will be removed if "foo"
242+
/// is also in the FieldMask.
243+
/// 2. Sort all paths in alphabetical order.
244+
public var canonical: Google_Protobuf_FieldMask {
245+
var mask = Google_Protobuf_FieldMask()
246+
let sortedPaths = self.paths.sorted()
247+
for path in sortedPaths {
248+
if let lastPath = mask.paths.last {
249+
if path != lastPath, !path.hasPrefix("\(lastPath).") {
250+
mask.paths.append(path)
251+
}
252+
} else {
253+
mask.paths.append(path)
254+
}
255+
}
256+
return mask
257+
}
258+
259+
/// Creates an union of two FieldMasks.
260+
///
261+
/// - Parameter mask: FieldMask to union with.
262+
/// - Returns: FieldMask with union of two path sets.
263+
public func union(
264+
_ mask: Google_Protobuf_FieldMask
265+
) -> Google_Protobuf_FieldMask {
266+
var buffer: Set<String> = .init()
267+
var paths: [String] = []
268+
let allPaths = self.paths + mask.paths
269+
for path in allPaths where !buffer.contains(path) {
270+
buffer.insert(path)
271+
paths.append(path)
272+
}
273+
return .with { mask in
274+
mask.paths = paths
275+
}
276+
}
277+
278+
/// Creates an intersection of two FieldMasks.
279+
///
280+
/// - Parameter mask: FieldMask to intersect with.
281+
/// - Returns: FieldMask with intersection of two path sets.
282+
public func intersect(
283+
_ mask: Google_Protobuf_FieldMask
284+
) -> Google_Protobuf_FieldMask {
285+
let set = Set<String>(mask.paths)
286+
var paths: [String] = []
287+
var buffer = Set<String>()
288+
for path in self.paths where set.contains(path) && !buffer.contains(path) {
289+
buffer.insert(path)
290+
paths.append(path)
291+
}
292+
return .with { mask in
293+
mask.paths = paths
294+
}
295+
}
296+
297+
/// Creates a FieldMasks with paths of the original FieldMask
298+
/// that does not included in mask.
299+
///
300+
/// - Parameter mask: FieldMask with paths should be substracted.
301+
/// - Returns: FieldMask with all paths does not included in mask.
302+
public func subtract(
303+
_ mask: Google_Protobuf_FieldMask
304+
) -> Google_Protobuf_FieldMask {
305+
let set = Set<String>(mask.paths)
306+
var paths: [String] = []
307+
var buffer = Set<String>()
308+
for path in self.paths where !set.contains(path) && !buffer.contains(path) {
309+
buffer.insert(path)
310+
paths.append(path)
311+
}
312+
return .with { mask in
313+
mask.paths = paths
314+
}
315+
}
316+
317+
/// Returns true if path is covered by the given FieldMask. Note that path
318+
/// "foo.bar" covers all paths like "foo.bar.baz", "foo.bar.quz.x", etc.
319+
/// Also note that parent paths are not covered by explicit child path, i.e.
320+
/// "foo.bar" does NOT cover "foo", even if "bar" is the only child.
321+
///
322+
/// - Parameter path: Path to be checked.
323+
/// - Returns: Boolean determines is path covered.
324+
public func contains(_ path: String) -> Bool {
325+
for fieldMaskPath in paths {
326+
if path.hasPrefix("\(fieldMaskPath).") || fieldMaskPath == path {
327+
return true
328+
}
329+
}
330+
return false
331+
}
332+
}
333+
334+
extension Google_Protobuf_FieldMask {
335+
336+
/// Checks whether the given FieldMask is valid for type M.
337+
///
338+
/// - Parameter messageType: Message type to paths check with.
339+
/// - Returns: Boolean determines FieldMask is valid.
340+
public func isValid<M: Message & _ProtoNameProviding>(
341+
for messageType: M.Type
342+
) -> Bool {
343+
var message = M()
344+
return paths.allSatisfy { path in
345+
message.isPathValid(path)
346+
}
347+
}
348+
}
349+
350+
/// Describes errors could happen during FieldMask utilities.
351+
public enum FieldMaskError: Error {
352+
353+
/// Describes a path is invalid for a Message type.
354+
case invalidPath
355+
356+
/// Describes a fieldNumber is invalid for a Message type.
357+
case invalidFieldNumber
358+
}
359+
360+
private extension Message where Self: _ProtoNameProviding {
361+
static func protoName(for number: Int) -> String? {
362+
Self._protobuf_nameMap.names(for: number)?.proto.description
363+
}
364+
365+
static var allProtoNames: [String] {
366+
Self._protobuf_nameMap.names.map(\.description)
367+
}
368+
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
// Sources/SwiftProtobuf/Message+FieldMask.swift - Message field mask extensions
2+
//
3+
// Copyright (c) 2014 - 2023 Apple Inc. and the project authors
4+
// Licensed under Apache License v2.0 with Runtime Library Exception
5+
//
6+
// See LICENSE.txt for license information:
7+
// https://github.com/apple/swift-protobuf/blob/main/LICENSE.txt
8+
//
9+
// -----------------------------------------------------------------------------
10+
///
11+
/// Extend the Message types with FieldMask utilities.
12+
///
13+
// -----------------------------------------------------------------------------
14+
15+
import Foundation
16+
17+
extension Message {
18+
19+
/// Checks whether the given path is valid for Message type.
20+
///
21+
/// - Parameter path: Path to be checked
22+
/// - Returns: Boolean determines path is valid.
23+
public static func isPathValid(
24+
_ path: String
25+
) -> Bool {
26+
var message = Self()
27+
return message.hasPath(path: path)
28+
}
29+
30+
internal mutating func hasPath(path: String) -> Bool {
31+
do {
32+
try set(path: path, value: nil, mergeOption: .init())
33+
return true
34+
} catch let error as PathDecodingError {
35+
return error != .pathNotFound
36+
} catch {
37+
return false
38+
}
39+
}
40+
41+
internal mutating func isPathValid(
42+
_ path: String
43+
) -> Bool {
44+
hasPath(path: path)
45+
}
46+
}
47+
48+
extension Google_Protobuf_FieldMask {
49+
50+
/// Defines available options for merging two messages.
51+
public struct MergeOptions {
52+
53+
public init() {}
54+
55+
/// The default merging behavior will append entries from the source
56+
/// repeated field to the destination repeated field. If you only want
57+
/// to keep the entries from the source repeated field, set this flag
58+
/// to true.
59+
public var replaceRepeatedFields = false
60+
}
61+
}
62+
63+
extension Message {
64+
65+
/// Merges fields specified in a FieldMask into another message.
66+
///
67+
/// - Parameters:
68+
/// - source: Message that should be merged to the original one.
69+
/// - fieldMask: FieldMask specifies which fields should be merged.
70+
public mutating func merge(
71+
from source: Self,
72+
fieldMask: Google_Protobuf_FieldMask,
73+
mergeOption: Google_Protobuf_FieldMask.MergeOptions = .init()
74+
) throws {
75+
var visitor = PathVisitor<Self>()
76+
try source.traverse(visitor: &visitor)
77+
let values = visitor.values
78+
// TODO: setting all values with only one decoding
79+
for path in fieldMask.paths {
80+
try? set(
81+
path: path,
82+
value: values[path],
83+
mergeOption: mergeOption
84+
)
85+
}
86+
}
87+
}
88+
89+
extension Message where Self: Equatable, Self: _ProtoNameProviding {
90+
91+
// TODO: Re-implement using clear fields instead of copying message
92+
93+
/// Removes from 'message' any field that is not represented in the given
94+
/// FieldMask. If the FieldMask is empty, does nothing.
95+
///
96+
/// - Parameter fieldMask: FieldMask specifies which fields should be kept.
97+
/// - Returns: Boolean determines if the message is modified
98+
@discardableResult
99+
public mutating func trim(
100+
keeping fieldMask: Google_Protobuf_FieldMask
101+
) -> Bool {
102+
if !fieldMask.isValid(for: Self.self) {
103+
return false
104+
}
105+
if fieldMask.paths.isEmpty {
106+
return false
107+
}
108+
var tmp = Self(removingAllFieldsOf: self)
109+
do {
110+
try tmp.merge(from: self, fieldMask: fieldMask)
111+
let changed = tmp != self
112+
self = tmp
113+
return changed
114+
} catch {
115+
return false
116+
}
117+
}
118+
}
119+
120+
private extension Message {
121+
init(removingAllFieldsOf message: Self) {
122+
let newMessage: Self = .init()
123+
if var newExtensible = newMessage as? any ExtensibleMessage,
124+
let extensible = message as? any ExtensibleMessage {
125+
newExtensible._protobuf_extensionFieldValues = extensible._protobuf_extensionFieldValues
126+
self = newExtensible as? Self ?? newMessage
127+
} else {
128+
self = newMessage
129+
}
130+
self.unknownFields = message.unknownFields
131+
}
132+
}

Sources/SwiftProtobuf/NameMap.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,11 @@ public struct _NameMap: ExpressibleByDictionaryLiteral {
278278
let n = Name(transientUtf8Buffer: raw)
279279
return jsonToNumberMap[n]
280280
}
281+
282+
/// Returns all proto names
283+
internal var names: [Name] {
284+
numberToNameMap.map(\.value.proto)
285+
}
281286
}
282287

283288
// The `_NameMap` (and supporting types) are only mutated during their initial

0 commit comments

Comments
 (0)