Google_Protobuf_FieldMask+Extensions.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. // Sources/SwiftProtobuf/Google_Protobuf_FieldMask+Extensions.swift - Fieldmask extensions
  2. //
  3. // Copyright (c) 2014 - 2016 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 generated FieldMask message with customized JSON coding and
  12. /// convenience methods.
  13. ///
  14. // -----------------------------------------------------------------------------
  15. // True if the string only contains printable (non-control)
  16. // ASCII characters. Note: This follows the ASCII standard;
  17. // space is not a "printable" character.
  18. private func isPrintableASCII(_ s: String) -> Bool {
  19. for u in s.utf8 {
  20. if u <= 0x20 || u >= 0x7f {
  21. return false
  22. }
  23. }
  24. return true
  25. }
  26. private func ProtoToJSON(name: String) -> String? {
  27. guard isPrintableASCII(name) else { return nil }
  28. var jsonPath = String()
  29. var chars = name.makeIterator()
  30. while let c = chars.next() {
  31. switch c {
  32. case "_":
  33. if let toupper = chars.next() {
  34. switch toupper {
  35. case "a"..."z":
  36. jsonPath.append(String(toupper).uppercased())
  37. default:
  38. return nil
  39. }
  40. } else {
  41. return nil
  42. }
  43. case "A"..."Z":
  44. return nil
  45. case "a"..."z", "0"..."9", ".", "(", ")":
  46. jsonPath.append(c)
  47. default:
  48. // TODO: Change this to `return nil`
  49. // once we know everything legal is handled
  50. // above.
  51. jsonPath.append(c)
  52. }
  53. }
  54. return jsonPath
  55. }
  56. private func JSONToProto(name: String) -> String? {
  57. guard isPrintableASCII(name) else { return nil }
  58. var path = String()
  59. for c in name {
  60. switch c {
  61. case "_":
  62. return nil
  63. case "A"..."Z":
  64. path.append(Character("_"))
  65. path.append(String(c).lowercased())
  66. case "a"..."z", "0"..."9", ".", "(", ")":
  67. path.append(c)
  68. default:
  69. // TODO: Change to `return nil` once
  70. // we know everything legal is being
  71. // handled above
  72. path.append(c)
  73. }
  74. }
  75. return path
  76. }
  77. private func parseJSONFieldNames(names: String) -> [String]? {
  78. // An empty field mask is the empty string (no paths).
  79. guard !names.isEmpty else { return [] }
  80. var fieldNameCount = 0
  81. var fieldName = String()
  82. var split = [String]()
  83. for c in names {
  84. switch c {
  85. case ",":
  86. if fieldNameCount == 0 {
  87. return nil
  88. }
  89. if let pbName = JSONToProto(name: fieldName) {
  90. split.append(pbName)
  91. } else {
  92. return nil
  93. }
  94. fieldName = String()
  95. fieldNameCount = 0
  96. default:
  97. fieldName.append(c)
  98. fieldNameCount += 1
  99. }
  100. }
  101. if fieldNameCount == 0 { // Last field name can't be empty
  102. return nil
  103. }
  104. if let pbName = JSONToProto(name: fieldName) {
  105. split.append(pbName)
  106. } else {
  107. return nil
  108. }
  109. return split
  110. }
  111. extension Google_Protobuf_FieldMask {
  112. /// Creates a new `Google_Protobuf_FieldMask` from the given array of paths.
  113. ///
  114. /// The paths should match the names used in the .proto file, which may be
  115. /// different than the corresponding Swift property names.
  116. ///
  117. /// - Parameter protoPaths: The paths from which to create the field mask,
  118. /// defined using the .proto names for the fields.
  119. public init(protoPaths: [String]) {
  120. self.init()
  121. paths = protoPaths
  122. }
  123. /// Creates a new `Google_Protobuf_FieldMask` from the given paths.
  124. ///
  125. /// The paths should match the names used in the .proto file, which may be
  126. /// different than the corresponding Swift property names.
  127. ///
  128. /// - Parameter protoPaths: The paths from which to create the field mask,
  129. /// defined using the .proto names for the fields.
  130. public init(protoPaths: String...) {
  131. self.init(protoPaths: protoPaths)
  132. }
  133. /// Creates a new `Google_Protobuf_FieldMask` from the given paths.
  134. ///
  135. /// The paths should match the JSON names of the fields, which may be
  136. /// different than the corresponding Swift property names.
  137. ///
  138. /// - Parameter jsonPaths: The paths from which to create the field mask,
  139. /// defined using the JSON names for the fields.
  140. public init?(jsonPaths: String...) {
  141. // TODO: This should fail if any of the conversions from JSON fails
  142. self.init(protoPaths: jsonPaths.compactMap(JSONToProto))
  143. }
  144. // It would be nice if to have an initializer that accepted Swift property
  145. // names, but translating between swift and protobuf/json property
  146. // names is not entirely deterministic.
  147. }
  148. extension Google_Protobuf_FieldMask: _CustomJSONCodable {
  149. mutating func decodeJSON(from decoder: inout JSONDecoder) throws {
  150. let s = try decoder.scanner.nextQuotedString()
  151. if let names = parseJSONFieldNames(names: s) {
  152. paths = names
  153. } else {
  154. throw JSONDecodingError.malformedFieldMask
  155. }
  156. }
  157. func encodedJSONString(options: JSONEncodingOptions) throws -> String {
  158. // Note: Proto requires alphanumeric field names, so there
  159. // cannot be a ',' or '"' character to mess up this formatting.
  160. var jsonPaths = [String]()
  161. for p in paths {
  162. if let jsonPath = ProtoToJSON(name: p) {
  163. jsonPaths.append(jsonPath)
  164. } else {
  165. throw JSONEncodingError.fieldMaskConversion
  166. }
  167. }
  168. return "\"" + jsonPaths.joined(separator: ",") + "\""
  169. }
  170. }
  171. extension Google_Protobuf_FieldMask {
  172. /// Initiates a field mask with all fields of the message type.
  173. ///
  174. /// - Parameter messageType: Message type to get all paths from.
  175. public init<M: Message & _ProtoNameProviding>(
  176. allFieldsOf messageType: M.Type
  177. ) {
  178. self = .with { mask in
  179. mask.paths = M.allProtoNames
  180. }
  181. }
  182. /// Initiates a field mask from some particular field numbers of a message
  183. ///
  184. /// - Parameters:
  185. /// - messageType: Message type to get all paths from.
  186. /// - fieldNumbers: Field numbers of paths to be included.
  187. /// - Returns: Field mask that include paths of corresponding field numbers.
  188. /// - Throws: `FieldMaskError.invalidFieldNumber` if the field number
  189. /// is not on the message
  190. public init<M: Message & _ProtoNameProviding>(
  191. fieldNumbers: [Int],
  192. of messageType: M.Type
  193. ) throws {
  194. var paths: [String] = []
  195. for number in fieldNumbers {
  196. guard let name = M.protoName(for: number) else {
  197. throw FieldMaskError.invalidFieldNumber
  198. }
  199. paths.append(name)
  200. }
  201. self = .with { mask in
  202. mask.paths = paths
  203. }
  204. }
  205. }
  206. extension Google_Protobuf_FieldMask {
  207. /// Adds a path to FieldMask after checking whether the given path is valid.
  208. /// This method check-fails if the path is not a valid path for Message type.
  209. ///
  210. /// - Parameters:
  211. /// - path: Path to be added to FieldMask.
  212. /// - messageType: Message type to check validity.
  213. public mutating func addPath<M: Message>(
  214. _ path: String,
  215. of messageType: M.Type
  216. ) throws {
  217. guard M.isPathValid(path) else {
  218. throw FieldMaskError.invalidPath
  219. }
  220. paths.append(path)
  221. }
  222. /// Converts a FieldMask to the canonical form. It will:
  223. /// 1. Remove paths that are covered by another path. For example,
  224. /// "foo.bar" is covered by "foo" and will be removed if "foo"
  225. /// is also in the FieldMask.
  226. /// 2. Sort all paths in alphabetical order.
  227. public var canonical: Google_Protobuf_FieldMask {
  228. var mask = Google_Protobuf_FieldMask()
  229. let sortedPaths = self.paths.sorted()
  230. for path in sortedPaths {
  231. if let lastPath = mask.paths.last {
  232. if path != lastPath, !path.hasPrefix("\(lastPath).") {
  233. mask.paths.append(path)
  234. }
  235. } else {
  236. mask.paths.append(path)
  237. }
  238. }
  239. return mask
  240. }
  241. /// Creates an union of two FieldMasks.
  242. ///
  243. /// - Parameter mask: FieldMask to union with.
  244. /// - Returns: FieldMask with union of two path sets.
  245. public func union(
  246. _ mask: Google_Protobuf_FieldMask
  247. ) -> Google_Protobuf_FieldMask {
  248. var buffer: Set<String> = .init()
  249. var paths: [String] = []
  250. let allPaths = self.paths + mask.paths
  251. for path in allPaths where !buffer.contains(path) {
  252. buffer.insert(path)
  253. paths.append(path)
  254. }
  255. return .with { mask in
  256. mask.paths = paths
  257. }
  258. }
  259. /// Creates an intersection of two FieldMasks.
  260. ///
  261. /// - Parameter mask: FieldMask to intersect with.
  262. /// - Returns: FieldMask with intersection of two path sets.
  263. public func intersect(
  264. _ mask: Google_Protobuf_FieldMask
  265. ) -> Google_Protobuf_FieldMask {
  266. let set = Set<String>(mask.paths)
  267. var paths: [String] = []
  268. var buffer = Set<String>()
  269. for path in self.paths where set.contains(path) && !buffer.contains(path) {
  270. buffer.insert(path)
  271. paths.append(path)
  272. }
  273. return .with { mask in
  274. mask.paths = paths
  275. }
  276. }
  277. /// Creates a FieldMasks with paths of the original FieldMask
  278. /// that does not included in mask.
  279. ///
  280. /// - Parameter mask: FieldMask with paths should be substracted.
  281. /// - Returns: FieldMask with all paths does not included in mask.
  282. public func subtract(
  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. /// Returns true if path is covered by the given FieldMask. Note that path
  297. /// "foo.bar" covers all paths like "foo.bar.baz", "foo.bar.quz.x", etc.
  298. /// Also note that parent paths are not covered by explicit child path, i.e.
  299. /// "foo.bar" does NOT cover "foo", even if "bar" is the only child.
  300. ///
  301. /// - Parameter path: Path to be checked.
  302. /// - Returns: Boolean determines is path covered.
  303. public func contains(_ path: String) -> Bool {
  304. for fieldMaskPath in paths {
  305. if path.hasPrefix("\(fieldMaskPath).") || fieldMaskPath == path {
  306. return true
  307. }
  308. }
  309. return false
  310. }
  311. }
  312. extension Google_Protobuf_FieldMask {
  313. /// Checks whether the given FieldMask is valid for type M.
  314. ///
  315. /// - Parameter messageType: Message type to paths check with.
  316. /// - Returns: Boolean determines FieldMask is valid.
  317. public func isValid<M: Message & _ProtoNameProviding>(
  318. for messageType: M.Type
  319. ) -> Bool {
  320. var message = M()
  321. return paths.allSatisfy { path in
  322. message.isPathValid(path)
  323. }
  324. }
  325. }
  326. /// Describes errors could happen during FieldMask utilities.
  327. public enum FieldMaskError: Error {
  328. /// Describes a path is invalid for a Message type.
  329. case invalidPath
  330. /// Describes a fieldNumber is invalid for a Message type.
  331. case invalidFieldNumber
  332. }
  333. extension Message where Self: _ProtoNameProviding {
  334. fileprivate static func protoName(for number: Int) -> String? {
  335. Self._protobuf_nameMap.names(for: number)?.proto.description
  336. }
  337. fileprivate static var allProtoNames: [String] {
  338. Self._protobuf_nameMap.names.map(\.description)
  339. }
  340. }