Skip to content

Commit

Permalink
fix: do not modify custom attributes casing (customerio#234)
Browse files Browse the repository at this point in the history
  • Loading branch information
levibostian committed Dec 9, 2022
1 parent 455a6a1 commit 8160fdf
Show file tree
Hide file tree
Showing 50 changed files with 530 additions and 158 deletions.
1 change: 1 addition & 0 deletions .swiftlint.yml
Expand Up @@ -4,6 +4,7 @@
disabled_rules:
- line_length # The swiftformat tool formats the swift code for us to not have super long lines. This rule after swiftformat has become more of an annoyance then a value where most errors are error message strings being too long.
- unused_optional_binding # `let _ =` can be easier to read sometimes then `!= nil` which is what this rule is trying to catch.
- nesting # It can be easier to read code when a class has many nested classes inside of it (example: JSON Codable structs for deserializing JSON).

excluded: # paths or files to ignore during linting. Takes precedence over `included`.
- .build
Expand Down
5 changes: 4 additions & 1 deletion Package.swift
Expand Up @@ -45,7 +45,10 @@ let package = Package(
// shared code dependency that other test targets use.
.target(name: "SharedTests",
dependencies: ["CioTracking"],
path: "Tests/Shared"),
path: "Tests/Shared",
resources: [
.copy("SampleDataFiles") // static files that are used in test funnctions.
]),

// Messaging Push
.target(name: "CioMessagingPush",
Expand Down
2 changes: 1 addition & 1 deletion Sources/Common/Background Queue/ApiSyncQueueRunner.swift
Expand Up @@ -18,7 +18,7 @@ open class ApiSyncQueueRunner {

// (1) less code for `runTask` function to decode JSON and (2) one place to do error logging if decoding wrong.
public func getTaskData<T: Decodable>(_ task: QueueTask, type: T.Type) -> T? {
let taskData: T? = jsonAdapter.fromJson(task.data, decoder: nil)
let taskData: T? = jsonAdapter.fromJson(task.data)

if taskData == nil {
// log as error because it's a developer error since SDK is who encoded the TaskData in the first place
Expand Down
2 changes: 1 addition & 1 deletion Sources/Common/Background Queue/Queue.swift
Expand Up @@ -144,7 +144,7 @@ public class CioQueue: Queue {
) -> ModifyQueueResult {
logger.info("adding queue task \(type)")

guard let data = jsonAdapter.toJson(data, encoder: nil) else {
guard let data = jsonAdapter.toJson(data) else {
logger.error("fail adding queue task, json encoding fail.")

return (
Expand Down
8 changes: 4 additions & 4 deletions Sources/Common/Background Queue/QueueStorage.swift
Expand Up @@ -70,7 +70,7 @@ public class FileManagerQueueStorage: QueueStorage {

guard let data = fileStorage.get(type: .queueInventory, fileId: nil) else { return [] }

let inventory: [QueueTaskMetadata] = jsonAdapter.fromJson(data, decoder: nil) ?? []
let inventory: [QueueTaskMetadata] = jsonAdapter.fromJson(data) ?? []

return inventory
}
Expand All @@ -79,7 +79,7 @@ public class FileManagerQueueStorage: QueueStorage {
lock.lock()
defer { lock.unlock() }

guard let data = jsonAdapter.toJson(inventory, encoder: nil) else {
guard let data = jsonAdapter.toJson(inventory) else {
return false
}

Expand Down Expand Up @@ -161,7 +161,7 @@ public class FileManagerQueueStorage: QueueStorage {
defer { lock.unlock() }

guard let data = fileStorage.get(type: .queueTask, fileId: storageId),
let task: QueueTask = jsonAdapter.fromJson(data, decoder: nil)
let task: QueueTask = jsonAdapter.fromJson(data)
else {
return nil
}
Expand Down Expand Up @@ -232,7 +232,7 @@ public class FileManagerQueueStorage: QueueStorage {

public extension FileManagerQueueStorage {
private func update(queueTask: QueueTask) -> Bool {
guard let data = jsonAdapter.toJson(queueTask, encoder: nil) else {
guard let data = jsonAdapter.toJson(queueTask) else {
return false
}

Expand Down
7 changes: 7 additions & 0 deletions Sources/Common/Background Queue/Type/QueueTask.swift
Expand Up @@ -47,6 +47,13 @@ public struct QueueTask: Codable, AutoLenses, Equatable {
public let data: Data
/// the current run results of the task. keeping track of the history of the task
public let runResults: QueueTaskRunResults

enum CodingKeys: String, CodingKey {
case storageId = "storage_id"
case type
case data
case runResults = "run_results"
}
}

internal extension QueueTask {
Expand Down
8 changes: 8 additions & 0 deletions Sources/Common/Background Queue/Type/QueueTaskMetadata.swift
Expand Up @@ -14,6 +14,14 @@ public struct QueueTaskMetadata: Codable, Equatable, Hashable, AutoLenses {
let groupMember: [String]?
/// Populated when the task is added to the queue.
let createdAt: Date

enum CodingKeys: String, CodingKey {
case taskPersistedId = "task_persisted_id"
case taskType = "task_type"
case groupStart = "group_start"
case groupMember = "group_member"
case createdAt = "created_at"
}
}

internal extension QueueTaskMetadata {
Expand Down
Expand Up @@ -2,4 +2,8 @@ import Foundation

public struct QueueTaskRunResults: Codable, AutoLenses, Equatable {
let totalRuns: Int

enum CodingKeys: String, CodingKey {
case totalRuns = "total_runs"
}
}
2 changes: 0 additions & 2 deletions Sources/Common/Service/HttpClient.swift
Expand Up @@ -111,13 +111,11 @@ public class CIOHttpClient: HttpClient {
// we are bound to fail more often and don't want to log errors that are not super helpful to us.
if let errorMessageBody: ErrorMessageResponse = jsonAdapter.fromJson(
data,
decoder: nil,
logErrors: false
) {
errorBodyString = errorMessageBody.meta.error
} else if let errorMessageBody: ErrorsMessageResponse = jsonAdapter.fromJson(
data,
decoder: nil,
logErrors: false
) {
errorBodyString = errorMessageBody.meta.errors.joined(separator: ",")
Expand Down
5 changes: 5 additions & 0 deletions Sources/Common/Service/Request/InAppMetric.swift
Expand Up @@ -4,4 +4,9 @@ import Foundation
public enum InAppMetric: String, Codable {
case opened
case clicked

enum CodingKeys: String, CodingKey {
case opened
case clicked
}
}
11 changes: 11 additions & 0 deletions Sources/Common/Service/Request/TrackDeliveryEventRequestBody.swift
Expand Up @@ -3,12 +3,23 @@ import Foundation
internal struct TrackDeliveryEventRequestBody: Codable {
internal let type: DeliveryType
internal let payload: DeliveryPayload

enum CodingKeys: String, CodingKey {
case type
case payload
}
}

internal struct DeliveryPayload: Codable {
internal let deliveryId: String
internal let event: InAppMetric
internal let timestamp: Date

enum CodingKeys: String, CodingKey {
case deliveryId = "delivery_id"
case event
case timestamp
}
}

internal enum DeliveryType: String, Codable {
Expand Down
16 changes: 16 additions & 0 deletions Sources/Common/Service/Response/ErrorMessageResponse.swift
Expand Up @@ -11,6 +11,14 @@ public class ErrorMessageResponse: Codable {

public class Meta: Codable {
let error: String

enum CodingKeys: String, CodingKey {
case error
}
}

enum CodingKeys: String, CodingKey {
case meta
}
}

Expand All @@ -25,5 +33,13 @@ public class ErrorsMessageResponse: Codable {

public class Meta: Codable {
let errors: [String]

enum CodingKeys: String, CodingKey {
case errors
}
}

enum CodingKeys: String, CodingKey {
case meta
}
}
7 changes: 7 additions & 0 deletions Sources/Common/Store/GlobalDataStore.swift
Expand Up @@ -7,6 +7,9 @@ public protocol GlobalDataStore: AutoMockable {
// HTTP requests can be paused to avoid spamming the API too hard.
// This Date is when a pause is able to be lifted.
var httpRequestsPauseEnds: Date? { get set }

// Used for testing
func deleteAll()
}

// sourcery: InjectRegister = "GlobalDataStore"
Expand Down Expand Up @@ -36,4 +39,8 @@ public class CioGlobalDataStore: GlobalDataStore {

self.keyValueStorage.switchToGlobalDataStore()
}

public func deleteAll() {
keyValueStorage.deleteAll()
}
}
48 changes: 15 additions & 33 deletions Sources/Common/Util/JsonAdapter.swift
Expand Up @@ -30,15 +30,18 @@ import Foundation
public class JsonAdapter {
var decoder: JSONDecoder {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .secondsSince1970
return decoder
}

var encoder: JSONEncoder {
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
encoder.outputFormatting = .sortedKeys
// Do not modify the casing of JSON keys. It modifies custom attributes that customers give us.
// Instead, add `CodingKeys` to your `Codable/Encodable` struct that JsonAdapter serializes to json.
// encoder.keyEncodingStrategy = .convertToSnakeCase
encoder
.outputFormatting =
.sortedKeys // for automated tests to compare JSON strings, makes keys never in a random order
// We are using custom date encoding because if there are milliseconds in Date object,
// the default `secondsSince1970` will give a unix time with a decimal. The
// Customer.io API does not accept timestamps with a decimal value unix time.
Expand All @@ -47,7 +50,6 @@ public class JsonAdapter {
let seconds = Int(date.timeIntervalSince1970)
try container.encode(seconds)
}
encoder.outputFormatting = .sortedKeys
return encoder
}

Expand All @@ -57,23 +59,20 @@ public class JsonAdapter {
self.log = log
}

public func fromDictionary<T: Decodable>(
_ dictionary: [AnyHashable: Any],
decoder override: JSONDecoder? = nil
) -> T? {
public func fromDictionary<T: Decodable>(_ dictionary: [AnyHashable: Any]) -> T? {
do {
let jsonData = try JSONSerialization.data(withJSONObject: dictionary)

return fromJson(jsonData, decoder: decoder)
return fromJson(jsonData)
} catch {
log.error("\(error.localizedDescription), dictionary: \(dictionary)")
}

return nil
}

public func toDictionary<T: Encodable>(_ obj: T, encoder override: JSONEncoder? = nil) -> [AnyHashable: Any]? {
guard let data = toJson(obj, encoder: encoder) else {
public func toDictionary<T: Encodable>(_ obj: T) -> [AnyHashable: Any]? {
guard let data = toJson(obj) else {
return nil
}

Expand All @@ -97,15 +96,11 @@ public class JsonAdapter {
expect to get an error. If we need this functionality, perhaps we should create a 2nd set of
methods to this class that `throw` so you choose which function to use?
*/
public func fromJson<T: Decodable>(
_ json: Data,
decoder override: JSONDecoder? = nil,
logErrors: Bool = true
) -> T? {
public func fromJson<T: Decodable>(_ json: Data, logErrors: Bool = true) -> T? {
var errorStringToLog: String?

do {
let value = try (override ?? decoder).decode(T.self, from: json)
let value = try decoder.decode(T.self, from: json)
return value
} catch DecodingError.keyNotFound(let key, let context) {
errorStringToLog = """
Expand Down Expand Up @@ -148,9 +143,9 @@ public class JsonAdapter {
return nil
}

public func toJson<T: Encodable>(_ obj: T, encoder override: JSONEncoder? = nil) -> Data? {
public func toJson<T: Encodable>(_ obj: T) -> Data? {
do {
let value = try (override ?? encoder).encode(obj)
let value = try encoder.encode(obj)
return value
} catch EncodingError.invalidValue(let value, let context) {
self.log
Expand All @@ -166,10 +161,9 @@ public class JsonAdapter {
// They are to meet the requirements of our API.
public func toJsonString<T: Encodable>(
_ obj: T,
convertKeysToSnakecase: Bool = true,
nilIfEmpty: Bool = true
) -> String? {
guard let data = toJson(obj, encoder: getEncoder(convertKeysToSnakecase: convertKeysToSnakecase))
guard let data = toJson(obj)
else { return nil }

let jsonString = data.string
Expand All @@ -183,16 +177,4 @@ public class JsonAdapter {

return jsonString
}

// modify the default encoder to change it's behavior for certain use cases.
private func getEncoder(convertKeysToSnakecase: Bool) -> JSONEncoder {
let modifiedEncoder = encoder
if convertKeysToSnakecase {
modifiedEncoder.keyEncodingStrategy = .convertToSnakeCase
} else {
modifiedEncoder.keyEncodingStrategy = .useDefaultKeys
}

return modifiedEncoder
}
}
25 changes: 25 additions & 0 deletions Sources/Common/autogenerated/AutoMockable.generated.swift
Expand Up @@ -758,6 +758,31 @@ public class GlobalDataStoreMock: GlobalDataStore, Mock {
httpRequestsPauseEnds = nil
httpRequestsPauseEndsGetCallsCount = 0
httpRequestsPauseEndsSetCallsCount = 0
deleteAllCallsCount = 0

mockCalled = false // do last as resetting properties above can make this true
}

// MARK: - deleteAll

/// Number of times the function was called.
public private(set) var deleteAllCallsCount = 0
/// `true` if the function was ever called.
public var deleteAllCalled: Bool {
deleteAllCallsCount > 0
}

/**
Set closure to get called when function gets called. Great way to test logic or return a value for the function.
*/
public var deleteAllClosure: (() -> Void)?

/// Mocked function for `deleteAll()`. Your opportunity to return a mocked value and check result of mock in test
/// code.
public func deleteAll() {
mockCalled = true
deleteAllCallsCount += 1
deleteAllClosure?()
}
}

Expand Down
9 changes: 9 additions & 0 deletions Sources/MessagingPush/CustomerIOParsedPushPayload.swift
Expand Up @@ -134,5 +134,14 @@ struct CioPushPayload: Codable {
struct Push: Codable, AutoLenses {
let link: String?
let image: String?

enum CodingKeys: String, CodingKey {
case link
case image
}
}

enum CodingKeys: String, CodingKey {
case push
}
}
Expand Up @@ -3,4 +3,9 @@ import Foundation
struct DeletePushNotificationQueueTaskData: Codable {
let profileIdentifier: String
let deviceToken: String

enum CodingKeys: String, CodingKey {
case profileIdentifier = "profile_identifier"
case deviceToken = "device_token"
}
}
Expand Up @@ -4,4 +4,9 @@ struct IdentifyProfileQueueTaskData: Codable {
let identifier: String
/// JSON string: '{"foo": "bar"}'
let attributesJsonString: String?

enum CodingKeys: String, CodingKey {
case identifier
case attributesJsonString = "attributes_json_string"
}
}

0 comments on commit 8160fdf

Please sign in to comment.