|
13 | 13 | ///
|
14 | 14 | // -----------------------------------------------------------------------------
|
15 | 15 |
|
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. |
21 | 16 |
|
22 | 17 | // True if the string only contains printable (non-control)
|
23 | 18 | // ASCII characters. Note: This follows the ASCII standard;
|
@@ -184,3 +179,190 @@ extension Google_Protobuf_FieldMask: _CustomJSONCodable {
|
184 | 179 | return "\"" + jsonPaths.joined(separator: ",") + "\""
|
185 | 180 | }
|
186 | 181 | }
|
| 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 | +} |
0 commit comments