Skip to content
This repository was archived by the owner on Mar 8, 2024. It is now read-only.
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ Package.resolved
.swiftpm
.icloud
.vscode
.idea
docs
2 changes: 1 addition & 1 deletion Sources/Squid/Core/Body/HttpData+Json.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ extension HttpData {
/// - Parameter encoder: The JSON encoder to use for encoding. When set to `nil`, a JSON
/// encoder is used where camel case attribute names are converted into
/// snake case.
public init(_ value: T, encoder: JSONEncoder? = nil) {
public init(_ value: T, encoder: JSONEncoder? = SquidCoders.shared.encoder) {
self.value = value
self.encoder = encoder ?? snakeCaseJSONEncoder()
}
Expand Down
10 changes: 10 additions & 0 deletions Sources/Squid/Core/Request/HttpHeader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,16 @@ extension HttpHeader: Hashable {
}
}

extension HttpHeader: CustomStringConvertible {
public var description: String {
var headers: [String: String] = [:]
fields.forEach { (key: HttpHeader.Field, value: String) in
headers[key.name] = value
}
return headers.httpHeaderDescription ?? ""
}
}

extension HttpHeader: ExpressibleByDictionaryLiteral {

public init(dictionaryLiteral elements: (Field, String)...) {
Expand Down
13 changes: 13 additions & 0 deletions Sources/Squid/Core/Request/HttpQuery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,19 @@ extension HttpQuery: ExpressibleByDictionaryLiteral {
}
}

extension HttpQuery: CustomStringConvertible {
public var description: String {
parameters
.reduce([]) { (result: [String], tuple: (key: String, value: String)) -> [String] in
var res: [String] = []
res.append(contentsOf: result)
res.append("\(tuple.key)=\(tuple.value)")
return res
}
.joined(separator: "&")
}
}

extension HttpQuery: Equatable {

public static func == (lhs: HttpQuery, rhs: HttpQuery) -> Bool {
Expand Down
6 changes: 6 additions & 0 deletions Sources/Squid/Core/Request/HttpRoute.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ extension HttpRoute: Hashable {
}
}

extension HttpRoute: CustomStringConvertible {
public var description: String {
paths.joined(separator: "/")
}
}

// MARK: Operators
extension HttpRoute {

Expand Down
16 changes: 15 additions & 1 deletion Sources/Squid/Request/NetworkRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@

import Foundation

public protocol RequestStringConvertible {
func description<S>(with service: S) -> String where S: HttpService
}

/// The network request is a protocol that serves as a base protocol for requests on remote servers.
/// The protocol is mainly used as a common base for `Request` and `StreamRequest`. You should
/// never directly use this protocol as it hardly provides any functionality.
public protocol NetworkRequest {
public protocol NetworkRequest: RequestStringConvertible {

// MARK: Protocol
/// Whether the request makes use of the secure counterpart of the protocol (e.g. "https" for
Expand Down Expand Up @@ -78,4 +82,14 @@ extension NetworkRequest {
public var timeout: TimeInterval {
return TimeInterval.infinity
}

public func description<S>(with service: S) -> String where S: HttpService {
let urlString = "\(routes.description)\(query.description.isEmpty ? "" : "?")\(query.description)"
let headersString = [service.header.description,header.description].filter({$0.isEmpty == false}).joined(separator: "\n")
return """
- Route: \(urlString)
- Headers: \(headersString.indent(spaces: 12, skipLines: 1))
"""
}
}

37 changes: 15 additions & 22 deletions Sources/Squid/Request/Request.swift
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,17 @@ extension Request {
zeroBasedPageIndex: zeroBasedPageIndex, decode: decode
)
}

public func description<S>(with service: S) -> String where S: HttpService {
let urlString = "\(routes.description)\(query.description.isEmpty ? "" : "?")\(query.description)"
let headersString = [service.header.description,header.description].filter({$0.isEmpty == false}).joined(separator: "\n")
return """
- Method: \(method.name)
- Route: \(urlString)
- Headers: \(headersString.indent(spaces: 12, skipLines: 1))
- Body: \(body.description.indent(spaces: 12, skipLines: 1))
"""
}
}

extension Request where Result == Data {
Expand Down Expand Up @@ -174,28 +185,13 @@ extension Request {
/// working with a JSON API where the returned data is a JSON object. As a requirement, the
/// request's result type must implement the `Decodable` protocol. The `decode(_:)` method is then
/// synthesized automatically by using a `JSONDecoder` and decoding the raw data to the specified
/// type. `decodeSnakeCase` can further be used to modify the behavior of the aforementioned
/// decoder.
public protocol JsonRequest: Request where Result: Decodable {

/// Defines whether the decoder decoding the raw data to the result type should consider
/// camel case in the Swift code as snake case in the JSON (i.e. `userID` would be parsed from
/// the field `user_id` if not specified explicity in the type to decode to). By default,
/// attributes are decoded using snake case attribute names.
var decodeSnakeCase: Bool { get }
}
/// type.
public protocol JsonRequest: Request where Result: Decodable { }

extension JsonRequest {

public var decodeSnakeCase: Bool {
return true
}

public func decode(_ data: Data) throws -> Result {
let decoder = JSONDecoder()
if self.decodeSnakeCase {
decoder.keyDecodingStrategy = .convertFromSnakeCase
}
let decoder = SquidCoders.shared.decoder
return try decoder.decode(Result.self, from: data)
}
}
Expand Down Expand Up @@ -226,10 +222,7 @@ extension JsonRequest {
return Paginator(
base: self, service: service, chunk: chunk, zeroBasedPageIndex: zeroBasedPageIndex
) { data, _ -> P in
let decoder = JSONDecoder()
if self.decodeSnakeCase {
decoder.keyDecodingStrategy = .convertFromSnakeCase
}
let decoder = SquidCoders.shared.decoder
return try decoder.decode(P.self, from: data)
}
}
Expand Down
3 changes: 2 additions & 1 deletion Sources/Squid/Scheduler/NetworkScheduler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ internal class NetworkScheduler {
let response = request
.responsePublisher(
service: service, session: session, socket: socket, requestId: requestId
).handleFailureServiceHook(service.hook)
)
.tryMap { result -> Result<R.Result, Squid.Error> in
switch result {
case .success(let message):
Expand All @@ -109,6 +109,7 @@ internal class NetworkScheduler {
return .failure(error)
}
}.mapError(Squid.Error.ensure(_:))
.handleFailureServiceHook(service.hook, for: request)
.mapError(service.mapError(_:))
.subscribe(on: queue)

Expand Down
16 changes: 8 additions & 8 deletions Sources/Squid/Service/Hooks/ServiceHook.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public protocol ServiceHook {
/// the user.
///
/// - Parameter error: The error that caused a request to fail.
func onFailure(_ error: Error)
func onFailure<R>(_ error: Error, _ urlRequest: R) where R: NetworkRequest
}

extension ServiceHook {
Expand All @@ -64,7 +64,7 @@ extension ServiceHook {
}

/// By default, no operation is performed.
public func onFailure(_ error: Error) {
public func onFailure<R>(_ error: Error, _ urlRequest: R) where R: NetworkRequest {
return
}
}
Expand Down Expand Up @@ -95,22 +95,22 @@ extension Publisher {
hook.onSuccess(request, output.1, result: output.0.body)
}, receiveCompletion: { completion in
if case .failure(let error) = completion {
hook.onFailure(error)
hook.onFailure(error, request)
}
})
}

internal func handleFailureServiceHook<R, E>(
_ hook: ServiceHook
) -> Publishers.HandleEvents<Self> where E: Error, Output == Result<R, E> {
internal func handleFailureServiceHook<R>(
_ hook: ServiceHook, for request: R
) -> Publishers.HandleEvents<Self> where R:StreamRequest, Output == Result<R.Result, Squid.Error> {
return self.handleEvents(
receiveOutput: { output in
if case .failure(let error) = output {
hook.onFailure(error)
hook.onFailure(error, request)
}
}, receiveCompletion: { completion in
if case .failure(let error) = completion {
hook.onFailure(error)
hook.onFailure(error, request)
}
})
}
Expand Down
26 changes: 3 additions & 23 deletions Sources/Squid/StreamRequest/StreamRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -122,38 +122,18 @@ extension StreamRequest where Result == String {
/// messages are always encoded to `Data` for a more efficient transmission. Messages received can
/// be either `String` or `Data`: in both cases, they are decoded equally (note that `Data` messages
/// might be more efficient.
public protocol JsonStreamRequest: StreamRequest where Message: Encodable, Result: Decodable {

/// Defines whether the encoder and decoder camel case in the Swift code as snake case in the
/// JSON (i.e. `userID` would be encoded as/decoded from the field `user_id` if not specified
/// explicity in the type to decode to). By default, attributes are encoded/decoded using snake
/// case attribute names.
var decodeSnakeCase: Bool { get }
}

extension JsonStreamRequest {

public var decodeSnakeCase: Bool {
return true
}
}
public protocol JsonStreamRequest: StreamRequest where Message: Encodable, Result: Decodable {}

extension JsonStreamRequest {

public func encode(_ message: Message) throws -> URLSessionWebSocketTask.Message {
let encoder = JSONEncoder()
if self.decodeSnakeCase {
encoder.keyEncodingStrategy = .convertToSnakeCase
}
let encoder = SquidCoders.shared.encoder

return .data(try encoder.encode(message))
}

public func decode(_ message: URLSessionWebSocketTask.Message) throws -> Result {
let decoder = JSONDecoder()
if self.decodeSnakeCase {
decoder.keyDecodingStrategy = .convertFromSnakeCase
}
let decoder = SquidCoders.shared.decoder

switch message {
case .string(let string):
Expand Down
9 changes: 9 additions & 0 deletions Sources/Squid/Utils/SquidCoders.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Foundation

public class SquidCoders {
private init() {}

public static let shared = SquidCoders()
public var decoder = JSONDecoder()
public var encoder = JSONEncoder()
}
4 changes: 4 additions & 0 deletions Squid.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
objects = {

/* Begin PBXBuildFile section */
609C3596948A29D590F2BCE7 /* SquidCoders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 609C34B982B6A7563A33C91F /* SquidCoders.swift */; };
C908D40024218035008352C0 /* ServiceHookTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C908D3FF24218035008352C0 /* ServiceHookTests.swift */; };
C908D402242180A5008352C0 /* Hook.swift in Sources */ = {isa = PBXBuildFile; fileRef = C908D401242180A5008352C0 /* Hook.swift */; };
C911D3CC23E7C2570065CD7D /* Squid.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C911D3C223E7C2570065CD7D /* Squid.framework */; };
Expand Down Expand Up @@ -86,6 +87,7 @@
/* End PBXContainerItemProxy section */

/* Begin PBXFileReference section */
609C34B982B6A7563A33C91F /* SquidCoders.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SquidCoders.swift; sourceTree = "<group>"; };
C908D3FF24218035008352C0 /* ServiceHookTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceHookTests.swift; sourceTree = "<group>"; };
C908D401242180A5008352C0 /* Hook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Hook.swift; sourceTree = "<group>"; };
C911D3C223E7C2570065CD7D /* Squid.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Squid.framework; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -360,6 +362,7 @@
C911D41023E7C3010065CD7D /* Dictionary.swift */,
C911D41123E7C3010065CD7D /* Url.swift */,
C911D41223E7C3010065CD7D /* String.swift */,
609C34B982B6A7563A33C91F /* SquidCoders.swift */,
);
path = Utils;
sourceTree = "<group>";
Expand Down Expand Up @@ -646,6 +649,7 @@
C911D42323E7C3010065CD7D /* HttpData+Image.swift in Sources */,
C9F2E25424155181007F2977 /* ServiceHook.swift in Sources */,
C911D43223E7C3010065CD7D /* AnyRetrierFactory.swift in Sources */,
609C3596948A29D590F2BCE7 /* SquidCoders.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down