SwiftyNetworking

0.8.3

Simple networking layer inspired by SwiftUI views building style
piotrekjeremicz/SwiftyNetworking

What's New

SwiftyNetworking v0.8.3 - Release

2023-11-22T10:42:25Z
  • Fixed afterEach middleware

SwiftyNetworking

Keep in mind - this package is in the process of heavy development! 👨🏻‍💻 🚀

Overview

SwiftyNetworking is a simple package that supports the networking layer and provide, similar to SwiftUI's ViewBuilder, request building pattern.

Note: The package is under heavy development. The structure of types and methods seems to be final, but over time there may be some changes resulting from the need to implement a new function. Version 0.8 is very close to the final release. Before this happens, I would like to add mocks and tests to complete everything that I would like to have in this package..

How to use it?

  1. Create service that provides relevant API.
struct ExampleService: Service {
    var baseURL: URL { URL(string: "https://www.example.com")! }
}
  1. Prepare models for data and error responses.
struct ExampleResponseModel: Codable {
    let foo: String
    let bar: Int
    let buzz: Bool
}

struct ExampleErrorModel: Codable {
    let status: Int
    let message: String
}
  1. Describe request by using Request macro.
@Request
struct ExampleRequest {

    let bar: String
    
    var body: some Request {
        Get("foo", bar, "buzz", from: ExampleService())
            .headers {
                X_Api_Key(value: "sample_token")
            }
            .queryItems {
                Key("hello", value: "world")
            }
            .body {
                Key("array") {
                Key("int", value: 42)
                Key("double", value: 3.14)
                Key("bool", value: true)
                Key("string", value: "foo")
                Key("array", value: ["foo", "bar", "buzz"])
            }
            .responseBody(ExampleResponseModel.self)
            .responseError(ExampleErrorModel.self)
        }
    }
}
  1. Create session and send request. Of course, you can cancel it as you want. 😉
let session = Session()
let (result, error) = await session.send(request: ExampleRequest(bar: "buzz"))

if sometingIsWrong {
    session.cancel(requests: .only(request.id))
}

And that’s it!

Advanced usage

Template

We love to optimize our work! This is one of the reasons why I prepared a template for the basic implementation of Request. Another reason was the discovery of the token menu! Do you like this approach? Give it a star! ⭐️ Request template You can easly install the teplate by running the install.sh script located in Templates directory.

Authorization

SwiftyNetworking provides easy to use and elastic authorization model. Assuming that most authorizations consist in obtaining a token from one request and using it in the others, this package contains a simple system that allows you to catch and use such values.

  1. Create a new inheritance structure from AuthorizationService. There are two variables and one method that are needed to store sensitive data. The most important part is func authorize<R: Request>(_ request: R) -> R which is a place where you can inject token from the store.
struct BackendAuthorization: AuthorizationProvider {

    var store: AuthorizationStore = BackendAuthorizationStore()
    var afterAuthorization: ((Codable, AuthorizationStore) -> Void)? = nil
        
    func authorize<R: Request>(_ request: R) -> R {
        if let token = store.get(key: .token) {
            return request.headers {
                Authorization(.bearer(token: token))
            }
        } else {
            return request
        }
    }
}
  1. You can use default KeychainAuthorizationStore or create a new inheritance structure from AuthorizationStore.
struct BackendAuthorizationStore: AuthorizationStore {
    let keychain = Keychain(service: "com.example.app")
    
    static var tokenKey: String { "com.example.app.token" }
    static var refreshTokenKey: String { "com.example.app.refresh-token" }
    static var usernameKey: String { "com.example.app.username" }
    static var passwordKey: String { "com.example.app.password" }
    
    //I would like to make it better
    func store(key: AuthorizationKey, value: String) {
        try? keychain.set(value, key: key.representant(for: Self.self))
    }
    
    func get(key: AuthorizationKey) -> String? {
        try? keychain.get(key.representant(for: Self.self))
    }
}
  1. We are ready to catch our credentials. In this case, it will be a token that the server returns after authentication process.
@Request
struct ExampleLoginRequest {    
    var body: some Request {
        Get("login", from: ExampleService())
            //[...]
            .responseBody(LoginResponse.self)
            .afterAutorization { response, store in
                store.value(.token(response.token))
            }
        }
  
  1. Add authorize() modifier to each request that requires authorization.
@Request
struct ExampleAuthorizedRequest {    
    var body: some Request {
        Get("foo", bar, "buzz", from: ExampleService())
            //[...]
            .authorize()
        }
    }
}

And that is it!

Middleware

Working with the network layer, we very often perform repetitive actions such as adding the appropriate authorization header or want to save the effect of the request sent. SwiftyNetworking allows you to perform actions just before completing the query and just after receiving a response.

struct ExampleService: Service {

    //[...]
    
    func beforeEach<R>(_ request: R) -> R where R: Request {
        request
            .headers {
                X_Api_Key(value: "secret_token")
            }
    }

    func afterEach<B>(_ response: Response<B>) -> Response<B> where B: Codable {
        statistics.store(response.statusCode)
    }
}

Logger

SwiftyNetworking provides default OSLog entity. You can use your own Logger object.

import OSLog

struct ExampleService: Service {
    //[...]
    
    let logger = Logger(subsystem: "com.example.app", category: "networking")
}

Roadmap

  • Version 0.9: add Mock result that will be an alternative output for Request
  • Version 1.0: refactor, unit tests and whatever else that will be needed to be proud of this package 😇

What’s next?

There are a few more things I want to add and support::

Mocking data

// Dummy code
request
    .mocked(where: { response in
        switch response {
        case successed:
            //do something
        case failed:
            //do something
        }
    })

Queueing requests

// Dummy code
@Request
struct ExampleRequest {
    var body: some Request {
        Queue {
            Get("/example/1", from: ExampleService())
            Get("/example/2", from: ExampleService())
            Get("/example/3", from: ExampleService())
        }
    }
}

Networking preview

// Dummy code
@Request
struct ExampleRequest {
    var body: some Request { }
}

#NetworkingPreview {
    ExampleRequest()
}

Supporting curl strings

// Dummy code
@Request
struct ExampleRequest {
    typealias Response = ExampleResponseModel
    typealias ResponseError = ExampleErrorModel
    
    var body: some Request {
        "curl -X POST https://www.example/login/ -d 'username=example&password=examle'"
    }
}

More modifiers, more settings!

Description

  • Swift Tools 5.9.0
View More Packages from this Author

Dependencies

Last updated: Fri Apr 12 2024 21:21:30 GMT-0900 (Hawaii-Aleutian Daylight Time)