-
Notifications
You must be signed in to change notification settings - Fork 180
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
* Add Chat target, split packages * savepoint * restructure chat's project folder * fix schemas * Add JSONRPC and Commons packages * Moved AnyCodable to Commons * Fixed test import * Reintroduces either type * Add request and response types * Add simple response decode tests * Add response ID parsing tests * Fixed tests typo * Improved response round trip coding test * Error response decoding tests * Invalid response decode tests * Enabled code coverage for library * Response decoding tests for structured result values * Add flexible initializers with tests * Add descriptions to errors thrown in response decoding * Renamed response internalResult to outcome * Basic RPC request decoding tests * Tests for request empty cases and corner cases * Add flexible inits for requests * Add identifier generation inits * Joined request notification extensions * Renamed files * Implemented default JSONRPC error cases * Declared RPCRequestConvertible as public * Remove rebase artifacts * Added debug description to request param primitives error
- Loading branch information
André Vants
authored
Jun 8, 2022
1 parent
b3db843
commit 9e94672
Showing
21 changed files
with
1,321 additions
and
208 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,199 @@ | ||
import Foundation | ||
|
||
/** | ||
A type-erased codable object. | ||
The `AnyCodable` type allows to encode and decode data prior to knowing the underlying type, delaying the type-matching | ||
to a later point in execution. | ||
When dealing with serialized JSON data structures where a single key can match to different types of values, the `AnyCodable` | ||
type can be used as a placeholder for `Any` while preserving the `Codable` conformance of the containing type. Another use case | ||
for the `AnyCodable` type is to facilitate the encoding of arrays of heterogeneous-typed values. | ||
You can call `get(_:)` to transform the underlying value back to the type you specify. | ||
*/ | ||
public struct AnyCodable { | ||
|
||
public let value: Any | ||
|
||
private var dataEncoding: (() throws -> Data)? | ||
|
||
private var genericEncoding: ((Encoder) throws -> Void)? | ||
|
||
private init(_ value: Any) { | ||
self.value = value | ||
} | ||
|
||
/** | ||
Creates a type-erased codable value that wraps the given instance. | ||
- parameters: | ||
- codable: A codable value to wrap. | ||
*/ | ||
public init<C>(_ codable: C) where C: Codable { | ||
self.value = codable | ||
dataEncoding = { | ||
let encoder = JSONEncoder() | ||
encoder.outputFormatting = .sortedKeys | ||
return try encoder.encode(codable) | ||
} | ||
genericEncoding = { encoder in | ||
try codable.encode(to: encoder) | ||
} | ||
|
||
} | ||
|
||
/** | ||
Returns the underlying value, provided it matches the type spcified. | ||
Use this method to retrieve a strong-typed value, as long as it can be decoded from its underlying representation. | ||
- throws: If the value fails to decode to the specified type. | ||
- returns: The underlying value, if it can be decoded. | ||
``` | ||
let anyCodable = AnyCodable("a message") | ||
do { | ||
let value = try anyCodable.get(String.self) | ||
print(value) | ||
} catch { | ||
print("Error retrieving the value: \(error)") | ||
} | ||
``` | ||
*/ | ||
public func get<T: Codable>(_ type: T.Type) throws -> T { | ||
let valueData = try getDataRepresentation() | ||
return try JSONDecoder().decode(type, from: valueData) | ||
} | ||
|
||
/// A textual representation of the underlying encoded data. Returns an empty string if the type fails to encode. | ||
public var stringRepresentation: String { | ||
guard | ||
let valueData = try? getDataRepresentation(), | ||
let string = String(data: valueData, encoding: .utf8) | ||
else { | ||
return "" | ||
} | ||
return string | ||
} | ||
|
||
private func getDataRepresentation() throws -> Data { | ||
if let encodeToData = dataEncoding { | ||
return try encodeToData() | ||
} else { | ||
return try JSONSerialization.data(withJSONObject: value, options: [.fragmentsAllowed, .sortedKeys]) | ||
} | ||
} | ||
} | ||
|
||
extension AnyCodable: Equatable { | ||
|
||
public static func == (lhs: AnyCodable, rhs: AnyCodable) -> Bool { | ||
do { | ||
let lhsData = try lhs.getDataRepresentation() | ||
let rhsData = try rhs.getDataRepresentation() | ||
return lhsData == rhsData | ||
} catch { | ||
return false | ||
} | ||
} | ||
} | ||
|
||
extension AnyCodable: Hashable { | ||
public func hash(into hasher: inout Hasher) { | ||
hasher.combine(stringRepresentation) | ||
} | ||
} | ||
|
||
extension AnyCodable: CustomStringConvertible { | ||
|
||
public var description: String { | ||
let stringSelf = stringRepresentation | ||
let description = stringSelf.isEmpty ? "invalid data" : stringSelf | ||
return "AnyCodable: \"\(description)\"" | ||
} | ||
} | ||
|
||
extension AnyCodable: Decodable, Encodable { | ||
|
||
struct CodingKeys: CodingKey { | ||
|
||
let stringValue: String | ||
let intValue: Int? | ||
|
||
init?(intValue: Int) { | ||
self.stringValue = String(intValue) | ||
self.intValue = intValue | ||
} | ||
|
||
init?(stringValue: String) { | ||
self.stringValue = stringValue | ||
self.intValue = Int(stringValue) | ||
} | ||
} | ||
|
||
public init(from decoder: Decoder) throws { | ||
if let container = try? decoder.container(keyedBy: CodingKeys.self) { | ||
var result = [String: Any]() | ||
try container.allKeys.forEach { (key) throws in | ||
result[key.stringValue] = try container.decode(AnyCodable.self, forKey: key).value | ||
} | ||
value = result | ||
} | ||
else if var container = try? decoder.unkeyedContainer() { | ||
var result = [Any]() | ||
while !container.isAtEnd { | ||
result.append(try container.decode(AnyCodable.self).value) | ||
} | ||
value = result | ||
} | ||
else if let container = try? decoder.singleValueContainer() { | ||
if let intVal = try? container.decode(Int.self) { | ||
value = intVal | ||
} else if let doubleVal = try? container.decode(Double.self) { | ||
value = doubleVal | ||
} else if let boolVal = try? container.decode(Bool.self) { | ||
value = boolVal | ||
} else if let stringVal = try? container.decode(String.self) { | ||
value = stringVal | ||
} else { | ||
throw DecodingError.dataCorruptedError(in: container, debugDescription: "The container contains nothing serializable.") | ||
} | ||
} else { | ||
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "No data found in the decoder.")) | ||
} | ||
} | ||
|
||
public func encode(to encoder: Encoder) throws { | ||
if let encoding = genericEncoding { | ||
try encoding(encoder) | ||
} else if let array = value as? [Any] { | ||
var container = encoder.unkeyedContainer() | ||
for value in array { | ||
let decodable = AnyCodable(value) | ||
try container.encode(decodable) | ||
} | ||
} else if let dictionary = value as? [String: Any] { | ||
var container = encoder.container(keyedBy: CodingKeys.self) | ||
for (key, value) in dictionary { | ||
let codingKey = CodingKeys(stringValue: key)! | ||
let decodable = AnyCodable(value) | ||
try container.encode(decodable, forKey: codingKey) | ||
} | ||
} else { | ||
var container = encoder.singleValueContainer() | ||
if let intVal = value as? Int { | ||
try container.encode(intVal) | ||
} else if let doubleVal = value as? Double { | ||
try container.encode(doubleVal) | ||
} else if let boolVal = value as? Bool { | ||
try container.encode(boolVal) | ||
} else if let stringVal = value as? String { | ||
try container.encode(stringVal) | ||
} else { | ||
throw EncodingError.invalidValue(value, EncodingError.Context.init(codingPath: [], debugDescription: "The value is not encodable.")) | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
public enum Either<L, R> { | ||
case left(L) | ||
case right(R) | ||
} | ||
|
||
public extension Either { | ||
|
||
init(_ left: L) { | ||
self = .left(left) | ||
} | ||
|
||
init(_ right: R) { | ||
self = .right(right) | ||
} | ||
|
||
var left: L? { | ||
guard case let .left(left) = self else { return nil } | ||
return left | ||
} | ||
|
||
var right: R? { | ||
guard case let .right(right) = self else { return nil } | ||
return right | ||
} | ||
} | ||
|
||
extension Either: Equatable where L: Equatable, R: Equatable { | ||
|
||
public static func == (lhs: Self, rhs: Self) -> Bool { | ||
switch (lhs, rhs) { | ||
case let (.left(lhs), .left(rhs)): | ||
return lhs == rhs | ||
case let (.right(lhs), .right(rhs)): | ||
return lhs == rhs | ||
default: | ||
return false | ||
} | ||
} | ||
} | ||
|
||
extension Either: Hashable where L: Hashable, R: Hashable {} | ||
|
||
extension Either: Codable where L: Codable, R: Codable { | ||
|
||
public init(from decoder: Decoder) throws { | ||
if let left = try? L(from: decoder) { | ||
self.init(left) | ||
} else if let right = try? R(from: decoder) { | ||
self.init(right) | ||
} else { | ||
let errorDescription = "Data couldn't be decoded into either of the underlying types." | ||
let errorContext = DecodingError.Context(codingPath: decoder.codingPath, debugDescription: errorDescription) | ||
throw DecodingError.typeMismatch(Self.self, errorContext) | ||
} | ||
} | ||
|
||
public func encode(to encoder: Encoder) throws { | ||
switch self { | ||
case let .left(left): | ||
try left.encode(to: encoder) | ||
case let .right(right): | ||
try right.encode(to: encoder) | ||
} | ||
} | ||
} |
Oops, something went wrong.