Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds support to encode & decode Foundation.URL #136

Merged
merged 1 commit into from
Mar 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,16 @@ struct AutomergeKeyedDecodingContainer<K: CodingKey>: KeyedDecodingContainerProt
debugDescription: "Expected to decode \(T.self) from \(retrievedValue), but it wasn't a `.text` object."
))
}
case is URL.Type:
let retrievedValue = try getValue(forKey: key)
guard case let .Scalar(.String(urlString)) = retrievedValue, let url = URL(string: urlString) else {
throw DecodingError.typeMismatch(T.self, .init(
codingPath: codingPath,
debugDescription: "Expected to decode \(URL.self) from \(retrievedValue), but it wasn't a `URL` type."
))
}

return url as! T
default:
let decoder = try decoderForKey(key)
return try T(from: decoder)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,13 @@ struct AutomergeKeyedEncodingContainer<K: CodingKey>: KeyedEncodingContainerProt
}
}
impl.mapKeysWritten.append(key.stringValue)
case is URL.Type:
let downcastData = value as! URL
if impl.cautiousWrite {
try checkTypeMatch(value: value, objectId: objectId, key: key, type: .uint)
}
try document.put(obj: objectId, key: key.stringValue, value: downcastData.toScalarValue())
impl.mapKeysWritten.append(key.stringValue)
default:
let newEncoder = AutomergeEncoderImpl(
userInfo: impl.userInfo,
Expand Down
57 changes: 57 additions & 0 deletions Sources/Automerge/ScalarValueRepresentable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,63 @@ extension Bool: ScalarValueRepresentable {
}
}

// MARK: URL Conversions

/// A failure to convert an Automerge scalar value to or from a Boolean representation.
public enum URLScalarConversionError: LocalizedError {
case notStringValue(_ val: Value)
case notStringScalarValue(_ val: ScalarValue)
case notMatchingURLScheme(String)

/// A localized message describing what error occurred.
public var errorDescription: String? {
switch self {
case .notStringScalarValue(let scalarValue):
return "Failed to read the scalar value \(scalarValue) as a String before converting to URL."
case .notStringValue(let value):
return "Failed to read the value \(value) as a String before converting to URL."
case .notMatchingURLScheme(let string):
return "Failed to convert the string \(string) to URL."
}
}

/// A localized message describing the reason for the failure.
public var failureReason: String? { nil }
}

extension URL: ScalarValueRepresentable {

public static func fromValue(_ value: Value) -> Result<Self, URLScalarConversionError> {
if case .Scalar(.String(let urlString)) = value {
if let url = URL(string: urlString) {
return .success(url)
} else {
return .failure(.notMatchingURLScheme(urlString))
}
} else {
return .failure(.notStringValue(value))
}
}

public static func fromScalarValue(_ val: ScalarValue) -> Result<URL, URLScalarConversionError> {
switch val {
case let .String(urlString):
if let url = URL(string: urlString) {
return .success(url)
} else {
return .failure(.notMatchingURLScheme(urlString))
}
default:
return .failure(.notStringScalarValue(val))
}
}

public func toScalarValue() -> ScalarValue {
.String(self.absoluteString)
}
}


// MARK: String Conversions

/// A failure to convert an Automerge scalar value to or from a String representation.
Expand Down
3 changes: 3 additions & 0 deletions Tests/AutomergeTests/CodableTests/AutomergeDecoderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ final class AutomergeDecoderTests: XCTestCase {
try! doc.put(obj: ObjId.ROOT, key: "flag", value: .Boolean(true))
try! doc.put(obj: ObjId.ROOT, key: "count", value: .Int(5))
try! doc.put(obj: ObjId.ROOT, key: "uuid", value: .String("99CEBB16-1062-4F21-8837-CF18EC09DCD7"))
try! doc.put(obj: ObjId.ROOT, key: "url", value: .String("http://url.com"))
try! doc.put(obj: ObjId.ROOT, key: "date", value: .Timestamp(-905182980))
try! doc.put(obj: ObjId.ROOT, key: "data", value: .Bytes(Data("hello".utf8)))

Expand Down Expand Up @@ -47,6 +48,7 @@ final class AutomergeDecoderTests: XCTestCase {
let date: Date
let data: Data
let uuid: UUID
let url: URL
let notes: AutomergeText
}
let decoder = AutomergeDecoder(doc: doc)
Expand All @@ -59,6 +61,7 @@ final class AutomergeDecoderTests: XCTestCase {
XCTAssertEqual(decodedStruct.duration, 3.14159, accuracy: 0.0001)
XCTAssertTrue(decodedStruct.flag)
XCTAssertEqual(decodedStruct.count, 5)
XCTAssertEqual(decodedStruct.url, URL(string: "http://url.com"))

let expectedUUID = UUID(uuidString: "99CEBB16-1062-4F21-8837-CF18EC09DCD7")!
XCTAssertEqual(decodedStruct.uuid, expectedUUID)
Expand Down
15 changes: 14 additions & 1 deletion Tests/AutomergeTests/CodableTests/AutomergeEncoderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ final class AutomergeEncoderTests: XCTestCase {
let date: Date
let data: Data
let uuid: UUID
let url: URL
let notes: AutomergeText
}
let automergeEncoder = AutomergeEncoder(doc: doc)
Expand All @@ -43,6 +44,7 @@ final class AutomergeEncoderTests: XCTestCase {
date: earlyDate,
data: Data("hello".utf8),
uuid: UUID(uuidString: "99CEBB16-1062-4F21-8837-CF18EC09DCD7")!,
url: URL(string: "http://url.com")!,
notes: AutomergeText("Something wicked this way comes.")
)

Expand Down Expand Up @@ -98,6 +100,8 @@ final class AutomergeEncoderTests: XCTestCase {
} else {
try XCTFail("Didn't find an object at \(String(describing: doc.get(obj: ObjId.ROOT, key: "notes")))")
}

XCTAssertEqual(try doc.get(obj: ObjId.ROOT, key: "url"), .Scalar(.String("http://url.com")))
try debugPrint(doc.get(obj: ObjId.ROOT, key: "notes") as Any)
}

Expand All @@ -107,6 +111,7 @@ final class AutomergeEncoderTests: XCTestCase {
let duration: Double
let flag: Bool
let count: Int
let url: URL
}

struct RootModel: Codable {
Expand All @@ -115,7 +120,14 @@ final class AutomergeEncoderTests: XCTestCase {

let automergeEncoder = AutomergeEncoder(doc: doc)

let sample = RootModel(example: SimpleStruct(name: "henry", duration: 3.14159, flag: true, count: 5))
let sample = RootModel(
example: SimpleStruct(
name: "henry",
duration: 3.14159,
flag: true,
count: 5,
url: URL(string: "http://url.com")!)
)

try automergeEncoder.encode(sample)

Expand Down Expand Up @@ -145,6 +157,7 @@ final class AutomergeEncoderTests: XCTestCase {
} else {
try XCTFail("Didn't find: \(String(describing: doc.get(obj: container_id, key: "count")))")
}
XCTAssertEqual(try doc.get(obj: container_id, key: "url"), .Scalar(.String("http://url.com")))
} else {
try XCTFail("Didn't find: \(String(describing: doc.get(obj: ObjId.ROOT, key: "example")))")
}
Expand Down
Loading