Skip to content

Commit cf78903

Browse files
authored
Add JSONSafeEncoder for NaN/Infinity handling in JSON (#280)
* Add JSONSafeEncoder for NaN/Infinity handling in JSON * Make it all user-settable. * Add tests * Fix missing comma. * Specified value for zero test * Set configuration value to JSON's static prop. * Updated tests * Fixed example build.
1 parent 56b1c19 commit cf78903

File tree

12 files changed

+185
-44
lines changed

12 files changed

+185
-44
lines changed

Examples/apps/DestinationsExample/DestinationsExample/ViewController.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ class ViewController: UIViewController {
8484
case .alias:
8585
aliasEvent()
8686
case .none:
87-
analytics?.log(message: "Failed to establish event type", kind: .error)
87+
analytics?.log(message: "Failed to establish event type")
8888
}
8989

9090
clearAll()

Package.resolved

Lines changed: 20 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,17 @@ let package = Package(
2020
dependencies: [
2121
// Dependencies declare other packages that this package depends on.
2222
// .package(url: /* package url */, from: "1.0.0"),
23-
.package(url: "https://github.com/segmentio/sovran-swift.git", from: "1.1.0")
23+
.package(url: "https://github.com/segmentio/sovran-swift.git", from: "1.1.0"),
24+
.package(url: "https://github.com/segmentio/jsonsafeencoder-swift.git", from: "1.0.0")
2425
],
2526
targets: [
2627
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
2728
// Targets can depend on other targets in this package, and on products in packages this package depends on.
2829
.target(
2930
name: "Segment",
3031
dependencies: [
31-
.product(name: "Sovran", package: "sovran-swift")
32+
.product(name: "Sovran", package: "sovran-swift"),
33+
.product(name: "JSONSafeEncoder", package: "jsonsafeencoder-swift")
3234
],
3335
exclude: ["PrivacyInfo.xcprivacy"]),
3436
.testTarget(

[email protected]

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,17 @@ let package = Package(
2121
dependencies: [
2222
// Dependencies declare other packages that this package depends on.
2323
// .package(url: /* package url */, from: "1.0.0"),
24-
.package(url: "https://github.com/segmentio/sovran-swift.git", from: "1.1.0")
24+
.package(url: "https://github.com/segmentio/sovran-swift.git", from: "1.1.0"),
25+
.package(url: "https://github.com/segmentio/jsonsafeencoder-swift.git", from: "1.0.0")
2526
],
2627
targets: [
2728
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
2829
// Targets can depend on other targets in this package, and on products in packages this package depends on.
2930
.target(
3031
name: "Segment",
3132
dependencies: [
32-
.product(name: "Sovran", package: "sovran-swift")
33+
.product(name: "Sovran", package: "sovran-swift"),
34+
.product(name: "JSONSafeEncoder", package: "jsonsafeencoder-swift")
3335
],
3436
exclude: ["PrivacyInfo.xcprivacy"]),
3537
.testTarget(

Sources/Segment/Configuration.swift

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
//
77

88
import Foundation
9+
import JSONSafeEncoder
910
#if os(Linux)
1011
import FoundationNetworking
1112
#endif
@@ -37,10 +38,10 @@ public class Configuration {
3738
var requestFactory: ((URLRequest) -> URLRequest)? = nil
3839
var errorHandler: ((Error) -> Void)? = nil
3940
var flushPolicies: [FlushPolicy] = [CountBasedFlushPolicy(), IntervalBasedFlushPolicy()]
40-
4141
var operatingMode: OperatingMode = .asynchronous
4242
var flushQueue: DispatchQueue = OperatingMode.defaultQueue
4343
var userAgent: String? = nil
44+
var jsonNonConformingNumberStrategy: JSONSafeEncoder.NonConformingFloatEncodingStrategy = .zero
4445
}
4546

4647
internal var values: Values
@@ -50,6 +51,7 @@ public class Configuration {
5051
/// - Parameter writeKey: Your Segment write key value
5152
public init(writeKey: String) {
5253
self.values = Values(writeKey: writeKey)
54+
JSON.jsonNonConformingNumberStrategy = self.values.jsonNonConformingNumberStrategy
5355
// enable segment destination by default
5456
var settings = Settings(writeKey: writeKey)
5557
settings.integrations = try? JSON([
@@ -216,11 +218,21 @@ public extension Configuration {
216218
return self
217219
}
218220

221+
/// Specify a custom UserAgent string. This bypasses the OS dependent check entirely.
219222
@discardableResult
220223
func userAgent(_ userAgent: String) -> Configuration {
221224
values.userAgent = userAgent
222225
return self
223226
}
227+
228+
/// This option specifies how NaN/Infinity are handled when encoding JSON.
229+
/// The default is .zero. See JSONSafeEncoder.NonConformingFloatEncodingStrategy for more informatino.
230+
@discardableResult
231+
func jsonNonConformingNumberStrategy(_ strategy: JSONSafeEncoder.NonConformingFloatEncodingStrategy) -> Configuration {
232+
values.jsonNonConformingNumberStrategy = strategy
233+
JSON.jsonNonConformingNumberStrategy = values.jsonNonConformingNumberStrategy
234+
return self
235+
}
224236
}
225237

226238
extension Analytics {

Sources/Segment/ObjC/ObjCAnalytics.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#if !os(Linux)
99

1010
import Foundation
11+
import JSONSafeEncoder
1112

1213
// MARK: - ObjC Compatibility
1314

@@ -164,7 +165,7 @@ extension ObjCAnalytics {
164165
var result: [String: Any]? = nil
165166
if let system: System = analytics.store.currentState() {
166167
do {
167-
let encoder = JSONEncoder.default
168+
let encoder = JSONSafeEncoder.default
168169
let json = try encoder.encode(system.settings)
169170
if let r = try JSONSerialization.jsonObject(with: json) as? [String: Any] {
170171
result = r

Sources/Segment/ObjC/ObjCConfiguration.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#if !os(Linux)
99

1010
import Foundation
11+
import JSONSafeEncoder
1112

1213
@objc(SEGConfiguration)
1314
public class ObjCConfiguration: NSObject {
@@ -75,7 +76,7 @@ public class ObjCConfiguration: NSObject {
7576
get {
7677
var result = [String: Any]()
7778
do {
78-
let encoder = JSONEncoder.default
79+
let encoder = JSONSafeEncoder.default
7980
let json = try encoder.encode(configuration.values.defaultSettings)
8081
if let r = try JSONSerialization.jsonObject(with: json) as? [String: Any] {
8182
result = r

Sources/Segment/Utilities/JSON.swift

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,24 @@
66
//
77

88
import Foundation
9+
import JSONSafeEncoder
910

11+
extension JSONDecoder {
12+
static var `default`: JSONDecoder {
13+
let d = JSONDecoder()
14+
d.dateDecodingStrategy = .formatted(DateFormatter.iso8601)
15+
return d
16+
}
17+
}
18+
19+
extension JSONSafeEncoder {
20+
static var `default`: JSONSafeEncoder {
21+
let e = JSONSafeEncoder()
22+
e.dateEncodingStrategy = .formatted(DateFormatter.iso8601)
23+
e.nonConformingFloatEncodingStrategy = JSON.jsonNonConformingNumberStrategy
24+
return e
25+
}
26+
}
1027

1128
// MARK: - JSON Definition
1229

@@ -18,6 +35,8 @@ public enum JSON: Equatable {
1835
case array([JSON])
1936
case object([String: JSON])
2037

38+
static var jsonNonConformingNumberStrategy: JSONSafeEncoder.NonConformingFloatEncodingStrategy = .zero
39+
2140
internal enum JSONError: Error {
2241
case unknown
2342
case nonJSONType(type: String)
@@ -35,7 +54,7 @@ public enum JSON: Equatable {
3554

3655
// For Value types
3756
public init<T: Codable>(with value: T) throws {
38-
let encoder = JSONEncoder.default
57+
let encoder = JSONSafeEncoder.default
3958
let json = try encoder.encode(value)
4059
let output = try JSONSerialization.jsonObject(with: json)
4160
try self.init(output)
@@ -136,7 +155,7 @@ extension Encodable {
136155
public func toString(pretty: Bool) -> String {
137156
var returnString = ""
138157
do {
139-
let encoder = JSONEncoder.default
158+
let encoder = JSONSafeEncoder.default
140159
if pretty {
141160
encoder.outputFormatting = .prettyPrinted
142161
}

Sources/Segment/Utilities/Utils.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ extension Optional: Flattenable {
6969
}
7070
}
7171

72+
/* for dev testing only
73+
#if DEBUG
7274
class TrackingDispatchGroup: CustomStringConvertible {
7375
internal let group = DispatchGroup()
7476

@@ -102,3 +104,5 @@ class TrackingDispatchGroup: CustomStringConvertible {
102104
group.notify(qos: qos, flags: flags, queue: queue, execute: work)
103105
}
104106
}
107+
#endif
108+
*/

Sources/Segment/Utilities/iso8601.swift

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
//
77

88
import Foundation
9+
import JSONSafeEncoder
910

1011
enum SegmentISO8601DateFormatter {
1112
static let shared: ISO8601DateFormatter = {
@@ -16,7 +17,6 @@ enum SegmentISO8601DateFormatter {
1617
}
1718

1819
internal extension Date {
19-
// TODO: support nanoseconds
2020
func iso8601() -> String {
2121
return SegmentISO8601DateFormatter.shared.string(from: self)
2222
}
@@ -38,19 +38,3 @@ extension DateFormatter {
3838
return formatter
3939
}()
4040
}
41-
42-
extension JSONDecoder {
43-
static var `default`: JSONDecoder {
44-
let d = JSONDecoder()
45-
d.dateDecodingStrategy = .formatted(DateFormatter.iso8601)
46-
return d
47-
}
48-
}
49-
50-
extension JSONEncoder {
51-
static var `default`: JSONEncoder {
52-
let e = JSONEncoder()
53-
e.dateEncodingStrategy = .formatted(DateFormatter.iso8601)
54-
return e
55-
}
56-
}

Tests/Segment-Tests/Analytics_Tests.swift

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -823,4 +823,39 @@ final class Analytics_Tests: XCTestCase {
823823
XCTAssertEqual(ziggysFound!.count, 3)
824824
XCTAssertEqual(goobersFound!.count, 2)
825825
}
826+
827+
func testJSONNaNDefaultHandlingZero() throws {
828+
// notice we didn't set the nan handling option. zero is the default.
829+
let analytics = Analytics(configuration: Configuration(writeKey: "test"))
830+
let outputReader = OutputReaderPlugin()
831+
analytics.add(plugin: outputReader)
832+
833+
waitUntilStarted(analytics: analytics)
834+
835+
analytics.track(name: "test track", properties: ["TestNaN": Double.nan])
836+
837+
let trackEvent: TrackEvent? = outputReader.lastEvent as? TrackEvent
838+
XCTAssertTrue(trackEvent?.event == "test track")
839+
XCTAssertTrue(trackEvent?.type == "track")
840+
let d: Double? = trackEvent?.properties?.value(forKeyPath: "TestNaN")
841+
XCTAssertTrue(d! == 0)
842+
}
843+
844+
func testJSONNaNHandlingNull() throws {
845+
let analytics = Analytics(configuration: Configuration(writeKey: "test")
846+
.jsonNonConformingNumberStrategy(.null)
847+
)
848+
let outputReader = OutputReaderPlugin()
849+
analytics.add(plugin: outputReader)
850+
851+
waitUntilStarted(analytics: analytics)
852+
853+
analytics.track(name: "test track", properties: ["TestNaN": Double.nan])
854+
855+
let trackEvent: TrackEvent? = outputReader.lastEvent as? TrackEvent
856+
XCTAssertTrue(trackEvent?.event == "test track")
857+
XCTAssertTrue(trackEvent?.type == "track")
858+
let d: Double? = trackEvent?.properties?.value(forKeyPath: "TestNaN")
859+
XCTAssertNil(d)
860+
}
826861
}

0 commit comments

Comments
 (0)