Skip to content

Commit a5a54d2

Browse files
author
Lukasz Zalewski
committed
feat(object-matcher): implement object matcher with necessary changes to AnyCodable; add tests for ObjectMatcher
1 parent 7285d1c commit a5a54d2

File tree

4 files changed

+2007
-4
lines changed

4 files changed

+2007
-4
lines changed

CoatySwift.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@
8282
9E774EBD249B7A4800EF888A /* ExampleControllerObserve.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E774EBC249B7A4800EF888A /* ExampleControllerObserve.swift */; };
8383
9E774EBF249B7A5E00EF888A /* ExampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E774EBE249B7A5E00EF888A /* ExampleViewController.swift */; };
8484
9E774EC1249B7BCC00EF888A /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E774EC0249B7BCC00EF888A /* SceneDelegate.swift */; };
85+
9E774EC3249B9D6200EF888A /* ObjectMatcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E774EC2249B9D6200EF888A /* ObjectMatcherTests.swift */; };
86+
9E774EC9249B9E7E00EF888A /* ObjectMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E774EC8249B9E7E00EF888A /* ObjectMatcher.swift */; };
8587
ACB0B1C91E23DC90548BF9E5 /* Pods_CoatySwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0A3E0AA54419785B21762EE7 /* Pods_CoatySwift.framework */; };
8688
/* End PBXBuildFile section */
8789

@@ -177,6 +179,8 @@
177179
9E774EBC249B7A4800EF888A /* ExampleControllerObserve.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleControllerObserve.swift; sourceTree = "<group>"; };
178180
9E774EBE249B7A5E00EF888A /* ExampleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleViewController.swift; sourceTree = "<group>"; };
179181
9E774EC0249B7BCC00EF888A /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
182+
9E774EC2249B9D6200EF888A /* ObjectMatcherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjectMatcherTests.swift; sourceTree = "<group>"; };
183+
9E774EC8249B9E7E00EF888A /* ObjectMatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjectMatcher.swift; sourceTree = "<group>"; };
180184
A1DA605E3E3430C821B9B615 /* Pods-CoatySwiftTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CoatySwiftTests.release.xcconfig"; path = "Target Support Files/Pods-CoatySwiftTests/Pods-CoatySwiftTests.release.xcconfig"; sourceTree = "<group>"; };
181185
A50E9FCB24FDCD197C3AE970 /* Pods-CoatySwiftExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CoatySwiftExample.release.xcconfig"; path = "Target Support Files/Pods-CoatySwiftExample/Pods-CoatySwiftExample.release.xcconfig"; sourceTree = "<group>"; };
182186
B9633893E0B3A21A53823626 /* Pods-CoatySwiftExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CoatySwiftExample.debug.xcconfig"; path = "Target Support Files/Pods-CoatySwiftExample/Pods-CoatySwiftExample.debug.xcconfig"; sourceTree = "<group>"; };
@@ -290,6 +294,7 @@
290294
isa = PBXGroup;
291295
children = (
292296
9E774E28249B75B500EF888A /* CoatySwiftTests.swift */,
297+
9E774EC2249B9D6200EF888A /* ObjectMatcherTests.swift */,
293298
9E774E2A249B75B500EF888A /* Info.plist */,
294299
);
295300
path = CoatySwiftTests;
@@ -436,6 +441,7 @@
436441
9E774E6C249B772B00EF888A /* ObjectFilter.swift */,
437442
9E774E6D249B772B00EF888A /* ObjectJoinCondition.swift */,
438443
9E774E6E249B772B00EF888A /* Core Types */,
444+
9E774EC8249B9E7E00EF888A /* ObjectMatcher.swift */,
439445
);
440446
path = Model;
441447
sourceTree = "<group>";
@@ -738,6 +744,7 @@
738744
9E774E91249B772B00EF888A /* BonjourResolverDelegate.swift in Sources */,
739745
9E774E8F249B772B00EF888A /* RetrieveEvent.swift in Sources */,
740746
9E774E95249B772B00EF888A /* BonjourConfiguration.swift in Sources */,
747+
9E774EC9249B9E7E00EF888A /* ObjectMatcher.swift in Sources */,
741748
9E774EAD249B772B00EF888A /* CoatyObject.swift in Sources */,
742749
9E774EA2249B772B00EF888A /* AnyEncodable.swift in Sources */,
743750
9E774EB7249B772B00EF888A /* IoPoint.swift in Sources */,
@@ -789,6 +796,7 @@
789796
buildActionMask = 2147483647;
790797
files = (
791798
9E774E29249B75B500EF888A /* CoatySwiftTests.swift in Sources */,
799+
9E774EC3249B9D6200EF888A /* ObjectMatcherTests.swift in Sources */,
792800
);
793801
runOnlyForDeploymentPostprocessing = 0;
794802
};

CoatySwift/Classes/Common/AnyCodable/AnyCodable.swift

Lines changed: 311 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,318 @@ extension AnyCodable: Equatable {
5959
return lhs == rhs
6060
case let (lhs as String, rhs as String):
6161
return lhs == rhs
62-
case (let lhs as [String: AnyCodable], let rhs as [String: AnyCodable]):
63-
return lhs == rhs
64-
case (let lhs as [AnyCodable], let rhs as [AnyCodable]):
65-
return lhs == rhs
6662
case (let lhs as CoatyUUID, let rhs as CoatyUUID):
6763
return lhs == rhs
64+
case let (lhs as CoatyObject, rhs as CoatyObject):
65+
return AnyCodable.deepEquals(lhs, rhs)
66+
case (let lhs as [String: AnyCodable], let rhs as [String: AnyCodable]):
67+
return AnyCodable.deepEquals(lhs, rhs)
68+
case (let lhs as [Any], let rhs as [Any]):
69+
return AnyCodable.deepEquals(lhs, rhs)
70+
default:
71+
return false
72+
}
73+
}
74+
}
75+
76+
extension AnyCodable {
77+
/// - Note: Internal for Internal use in framework only
78+
///
79+
/// Utitlity function to pack Any as AnyCodable while using the most specific type for the value.
80+
///
81+
/// - Parameters:
82+
/// - value: value to be packed as AnyCodable
83+
/// - Returns: AnyCodable of the value (with the most specific type); nil if the value should never be packed as AnyCodable
84+
internal static func _getAnyAsAnyCodable(_ value: Any) -> AnyCodable? {
85+
if let v = value as? Bool {
86+
return AnyCodable(booleanLiteral: v)
87+
} else if let v = value as? Int {
88+
return AnyCodable(integerLiteral: v)
89+
} else if let v = value as? Int8 {
90+
return AnyCodable(v)
91+
} else if let v = value as? Int16 {
92+
return AnyCodable(v)
93+
} else if let v = value as? Int32 {
94+
return AnyCodable(v)
95+
} else if let v = value as? Int64 {
96+
return AnyCodable(v)
97+
} else if let v = value as? UInt {
98+
return AnyCodable(v)
99+
} else if let v = value as? UInt8 {
100+
return AnyCodable(v)
101+
} else if let v = value as? UInt16 {
102+
return AnyCodable(v)
103+
} else if let v = value as? UInt32 {
104+
return AnyCodable(v)
105+
} else if let v = value as? UInt64 {
106+
return AnyCodable(v)
107+
} else if let v = value as? Float {
108+
return AnyCodable(v)
109+
} else if let v = value as? Double {
110+
return AnyCodable(v)
111+
} else if let v = value as? String {
112+
return AnyCodable(stringLiteral: v)
113+
} else if let v = value as? CoatyUUID {
114+
return AnyCodable(v)
115+
} else if let v = value as? CoatyObject {
116+
return AnyCodable(v)
117+
} else if let v = value as? [Any] {
118+
let result = v.map { any -> AnyCodable in
119+
if let nonNil = AnyCodable._getAnyAsAnyCodable(any) {
120+
return nonNil
121+
} else {
122+
return AnyCodable(nil)
123+
}
124+
}
125+
return AnyCodable(result)
126+
} else if let v = value as? [String: Any] {
127+
var result = [String: AnyCodable]()
128+
for key in v.keys {
129+
if let unwrappedValue = AnyCodable._getAnyAsAnyCodable(v[key]!) {
130+
result[key] = unwrappedValue
131+
} else {
132+
result[key] = nil
133+
}
134+
}
135+
return AnyCodable(result)
136+
} else if let v = value as? AnyCodable {
137+
return AnyCodable._getAnyAsAnyCodable(v.value)
138+
} else {
139+
return nil
140+
}
141+
}
142+
}
143+
144+
extension AnyCodable {
145+
/// - Note: Internal for internal use in framework only
146+
///
147+
/// Determines whether two Arrays [Any] are structurally equal
148+
/// (aka. deep equal) according to a recursive equality algorithm.
149+
///
150+
/// - Parameters:
151+
/// - lhs: an array of type [Any]
152+
/// - rhs: an array of type [Any]
153+
internal static func deepEquals(_ lhs: [Any], _ rhs: [Any]) -> Bool {
154+
// All elements have to be of type AnyCodable to perform element-wise comparisons
155+
let lhsAsCodables = lhs.map { AnyCodable._getAnyAsAnyCodable($0) }
156+
let rhsAsCodables = rhs.map { AnyCodable._getAnyAsAnyCodable($0) }
157+
158+
return lhsAsCodables == rhsAsCodables
159+
}
160+
161+
/// - Note: Internal for internal use in framework only
162+
///
163+
/// Determines whether two Dictionaries [String: AnyCodable] are structurally equal
164+
/// (aka. deep equal) according to a recursive equality algorithm.
165+
///
166+
/// - Parameters:
167+
/// - lhs: a dictionary of type [String: AnyCodable]
168+
/// - rhs: a dictionary of type [String: AnyCodable]
169+
/// - Returns: true if two dictionaries are structurally equal
170+
internal static func deepEquals(_ lhs: [String: AnyCodable], _ rhs: [String: AnyCodable]) -> Bool {
171+
if lhs.keys.count != rhs.keys.count {
172+
return false
173+
}
174+
175+
for prop in lhs.keys {
176+
if let lhsVal = lhs[prop],
177+
let rhsVal = rhs[prop],
178+
lhsVal != rhsVal {
179+
return false
180+
}
181+
}
182+
183+
return true
184+
}
185+
186+
/// - Note: Internal for internal use in framework only
187+
///
188+
/// Determines whether two CoatyObjects are structurally equal
189+
/// (aka. deep equal) according to a recursive equality algorithm.
190+
///
191+
/// - Parameters:
192+
/// - lhs: a Coaty object
193+
/// - rhs: a Coaty object
194+
/// - Returns: true if the two objects are structurally equal (property names and values must be the same)
195+
internal static func deepEquals(_ lhs: CoatyObject, _ rhs: CoatyObject) -> Bool {
196+
let lhsProperties = AnyCodable.getDictionaryOfProperties(from: Mirror(reflecting: lhs))
197+
let rhsProperties = AnyCodable.getDictionaryOfProperties(from: Mirror(reflecting: rhs))
198+
199+
if lhsProperties.keys.count != rhsProperties.keys.count {
200+
return false
201+
}
202+
203+
for prop in lhsProperties.keys {
204+
if let lhsVal = lhsProperties[prop],
205+
let rhsVal = rhsProperties[prop],
206+
lhsVal != rhsVal {
207+
return false
208+
}
209+
}
210+
211+
return true
212+
}
213+
}
214+
215+
extension AnyCodable {
216+
/// Checks if a JavaScript value (usually an object or array) contains
217+
/// other values. Primitive value types (number, string, boolean, null,undefined) contain
218+
/// only the identical value. Object properties match if all the key-value
219+
/// pairs of the specified object are contained in them. Array properties
220+
/// match if all the specified array elements are contained in them.
221+
///
222+
/// The general principle is that the contained object must match the containing object
223+
/// as to structure and data contents recursively on all levels, possibly after discarding
224+
/// some non-matching array elements or object key/value pairs from the containing object.
225+
/// But remember that the order of array elements is not significant when doing a containment match,
226+
/// and duplicate array elements are effectively considered only once.
227+
///
228+
/// As a special exception to the general principle that the structures must match, an
229+
/// array on *toplevel* may contain a primitive value:
230+
/// ```ts
231+
/// contains([1, 2, 3], [3]) => true
232+
/// contains([1, 2, 3], 3) => true
233+
/// ```
234+
///
235+
/// @param a a JavaScript value containing another value
236+
/// @param b a JavaScript value to be contained in another value
237+
internal static func deepContains(_ a: AnyCodable, _ b: AnyCodable) -> Bool {
238+
239+
let bAsAnyCodable = AnyCodable._getAnyAsAnyCodable(b)!
240+
let aAsAnyCodable = AnyCodable._getAnyAsAnyCodable(a)!
241+
return AnyCodable._deepContains(aAsAnyCodable, bAsAnyCodable, true)
242+
}
243+
244+
internal static func _deepContains(_ x: AnyCodable, _ y: AnyCodable, _ isTopLevel: Bool) -> Bool {
245+
if let xValues = x.value as? [AnyCodable] {
246+
if let yValues = y.value as? [AnyCodable] {
247+
return yValues.allSatisfy { yv -> Bool in
248+
return xValues.contains { xv -> Bool in
249+
AnyCodable._deepContains(xv, yv, false)
250+
}
251+
}
252+
} else {
253+
// Special exception: check containment of a primitive array element on toplevel
254+
if isTopLevel {
255+
return xValues.contains { xv -> Bool in
256+
xv == y
257+
}
258+
}
259+
return false
260+
}
261+
262+
}
263+
264+
if let xAsObject = x.value as? CoatyObject {
265+
if let yAsObject = y.value as? CoatyObject {
266+
let xProperties = AnyCodable.getDictionaryOfProperties(from: Mirror(reflecting: xAsObject))
267+
let yProperties = AnyCodable.getDictionaryOfProperties(from: Mirror(reflecting: yAsObject))
268+
269+
return xProperties.keys.allSatisfy { xk -> Bool in
270+
if yProperties.index(forKey: xk) == nil {
271+
return false
272+
}
273+
return AnyCodable._deepContains(xProperties[xk]!, yProperties[xk]!, false)
274+
}
275+
} else {
276+
return false
277+
}
278+
}
279+
280+
return x == y
281+
}
282+
}
283+
284+
extension AnyCodable {
285+
286+
/// - Note: Internal for internal use in framework only
287+
///
288+
/// Checks if a value is included on toplevel in the given
289+
/// operand array of values which may be primitive types (number, string, boolean, null)
290+
/// or object types compared using the == equality operator.
291+
///
292+
/// - Parameters:
293+
/// - lhs: an array containing another value on toplevel
294+
/// - rhs: a value to be contained on toplevel in an array
295+
/// - Returns: true if rhs is contained in lhs
296+
internal static func deepIncludes(_ lhs: AnyCodable, _ rhs: AnyCodable) -> Bool {
297+
// First argument must be an array of Any values
298+
guard let lhsAsArray = lhs.value as? [Any] else {
299+
return false
300+
}
301+
302+
let lhsAsCodables = lhsAsArray.compactMap { AnyCodable._getAnyAsAnyCodable($0) }
303+
304+
return lhsAsCodables.contains { element -> Bool in
305+
return element == rhs
306+
}
307+
}
308+
}
309+
310+
extension AnyCodable {
311+
/// - Note: Internal for interal use in framework only
312+
///
313+
/// Recursively get a dictionary of all properties as [String: AnyCodable] associated with a given mirror of an object
314+
///
315+
/// - Parameters:
316+
/// - mirror: mirror of the object that properties need to be extracted
317+
/// - Returns: a dictionary of all properties associated with the object (only those properties that can be represented as AnyCodable)
318+
internal static func getDictionaryOfProperties(from mirror: Mirror) -> [String: AnyCodable] {
319+
var result = [String: AnyCodable]()
320+
321+
for child in mirror.children {
322+
result[child.label!] = AnyCodable._getAnyAsAnyCodable(child.value)
323+
}
324+
325+
guard let superMirror = mirror.superclassMirror else {
326+
return result
327+
}
328+
329+
return result.merging(AnyCodable.getDictionaryOfProperties(from: superMirror)) { (_, new) in
330+
new
331+
}
332+
}
333+
}
334+
335+
extension AnyCodable: Comparable {
336+
public static func < (lhs: AnyCodable, rhs: AnyCodable) -> Bool {
337+
switch (lhs.value, rhs.value) {
338+
case is (Void, Void):
339+
return false
340+
case let (lhs as Bool, rhs as Bool):
341+
return (lhs == false) && (rhs == true)
342+
case let (lhs as Int, rhs as Int):
343+
return lhs < rhs
344+
case let (lhs as Int8, rhs as Int8):
345+
return lhs < rhs
346+
case let (lhs as Int16, rhs as Int16):
347+
return lhs < rhs
348+
case let (lhs as Int32, rhs as Int32):
349+
return lhs < rhs
350+
case let (lhs as Int64, rhs as Int64):
351+
return lhs < rhs
352+
case let (lhs as UInt, rhs as UInt):
353+
return lhs < rhs
354+
case let (lhs as UInt8, rhs as UInt8):
355+
return lhs < rhs
356+
case let (lhs as UInt16, rhs as UInt16):
357+
return lhs < rhs
358+
case let (lhs as UInt32, rhs as UInt32):
359+
return lhs < rhs
360+
case let (lhs as UInt64, rhs as UInt64):
361+
return lhs < rhs
362+
case let (lhs as Float, rhs as Float):
363+
return lhs < rhs
364+
case let (lhs as Double, rhs as Double):
365+
return lhs < rhs
366+
case let (lhs as String, rhs as String):
367+
switch lhs.localizedCompare(rhs) {
368+
case .orderedAscending:
369+
return true
370+
default:
371+
return false
372+
}
373+
// CoatyUUID (description)
68374
default:
69375
return false
70376
}
@@ -96,3 +402,4 @@ extension AnyCodable: CustomDebugStringConvertible {
96402
}
97403

98404
extension AnyCodable: ExpressibleByNilLiteral, ExpressibleByBooleanLiteral, ExpressibleByIntegerLiteral, ExpressibleByFloatLiteral, ExpressibleByStringLiteral, ExpressibleByArrayLiteral, ExpressibleByDictionaryLiteral {}
405+

0 commit comments

Comments
 (0)