Declarative and Reactive Networking for Swift.

Overview

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.

Comments
  • Ideas on how to implement a caching layer

    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!

    opened by SilverTab 9
  • Feedback, some ideas for improvements

    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.

    opened by kevinrenskers 6
  • Crashes as soon as I schedule a request

    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.

    opened by SilverTab 5
  • Allow for a way to mutate URLRequests before they're sent

    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!

    opened by SilverTab 5
  • Global error mapping

    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.

    opened by kevinrenskers 4
  • Paginator examples?

    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 :)

    opened by AAAstorga 4
  • How to see response headers

    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!

    opened by AAAstorga 3
  • Headers are getting URL encoded?

    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?

    opened by kevinrenskers 3
  • Request is doing a GET when it's supposed to do a POST

    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? 🤔

    opened by kevinrenskers 2
  • http instead of https?

    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? 🤔

    opened by kevinrenskers 2
  • Sink not printing any values

    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)")
            }
        }
    
    opened by simplisafevish 2
  • Minor improvements

    Minor improvements

    • [x] The onFailure callback now has the request paramater, useful for debugging/logging purposes
    • [x] The Squid encoder is passed to requests rather than forcing camelCase
    • [x] Improves description of Http Query/Route/Header ... (useful for logging purposes)
    opened by apouche 0
  • Thread Sanitizer warnings

    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

    opened by jessegrosjean 0
  • Mock requests for testing

    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?

    opened by jessegrosjean 0
  • Does it make sense to combine retriers? How would I go about it?

    Does it make sense to combine retriers? How would I go about it?

    For example I have an authenticated api, so I want to use an authentication retrier, but it also might have network errors, so I want exponential backoff. Is there a suggested way to combine the two?

    opened by jessegrosjean 0
  • Add request cache policy

    Add request cache policy

    Idea

    Sometimes it is wise not to cache some requests that belong to the same HttpService. It's not possible right now, and as a result, you must create multiply HttpServices with cache hook and without. The idea is that let request decide if the response should be cached.

    Solution

    Add a new property for the Request protocol, which is then used by the cache hook to determine if the response should be cached. For example:

    struct SomeRequest: JsonRequest {
        ...
        var shouldCacheResult: Bool {
            return false
        }
        ...
    }
    

    This property will be used in default CachingServiceHook, but you are also free to use this property in your cache-layer implementation

    opened by yapryntsev 0
Releases(1.5.1)
Owner
Oliver Borchert
MSc Data Engineering and Analytics @ TUM | Applied Science Intern @ AWS
Oliver Borchert
Swift-flows - Simplistic hot and cold flow-based reactive observer pattern for Swift… ideal for MVVM architectures

SwiftFlows Simplistic hot and cold flow-based reactive observer pattern for Swif

Tyler Suehr 0 Feb 2, 2022
Reactive WebSockets

RxWebSocket Reactive extensions for websockets. A lightweight abstraction layer over Starscream to make it reactive. Installation RxWebSocket is avail

Flávio Caetano 57 Jul 22, 2022
A reactive library for using URLSession

Reactive wrapper for URLSession using Combine. At its core, the library consist of the NetworkServiceClient protocol along with a minimal implementation NetworkService.

MFB Technologies, Inc. 2 Nov 20, 2022
Write clean, concise and declarative network code relying on URLSession, with the power of RxSwift. Inspired by Retrofit.

ReactiveAPI Reactive API library allows you to write clean, concise and declarative network code, with the power of RxSwift and URLSession! iOS Demo A

Sky UK Ltd 79 Nov 19, 2022
Deal with query items, HTTP headers, request body and more in an easy, declarative way

Reusable system for complex URL requests with Swift. Deal with query items, HTTP headers, request body and more in an easy, declarative way. Check out our engineering blog to learn more!

Parable Health 19 Sep 5, 2022
🌏 A zero-dependency networking solution for building modern and secure iOS, watchOS, macOS and tvOS applications.

A zero-dependency networking solution for building modern and secure iOS, watchOS, macOS and tvOS applications. ?? TermiNetwork was tested in a produc

Bill Panagiotopoulos 90 Dec 17, 2022
Lightweight Networking and Parsing framework made for iOS, Mac, WatchOS and tvOS.

NetworkKit A lightweight iOS, Mac and Watch OS framework that makes networking and parsing super simple. Uses the open-sourced JSONHelper with functio

Alex Telek 30 Nov 19, 2022
Bonjour networking for discovery and connection between iOS, macOS and tvOS devices.

Merhaba Bonjour networking for discovery and connection between iOS, macOS and tvOS devices. Features Creating Service Start & Stop Service Stop Brows

Abdullah Selek 67 Dec 5, 2022
QwikHttp is a robust, yet lightweight and simple to use HTTP networking library for iOS, tvOS and watchOS

QwikHttp is a robust, yet lightweight and simple to use HTTP networking library. It allows you to customize every aspect of your http requests within a single line of code, using a Builder style syntax to keep your code super clean.

Logan Sease 2 Mar 20, 2022
A Swift Multiplatform Single-threaded Non-blocking Web and Networking Framework

Serverside non-blocking IO in Swift Ask questions in our Slack channel! Lightning (formerly Edge) Node Lightning is an HTTP Server and TCP Client/Serv

SkyLab 316 Oct 6, 2022
Meet Corvus, the first strongly declarative server-side framework.

Corvus Corvus is the first truly declarative server-side framework for Swift. It provides a declarative, composable syntax which makes it easy to get

null 42 Jun 29, 2022
Asynchronous socket networking library for Mac and iOS

CocoaAsyncSocket CocoaAsyncSocket provides easy-to-use and powerful asynchronous socket libraries for macOS, iOS, and tvOS. The classes are described

Robbie Hanson 12.3k Jan 8, 2023
A delightful networking framework for iOS, macOS, watchOS, and tvOS.

AFNetworking is a delightful networking library for iOS, macOS, watchOS, and tvOS. It's built on top of the Foundation URL Loading System, extending t

AFNetworking 33.3k Jan 5, 2023
Showcasing simple SwiftUI and networking capabilities

CovidCounts CovidCounts is powered by SwiftUI. It allows a user to view COVID related data for different U.S. States. App Purpose This app is showcasi

Joel Sereno 1 Oct 15, 2021
Swish is a networking library that is particularly meant for requesting and decoding JSON via Decodable

Swish Nothing but net(working). Swish is a networking library that is particularly meant for requesting and decoding JSON via Decodable. It is protoco

thoughtbot, inc. 369 Nov 14, 2022
A networking library for iOS, macOS, watchOS and tvOS

Thunder Request Thunder Request is a Framework used to simplify making http and https web requests. Installation Setting up your app to use ThunderBas

3 SIDED CUBE 16 Nov 19, 2022
Elegant HTTP Networking in Swift

Alamofire is an HTTP networking library written in Swift. Features Component Libraries Requirements Migration Guides Communication Installation Usage

Alamofire 38.7k Jan 8, 2023
Robust Swift networking for web APIs

Conduit Conduit is a session-based Swift HTTP networking and auth library. Within each session, requests are sent through a serial pipeline before bei

Mindbody 52 Oct 26, 2022
Versatile HTTP Networking in Swift

Net is a versatile HTTP networking library written in Swift. ?? Features URL / JSON / Property List Parameter Encoding Upload File / Data / Stream / M

Intelygenz 124 Dec 6, 2022