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