Skip to content

Commit

Permalink
Merge pull request #1505 from pouyayarandi/add-proto-reflection
Browse files Browse the repository at this point in the history
Add FieldMask utilities to Message types
  • Loading branch information
tbkka authored Aug 16, 2024
2 parents faf05e8 + e3d01ea commit 3a4c97d
Show file tree
Hide file tree
Showing 7 changed files with 1,759 additions and 9 deletions.
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.
/// - 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 {
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(
_ 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 {
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
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,6 +278,11 @@ public struct _NameMap: ExpressibleByDictionaryLiteral {
let n = Name(transientUtf8Buffer: raw)
return jsonToNumberMap[n]
}

/// Returns all proto names
internal var names: [Name] {
numberToNameMap.map(\.value.proto)
}
}

// The `_NameMap` (and supporting types) are only mutated during their initial
Expand Down
Loading

0 comments on commit 3a4c97d

Please sign in to comment.