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

#256 JSON-RPC Package (& Commons) #261

Merged
merged 30 commits into from
Jun 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
db4436e
Add Chat target, split packages
llbartekll May 30, 2022
350f476
savepoint
llbartekll May 30, 2022
f2ce369
restructure chat's project folder
llbartekll May 31, 2022
7bea2e1
fix schemas
llbartekll May 31, 2022
972b936
Add JSONRPC and Commons packages
Jun 1, 2022
3cb6494
Moved AnyCodable to Commons
Jun 1, 2022
1547071
Fixed test import
Jun 1, 2022
8ac21a1
Reintroduces either type
Jun 1, 2022
2ab5f8c
Add request and response types
Jun 2, 2022
3ea5404
Add simple response decode tests
Jun 2, 2022
a6f9374
Add response ID parsing tests
Jun 2, 2022
1249c04
Fixed tests typo
Jun 2, 2022
4ae9803
Improved response round trip coding test
Jun 2, 2022
231a34c
Error response decoding tests
Jun 2, 2022
e72eea4
Invalid response decode tests
Jun 2, 2022
48e1e08
Enabled code coverage for library
Jun 2, 2022
295d9aa
Response decoding tests for structured result values
Jun 2, 2022
f0dc87a
Add flexible initializers with tests
Jun 2, 2022
945033f
Add descriptions to errors thrown in response decoding
Jun 3, 2022
3fb8647
Renamed response internalResult to outcome
Jun 3, 2022
22f4fe9
Basic RPC request decoding tests
Jun 5, 2022
5eab9d0
Tests for request empty cases and corner cases
Jun 5, 2022
781ca67
Add flexible inits for requests
Jun 5, 2022
915fc82
Add identifier generation inits
Jun 5, 2022
fb1846b
Joined request notification extensions
Jun 5, 2022
a681189
Renamed files
Jun 5, 2022
9aa5f3d
Implemented default JSONRPC error cases
Jun 5, 2022
0df90e2
Declared RPCRequestConvertible as public
Jun 5, 2022
9ed78fd
Remove rebase artifacts
Jun 5, 2022
68f1484
Added debug description to request param primitives error
Jun 8, 2022
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
74 changes: 73 additions & 1 deletion .swiftpm/xcode/xcshareddata/xcschemes/WalletConnect.xcscheme
Original file line number Diff line number Diff line change
Expand Up @@ -132,14 +132,56 @@
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "Chat"
BuildableName = "Chat"
BlueprintName = "Chat"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "Commons"
BuildableName = "Commons"
BlueprintName = "Commons"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "JSONRPC"
BuildableName = "JSONRPC"
BlueprintName = "JSONRPC"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
onlyGenerateCoverageForSpecifiedTargets = "YES">
codeCoverageEnabled = "YES">
<CodeCoverageTargets>
<BuildableReference
BuildableIdentifier = "primary"
Expand Down Expand Up @@ -200,6 +242,36 @@
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "CommonsTests"
BuildableName = "CommonsTests"
BlueprintName = "CommonsTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "WalletConnectUtilsTests"
BuildableName = "WalletConnectUtilsTests"
BlueprintName = "WalletConnectUtilsTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "JSONRPCTests"
BuildableName = "JSONRPCTests"
BlueprintName = "JSONRPCTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
Expand Down
12 changes: 12 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ let package = Package(
path: "Sources/WalletConnectKMS"),
.target(
name: "WalletConnectUtils",
dependencies: ["Commons"]),
Comment on lines 37 to +39
Copy link
Contributor

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 a Commons ?

Copy link
Author

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 rename WalletConnectUtils to WalletConnectCore and, over time, extract the components (storage, logging) to their own packages.

.target(
name: "JSONRPC",
dependencies: ["Commons"]),
.target(
name: "Commons",
dependencies: []),
.testTarget(
name: "WalletConnectSignTests",
Expand All @@ -59,6 +65,12 @@ let package = Package(
.testTarget(
name: "WalletConnectUtilsTests",
dependencies: ["WalletConnectUtils"]),
.testTarget(
name: "JSONRPCTests",
dependencies: ["JSONRPC", "TestingUtils"]),
.testTarget(
name: "CommonsTests",
dependencies: ["Commons", "TestingUtils"]),
],
swiftLanguageVersions: [.v5]
)
199 changes: 199 additions & 0 deletions Sources/Commons/AnyCodable.swift
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."))
}
}
}
}
65 changes: 65 additions & 0 deletions Sources/Commons/Either.swift
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)
}
}
}
Loading