Repository: segmentio/analytics-swift (swift)

Issue:
Importing Segment in an Observable Model results in KeyPath conflict
**Describe the bug**
Importing the Segment SDK in a SwiftUI Observable model causes a compiler crash due to a naming conflict with Swift’s built-in KeyPath type. Segment defines a public struct KeyPath in its codebase, which is picked up during Swift macro expansion, leading to a fatal error:

Cannot specialize non-generic type 'KeyPath'

This breaks any use of Swift’s new Observable macro in the same file.

**To Reproduce**
Steps to reproduce the behavior:
1. Create a SwiftUI Project with an Observable Model
2. Import Segment 
3. Compile the project

**Expected behavior**
Project should compile successfully without failing due to internal name conflicts.

Pull request:
Fixes #393

Patch:
diff --git a/Sources/Segment/Timeline.swift b/Sources/Segment/Timeline.swift
index 4f9901dd..2c0b4f6b 100644
--- a/Sources/Segment/Timeline.swift
+++ b/Sources/Segment/Timeline.swift
@@ -268,7 +268,7 @@ extension DestinationPlugin {
     
     internal func isDestinationEnabled(event: RawEvent) -> Bool {
         var customerDisabled = false
-        if let disabled: Bool = event.integrations?.value(forKeyPath: KeyPath(self.key)), disabled == false {
+        if let disabled: Bool = event.integrations?.value(forKeyPath: JSONKeyPath(self.key)), disabled == false {
             customerDisabled = true
         }
         
diff --git a/Sources/Segment/Utilities/JSON.swift b/Sources/Segment/Utilities/JSON.swift
index 4d118cbe..b436db43 100644
--- a/Sources/Segment/Utilities/JSON.swift
+++ b/Sources/Segment/Utilities/JSON.swift
@@ -438,7 +438,7 @@ extension JSON {
     }
 
     /// Directly access or set a value within the JSON object using a key path.
-    public subscript<T: Codable>(keyPath keyPath: KeyPath) -> T? {
+    public subscript<T: Codable>(keyPath keyPath: JSONKeyPath) -> T? {
         get {
             var result: T? = nil
             switch self {
@@ -494,14 +494,14 @@ extension JSON {
     ///   - forKeyPath: The keypath within the object to retrieve.  eg: `context.device.ip`
     ///
     /// - Returns: The value as typed, or nil.
-    public func value<T: Codable>(forKeyPath keyPath: KeyPath) -> T? {
+    public func value<T: Codable>(forKeyPath keyPath: JSONKeyPath) -> T? {
         return self[keyPath: keyPath]
     }
     
     /// Directly access a value within the JSON object using a key path.
     /// - Parameters:
     ///   - forKeyPath: The keypath within the object to set.  eg: `context.device.ip`
-    public mutating func setValue<T: Codable>(_ value: T?, forKeyPath keyPath: KeyPath) {
+    public mutating func setValue<T: Codable>(_ value: T?, forKeyPath keyPath: JSONKeyPath) {
         self[keyPath: keyPath] = value
     }
 
diff --git a/Sources/Segment/Utilities/KeyPath.swift b/Sources/Segment/Utilities/JSONKeyPath.swift
similarity index 76%
rename from Sources/Segment/Utilities/KeyPath.swift
rename to Sources/Segment/Utilities/JSONKeyPath.swift
index deecf0ef..e0535a53 100644
--- a/Sources/Segment/Utilities/KeyPath.swift
+++ b/Sources/Segment/Utilities/JSONKeyPath.swift
@@ -8,19 +8,19 @@
 import Foundation
 
 protocol KeyPathHandler {
-    func isHandled(_ keyPath: KeyPath, forInput: Any?) -> Bool
-    func value(keyPath: KeyPath, input: Any?, reference: Any?) -> Any?
+    func isHandled(_ keyPath: JSONKeyPath, forInput: Any?) -> Bool
+    func value(keyPath: JSONKeyPath, input: Any?, reference: Any?) -> Any?
 }
 
 public struct BasicHandler: KeyPathHandler {
-    func value(keyPath: KeyPath, input: Any?, reference: Any?) -> Any? {
+    func value(keyPath: JSONKeyPath, input: Any?, reference: Any?) -> Any? {
         guard let input = input as? [String: Any] else { return nil }
         var result: Any? = nil
         if keyPath.remaining.isEmpty {
             result = input[keyPath.current]
         } else {
             if let nestedDict = input[keyPath.current] as? [String: Any] {
-                result = nestedDict[keyPath: KeyPath(keyPath.remainingPath)]
+                result = nestedDict[keyPath: JSONKeyPath(keyPath.remainingPath)]
             } else {
                 result = nil
             }
@@ -28,12 +28,12 @@ public struct BasicHandler: KeyPathHandler {
         return result
     }
     
-    func isHandled(_ keyPath: KeyPath, forInput: Any?) -> Bool {
+    func isHandled(_ keyPath: JSONKeyPath, forInput: Any?) -> Bool {
         return true
     }
 }
 
-public struct KeyPath {
+public struct JSONKeyPath {
     var current: String
     var remaining: [String]
     
@@ -47,7 +47,7 @@ public struct KeyPath {
     
     internal static var handlers: [KeyPathHandler] = [PathHandler(), IfHandler(), BasicHandler()]
     static func register(_ handler: KeyPathHandler) { handlers.insert(handler, at: 0) }
-    static func handlerFor(keyPath: KeyPath, input: Any?) -> KeyPathHandler? {
+    static func handlerFor(keyPath: JSONKeyPath, input: Any?) -> KeyPathHandler? {
         guard let input = input as? [String: Any] else { return nil }
         for item in handlers {
             if item.isHandled(keyPath, forInput: input[keyPath.current]) {
@@ -59,7 +59,7 @@ public struct KeyPath {
 }
 
 
-extension KeyPath: ExpressibleByStringLiteral {
+extension JSONKeyPath: ExpressibleByStringLiteral {
     public init(stringLiteral value: String) {
         self.init(value)
     }
@@ -72,13 +72,13 @@ extension KeyPath: ExpressibleByStringLiteral {
 }
 
 extension Dictionary where Key: StringProtocol, Value: Any {
-    internal func value(keyPath: KeyPath, reference: Any?) -> Any? {
-        let handler = KeyPath.handlerFor(keyPath: keyPath, input: self)
+    internal func value(keyPath: JSONKeyPath, reference: Any?) -> Any? {
+        let handler = JSONKeyPath.handlerFor(keyPath: keyPath, input: self)
         let result = handler?.value(keyPath: keyPath, input: self, reference: reference)
         return result
     }
     
-    internal mutating func setValue(_ value: Any?, keyPath: KeyPath) {
+    internal mutating func setValue(_ value: Any?, keyPath: JSONKeyPath) {
         guard let key = keyPath.current as? Key else { return }
         
         if keyPath.remaining.isEmpty {
@@ -89,28 +89,28 @@ extension Dictionary where Key: StringProtocol, Value: Any {
             }
         } else {
             if var nestedDict = self[key] as? [String: Any] {
-                nestedDict[keyPath: KeyPath(keyPath.remainingPath)] = value
+                nestedDict[keyPath: JSONKeyPath(keyPath.remainingPath)] = value
                 self[key] = (nestedDict as! Value)
             } else {
                 // this nested key doesn't exist but we're not at the end of the chain, need to create it
                 var nestedDict = [String: Any]()
-                nestedDict[keyPath: KeyPath(keyPath.remainingPath)] = value
+                nestedDict[keyPath: JSONKeyPath(keyPath.remainingPath)] = value
                 self[key] = (nestedDict as! Value)
             }
         }
     }
     
-    public subscript(keyPath keyPath: KeyPath) -> Any? {
+    public subscript(keyPath keyPath: JSONKeyPath) -> Any? {
         get { return value(keyPath: keyPath, reference: nil) }
         set { setValue(newValue as Any, keyPath: keyPath) }
     }
 
-    public subscript(keyPath keyPath: KeyPath, reference reference: Any?) -> Any? {
+    public subscript(keyPath keyPath: JSONKeyPath, reference reference: Any?) -> Any? {
         get { return value(keyPath: keyPath, reference: reference) }
         set { setValue(newValue as Any, keyPath: keyPath) }
     }
     
-    public func exists(keyPath: KeyPath, reference: Any? = nil) -> Bool {
+    public func exists(keyPath: JSONKeyPath, reference: Any? = nil) -> Bool {
         return (value(keyPath: keyPath, reference: reference) != nil)
     }
 }
@@ -125,13 +125,13 @@ extension String {
 
 
 struct IfHandler: KeyPathHandler {
-    func isHandled(_ keyPath: KeyPath, forInput: Any?) -> Bool {
+    func isHandled(_ keyPath: JSONKeyPath, forInput: Any?) -> Bool {
         guard let input = forInput as? [String: Any] else { return false }
         if input["@if"] != nil { return true }
         return false
     }
     
-    func value(keyPath: KeyPath, input: Any?, reference: Any?) -> Any? {
+    func value(keyPath: JSONKeyPath, input: Any?, reference: Any?) -> Any? {
         guard let input = input as? [String: Any] else { return nil }
         let current = input[keyPath.current] as? [String: Any]
         let conditional = current?["@if"] as? [String: Any]
@@ -166,7 +166,7 @@ struct IfHandler: KeyPathHandler {
 }
 
 struct PathHandler: KeyPathHandler {
-    func isHandled(_ keyPath: KeyPath, forInput: Any?) -> Bool {
+    func isHandled(_ keyPath: JSONKeyPath, forInput: Any?) -> Bool {
         guard let input = forInput as? [String: Any] else { return false }
         if input["@path"] != nil {
             return true
@@ -174,14 +174,14 @@ struct PathHandler: KeyPathHandler {
         return false
     }
     
-    func value(keyPath: KeyPath, input: Any?, reference: Any?) -> Any? {
+    func value(keyPath: JSONKeyPath, input: Any?, reference: Any?) -> Any? {
         guard let input = input as? [String: Any] else { return nil }
         let current = input[keyPath.current] as? [String: Any]
         let path = (current?["@path"] as? String)?.strippedReference
         
         var result: Any? = nil
         if let path = path, let reference = reference as? [String: Any] {
-            result = reference[keyPath: KeyPath(path)]
+            result = reference[keyPath: JSONKeyPath(path)]
         }
         return result
     }


Test patch:
diff --git a/Tests/Segment-Tests/KeyPath_Tests.swift b/Tests/Segment-Tests/KeyPath_Tests.swift
index fa08df12..e2d0169a 100644
--- a/Tests/Segment-Tests/KeyPath_Tests.swift
+++ b/Tests/Segment-Tests/KeyPath_Tests.swift
@@ -133,7 +133,7 @@ class KeyPath_Tests: XCTestCase {
             "anonymousId": "123456"
         ]
         for key in keys {
-            dict[key] = mapping[keyPath: KeyPath(key), reference: event1]
+            dict[key] = mapping[keyPath: JSONKeyPath(key), reference: event1]
         }
         XCTAssertTrue(dict["device_id"] as? String == "ABCDEF")
         
@@ -148,7 +148,7 @@ class KeyPath_Tests: XCTestCase {
         ]
         dict = [String: Any]()
         for key in keys {
-            dict[key] = mapping[keyPath: KeyPath(key), reference: event2]
+            dict[key] = mapping[keyPath: JSONKeyPath(key), reference: event2]
         }
         XCTAssertTrue(dict["device_id"] as? String == "123456")
     }
@@ -167,7 +167,7 @@ class KeyPath_Tests: XCTestCase {
             "anonymousId": "123456"
         ]
         for key in keys {
-            dict[key] = mapping[keyPath: KeyPath(key), reference: event1]
+            dict[key] = mapping[keyPath: JSONKeyPath(key), reference: event1]
         }
         XCTAssertTrue(dict["blank_no"] as? String == "ABCDEF")
         
@@ -177,7 +177,7 @@ class KeyPath_Tests: XCTestCase {
         ]
         dict = [String: Any]()
         for key in keys {
-            dict[key] = mapping[keyPath: KeyPath(key), reference: event2]
+            dict[key] = mapping[keyPath: JSONKeyPath(key), reference: event2]
         }
         XCTAssertTrue(dict["blank_yes"] as? String == "yep")
     }
@@ -195,7 +195,7 @@ class KeyPath_Tests: XCTestCase {
             ]
         ]
         for key in keys {
-            dict[key] = mapping[keyPath: KeyPath(key), reference: event1]
+            dict[key] = mapping[keyPath: JSONKeyPath(key), reference: event1]
         }
         let traits = dict["user_properties"] as? [String: Any]
         XCTAssertTrue(traits?["hoot"] as? String == "nanny")