Coder Social home page Coder Social logo

borchero / squid Goto Github PK

View Code? Open in Web Editor NEW
71.0 8.0 6.0 2.26 MB

Declarative and Reactive Networking for Swift.

Home Page: https://squid.borchero.com

License: MIT License

Swift 99.49% Ruby 0.51%
swift networking combine websockets http pagination retrying declarative reactive urlsession

squid's Introduction

Squid

Cocoapods Build CocoaPods Documentation

Squid is a declarative and reactive networking library for Swift. Developed for Swift 5, it aims to make use of the latest language features. The framework's ultimate goal is to enable easy networking that makes it easy to write well-maintainable code.

In its very core, it is built on top of Apple's Combine framework and uses Apple's builtin URL loading system for networking.

Features

At the moment, the most important features of Squid can be summarized as follows:

  • Sending HTTP requests and receiving server responses.
  • Retrying HTTP requests with a wide range of retriers.
  • Automated requesting of new pages for paginated HTTP requests.
  • Sending and receiving messages over WebSockets.
  • Abstraction of API endpoints and security mechanisms for a set of requests.

Quickstart

When first using Squid, you might want to try out requests against a Test API.

To perform a sample request at this API, we first define an API to manage its endpoint:

struct MyApi: HttpService {

    var apiUrl: UrlConvertible {
        "jsonplaceholder.typicode.com"
    }
}

Afterwards, we can define the request itself:

struct Todo: Decodable {

    let userId: Int
    let id: Int
    let title: String
    let completed: Bool
}

struct TodoRequest: JsonRequest {

    typealias Result = Todo
    
    let id: Int
    
    var routes: HttpRoute {
        ["todos", id]
    }
}

And schedule the request as follows:

let api = MyApi()
let request = TodoRequest(id: 1)

// The following request will be scheduled to `https://jsonplaceholder.typicode.com/todos/1`
request.schedule(with: api).ignoreError().sink { todo in 
    // work with `todo` here
}

Installation

Squid is available via the Swift Package Manager as well as CocoaPods.

Swift Package Manager

Using the Swift Package Manager is the simplest option to use Squid. In Xcode, simply go to File > Swift Packages > Add Package Dependency... and add this repository.

If you are developing a Swift package, adding Squid as a dependency is as easy as adding it to the dependencies of your Package.swift like so:

dependencies: [
    .package(url: "https://github.com/borchero/Squid.git")
]

CocoaPods

If you are still using CocoaPods or are required to use it due to other dependencies that are not yet available for the Swift Package Manager, you can include the following line in your Podfile to use the latest version of Squid:

pod 'Squid'

Documentation

Documentation is available here and provides both comprehensive documentation of the library's public interface as well as a series of guides teaching you how to use Squid to great effect. Expect more guides to be added shortly.

License

Squid is licensed under the MIT License.

squid's People

Contributors

aplr avatar borchero avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

squid's Issues

Fetch Token Before Sending Request

Hello,

I stumbled upon this library and decided to give it a try! I have a public and private API used by my app and the private API is behind Firebase Authentication. Is there a way to fetch the authorization token right before making the request. The call to get the token is async.

if let cu = firebaseAuth.currentUser {
                    cu.getIDTokenForcingRefresh(true) { (token, error) in
                        if let t = token {
                            urlRequest.setValue("Bearer \(t)", forHTTPHeaderField: "Authorization")
                            closure(.success(urlRequest))
                        } else {
                            closure(.failure(.underlying(NetworkingErrors.noToken, nil)))
                        }
                    }
                }

Here is an example of how I currently do it. This closure is called after the new token as been set. I see there is a synchronous way to accomplish this with this library, but how about an async one? ๐Ÿ˜„

http instead of https?

I'm using a local development server, which doesn't work over http. Yet Squid always loads everything over https. I see there's a usesSecureProtocol property but that has to be specified per request, not on the HttpService level. Am I missing something? ๐Ÿค”

Crashes as soon as I schedule a request

I'm using the sample code found in the "Basic" guide, however, as soon as I schedule the request, my app crashes in Concurrency.swift on line 60 with an EXC_BAD_INSTRUCTION:

image

I can paste my code, but it's literally just the sample code found in the documentation, in a new (empty) project.

Failure and Loading?

I love this library really amazing and great work. I was wondering if you could give an example as to wrap the response in a Loadable or something that shows loading state? and also how to do error handling?

Not receiving completion for 401 Unauthorize

`

func create(roleName: String, claims: [Claim]) {
        self.cancellables = []

    let request = RoleCreateRequest(name: roleName, claims: claims)
    let response = request.schedule(with: service)

    response
        .print("role")
        .receive(on: RunLoop.main)
        .sink(receiveCompletion: { completion in
            switch completion {
            case .failure(let error):
                switch error {
                case .requestFailed(statusCode: 401, response: _):
                    self.manager.user = LoginResponse.empty
                default:
                    print("RolesStore create error:", error.localizedDescription)
                }
            case .finished:
                print("RolesStore create finished")
            }
        }, receiveValue: { role in
            self.roles.append(role)
        })
        .store(in: &cancellables)
}

`

I have no problem receiving value in the response, but when the token expires, I am not hitting the completion .failure. Here is the log when receiving 401:

role: receive subscription: (Filter)
role: request unlimited
[Squid]
[Squid] Scheduled request RoleCreateRequest with identifier 5:
[Squid] - Method: POST
[Squid] - Url: http://192.168.100.9:3000/api/v1/roles
[Squid] - Headers: * Authorization => Bearer%20eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI1Y2Q1NGZmYmYzM2M2NzAwMjMzMDdiZGYiLCJyb2xlIjoiNWM2YmUzYWM3MTBkYjMyYTljNzdhMTJhIiwiaWF0IjoxNTgwNjIwNDEzLCJleHAiOjE1ODA2MjA0NzMsImF1ZCI6Imh0dHA6Ly9hcGkvdjEvcHJvdGVjdGVkIiwiaXNzIjoiaHR0cHM6Ly9hdml0ZWxsLm5vIiwic3ViIjoiYXZpdGVsbEBhdml0ZWxsLm5vIn0.FPJEARQJekgnQOUXH7XnYWe3OPjPD487f97g33a_mziO22j3QmqPRaldbOEKGdSoDXr0_4GeVDK5Kg6POGhFsw
[Squid] * Content-Type => application/json
[Squid] - Body: {
[Squid] "name" : "Splittegal",
[Squid] "claims" : [
[Squid] {
[Squid] "_id" : "5ce56067e242d4f812286013",
[Squid] "name" : "appearance.read"
[Squid] },
[Squid] {
[Squid] "_id" : "5cd9488e710db3496cad2f13",
[Squid] "name" : "building.kristiansand"
[Squid] },
[Squid] {
[Squid] "_id" : "5da81ce676ce794950a29a22",
[Squid] "name" : "building.luftslottet"
[Squid] }
[Squid] ]
[Squid] }
[Squid]
[Squid]
[Squid] Finished request RoleCreateRequest with identifier 5:
[Squid] - Status: 401
[Squid] - Headers: * Connection => keep-alive
[Squid] * Content-Length => 12
[Squid] * Content-Type => text/plain; charset=utf-8
[Squid] * Date => Sun, 02 Feb 2020 05:14:51 GMT
[Squid] * Strict-Transport-Security => max-age=15552000; includeSubDomains
[Squid] * Vary => Origin
[Squid] * X-Content-Type-Options => nosniff
[Squid] * X-DNS-Prefetch-Control => off
[Squid] * X-Download-Options => noopen
[Squid] * X-Frame-Options => SAMEORIGIN
[Squid] * X-XSS-Protection => 1; mode=block
[Squid] - Body: <12 bytes>
[Squid]
KeyChain load
SecureStore getValue
[Squid]
[Squid] Scheduled request TokenRequest with identifier 6:
[Squid] - Method: POST
[Squid] - Url: https://192.168.100.9:3000/api/v1/login
[Squid] - Headers: * Content-Type => application/json
[Squid] - Body: {
[Squid] "refresh_token" : ""
[Squid] }
[Squid]
2020-02-02 06:14:51.426697+0100 DuncanIdaho[2572:1247962] [BoringSSL] boringssl_context_handle_fatal_alert(1872) [C6:2][0x102ee5780] write alert, level: fatal, description: protocol version
2020-02-02 06:14:51.427256+0100 DuncanIdaho[2572:1247962] [BoringSSL] boringssl_context_error_print(1862) boringssl ctx 0x281a03d50: 4367252696:error:100000f7:SSL routines:OPENSSL_internal:WRONG_VERSION_NUMBER:/BuildRoot/Library/Caches/com.apple.xbs/Sources/boringssl/boringssl-283.60.3/ssl/tls_record.cc:242:
2020-02-02 06:14:51.434045+0100 DuncanIdaho[2572:1247962] [BoringSSL] boringssl_session_handshake_error_print(111) [C6:2][0x102ee5780] 4367252696:error:100000f7:SSL routines:OPENSSL_internal:WRONG_VERSION_NUMBER:/BuildRoot/Library/Caches/com.apple.xbs/Sources/boringssl/boringssl-283.60.3/ssl/tls_record.cc:242:
2020-02-02 06:14:51.434510+0100 DuncanIdaho[2572:1247962] [BoringSSL] nw_protocol_boringssl_handshake_negotiate_proceed(726) [C6:2][0x102ee5780] handshake failed at state 12288
2020-02-02 06:14:51.435847+0100 DuncanIdaho[2572:1247962] [] tcp_input [C6:3] flags=[R] seq=1341546743, ack=0, win=0 state=LAST_ACK rcv_nxt=1341546743, snd_una=3618034709
2020-02-02 06:14:51.436225+0100 DuncanIdaho[2572:1247962] Connection 6: received failure notification
2020-02-02 06:14:51.436471+0100 DuncanIdaho[2572:1247962] Connection 6: failed to connect 3:-9858, reason -1
2020-02-02 06:14:51.436512+0100 DuncanIdaho[2572:1247962] Connection 6: encountered error(3:-9858)
2020-02-02 06:14:51.449717+0100 DuncanIdaho[2572:1247962] [BoringSSL] boringssl_context_handle_fatal_alert(1872) [C7:2][0x1044f16e0] write alert, level: fatal, description: protocol version
2020-02-02 06:14:51.449976+0100 DuncanIdaho[2572:1247962] [BoringSSL] boringssl_context_error_print(1862) boringssl ctx 0x281a39770: 4367252696:error:100000f7:SSL routines:OPENSSL_internal:WRONG_VERSION_NUMBER:/BuildRoot/Library/Caches/com.apple.xbs/Sources/boringssl/boringssl-283.60.3/ssl/tls_record.cc:242:
2020-02-02 06:14:51.450736+0100 DuncanIdaho[2572:1247962] [BoringSSL] boringssl_session_handshake_error_print(111) [C7:2][0x1044f16e0] 4367252696:error:100000f7:SSL routines:OPENSSL_internal:WRONG_VERSION_NUMBER:/BuildRoot/Library/Caches/com.apple.xbs/Sources/boringssl/boringssl-283.60.3/ssl/tls_record.cc:242:
2020-02-02 06:14:51.450788+0100 DuncanIdaho[2572:1247962] [BoringSSL] nw_protocol_boringssl_handshake_negotiate_proceed(726) [C7:2][0x1044f16e0] handshake failed at state 12288
2020-02-02 06:14:51.451739+0100 DuncanIdaho[2572:1247962] Connection 7: received failure notification
2020-02-02 06:14:51.451929+0100 DuncanIdaho[2572:1247962] Connection 7: failed to connect 3:-9858, reason -1
2020-02-02 06:14:51.451967+0100 DuncanIdaho[2572:1247962] Connection 7: encountered error(3:-9858)
2020-02-02 06:14:51.454251+0100 DuncanIdaho[2572:1247962] [] tcp_input [C7:3] flags=[R] seq=55613446, ack=0, win=0 state=LAST_ACK rcv_nxt=55613446, snd_una=2297212123
2020-02-02 06:14:51.454620+0100 DuncanIdaho[2572:1247962] [] tcp_input [C7:3] flags=[R] seq=55613446, ack=0, win=0 state=CLOSED rcv_nxt=55613446, snd_una=2297212123
2020-02-02 06:14:51.464957+0100 DuncanIdaho[2572:1248179] [BoringSSL] boringssl_context_handle_fatal_alert(1872) [C8:2][0x102ee58f0] write alert, level: fatal, description: protocol version
2020-02-02 06:14:51.465033+0100 DuncanIdaho[2572:1248179] [BoringSSL] boringssl_context_error_print(1862) boringssl ctx 0x281a03d50: 4344147880:error:100000f7:SSL routines:OPENSSL_internal:WRONG_VERSION_NUMBER:/BuildRoot/Library/Caches/com.apple.xbs/Sources/boringssl/boringssl-283.60.3/ssl/tls_record.cc:242:
2020-02-02 06:14:51.465445+0100 DuncanIdaho[2572:1248179] [BoringSSL] boringssl_session_handshake_error_print(111) [C8:2][0x102ee58f0] 4344147880:error:100000f7:SSL routines:OPENSSL_internal:WRONG_VERSION_NUMBER:/BuildRoot/Library/Caches/com.apple.xbs/Sources/boringssl/boringssl-283.60.3/ssl/tls_record.cc:242:
2020-02-02 06:14:51.465501+0100 DuncanIdaho[2572:1248179] [BoringSSL] nw_protocol_boringssl_handshake_negotiate_proceed(726) [C8:2][0x102ee58f0] handshake failed at state 12288
2020-02-02 06:14:51.467352+0100 DuncanIdaho[2572:1248179] [] tcp_input [C8:3] flags=[R] seq=2124226899, ack=0, win=0 state=LAST_ACK rcv_nxt=2124226899, snd_una=3091635292
2020-02-02 06:14:51.467565+0100 DuncanIdaho[2572:1248179] Connection 8: received failure notification
2020-02-02 06:14:51.467845+0100 DuncanIdaho[2572:1248179] Connection 8: failed to connect 3:-9858, reason -1
2020-02-02 06:14:51.467882+0100 DuncanIdaho[2572:1248179] Connection 8: encountered error(3:-9858)
2020-02-02 06:14:51.470137+0100 DuncanIdaho[2572:1248179] Task .<8> HTTP load failed, 0/0 bytes (error code: -1200 [3:-9858])
2020-02-02 06:14:51.470705+0100 DuncanIdaho[2572:1248179] [] tcp_input [C8:3] flags=[R] seq=2124226899, ack=0, win=0 state=CLOSED rcv_nxt=2124226899, snd_una=3091635292
2020-02-02 06:14:51.474155+0100 DuncanIdaho[2572:1248599] Task .<8> finished with error [-1200] Error Domain=NSURLErrorDomain Code=-1200 "An SSL error has occurred and a secure connection to the server cannot be made." UserInfo={NSErrorFailingURLStringKey=https://192.168.100.9:3000/api/v1/login, NSLocalizedRecoverySuggestion=Would you like to connect to the server anyway?, _kCFStreamErrorDomainKey=3, _NSURLErrorFailingURLSessionTaskErrorKey=LocalUploadTask .<8>, _NSURLErrorRelatedURLSessionTaskErrorKey=(
"LocalUploadTask .<8>"
), NSLocalizedDescription=An SSL error has occurred and a secure connection to the server cannot be made., NSErrorFailingURLKey=https://192.168.100.9:3000/api/v1/login, NSUnderlyingError=0x281711170 {Error Domain=kCFErrorDomainCFNetwork Code=-1200 "(null)" UserInfo={_kCFStreamPropertySSLClientCertificateState=0, _kCFNetworkCFStreamSSLErrorOriginalValue=-9858, _kCFStreamErrorDomainKey=3, _kCFStreamErrorCodeKey=-9858}}, _kCFStreamErrorCodeKey=-9858}
[Squid]
[Squid] Finished request TokenRequest with identifier 6:
[Squid] - Unknown error: unknown(Error Domain=NSURLErrorDomain Code=-1200 "An SSL error has occurred and a secure connection to the server cannot be made." UserInfo={NSErrorFailingURLStringKey=https://192.168.100.9:3000/api/v1/login, NSLocalizedRecoverySuggestion=Would you like to connect to the server anyway?, _kCFStreamErrorDomainKey=3, _NSURLErrorFailingURLSessionTaskErrorKey=LocalUploadTask .<8>, _NSURLErrorRelatedURLSessionTaskErrorKey=(
[Squid] "LocalUploadTask .<8>"
[Squid] ), NSLocalizedDescription=An SSL error has occurred and a secure connection to the server cannot be made., NSErrorFailingURLKey=https://192.168.100.9:3000/api/v1/login, NSUnderlyingError=0x281711170 {Error Domain=kCFErrorDomainCFNetwork Code=-1200 "(null)" UserInfo={_kCFStreamPropertySSLClientCertificateState=0, _kCFNetworkCFStreamSSLErrorOriginalValue=-9858, _kCFStreamErrorDomainKey=3, _kCFStreamErrorCodeKey=-9858}}, _kCFStreamErrorCodeKey=-9858})
[Squid]

Thread Sanitizer warnings

I get Swift access race in Squid.Locked.lock() -> () at 0x7b080002f4e0 when running Squid under Xcode thread sanitizer. My understanding is there's not currently an actual issue in the threading code due to the way that Swift is generating code, but the thread analyzer can't be sure, so generates the warning.

I think to fix warning you can replace:

internal class Locked<Value> {
    private var _lock = os_unfair_lock()
    ...

with:

internal class Locked<Value> {
    private var _lock: UnsafeMutablePointer<os_unfair_lock>

    init(_ value: Value) {
        _lock = UnsafeMutablePointer<os_unfair_lock>.allocate(capacity: 1)
        _lock.initialize(to: os_unfair_lock())
        self._value = value
    }

I found this solution and an explanation here:

http://www.russbishop.net/the-law

Cache specific request, not whole service.

I think that is really a common situation, when you need to cache result of a specific request that ideological belongs to some HttpService. But now all requests that belong to the service with cache hook are caching.

For example, I have a service that provides auth methods and caching ableProfile request is a good idea, but caching isLogined request looks pretty weird. I really don't want to create HttpService for each request I need to cache.

Do you plan implement that sort of functionality or did I miss something and there is some elegant way to do it?

Request headers not updating when retrying

I am using the retrier and updating the stateful service that provides the accessToken & refreshToken values exactly as is shown in your example, but the retried request always uses the values from the original request, not the updated values.

Is there any way to change the header values before retrying the request?

Headers are getting URL encoded?

In my HTTPService I set a header:

var header: HttpHeader {
  HttpHeader([
    .accept: HttpMimeType.json.rawValue,
    .authorization: "Bearer \(token)",
  ])
}

This is getting sent as Bearer%20DsNYO6w84ZaJBJyYWHEtStTBoRCquu3XAQc0tVH8ggq3orn7UAYGYvfrU8ZjYKQTHvKB2W28xZuUjMmZ, and the server really doesn't like this. Why is it getting encoded, and can I do something about that?

Decoding snake case

I am posting a request:

struct UserCreateRequest: JsonRequest {
    typealias Result = User

    var firstName: String
    var lastName: String
    var email: String
    var password: String
    var role: String

    var routes: HttpRoute {
        ["users"]
    }
    var method: HttpMethod { .post }
    var body: HttpBody {
        HttpData.Json(UserCreateBody(firstName: firstName, lastName: lastName
        , email: email, password: password, role: role))
    }
    var usesSecureProtocol: Bool {
        false
    }
    var decodeSnakeCase: Bool {
        false
    }
}

The body of the request looks OK in the log, with camelcase:

[Squid] Scheduled request UserCreateRequest with identifier 5:
[Squid] - Method: POST
[Squid] - Url: http://192.168.100.9:3000/api/v1/users
[Squid] - Headers: * Authorization => Bearer%20eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI1Y2Q1NGZmYmYzM2M2NzAwMjMzMDdiZGYiLCJyb2xlIjoiNWM2YmUzYWM3MTBkYjMyYTljNzdhMTJhIiwiaWF0IjoxNTgxMTYzMjgzLCJleHAiOjE1ODExNjY4ODMsImF1ZCI6Imh0dHA6Ly9hcGkvdjEvcHJvdGVjdGVkIiwiaXNzIjoiaHR0cHM6Ly9hdml0ZWxsLm5vIiwic3ViIjoiYXZpdGVsbEBhdml0ZWxsLm5vIn0.OttzMT3fCSyNJ9BtQ9HNb-8-gWFwxPUqFSUbDNJTp97BrXIOruAIhoytgRZ00N_osPju8jMGQVdF0dEBlE-ksg
[Squid] * Content-Type => application/json
[Squid] - Body: {
[Squid] "firstName" : "Donald",
[Squid] "email" : "[email protected]",
[Squid] "lastName" : "Duck",
[Squid] "password" : "duckduckgo",
[Squid] "role" : "5c6be3ac710db32a9c77a12b"
[Squid] }

But what is sent is snake case:
users controller create ctx.request.body: {
email: '[email protected]',
password: 'duckduckgo',
last_name: 'Duck',
first_name: 'Donald',
role: '5c6be3ac710db32a9c77a12b'
}

ValidationError: User validation failed: lastName: Path lastName is required., firstName: Path firstName is required.
at new ValidationError (/Users/imyrvold/development/Advantek/sdweb/backend/node_modules/mongoose/lib/error/validation.js:30:11)
at model.Document.invalidate (/Users/imyrvold/development/Advantek/sdweb/backend/node_modules/mongoose/lib/document.js:2250:32)
at p.doValidate.skipSchemaValidators (/Users/imyrvold/development/Advantek/sdweb/backend/node_modules/mongoose/lib/document.js:2099:17)
at /Users/imyrvold/development/Advantek/sdweb/backend/node_modules/mongoose/lib/schematype.js:978:9
at processTicksAndRejections (internal/process/task_queues.js:75:11)

Allow for a way to mutate URLRequests before they're sent

Some context: I'm working with an API that requires a header field that is a hash computed with several elements of the http request (the POST body, the path, etc.).

I was able to do it by forking and making a couple of internal structs public (HttpRequest and the Network Scheduler), but it feels rather hack-ish, not quite sure what a good implementation would be, will try to think of a nicer implementation, but in the meantime, thought I'd let you know that this is a feature that some people might need. Loving the library so far BTW, nice work!

Mock requests for testing

I'm trying to Mock endpoints with Squid and Mocker.

Here's the basic pattern of what I think I want to do:

import Mocker
import Squid

extension HttpService {
    func mock<R>(request: R, statusCode: Int = 200, data: [Mock.HTTPMethod : Data]) where R: Request {
        Mock(url: ..., dataType: .json, statusCode: statusCode, data: data)
    }
}

The problem I'm not sure how to generate a final URL from a Squid Request and Squid HttpService. I know it does this internally, but I can't figure how to to make it generate the URL myself. Is there some method that I'm overlooking? Or is this something that could be added?

Multipart body payload?

Are there any plans to add support for multipart data? I have one request where I need to upload an image with some data in one multipart data request, and sadly this doesn't seem possible with Squid.

Ideas on how to implement a caching layer

I'm trying to add a caching layer to my service (the url building/fetching/etc. is done with Squid). The obvious thing to do would be to use request URLs as cache keys, and return results from the cache before scheduling requests, however, the fact that all the URL logic is abstracted away (by HttpRoute, HttpQuery etc.) makes it hard. I tried to hack into the "prepare" function but couldn't really come up with a solution.

I was wondering if you had any ideas on how I could achieve such a goal? Any ideas/pointers would be appreciated!

Feedback, some ideas for improvements

So far I am really liking this library, well done!

However, there is one thing that is kind of bugging me: making the request structs gets tedious, and takes quite a lot of lines of code.

struct LoginTokenRequest: JsonRequest {
  private struct Payload: Encodable {
    let email: String
    let password: String
  }

  typealias Result = Token

  let email: String
  let password: String

  var method: HttpMethod = .post
  var routes: HttpRoute = ["auth", "tokens"]

  var body: HttpBody {
    HttpData.Json(Payload(email: email, password: password))
  }
}

Now imagine having tens of requests - this gets so long so fast.

A few observations.

It's a bit annoying that the HttpData.Json payload needs to be an Encodable thing. This causes the need for one-off payload structs, and repeated code to copy the properties over. On the other hand, HttpQuery can be created using a simple dictionary. Is it an idea to add an initializer that takes a dictionary instead of an Encodable struct/object? That way we could get rid of the Payload struct in my example. Turning a dict into Data is simple enough of course.

Or... could LoginTokenRequest itself be the payload? It has the email and password properties after all, so in theory it could be the payload? ๐Ÿค” Like, any properties on the request will be sent as body payload. Not sure if that's a terrible idea haha.

Another thing that could work to reduce the code is if JsonRequest wasn't a protocol but a Struct - that way you could simply create them using an initializer, have them in an enum, whatever.

I tried to implement something like that myself, but the typealias Result is making me hit roadblocks:

struct CoreRequest<Result: Decodable>: JsonRequest {
  var method: HttpMethod
  var routes: HttpRoute
  var body: HttpBody
}

enum Requests {
  case getLoginToken(email: String, password: String)

  var request: CoreRequest {
    switch self {
    case .getLoginToken(let email, let password):
      return CoreRequest<Token>(method: .post, routes: ["auth", "tokens"], body: HttpData.Json(payload))
    }
  }
}

As you can see, having 10 or 20 requests like this vs the "normal" method of one struct per request would save a LOT of repeating typing. But sadly this doesn't work since there's the generic type requirement. I would love to get this to work though. See also https://github.com/gonzalezreal/SimpleNetworking for inspiration.

Very curious about your thoughts and I'd be happy to brainstorm some ideas, test any of them, etc.

Add Package.swift definition

As a package developer I want to copy/paste the dependency definition from the readme, which is missing here. You could add sth like that to the readme to simplify usage:

If you are developing a Swift package, adding Squid as a dependency is as easy as adding it to the dependencies value of your Package.swift like so:

dependencies: [
    .package(url: "https://github.com/borchero/Squid.git", from: "1.0.0")
]

See my PR #2

Set a default, global encoder/decoder

The API that I am working with does not use snake_case, but rather camelCase. That means that I now have to supply my own JSONEncoder every time I use a HttpData.Json body. Would it be possible to set a global default instead?

Global error mapping

It would be nice to set a global error mapping. My API returns errors in a certain format, so I now have to add .mapError every time I make a request.

PasswordResetTokenRequest(email: email)
  .schedule(with: api)
  .receive(on: DispatchQueue.main)
  .mapError(CoreAPIError.fromError)
  .sink(
    // etc

CoreAPIError looks like this, in case you're wondering:

struct CoreAPIError: Decodable, Error, LocalizedError {
  let message: String?
  let errors: [String: [String]]?

  var errorDescription: String? {
    if let errors = errors {
      let values = errors.flatMap { $0.value }
      return values.joined(separator: "\n")
    }
    return message ?? "An error occured"
  }

  static func fromError(_ error: Squid.Error) -> CoreAPIError {
    if case .requestFailed(_, let data) = error, let coreError = try? JSONDecoder().decode(CoreAPIError.self, from: data) {
      return coreError
    }

    return CoreAPIError(message: error.localizedDescription, errors: nil)
  }
}

The problem is that I always have to remember to add this one line to every call site. It would be great if it would be possible to set this as a default, global thing that happens to all requests.

Paginator examples?

I love this library, it is awesome. Could you write some Paginator examples? I have parts of my app where I want to have an endless scroll while loading more data from the server, but not sure on how to use Paging properly in Squid!

Thanks again :)

How to see response headers

import Squid
import Foundation

struct ListPlaylistsRequest: JsonRequest {
    typealias Result = [Playlist]

    var routes: HttpRoute {
        ["playlist", "v1"]
    }

    var method: HttpMethod {
        .get
    }
}

This request talks to my server and there are some headers that are returned from the request that I want to have access to. I tried using a ServiceHook but I don't think I get the response headers. Ideally I can still have it decoded automatically to JSON as well. I am not sure how to proceed. Love the library!

Request is doing a GET when it's supposed to do a POST

I have a very simple request:

struct LoginRequest: JsonRequest {
  typealias Result = LoginResponse

  let username: String
  let password: String

  let method: HttpMethod = .post
  let routes: HttpRoute = ["user", "login"]

  var body: HttpBody {
    HttpData.Json(["username": username, "password": password])
  }
}

LoginRequest(username: username, password: password)
        .schedule(with: apiService)

But I get this error:

[Squid] Scheduled request `LoginRequest` with identifier 3:
[Squid]     - Method:   POST
[Squid]     - Url:      https://example.com/api/user/login
[Squid]     - Headers:  * Content-Type => application/json
[Squid]     - Body:     {
[Squid]                   "username" : "s",
[Squid]                   "password" : "d"
[Squid]                 }
[Squid]  
[Squid @ 22:01:47.987] 
[Squid] Finished request `LoginRequest` with identifier 3:
[Squid]     - Status:   405
[Squid]     - Headers:  * Allow => POST, OPTIONS
[Squid]                 * Connection => keep-alive
[Squid]                 * Content-Length => 40
[Squid]                 * Content-Type => application/json
[Squid]                 * Date => Thu, 15 Oct 2020 20:01:47 GMT
[Squid]                 * Server => gunicorn/20.0.4
[Squid]                 * Strict-Transport-Security => max-age=60; includeSubDomains; preload
[Squid]                 * Via => 1.1 vegur
[Squid]                 * X-Content-Type-Options => nosniff
[Squid]                 * X-Frame-Options => DENY
[Squid]                 * X-Xss-Protection => 1; mode=block
[Squid]     - Body:     <40 bytes>
[Squid]                 {
[Squid]                   "detail" : "Method \"GET\" not allowed."
[Squid]                 }
[Squid]  

If I run it with the Charles proxy running, then yea it is actually doing a GET request. If I change LoginRequest.method to .put, the error I get back changes to "Method \"PUT\" not allowed.".

Why is .post not actually doing a POST but a GET? ๐Ÿค”

Sink not printing any values

Sink not working here, neither breakpoint nor printing users.

    func testNetworkAPI() {
        let service = MyApi()
        let request = UserRequest()
        let response = request.schedule(with: service)

        _ = response.sink(receiveCompletion: { completion in
            switch completion {
            case .failure(let error):
                print("Request failed due to: \(error)")
            case .finished:
                print("Request finished.")
            }
        }) { users in
            print("Received users: \(users)")
        }
    }

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.