Skip to content

Commit

Permalink
#256 JSON-RPC Package (& Commons) (#261)
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
Show file tree
Hide file tree
Showing 21 changed files with 1,321 additions and 208 deletions.
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"]),
.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

0 comments on commit 9e94672

Please sign in to comment.