Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add FieldMask utilities to Message types #1505

Merged
merged 33 commits into from
Aug 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
c889293
Add applying and tests
pouyayarandi Nov 25, 2023
f01f29c
Move extension to another file
Nov 27, 2023
eaa0666
Some minor changes in comments
Nov 27, 2023
9e33a9b
Revert "Some minor changes in comments"
Dec 1, 2023
b728a33
Revert "Move extension to another file"
Dec 1, 2023
634ca09
Revert "Add applying and tests"
Dec 1, 2023
ad59804
Implement field mask utils
Dec 3, 2023
1b5fbe9
Update cmakelist
Dec 3, 2023
c1e42b2
Add comments for path decoding error
Dec 3, 2023
da24d19
Change APIs
Dec 8, 2023
1c94065
Add comments
Dec 8, 2023
af441d1
Remove copy mechanism in some functions
Dec 13, 2023
249b066
Change path contain and canonical algorithm
Dec 13, 2023
795b22a
Rename field mask error
Dec 13, 2023
095dde2
Fix intersect and subtract
Dec 13, 2023
37f1872
Implement merge option
Dec 13, 2023
08270b5
Add comments
Dec 13, 2023
d657a81
Fix bug of extra paths
pouyayarandi Dec 14, 2023
3269433
Fix comment
pouyayarandi Dec 14, 2023
d564895
Change if to guard
pouyayarandi Dec 15, 2023
fcec7f2
Fix comments
Dec 22, 2023
787957f
Fix variable names with underscore
Dec 22, 2023
eedb785
Fix comments
Jan 17, 2024
d7ba532
Fix comments
Feb 2, 2024
03f5f0d
Fix build errors in swift 5.8 and later using any keyword for type-ca…
Mar 29, 2024
9e15359
Add a test for trim with extensible messages
Mar 29, 2024
dcfa442
Rename merge(from:) to merge(with:)
Apr 13, 2024
aae790b
Replace GetPathDecoder with PathVisitor
May 3, 2024
db6be11
Improve tests
May 4, 2024
1ef69c9
Fix comments
May 15, 2024
35163e1
Fix failing test
May 16, 2024
702679b
Fix issue of json path names
May 16, 2024
e3d01ea
Fix comments
Aug 15, 2024
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
3 changes: 3 additions & 0 deletions Sources/SwiftProtobuf/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,15 @@ add_library(SwiftProtobuf
MathUtils.swift
Message+AnyAdditions.swift
Message+BinaryAdditions.swift
Message+FieldMask.swift
Message+JSONAdditions.swift
Message+JSONArrayAdditions.swift
Message+TextFormatAdditions.swift
Message.swift
MessageExtension.swift
NameMap.swift
PathDecoder.swift
PathVisitor.swift
ProtobufAPIVersionCheck.swift
ProtobufMap.swift
ProtoNameProviding.swift
Expand Down
192 changes: 187 additions & 5 deletions Sources/SwiftProtobuf/Google_Protobuf_FieldMask+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,6 @@
///
// -----------------------------------------------------------------------------

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

// True if the string only contains printable (non-control)
// ASCII characters. Note: This follows the ASCII standard;
Expand Down Expand Up @@ -184,3 +179,190 @@ extension Google_Protobuf_FieldMask: _CustomJSONCodable {
return "\"" + jsonPaths.joined(separator: ",") + "\""
}
}

extension Google_Protobuf_FieldMask {

/// Initiates a field mask with all fields of the message type.
///
/// - Parameter messageType: Message type to get all paths from.
public init<M: Message & _ProtoNameProviding>(
allFieldsOf messageType: M.Type
) {
self = .with { mask in
mask.paths = M.allProtoNames
}
}

/// Initiates a field mask from some particular field numbers of a message
///
/// - Parameters:
/// - messageType: Message type to get all paths from.
/// - fieldNumbers: Field numbers of paths to be included.
/// - Returns: Field mask that include paths of corresponding field numbers.
thomasvl marked this conversation as resolved.
Show resolved Hide resolved
/// - Throws: `FieldMaskError.invalidFieldNumber` if the field number
/// is not on the message
public init<M: Message & _ProtoNameProviding>(
fieldNumbers: [Int],
of messageType: M.Type
) throws {
var paths: [String] = []
for number in fieldNumbers {
guard let name = M.protoName(for: number) else {
throw FieldMaskError.invalidFieldNumber
}
paths.append(name)
}
self = .with { mask in
mask.paths = paths
}
}
}

extension Google_Protobuf_FieldMask {

/// Adds a path to FieldMask after checking whether the given path is valid.
/// This method check-fails if the path is not a valid path for Message type.
///
/// - Parameters:
/// - path: Path to be added to FieldMask.
/// - messageType: Message type to check validity.
public mutating func addPath<M: Message>(
_ path: String,
of messageType: M.Type
) throws {
guard M.isPathValid(path) else {
throw FieldMaskError.invalidPath
}
paths.append(path)
}

/// Converts a FieldMask to the canonical form. It will:
/// 1. Remove paths that are covered by another path. For example,
/// "foo.bar" is covered by "foo" and will be removed if "foo"
/// is also in the FieldMask.
/// 2. Sort all paths in alphabetical order.
public var canonical: Google_Protobuf_FieldMask {
var mask = Google_Protobuf_FieldMask()
let sortedPaths = self.paths.sorted()
for path in sortedPaths {
thomasvl marked this conversation as resolved.
Show resolved Hide resolved
if let lastPath = mask.paths.last {
if path != lastPath, !path.hasPrefix("\(lastPath).") {
mask.paths.append(path)
}
} else {
mask.paths.append(path)
}
}
return mask
}

/// Creates an union of two FieldMasks.
///
/// - Parameter mask: FieldMask to union with.
/// - Returns: FieldMask with union of two path sets.
public func union(
_ mask: Google_Protobuf_FieldMask
) -> Google_Protobuf_FieldMask {
var buffer: Set<String> = .init()
var paths: [String] = []
let allPaths = self.paths + mask.paths
for path in allPaths where !buffer.contains(path) {
buffer.insert(path)
paths.append(path)
}
return .with { mask in
mask.paths = paths
}
}

/// Creates an intersection of two FieldMasks.
///
/// - Parameter mask: FieldMask to intersect with.
/// - Returns: FieldMask with intersection of two path sets.
public func intersect(
_ mask: Google_Protobuf_FieldMask
) -> Google_Protobuf_FieldMask {
let set = Set<String>(mask.paths)
var paths: [String] = []
var buffer = Set<String>()
for path in self.paths where set.contains(path) && !buffer.contains(path) {
buffer.insert(path)
paths.append(path)
}
return .with { mask in
mask.paths = paths
}
}

/// Creates a FieldMasks with paths of the original FieldMask
/// that does not included in mask.
///
/// - Parameter mask: FieldMask with paths should be substracted.
/// - Returns: FieldMask with all paths does not included in mask.
public func subtract(
_ mask: Google_Protobuf_FieldMask
) -> Google_Protobuf_FieldMask {
let set = Set<String>(mask.paths)
var paths: [String] = []
var buffer = Set<String>()
for path in self.paths where !set.contains(path) && !buffer.contains(path) {
buffer.insert(path)
paths.append(path)
}
return .with { mask in
mask.paths = paths
}
}

/// Returns true if path is covered by the given FieldMask. Note that path
/// "foo.bar" covers all paths like "foo.bar.baz", "foo.bar.quz.x", etc.
/// Also note that parent paths are not covered by explicit child path, i.e.
/// "foo.bar" does NOT cover "foo", even if "bar" is the only child.
///
/// - Parameter path: Path to be checked.
/// - Returns: Boolean determines is path covered.
public func contains(_ path: String) -> Bool {
for fieldMaskPath in paths {
if path.hasPrefix("\(fieldMaskPath).") || fieldMaskPath == path {
return true
}
}
return false
}
}

extension Google_Protobuf_FieldMask {

/// Checks whether the given FieldMask is valid for type M.
///
/// - Parameter messageType: Message type to paths check with.
/// - Returns: Boolean determines FieldMask is valid.
public func isValid<M: Message & _ProtoNameProviding>(
for messageType: M.Type
) -> Bool {
var message = M()
return paths.allSatisfy { path in
message.isPathValid(path)
}
}
}

/// Describes errors could happen during FieldMask utilities.
public enum FieldMaskError: Error {

/// Describes a path is invalid for a Message type.
case invalidPath

/// Describes a fieldNumber is invalid for a Message type.
case invalidFieldNumber
}

private extension Message where Self: _ProtoNameProviding {
static func protoName(for number: Int) -> String? {
Self._protobuf_nameMap.names(for: number)?.proto.description
}

static var allProtoNames: [String] {
Self._protobuf_nameMap.names.map(\.description)
}
}
132 changes: 132 additions & 0 deletions Sources/SwiftProtobuf/Message+FieldMask.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// Sources/SwiftProtobuf/Message+FieldMask.swift - Message field mask extensions
//
// Copyright (c) 2014 - 2023 Apple Inc. and the project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See LICENSE.txt for license information:
// https://github.com/apple/swift-protobuf/blob/main/LICENSE.txt
//
// -----------------------------------------------------------------------------
///
/// Extend the Message types with FieldMask utilities.
///
// -----------------------------------------------------------------------------

import Foundation

extension Message {

/// Checks whether the given path is valid for Message type.
///
/// - Parameter path: Path to be checked
/// - Returns: Boolean determines path is valid.
public static func isPathValid(
_ path: String
) -> Bool {
var message = Self()
return message.hasPath(path: path)
}

internal mutating func hasPath(path: String) -> Bool {
do {
try set(path: path, value: nil, mergeOption: .init())
return true
} catch let error as PathDecodingError {
return error != .pathNotFound
} catch {
return false
}
}

internal mutating func isPathValid(
thomasvl marked this conversation as resolved.
Show resolved Hide resolved
_ path: String
) -> Bool {
hasPath(path: path)
}
}

extension Google_Protobuf_FieldMask {

/// Defines available options for merging two messages.
public struct MergeOptions {

public init() {}

/// The default merging behavior will append entries from the source
/// repeated field to the destination repeated field. If you only want
/// to keep the entries from the source repeated field, set this flag
/// to true.
public var replaceRepeatedFields = false
}
}

extension Message {

/// Merges fields specified in a FieldMask into another message.
///
/// - Parameters:
/// - source: Message that should be merged to the original one.
/// - fieldMask: FieldMask specifies which fields should be merged.
public mutating func merge(
from source: Self,
fieldMask: Google_Protobuf_FieldMask,
mergeOption: Google_Protobuf_FieldMask.MergeOptions = .init()
) throws {
var visitor = PathVisitor<Self>()
try source.traverse(visitor: &visitor)
let values = visitor.values
// TODO: setting all values with only one decoding
for path in fieldMask.paths {
thomasvl marked this conversation as resolved.
Show resolved Hide resolved
try? set(
path: path,
value: values[path],
mergeOption: mergeOption
)
}
}
}

extension Message where Self: Equatable, Self: _ProtoNameProviding {

// TODO: Re-implement using clear fields instead of copying message

/// Removes from 'message' any field that is not represented in the given
/// FieldMask. If the FieldMask is empty, does nothing.
///
/// - Parameter fieldMask: FieldMask specifies which fields should be kept.
/// - Returns: Boolean determines if the message is modified
@discardableResult
public mutating func trim(
keeping fieldMask: Google_Protobuf_FieldMask
) -> Bool {
if !fieldMask.isValid(for: Self.self) {
return false
}
if fieldMask.paths.isEmpty {
return false
}
var tmp = Self(removingAllFieldsOf: self)
do {
try tmp.merge(from: self, fieldMask: fieldMask)
let changed = tmp != self
self = tmp
thomasvl marked this conversation as resolved.
Show resolved Hide resolved
return changed
} catch {
return false
}
}
}

private extension Message {
init(removingAllFieldsOf message: Self) {
let newMessage: Self = .init()
if var newExtensible = newMessage as? any ExtensibleMessage,
let extensible = message as? any ExtensibleMessage {
newExtensible._protobuf_extensionFieldValues = extensible._protobuf_extensionFieldValues
self = newExtensible as? Self ?? newMessage
} else {
self = newMessage
}
self.unknownFields = message.unknownFields
}
}
5 changes: 5 additions & 0 deletions Sources/SwiftProtobuf/NameMap.swift
Original file line number Diff line number Diff line change
Expand Up @@ -278,4 +278,9 @@ public struct _NameMap: ExpressibleByDictionaryLiteral {
let n = Name(transientUtf8Buffer: raw)
return jsonToNumberMap[n]
}

/// Returns all proto names
internal var names: [Name] {
thomasvl marked this conversation as resolved.
Show resolved Hide resolved
numberToNameMap.map(\.value.proto)
}
}
Loading