Skip to content

Commit

Permalink
#13 - Created a type-safe enum for the RecordFieldDictionary's type p…
Browse files Browse the repository at this point in the history
…roperty

- Updated decode method
- Added test coverage for singular field types
  • Loading branch information
edorphy committed Jan 11, 2022
1 parent 0bdcfc0 commit b187efb
Show file tree
Hide file tree
Showing 3 changed files with 211 additions and 28 deletions.
7 changes: 3 additions & 4 deletions Sources/CloudKitWebServices/Query/CKWSQuery-Filter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,15 @@ public extension CKWSQuery.Filter {
internal func getRecordFieldValue() -> RecordFieldDictionary {
switch value {
case let stringValue as String:
return RecordFieldDictionary(value: stringValue, type: "STRING")
return RecordFieldDictionary(value: stringValue, type: .string)

case let referenceValue as ReferenceDictionary:
return RecordFieldDictionary(value: referenceValue, type: "REFERENCE")
return RecordFieldDictionary(value: referenceValue, type: .reference)

case let recordReferenceValue as CKWSRecord.Reference:
return RecordFieldDictionary(value: recordReferenceValue, type: "REFERENCE")
return RecordFieldDictionary(value: recordReferenceValue, type: .reference)

default:
assertionFailure("Need to implement this")
fatalError("if you encounter this, open up a PR for the unhandled type")
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,49 @@ import CoreLocation

struct RecordFieldDictionary: Codable {

// MARK: - Types

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

// This type isn't explicitly defined in the reference so it is internal.
// When integrating with the web API and inspecting the raw JSON you can find these type strings

// TODO: Like this name? It is internal but feels like it is a main type. Consider making it more verbose

enum FieldType: String, Codable {
case asset = "ASSETID"
// TODO: case byte = "BYTE"
case dateTime = "TIMESTAMP"
case double = "DOUBLE"
case int64 = "INT64"
case location = "LOCATION"
case reference = "REFERENCE"
case string = "STRING"

case assetList = "ASSETID_LIST"
// TODO: case byteList = "BYTE_LIST"
case dateTimeList = "TIMESTAMP_LIST"
case doubleList = "DOUBLE_LIST"
case int64List = "INT64_LIST"
case locationList = "LOCATION_LIST"
case referenceList = "REFERENCE_LIST"
case stringList = "STRING_LIST"

// Observed undocumented / unexpected type string 'UNKNOWN_LIST'
// Submitted feedback: FB9825479
// TODO: case unknownList = "UNKNOWN_LIST"
}


let value: CKWSRecordValueProtocol

// TODO: The reference states that type is optional, but what is the practical usage of this? Maybe make it required
let type: String?
let type: FieldType?

init(value: CKWSRecordValueProtocol, type: String?) {
init(value: CKWSRecordValueProtocol, type: FieldType?) {
self.value = value
self.type = type
}
Expand All @@ -29,6 +61,8 @@ struct RecordFieldDictionary: Codable {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(type, forKey: .type)

// TODO: Use the new type safe FieldType or guard type + data are same type?

switch value {

case let dataValue as Data:
Expand Down Expand Up @@ -65,91 +99,90 @@ struct RecordFieldDictionary: Codable {
// TODO: Add support for array encoding

default:
fatalError("RecordFieldDictionary encode not implemented for type \(type ?? "unspecified-type")")
fatalError("RecordFieldDictionary encode not implemented for type \(String(describing: type))")
}
}

// swiftlint:disable:next cyclomatic_complexity
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)

guard let type = try values.decodeIfPresent(String.self, forKey: .type) else {
// TODO: Not sure what to do here, it isn't documented
fatalError("figure out graceful decoding, maybe throw required key missing?")
guard let type = try values.decodeIfPresent(FieldType.self, forKey: .type) else {
let rawType = (try? values.decodeIfPresent(String.self, forKey: .type)) ?? "unknown type"
assertionFailure("Unexpected record field type: \(rawType), file an issue on GitHub with sample json/data and the record type from Console.")
self.type = .string
self.value = ""
return
}

self.type = type

switch type {

case "ASSETID":
case .asset:
let assetDictionary = try values.decode(AssetDictionary.self, forKey: .value)
// TODO: Figure out a way to detect if asset is remote or local, but for now since the library is read-only, it HAS to be remote
self.value = CKWSRemoteAsset(assetDictionary: assetDictionary)

// TODO: Bytes

case "TIMESTAMP":
case .dateTime:
// Documenation states an integer in milliseconds since 1970
let timeInterval = try values.decode(Int64.self, forKey: .value)
self.value = Date(timeIntervalSince1970: Double(timeInterval / 1000))

case "DOUBLE":
case .double:
let doubleValue = try values.decode(Double.self, forKey: .value)
self.value = doubleValue

case "INT64":
case .int64:
let intValue = try values.decode(Int64.self, forKey: .value)
self.value = intValue

case "LOCATION":
case .location:
let locationDictionary = try values.decode(LocationDictionary.self, forKey: .value)
self.value = CLLocation(locationDictionary: locationDictionary)

case "REFERENCE":
case .reference:
let reference = try values.decode(ReferenceDictionary.self, forKey: .value)
self.value = CKWSRecord.Reference(reference: reference)

case "STRING":
case .string:
let stringValue = try values.decode(String.self, forKey: .value)
self.value = stringValue

// MARK: - List Support

case "ASSETID_LIST":
case .assetList:
let assetDictionaries = try values.decode([AssetDictionary].self, forKey: .value)
// TODO: Figure out a way to detect if asset is remote or local, but for now since the library is read-only, it HAS to be remote
self.value = assetDictionaries.map { CKWSRemoteAsset(assetDictionary: $0) }

// TODO: Bytes List

case "TIMESTAMP_LIST":
case .dateTimeList:
// Documenation states an integer in milliseconds since 1970
let timeIntervals = try values.decode([Int64].self, forKey: .value)
self.value = timeIntervals.map { Date(timeIntervalSince1970: Double($0 / 1000)) }

case "DOUBLE_LIST":
case .doubleList:
let doubleValues = try values.decode([Double].self, forKey: .value)
self.value = doubleValues

case "INT64_LIST":
case .int64List:
self.value = try values.decode([Int64].self, forKey: .value)

case "LOCATION_LIST":
case .locationList:
let locationDictionaries = try values.decode([LocationDictionary].self, forKey: .value)
self.value = locationDictionaries.map { CLLocation(locationDictionary: $0) }

case "REFERENCE_LIST":
case .referenceList:
let references = try values.decode([ReferenceDictionary].self, forKey: .value)
self.value = references.map { CKWSRecord.Reference(reference: $0) }

case "STRING_LIST":
case .stringList:
let stringValues = try values.decode([String].self, forKey: .value)
self.value = stringValues

default:
print("Unhandeled type: \(type)")
fatalError("if you run into this, open up a PR with a new decoding strategy for the unhandled type")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
//
// RecordFieldDictionaryTests.swift
//
//
// Created by Eric Dorphy on 1/10/22.
//

@testable import CloudKitWebServices
import CoreLocation
import XCTest

class RecordFieldDictionaryTests: XCTestCase {
func testDecodeAssetType() throws {
let testData =
"""
{
"value": {
"fileChecksum": "ATiNtj034tTgyAwHN4aZsVKXSGyK",
"size": 102206,
"downloadURL": "https://cvws.icloud-content.com/B/somerecordpath"
},
"type": "ASSETID"
}
"""
.data(using: .utf8)!

let value = try JSONDecoder().decode(RecordFieldDictionary.self, from: testData)

XCTAssertEqual(value.type, .asset)

let asset = value.value as? CKWSRemoteAsset
XCTAssertNotNil(asset)
XCTAssertEqual(asset?.downloadURL, URL(string: "https://cvws.icloud-content.com/B/somerecordpath")!)
}

func testDecodeDateTimeType() throws {

// Jan 1, 2001 @ Midnight in milliseconds

let testData =
"""
{
"value": 978307200000,
"type": "TIMESTAMP"
}
"""
.data(using: .utf8)!

let value = try JSONDecoder().decode(RecordFieldDictionary.self, from: testData)

XCTAssertEqual(value.type, .dateTime)
XCTAssertEqual(value.value as? Date, Date(timeIntervalSinceReferenceDate: 0))
}

func testDecodeDoubleType() throws {
let testData =
"""
{
"value": 12345.6789,
"type": "DOUBLE"
}
"""
.data(using: .utf8)!

let value = try JSONDecoder().decode(RecordFieldDictionary.self, from: testData)

XCTAssertEqual(value.type, .double)
XCTAssertEqual(value.value as? Double, 12345.6789)
}

func testDecodeInt64Type() throws {
let testData =
"""
{
"value": 12345,
"type": "INT64"
}
"""
.data(using: .utf8)!

let value = try JSONDecoder().decode(RecordFieldDictionary.self, from: testData)

XCTAssertEqual(value.type, .int64)
XCTAssertEqual(value.value as? Int64, 12345)
}

func testDecodeLocationType() throws {
let testData =
"""
{
"value": {
"latitude": 45.000,
"longitude": -93.000,
},
"type": "LOCATION"
}
"""
.data(using: .utf8)!

let value = try JSONDecoder().decode(RecordFieldDictionary.self, from: testData)

XCTAssertEqual(value.type, .location)
let location = value.value as? CLLocation
XCTAssertNotNil(location)
XCTAssertEqual(location?.coordinate.latitude, 45.000)
XCTAssertEqual(location?.coordinate.longitude, -93.000)
}

func testDecodeReferenceType() throws {
let referenceRecordID: CKWSRecord.ID = CKWSRecord.ID(recordName: UUID().uuidString)

let testData =
"""
{
"value": {
"recordName": "\(referenceRecordID.recordName)",
"action": "NONE"
},
"type": "REFERENCE"
}
"""
.data(using: .utf8)!

let value = try JSONDecoder().decode(RecordFieldDictionary.self, from: testData)

XCTAssertEqual(value.type, .reference)

let reference = value.value as? CKWSRecord.Reference
XCTAssertNotNil(reference)
XCTAssertEqual(reference?.recordID, referenceRecordID)
XCTAssertEqual(reference?.action, CKWSRecord.ReferenceAction.none)
}

func testDecodeStringType() throws {
let testData =
"""
{
"value": "a test string value",
"type": "STRING"
}
"""
.data(using: .utf8)!

let value = try JSONDecoder().decode(RecordFieldDictionary.self, from: testData)

XCTAssertEqual(value.type, .string)
XCTAssertEqual(value.value as? String, "a test string value")
}

// TODO: Add tests for list fields
}

0 comments on commit b187efb

Please sign in to comment.