Skip to content

Commit

Permalink
feat: allow any dimension value for ParseAnalytics (#341)
Browse files Browse the repository at this point in the history
* feat: allow any dimension value for ParseAnalytics

* nit changelog

* nits

* Update .codecov.yml

* coverage

* more coverage

* improve
  • Loading branch information
cbaker6 authored Feb 7, 2022
1 parent 5307210 commit c8252bc
Show file tree
Hide file tree
Showing 15 changed files with 224 additions and 224 deletions.
10 changes: 8 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,15 @@

### main

[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/4.0.1...main)
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/4.1.0...main)
* _Contributing to this repo? Add info about your change here to be included in the next release_

### 4.1.0
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/4.0.1...4.1.0)

__Improvements__
- Let the OS and developer decide if app tracking authorization is required when using ParseAnalytics. ParseAnalytics can now take any Codable value in its' dimensions instead of just strings. Added a new property "date" to ParseAnalytics. The "at" property will be deprecated in ParseSwift 5.0.0, so developers should switch to "date". ParseAnalytics can now be properly decoded after encoding with a JSONEncoder. This is useful if ParseAnalytics need to be stored locally and sent to the server later ([#341](https://github.com/parse-community/Parse-Swift/pull/341)), thanks to [Corey Baker](https://github.com/cbaker6).

### 4.0.1
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/4.0.0...4.0.1)

Expand All @@ -21,7 +27,7 @@ __New features__
- Add DocC for SDK documentation ([#209](https://github.com/parse-community/Parse-Swift/pull/214)), thanks to [Corey Baker](https://github.com/cbaker6).

__Improvements__
- (Breaking Change) Make ParseRelation conform to Codable and add methods to make decoded stored ParseRelations "usable". ParseObjects can now contain properties of ParseRelation<Self>. In addition, ParseRelations can now be made from ParseObject pointers. For ParseRole, the computed properties: users and roles, are now optional. The queryRoles property has been changed to queryRoles() to improve the handling of thrown errors ([#328](https://github.com/parse-community/Parse-Swift/pull/328)), thanks to @cbaker6.
- (Breaking Change) Make ParseRelation conform to Codable and add methods to make decoded stored ParseRelations "usable". ParseObjects can now contain properties of ParseRelation<Self>. In addition, ParseRelations can now be made from ParseObject pointers. For ParseRole, the computed properties: users and roles, are now optional. The queryRoles property has been changed to queryRoles() to improve the handling of thrown errors ([#328](https://github.com/parse-community/Parse-Swift/pull/328)), thanks to [Corey Baker](https://github.com/cbaker6).
- (Breaking Change) Change the following method parameter names: isUsingMongoDB -> usingMongoDB, isIgnoreCustomObjectIdConfig -> ignoringCustomObjectIdConfig, isUsingEQ -> usingEqComparator ([#321](https://github.com/parse-community/Parse-Swift/pull/321)), thanks to [Corey Baker](https://github.com/cbaker6).
- (Breaking Change) Change the following method parameter names: isUsingMongoDB -> usingMongoDB, isIgnoreCustomObjectIdConfig -> ignoringCustomObjectIdConfig, isUsingEQ -> usingEqComparator ([#321](https://github.com/parse-community/Parse-Swift/pull/321)), thanks to [Corey Baker](https://github.com/cbaker6).
- (Breaking Change) Change the following method parameter names: isUsingTransactions -> usingTransactions, isAllowingCustomObjectIds -> allowingCustomObjectIds, isUsingEqualQueryConstraint -> usingEqualQueryConstraint, isMigratingFromObjcSDK -> migratingFromObjcSDK, isDeletingKeychainIfNeeded -> deletingKeychainIfNeeded ([#323](https://github.com/parse-community/Parse-Swift/pull/323)), thanks to [Corey Baker](https://github.com/cbaker6).
Expand Down
42 changes: 21 additions & 21 deletions ParseSwift.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Sources/ParseSwift/Coding/ParseEncoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ public struct ParseEncoder {
}

// MARK: _ParseEncoder
private class _ParseEncoder: JSONEncoder, Encoder {
internal class _ParseEncoder: JSONEncoder, Encoder {
var codingPath: [CodingKey]
let dictionary: NSMutableDictionary
let skippedKeys: Set<String>
Expand Down
2 changes: 1 addition & 1 deletion Sources/ParseSwift/LiveQuery/ParseLiveQuery+async.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import Foundation

extension ParseLiveQuery {
// MARK: Async/Await
// MARK: Connection - Async/Await

/**
Manually establish a connection to the `ParseLiveQuery` Server.
Expand Down
2 changes: 1 addition & 1 deletion Sources/ParseSwift/LiveQuery/ParseLiveQuery+combine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import Foundation
import Combine

extension ParseLiveQuery {
// MARK: Combine
// MARK: Connection - Combine

/**
Manually establish a connection to the `ParseLiveQuery` Server. Publishes when established.
Expand Down
1 change: 0 additions & 1 deletion Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,6 @@ Not attempting to open ParseLiveQuery socket anymore
}
}

// MARK: Helpers
extension ParseLiveQuery {

/// Current LiveQuery client.
Expand Down
4 changes: 2 additions & 2 deletions Sources/ParseSwift/Objects/ParseRole.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,11 @@ public extension ParseRole {
}

static func == (lhs: Self, rhs: Self) -> Bool {
lhs.name == rhs.name && lhs.className == rhs.className
lhs.debugDescription == rhs.debugDescription
}

func hash(into hasher: inout Hasher) {
hasher.combine("\(self.className)_\(String(describing: self.name))")
hasher.combine(self.debugDescription)
}
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/ParseSwift/ParseConstants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Foundation

enum ParseConstants {
static let sdk = "swift"
static let version = "4.0.1"
static let version = "4.1.0"
static let fileManagementDirectory = "parse/"
static let fileManagementPrivateDocumentsDirectory = "Private Documents/"
static let fileManagementLibraryDirectory = "Library/"
Expand Down
5 changes: 4 additions & 1 deletion Sources/ParseSwift/Types/ParseACL.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ public struct ParseACL: ParseType,
case write

public init(from decoder: Decoder) throws {
self = Access(rawValue: try decoder.singleValueContainer().decode(String.self))!
guard let decoded = Access(rawValue: try decoder.singleValueContainer().decode(String.self)) else {
throw ParseError(code: .unknownError, message: "Not able to decode ParseACL Access")
}
self = decoded
}

public func encode(to encoder: Encoder) throws {
Expand Down
22 changes: 11 additions & 11 deletions Sources/ParseSwift/Types/ParseAnalytics+async.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,19 +96,19 @@ public extension ParseAnalytics {
- parameter at: Explicitly set the time associated with a given event. If not provided the
server time will be used.
- parameter options: A set of header options sent to the server. Defaults to an empty set.
- warning: This method makes a copy of the current `ParseAnalytics` and then mutates
it. You will not have access to the mutated analytic after calling this method.
- throws: An error of type `ParseError`.
*/
func track(dimensions: [String: String]?,
at date: Date? = nil,
options: API.Options = []) async throws {
let _: Void = try await withCheckedThrowingContinuation { continuation in
var analytic = self
analytic.track(dimensions: dimensions,
at: date,
options: options,
completion: continuation.resume)
mutating func track(dimensions: [String: String]?,
at date: Date? = nil,
options: API.Options = []) async throws {
let result = try await withCheckedThrowingContinuation { continuation in
self.track(dimensions: dimensions,
at: date,
options: options,
completion: continuation.resume)
}
if case let .failure(error) = result {
throw error
}
}
}
Expand Down
164 changes: 78 additions & 86 deletions Sources/ParseSwift/Types/ParseAnalytics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,32 +11,46 @@ import Foundation
import UIKit
#endif

#if canImport(AppTrackingTransparency)
import AppTrackingTransparency
#endif

/**
`ParseAnalytics` provides an interface to Parse's logging and analytics
backend.
- warning: For iOS 14.0, macOS 11.0, macCatalyst 14.0, tvOS 14.0, you
will need to request tracking authorization for ParseAnalytics to work.
See Apple's [documentation](https://developer.apple.com/documentation/apptrackingtransparency) for more for details.
`ParseAnalytics` provides an interface to Parse's logging and analytics backend.
*/
public struct ParseAnalytics: ParseType, Hashable {

/// The name of the custom event to report to Parse as having happened.
public let name: String
public var name: String

/// Explicitly set the time associated with a given event. If not provided the server
/// time will be used.
public var at: Date? // swiftlint:disable:this identifier_name
/// - warning: This will be deprecated in ParseSwift 5.0.0 in favor of `date`.
public var at: Date? { // swiftlint:disable:this identifier_name
get {
date
}
set {
date = newValue
}
}

/// Explicitly set the time associated with a given event. If not provided the server
/// time will be used.
public var date: Date?

/// The dictionary of information by which to segment this event.
public var dimensions: [String: String]?
public var dimensions: [String: Codable]? {
get {
convertToString(dimensionsAnyCodable)
}
set {
dimensionsAnyCodable = convertToAnyCodable(newValue)
}
}

var dimensionsAnyCodable: [String: AnyCodable]?

enum CodingKeys: String, CodingKey {
case at, dimensions // swiftlint:disable:this identifier_name
case date = "at"
case dimensions
case name
}

/**
Expand All @@ -47,13 +61,55 @@ public struct ParseAnalytics: ParseType, Hashable {
time will be used. Defaults to `nil`.
*/
public init (name: String,
dimensions: [String: String]? = nil,
at: Date? = nil) { // swiftlint:disable:this identifier_name
dimensions: [String: Codable]? = nil,
at date: Date? = nil) {
self.name = name
self.dimensions = dimensions
self.at = at
self.dimensionsAnyCodable = convertToAnyCodable(dimensions)
self.date = date
}

public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encodeIfPresent(date, forKey: .date)
try container.encodeIfPresent(dimensionsAnyCodable, forKey: .dimensions)
if !(encoder is _ParseEncoder) {
try container.encode(name, forKey: .name)
}
}

public static func == (lhs: Self, rhs: Self) -> Bool {
lhs.debugDescription == rhs.debugDescription
}

public func hash(into hasher: inout Hasher) {
hasher.combine(self.debugDescription)
}

// MARK: Helpers
func convertToAnyCodable(_ dimensions: [String: Codable]?) -> [String: AnyCodable]? {
guard let dimensions = dimensions else {
return nil
}
var convertedDimensions = [String: AnyCodable]()
for (key, value) in dimensions {
convertedDimensions[key] = AnyCodable(value)
}
return convertedDimensions
}

func convertToString(_ dimensions: [String: AnyCodable]?) -> [String: String]? {
guard let dimensions = dimensions else {
return nil
}
var convertedDimensions = [String: String]()
for (key, value) in dimensions {
convertedDimensions[key] = "\(value.value)"
}
return convertedDimensions
}

// MARK: Intents

#if os(iOS)
/**
Tracks *asynchronously* this application being launched. If this happened as the result of the
Expand All @@ -79,22 +135,6 @@ public struct ParseAnalytics: ParseType, Hashable {
completion: @escaping (Result<Void, ParseError>) -> Void) {
var options = options
options.insert(.cachePolicy(.reloadIgnoringLocalCacheData))
#if canImport(AppTrackingTransparency)
if #available(macOS 11.0, iOS 14.0, macCatalyst 14.0, tvOS 14.0, *) {
if !ParseSwift.configuration.isTestingSDK {
let status = ATTrackingManager.trackingAuthorizationStatus
if status != .authorized {
callbackQueue.async {
let error = ParseError(code: .unknownError,
// swiftlint:disable:next line_length
message: "App tracking not authorized. Please request permission from user.")
completion(.failure(error))
}
return
}
}
}
#endif
var userInfo: [String: String]?
if let remoteOptions = launchOptions?[.remoteNotification] as? [String: String] {
userInfo = remoteOptions
Expand All @@ -118,7 +158,7 @@ public struct ParseAnalytics: ParseType, Hashable {
Tracks *asynchronously* this application being launched with additional dimensions.
- parameter dimensions: The dictionary of information by which to segment this
event and can be empty or `nil`.
event. Can be empty or `nil`.
- parameter at: Explicitly set the time associated with a given event. If not provided the
server time will be used.
- parameter options: A set of header options sent to the server. Defaults to an empty set.
Expand All @@ -135,22 +175,6 @@ public struct ParseAnalytics: ParseType, Hashable {
completion: @escaping (Result<Void, ParseError>) -> Void) {
var options = options
options.insert(.cachePolicy(.reloadIgnoringLocalCacheData))
#if canImport(AppTrackingTransparency)
if #available(macOS 11.0, iOS 14.0, macCatalyst 14.0, tvOS 14.0, *) {
if !ParseSwift.configuration.isTestingSDK {
let status = ATTrackingManager.trackingAuthorizationStatus
if status != .authorized {
callbackQueue.async {
let error = ParseError(code: .unknownError,
// swiftlint:disable:next line_length
message: "App tracking not authorized. Please request permission from user.")
completion(.failure(error))
}
return
}
}
}
#endif
let appOppened = ParseAnalytics(name: "AppOpened",
dimensions: dimensions,
at: date)
Expand Down Expand Up @@ -180,22 +204,6 @@ public struct ParseAnalytics: ParseType, Hashable {
completion: @escaping (Result<Void, ParseError>) -> Void) {
var options = options
options.insert(.cachePolicy(.reloadIgnoringLocalCacheData))
#if canImport(AppTrackingTransparency)
if #available(macOS 11.0, iOS 14.0, macCatalyst 14.0, tvOS 14.0, *) {
if !ParseSwift.configuration.isTestingSDK {
let status = ATTrackingManager.trackingAuthorizationStatus
if status != .authorized {
callbackQueue.async {
let error = ParseError(code: .unknownError,
// swiftlint:disable:next line_length
message: "App tracking not authorized. Please request permission from user.")
completion(.failure(error))
}
return
}
}
}
#endif
self.saveCommand().executeAsync(options: options,
callbackQueue: callbackQueue) { result in
switch result {
Expand All @@ -211,7 +219,7 @@ public struct ParseAnalytics: ParseType, Hashable {
Tracks *asynchronously* the occurrence of a custom event with additional dimensions.
- parameter dimensions: The dictionary of information by which to segment this
event and can be empty or `nil`.
event. Can be empty or `nil`.
- parameter at: Explicitly set the time associated with a given event. If not provided the
server time will be used.
- parameter options: A set of header options sent to the server. Defaults to an empty set.
Expand All @@ -228,24 +236,8 @@ public struct ParseAnalytics: ParseType, Hashable {
completion: @escaping (Result<Void, ParseError>) -> Void) {
var options = options
options.insert(.cachePolicy(.reloadIgnoringLocalCacheData))
#if canImport(AppTrackingTransparency)
if #available(macOS 11.0, iOS 14.0, macCatalyst 14.0, tvOS 14.0, *) {
if !ParseSwift.configuration.isTestingSDK {
let status = ATTrackingManager.trackingAuthorizationStatus
if status != .authorized {
callbackQueue.async {
let error = ParseError(code: .unknownError,
// swiftlint:disable:next line_length
message: "App tracking not authorized. Please request permission from user.")
completion(.failure(error))
}
return
}
}
}
#endif
self.dimensions = dimensions
self.at = date
self.dimensionsAnyCodable = convertToAnyCodable(dimensions)
self.date = date
self.saveCommand().executeAsync(options: options,
callbackQueue: callbackQueue) { result in
switch result {
Expand Down
16 changes: 16 additions & 0 deletions Tests/ParseSwiftTests/ParseACLTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,22 @@ class ParseACLTests: XCTestCase {
}
}

func testCodingAccess() throws {
let access = ParseACL.Access.read
let encoded = try ParseCoding.jsonEncoder().encode(access)
let decoded = try ParseCoding.jsonDecoder().decode(ParseACL.Access.self, from: encoded)
XCTAssertEqual(access, decoded)
let access2 = ParseACL.Access.write
let encoded2 = try ParseCoding.jsonEncoder().encode(access2)
let decoded2 = try ParseCoding.jsonDecoder().decode(ParseACL.Access.self, from: encoded2)
XCTAssertEqual(access2, decoded2)
guard let data = "hello".data(using: .utf8) else {
XCTFail("Should have unwrapped")
return
}
XCTAssertThrowsError(try ParseCoding.jsonDecoder().decode(ParseACL.Access.self, from: data))
}

func testDebugString() {
var acl = ParseACL()
acl.setReadAccess(objectId: "a", value: false)
Expand Down
Loading

0 comments on commit c8252bc

Please sign in to comment.