-
Notifications
You must be signed in to change notification settings - Fork 191
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
#256 JSON-RPC Package (& Commons) #261
Merged
Merged
Changes from 29 commits
Commits
Show all changes
30 commits
Select commit
Hold shift + click to select a range
db4436e
Add Chat target, split packages
llbartekll 350f476
savepoint
llbartekll f2ce369
restructure chat's project folder
llbartekll 7bea2e1
fix schemas
llbartekll 972b936
Add JSONRPC and Commons packages
3cb6494
Moved AnyCodable to Commons
1547071
Fixed test import
8ac21a1
Reintroduces either type
2ab5f8c
Add request and response types
3ea5404
Add simple response decode tests
a6f9374
Add response ID parsing tests
1249c04
Fixed tests typo
4ae9803
Improved response round trip coding test
231a34c
Error response decoding tests
e72eea4
Invalid response decode tests
48e1e08
Enabled code coverage for library
295d9aa
Response decoding tests for structured result values
f0dc87a
Add flexible initializers with tests
945033f
Add descriptions to errors thrown in response decoding
3fb8647
Renamed response internalResult to outcome
22f4fe9
Basic RPC request decoding tests
5eab9d0
Tests for request empty cases and corner cases
781ca67
Add flexible inits for requests
915fc82
Add identifier generation inits
fb1846b
Joined request notification extensions
a681189
Renamed files
9aa5f3d
Implemented default JSONRPC error cases
0df90e2
Declared RPCRequestConvertible as public
9ed78fd
Remove rebase artifacts
68f1484
Added debug description to request param primitives error
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we define a difference between
WalletConnectUtils
and aCommons
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WalletConnectUtils
should have types related to WalletConnect domain rules,Commons
should be always agnostic of any WalletConnect implementation. I think we could even renameWalletConnectUtils
toWalletConnectCore
and, over time, extract the components (storage, logging) to their own packages.